Full Code of jpochyla/psst for AI

main ae4f16dbc9fa cached
136 files
733.4 KB
189.8k tokens
1637 symbols
1 requests
Download .txt
Showing preview only (773K chars total). Download the full file or copy to clipboard to get everything.
Repository: jpochyla/psst
Branch: main
Commit: ae4f16dbc9fa
Files: 136
Total size: 733.4 KB

Directory structure:
gitextract_nr4iye45/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .homebrew/
│   └── generate_formula.sh
├── .pkg/
│   ├── APPIMAGE/
│   │   └── pkg2appimage-ingredients.yml
│   ├── DEBIAN/
│   │   └── control
│   ├── copyright
│   └── psst.desktop
├── .rustfmt.toml
├── Cargo.toml
├── Cross.toml
├── LICENSE.md
├── README.md
├── psst-cli/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── psst-core/
│   ├── Cargo.toml
│   ├── build.rs
│   └── src/
│       ├── actor.rs
│       ├── audio/
│       │   ├── decode.rs
│       │   ├── decrypt.rs
│       │   ├── mod.rs
│       │   ├── normalize.rs
│       │   ├── output/
│       │   │   ├── cpal.rs
│       │   │   ├── cubeb.rs
│       │   │   └── mod.rs
│       │   ├── probe.rs
│       │   ├── resample.rs
│       │   └── source.rs
│       ├── cache.rs
│       ├── cdn.rs
│       ├── connection/
│       │   ├── diffie_hellman.rs
│       │   ├── mod.rs
│       │   └── shannon_codec.rs
│       ├── error.rs
│       ├── item_id.rs
│       ├── lastfm.rs
│       ├── lib.rs
│       ├── metadata.rs
│       ├── oauth.rs
│       ├── player/
│       │   ├── file.rs
│       │   ├── item.rs
│       │   ├── mod.rs
│       │   ├── queue.rs
│       │   ├── storage.rs
│       │   └── worker.rs
│       ├── session/
│       │   ├── access_token.rs
│       │   ├── audio_key.rs
│       │   ├── client_token.rs
│       │   ├── login5.rs
│       │   ├── mercury.rs
│       │   ├── mod.rs
│       │   └── token.rs
│       ├── system_info.rs
│       └── util.rs
└── psst-gui/
    ├── Cargo.toml
    ├── assets/
    │   ├── logo.afdesign
    │   └── logo.icns
    ├── build-icons.sh
    ├── build.rs
    └── src/
        ├── cmd.rs
        ├── controller/
        │   ├── after_delay.rs
        │   ├── alert_cleanup.rs
        │   ├── ex_click.rs
        │   ├── ex_cursor.rs
        │   ├── ex_scroll.rs
        │   ├── input.rs
        │   ├── mod.rs
        │   ├── nav.rs
        │   ├── on_command.rs
        │   ├── on_command_async.rs
        │   ├── on_debounce.rs
        │   ├── on_update.rs
        │   ├── playback.rs
        │   ├── session.rs
        │   └── sort.rs
        ├── data/
        │   ├── album.rs
        │   ├── artist.rs
        │   ├── config.rs
        │   ├── ctx.rs
        │   ├── find.rs
        │   ├── id.rs
        │   ├── mod.rs
        │   ├── nav.rs
        │   ├── playback.rs
        │   ├── playlist.rs
        │   ├── promise.rs
        │   ├── recommend.rs
        │   ├── search.rs
        │   ├── show.rs
        │   ├── slider_scroll_scale.rs
        │   ├── track.rs
        │   ├── user.rs
        │   └── utils.rs
        ├── delegate.rs
        ├── error.rs
        ├── main.rs
        ├── ui/
        │   ├── album.rs
        │   ├── artist.rs
        │   ├── credits.rs
        │   ├── episode.rs
        │   ├── find.rs
        │   ├── home.rs
        │   ├── library.rs
        │   ├── lyrics.rs
        │   ├── menu.rs
        │   ├── mod.rs
        │   ├── playable.rs
        │   ├── playback.rs
        │   ├── playlist.rs
        │   ├── preferences.rs
        │   ├── recommend.rs
        │   ├── search.rs
        │   ├── show.rs
        │   ├── theme.rs
        │   ├── track.rs
        │   ├── user.rs
        │   └── utils.rs
        ├── webapi/
        │   ├── cache.rs
        │   ├── client.rs
        │   ├── local.rs
        │   └── mod.rs
        └── widget/
            ├── checkbox.rs
            ├── dispatcher.rs
            ├── empty.rs
            ├── fill_between.rs
            ├── icons.rs
            ├── link.rs
            ├── maybe.rs
            ├── mod.rs
            ├── overlay.rs
            ├── promise.rs
            ├── remote_image.rs
            ├── theme.rs
            └── utils.rs

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: jpochyla


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment**

- OS:
- Version:

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: ""
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/build.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  VERSION_DATE: ${{ format('{0}.{1}.{2}', github.run_number, github.run_id, github.run_attempt) }}
  VERSION_TIMESTAMP: ${{ github.event.repository.updated_at }}

jobs:
  code-style:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Setup Cache
        uses: Swatinem/rust-cache@v2

      - name: Install Linux Dependencies
        if: runner.os == 'Linux'
        run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libssl-dev libasound2-dev

      - name: Check Formatting
        run: cargo clippy -- -D warnings

  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
          - os: macos-latest
          - os: windows-latest

    runs-on: ${{ matrix.os }}
    env:
      MACOSX_DEPLOYMENT_TARGET: 11.0
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set Version Info
        id: version
        shell: bash
        env:
          FULL_SHA: ${{ github.sha }}
        run: |
          echo "BUILD_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
          echo "VERSION=0.1.0,$(date +'%Y%m%d.%H%M%S')" >> $GITHUB_ENV
          SHORT_SHA=${FULL_SHA::7}
          echo "RELEASE_VERSION=$(date +'%Y.%m.%d')-$SHORT_SHA" >> $GITHUB_ENV

      - name: Setup Rust Cache
        uses: Swatinem/rust-cache@v2
        with:
          key: ${{ hashFiles('Cross.toml') }}

      - name: Install Cross
        if: runner.os == 'Linux'
        run: cargo install cross

      - name: Build (Linux)
        if: runner.os == 'Linux'
        run: cross build --release --target ${{ matrix.target }}

      - name: Build Release (macOS)
        if: runner.os == 'macOS'
        run: |
          rustup target add x86_64-apple-darwin aarch64-apple-darwin
          cargo build --release --target x86_64-apple-darwin --target aarch64-apple-darwin

      - name: Build Release (Windows)
        if: runner.os == 'Windows'
        run: cargo build --release

      - name: Cache cargo-bundle and Homebrew
        id: cache-tools
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/cargo-bundle
            ~/Library/Caches/Homebrew
          key: ${{ runner.os }}-tools-${{ hashFiles('**/Cargo.lock', '.github/workflows/build.yml') }}
          restore-keys: |
            ${{ runner.os }}-tools-

      - name: Install cargo-bundle
        if: runner.os == 'macOS' && !steps.cache-tools.outputs.cache-hit
        run: cargo install cargo-bundle

      - name: Install create-dmg
        if: runner.os == 'macOS'
        run: brew install -q create-dmg

      - name: Create macOS universal binary
        if: runner.os == 'macOS'
        run: |
          mkdir -p target/release
          lipo -create \
            -arch x86_64 target/x86_64-apple-darwin/release/psst-gui \
            -arch arm64 target/aarch64-apple-darwin/release/psst-gui \
            -output target/release/psst-gui

      - name: Bundle macOS Release
        if: runner.os == 'macOS'
        env:
          CARGO_BUNDLE_SKIP_BUILD: "true"
        run: |
          cargo bundle --release -p psst-gui

      - name: Create DMG
        if: runner.os == 'macOS'
        run: |
          create-dmg \
            --volname "Psst" \
            --volicon "psst-gui/assets/logo.icns" \
            --window-pos 200 120 \
            --window-size 600 400 \
            --icon-size 100 \
            --icon "Psst.app" 150 160 \
            --hide-extension "Psst.app" \
            --app-drop-link 450 160 \
            "Psst.dmg" \
            "target/release/bundle/osx/Psst.app"

      - name: Upload macOS DMG
        uses: actions/upload-artifact@v4
        if: runner.os == 'macOS'
        with:
          name: Psst.dmg
          path: Psst.dmg

      - name: Make Linux Binary Executable
        if: runner.os == 'Linux'
        run: chmod +x target/${{ matrix.target }}/release/psst-gui

      - name: Rename Linux Binary
        if: runner.os == 'Linux'
        run: mv target/${{ matrix.target }}/release/psst-gui target/${{ matrix.target }}/release/psst

      - name: Upload Linux Binary
        uses: actions/upload-artifact@v4
        if: runner.os == 'Linux'
        with:
          name: psst-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/psst

      - name: Upload Windows Executable
        uses: actions/upload-artifact@v4
        if: runner.os == 'Windows'
        with:
          name: Psst.exe
          path: target/release/psst-gui.exe

  deb:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        include:
          - arch: amd64
            target: x86_64-unknown-linux-gnu
          - arch: arm64
            target: aarch64-unknown-linux-gnu
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Get full history to count number of commits for package version

      - name: Download Linux Binaries
        uses: actions/download-artifact@v4
        with:
          name: psst-${{ matrix.target }}
          path: binaries

      - name: Move Binary
        run: |
          mkdir -p pkg/usr/bin/
          mv binaries/psst pkg/usr/bin/

      - name: Move Desktop Entry
        run: mkdir -p pkg/usr/share/applications/; mv .pkg/psst.desktop pkg/usr/share/applications/

      - name: Add Icons
        run: |
          LOGOS=$(cd ./psst-gui/assets/ && ls logo_*.png)
          for LOGO in $LOGOS
          do
            LOGO_SIZE=$(echo "${LOGO}" | grep -oE '[[:digit:]]{2,}')
            mkdir -p "pkg/usr/share/icons/hicolor/${LOGO_SIZE}x${LOGO_SIZE}/"
            cp "./psst-gui/assets/${LOGO}" "pkg/usr/share/icons/hicolor/${LOGO_SIZE}x${LOGO_SIZE}/psst.png"
          done
          mkdir -p "pkg/usr/share/icons/hicolor/scalable/apps/"
          cp "./psst-gui/assets/logo.svg" "pkg/usr/share/icons/hicolor/scalable/apps/psst.svg"

      - name: Set Permissions
        run: chmod 755 pkg/usr/bin/psst

      - name: Move License
        run: mkdir -p pkg/usr/share/doc/psst-gui/; mv .pkg/copyright pkg/usr/share/doc/psst-gui/

      - name: Write Package Config
        run: |
          mkdir -p pkg/DEBIAN
          export ARCHITECTURE=${{ matrix.arch }}
          SANITIZED_BRANCH="$(echo ${GITHUB_HEAD_REF:+.$GITHUB_HEAD_REF}|tr '_/' '-')"
          export VERSION=0.1.0"$SANITIZED_BRANCH"+r"$(git rev-list --count HEAD)"-0
          envsubst < .pkg/DEBIAN/control > pkg/DEBIAN/control

      - name: Build Package
        run: |
          cat pkg/DEBIAN/control
          dpkg-deb -b pkg/ psst-${{ matrix.arch }}.deb

      - name: Upload Debian Package
        uses: actions/upload-artifact@v4
        with:
          name: psst-deb-${{ matrix.arch }}
          path: "*.deb"

  appimage:
    if: false # Disable temporarily: https://github.com/jpochyla/psst/actions/runs/3897410142/jobs/6655282029
    runs-on: ubuntu-latest
    needs: deb
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Download Debian Package
        uses: actions/download-artifact@v4
        with:
          name: psst-deb
          # Downloads to the root of the workspace by default if path is omitted or '.',
          # so removing explicit path to ${{github.workspace}}

      - name: Install Dependencies
        run: sudo apt-get update && sudo apt-get install -y libfuse2

      - name: Create Workspace
        run: mkdir -p appimage

      - name: Download the Latest pkg2appimage
        run: |
          latest_release_appimage_url=$(wget -q https://api.github.com/repos/AppImageCommunity/pkg2appimage/releases/latest -O - | jq -r '.assets[0].browser_download_url')
          wget --directory-prefix=appimage -c $latest_release_appimage_url

      - name: Create Path to pkg2appimage
        run: |
          pkg2appimage_executable=$(ls appimage)
          app_path=appimage/${pkg2appimage_executable}
          chmod +x ${app_path}
          echo "app_path=${app_path}" >> $GITHUB_ENV

      - name: Create Path to pkg2appimage's Recipe File
        run: |
          recipe_path=psst/.pkg/APPIMAGE/pkg2appimage-ingredients.yml
          echo "recipe_path=${recipe_path}" >> $GITHUB_ENV

      - name: Run pkg2appimage
        run: |
          ${{env.app_path}} ${{env.recipe_path}}

      - name: Upload AppImage
        uses: actions/upload-artifact@v4
        with:
          name: psst-appimage
          path: out/*.AppImage

  release:
    needs: [build, deb]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - name: Set Version Info and Paths
        id: set_paths
        env:
          FULL_SHA: ${{ github.sha }}
        run: |
          echo "BUILD_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
          echo "VERSION=0.1.0,$(date +'%Y%m%d.%H%M%S')" >> $GITHUB_ENV
          SHORT_SHA=${FULL_SHA::7}
          echo "RELEASE_VERSION=$(date +'%Y.%m.%d')-$SHORT_SHA" >> $GITHUB_ENV

      - name: Download All Artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Prepare Release Body Data
        id: release_data
        run: |
          echo "CURRENT_DATE_STR=$(date)" >> $GITHUB_ENV

      - name: Prepare Release Assets
        id: prep_assets
        run: |
          set -e
          mkdir -p artifacts_final

          find artifacts -type f -name 'Psst.dmg' -exec mv {} artifacts_final/Psst.dmg \;
          find artifacts -type f -name 'psst-gui.exe' -exec mv {} artifacts_final/Psst.exe \;
          find artifacts -type f -name 'psst-amd64.deb' -exec mv {} artifacts_final/psst-amd64.deb \;
          find artifacts -type f -name 'psst-arm64.deb' -exec mv {} artifacts_final/psst-arm64.deb \;

          find artifacts -type f -name 'psst' -path '*/psst-x86_64-unknown-linux-gnu/*' -exec mv {} artifacts_final/psst-linux-x86_64 \;
          find artifacts -type f -name 'psst' -path '*/psst-aarch64-unknown-linux-gnu/*' -exec mv {} artifacts_final/psst-linux-aarch64 \;

          rm -rf artifacts
          mv artifacts_final artifacts
          ls -l artifacts/

      - name: Create Main Release
        uses: softprops/action-gh-release@v2
        with:
          name: Continuous release (${{ env.RELEASE_VERSION }})
          tag_name: rolling
          make_latest: true
          prerelease: false
          body: |
            This is a rolling release of Psst, published automatically on every commit to main.

            Version: ${{ env.RELEASE_VERSION }}
            Commit: ${{ github.sha }}
            Built: ${{ env.CURRENT_DATE_STR }}
            Workflow: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

            See the release assets for SHA256 checksums.
          files: artifacts/*
          generate_release_notes: false


================================================
FILE: .gitignore
================================================
config.json
target
cache
.cargo
.idea
.DS_Store
.env
*.iml
rust-toolchain
*.ico

================================================
FILE: .homebrew/generate_formula.sh
================================================
#!/bin/bash

set -eo pipefail

REPO_OWNER="jpochyla"
REPO_NAME="psst"

cat <<EOF
cask "psst" do
  version :latest
  sha256 :no_check

  url "https://github.com/${REPO_OWNER}/${REPO_NAME}/releases/latest/download/Psst.dmg"
  name "Psst"
  desc "Fast and native Spotify client"
  homepage "https://github.com/${REPO_OWNER}/${REPO_NAME}/"

  depends_on macos: ">= :big_sur"

  app "Psst.app"

  zap trash: [
    "~/Library/Application Support/Psst",
    "~/Library/Caches/com.jpochyla.psst",
    "~/Library/Caches/Psst",
    "~/Library/HTTPStorages/com.jpochyla.psst",
    "~/Library/Preferences/com.jpochyla.psst.plist",
    "~/Library/Saved Application State/com.jpochyla.psst.savedState",
  ]
end
EOF


================================================
FILE: .pkg/APPIMAGE/pkg2appimage-ingredients.yml
================================================
ingredients:
  dist: focal
  sources:
    - deb http://us.archive.ubuntu.com/ubuntu/ focal main universe
  debs:
    - ../*.deb
  script:
    - mkdir -p /home/runner/work/psst/psst/.AppDir/
    - cp /home/runner/work/psst/psst/psst-gui/assets/logo_256.png /home/runner/work/psst/psst/.AppDir/psst.png


================================================
FILE: .pkg/DEBIAN/control
================================================
Package: psst-gui
Version: $VERSION
Architecture: $ARCHITECTURE
Maintainer: Jan Pochyla <jpochyla@gmail.com>
Section: sound
Priority: optional
Homepage: https://github.com/jpochyla/psst
Package-Type: deb
Depends: libssl3 | libssl1.1, libgtk-3-0, libcairo2
Description: Fast and native Spotify client


================================================
FILE: .pkg/copyright
================================================
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Psst
Source: https://github.com/jpochyla/psst

Files: *
Copyright: (c) 2020 Jan Pochyla
License: MIT

License: MIT
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: .pkg/psst.desktop
================================================
[Desktop Entry]
Type=Application
Name=Psst
Comment=Fast and native Spotify client
GenericName=Music Player
Icon=psst
TryExec=psst-gui
Exec=psst-gui %U
Terminal=false
MimeType=x-scheme-handler/psst;
Categories=Audio;Music;Player;AudioVideo;
StartupWMClass=psst-gui


================================================
FILE: .rustfmt.toml
================================================
imports_granularity = "Crate"
wrap_comments = true


================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = ["psst-core", "psst-cli", "psst-gui"]

[profile.dev]
opt-level = 1
debug = true
lto = false

[profile.release]
opt-level = 3
strip = true
lto = true
codegen-units = 1

[profile.dev.package.symphonia]
opt-level = 2

[profile.dev.package.libsamplerate]
opt-level = 2


================================================
FILE: Cross.toml
================================================
[build]
pre-build = ["""
    dpkg --add-architecture $CROSS_DEB_ARCH && \
    apt-get update && \
    apt-get --assume-yes install \
        libgtk-3-dev:$CROSS_DEB_ARCH \
        libssl-dev:$CROSS_DEB_ARCH \
        libasound2-dev:$CROSS_DEB_ARCH
"""]

[target.x86_64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/x86_64-unknown-linux-gnu:edge"

[target.aarch64-unknown-linux-gnu]
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge"


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2020 Jan Pochyla

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Psst

A fast Spotify client with a native GUI written in Rust, without Electron.
Psst is still very early in development, lacking in features, stability, and general user experience.
It's fully cross-platform, supporting Windows, Linux, and macOS.
Contributions are welcome!

**Note:** A Spotify Premium account is required.

[![Build](https://github.com/jpochyla/psst/actions/workflows/build.yml/badge.svg)](https://github.com/jpochyla/psst/actions)

![Screenshot](./psst-gui/assets/screenshot.png)

## Download

GitHub Actions automatically builds and releases new versions when changes are pushed to the `main` branch.
You can download the latest release for Windows, Linux, and macOS from the [GitHub Releases page](https://github.com/jpochyla/psst/releases/latest).

| Platform               | Download Link                                                                            |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| Linux (x86_64)         | [Download](https://github.com/jpochyla/psst/releases/latest/download/psst-linux-x86_64)  |
| Linux (aarch64)        | [Download](https://github.com/jpochyla/psst/releases/latest/download/psst-linux-aarch64) |
| Debian Package (amd64) | [Download](https://github.com/jpochyla/psst/releases/latest/download/psst-amd64.deb)     |
| Debian Package (arm64) | [Download](https://github.com/jpochyla/psst/releases/latest/download/psst-arm64.deb)     |
| macOS                  | [Download](https://github.com/jpochyla/psst/releases/latest/download/Psst.dmg)           |
| Windows                | [Download](https://github.com/jpochyla/psst/releases/latest/download/Psst.exe)           |

Unofficial builds of Psst are also available through the [AUR](https://aur.archlinux.org/packages/psst-git) and [Homebrew](https://formulae.brew.sh/cask/psst).

## Building

On all platforms, the **latest [Rust](https://rustup.rs/) stable** (at least 1.65.0) is required.
For platform-specific requirements, see the dropdowns below.

<details>
<summary>Linux</summary>

Our user-interface library, Druid, has two possible backends on Linux: GTK and pure X11, with a Wayland backend in the works.
The default Linux backend is GTK.
Before building on Linux, make sure the required dependencies are installed.

### Debian/Ubuntu

```shell
sudo apt-get install libssl-dev libgtk-3-dev libcairo2-dev libasound2-dev
```

### RHEL/Fedora

```shell
sudo dnf install openssl-devel gtk3-devel cairo-devel alsa-lib-devel
```

</details>

<details>
<summary>OpenBSD (WIP)</summary>

OpenBSD support is still a WIP, and things will likely not function as intended.
Similar to Linux, Druid defaults to GTK while also providing a pure X11 backend.
Furthermore, bindgen must be able to find LLVM through the expected environment variable.
Only OpenBSD/amd64 has been tested so far.

```shell
doas pkg_add gtk+3 cairo llvm
export LIBCLANG_PATH=/usr/local/lib
```

In case rustc(1) fails building bigger crates

```shell
memory allocation of xxxx bytes failed
error: could not compile `gtk`
Caused by:
  process didn't exit successfully: `rustc --crate-name gtk [...]` (signal: 6, SIGABRT: process abort signal)
warning: build failed, waiting for other jobs to finish...
```

try increasing your user's maximum heap size:

```shell
ulimit -d $(( 2 * `ulimit -d` ))
```

</details>

---

#### Build from Source

```shell
cargo build
# Append `--release` for a release build.
```

#### Run from Source

```shell
cargo run --bin psst-gui
# Append `--release` for a release build.
```

#### Build Installation Bundle (i.e., macOS .app)

```shell
cargo install cargo-bundle
cargo bundle --release
```

## Roadmap

- [x] Vorbis track playback
- [x] Browsing saved albums and tracks
- [x] Save / unsave albums and tracks
- [x] Browsing followed playlists
- [x] Search for artists, albums, and tracks
- [x] Podcast support
- [x] Media keys control
- [x] Open Spotify links through the search bar
- [x] Audio volume control
- [x] Audio loudness normalization
- [x] Genre playlists and "For You" content
- [x] Dark theme
- [x] Credits support
- [ ] Resilience to network errors (automatically retry timed-out requests)
- [ ] Managing playlists
  - Follow/unfollow
  - Add/remove tracks
  - Reorder tracks
  - Rename playlist
  - Playlist folders
- [x] Playback queue
- [ ] React to audio output device events
  - Pause after disconnecting headphones
  - Transfer playback after connecting headphones
- [ ] Better caching
  - Cache as many WebAPI responses as possible
  - Visualize cache utilization
    - Total cache usage in the config dialog
    - Show time origin of cached data, allow to refresh
- [ ] Trivia on the artist page, Wikipedia links
- [ ] Downloading encrypted tracks
- [ ] Reporting played tracks to Spotify servers
- [ ] OS-specific application bundles
- UI
  - [ ] Rethink the current design, consider a two-pane layout
    - Left pane for browsing
    - Right pane for current playback
  - [ ] Detect light/dark OS theme
  - [ ] Robust error states, ideally with a retry button
  - [ ] Correct playback highlight
    - Highlight now-playing track only in the correct album/playlist
    - Keep highlighted track in viewport
  - [ ] Paging or virtualized lists for albums and tracks
  - [ ] Grid for albums and artists
  - [ ] Robust active/inactive menu visualization
  - [ ] Save playback state

## Development

Contributions are very welcome!  
Here's the basic project structure:

- `/psst-core` - Core library, takes care of Spotify TCP session, audio file retrieval, decoding, audio output, playback queue, etc.
- `/psst-gui` - GUI application built with [Druid](https://github.com/linebender/druid)
- `/psst-cli` - Example CLI that plays a track. Credentials must be configured in the code.

## Privacy Policy

Psst connects only to the official Spotify servers and does not call home.
Caches of various things are stored locally and can be deleted anytime.
User credentials are not stored at all; instead, a re-usable authentication token from Spotify is used.

## Thanks

This project would not exist without the following:

- Big thank you to [`librespot`](https://github.com/librespot-org/librespot), the Open Source Spotify client library for Rust. Most of `psst-core` is directly inspired by the ideas and code of `librespot`, although with a few differences:
  - Spotify Connect (remote control) is not supported yet.
  - Psst is completely synchronous, without `tokio` or other `async` runtime, although it will probably change in the future.
  - Psst is using HTTPS-based CDN audio file retrieval, similar to the official Web client or [`librespot-java`](https://github.com/librespot-org/librespot-java), instead of the channel-based approach in `librespot`.
- [`druid`](https://github.com/linebender/druid) native GUI library for Rust.
- [`ncspot`](https://github.com/hrkfdn/ncspot) cross-platform ncurses Spotify client written in Rust, using `librespot`.
- ...and of course other libraries and projects.


================================================
FILE: psst-cli/Cargo.toml
================================================
[package]
name = "psst-cli"
version = "0.1.0"
authors = ["Jan Pochyla <jpochyla@gmail.com>"]
edition = "2021"

[features]
default = ["cpal"]
cpal = ["psst-core/cpal"]
cubeb = ["psst-core/cubeb"]

[dependencies]
psst-core = { path = "../psst-core" }

env_logger = "0.11.5"
log = "0.4.22"


================================================
FILE: psst-cli/src/main.rs
================================================
use psst_core::{
    audio::{
        normalize::NormalizationLevel,
        output::{AudioOutput, AudioSink, DefaultAudioOutput},
    },
    cache::{Cache, CacheHandle},
    cdn::{Cdn, CdnHandle},
    connection::Credentials,
    error::Error,
    item_id::{ItemId, ItemIdType},
    player::{item::PlaybackItem, PlaybackConfig, Player, PlayerCommand, PlayerEvent},
    session::{SessionConfig, SessionService},
};
use std::{env, io, io::BufRead, path::PathBuf, thread};

fn main() {
    env_logger::init();

    let args: Vec<String> = env::args().collect();
    let track_id = args
        .get(1)
        .expect("Expected <track_id> in the first parameter");
    let login_creds = Credentials::from_username_and_password(
        env::var("SPOTIFY_USERNAME").unwrap(),
        env::var("SPOTIFY_PASSWORD").unwrap(),
    );
    let session = SessionService::with_config(SessionConfig {
        login_creds,
        proxy_url: None,
    });

    start(track_id, session).unwrap();
}

fn start(track_id: &str, session: SessionService) -> Result<(), Error> {
    let cdn = Cdn::new(session.clone(), None)?;
    let cache = Cache::new(PathBuf::from("cache"))?;
    let item_id = ItemId::from_base62(track_id, ItemIdType::Track).unwrap();
    play_item(
        session,
        cdn,
        cache,
        PlaybackItem {
            item_id,
            norm_level: NormalizationLevel::Track,
        },
    )
}

fn play_item(
    session: SessionService,
    cdn: CdnHandle,
    cache: CacheHandle,
    item: PlaybackItem,
) -> Result<(), Error> {
    let output = DefaultAudioOutput::open()?;
    let config = PlaybackConfig::default();

    let mut player = Player::new(session, cdn, cache, config, &output);

    let _ui_thread = thread::spawn({
        let player_sender = player.sender();

        player_sender
            .send(PlayerEvent::Command(PlayerCommand::LoadQueue {
                items: vec![item, item, item],
                position: 0,
            }))
            .unwrap();

        move || {
            for line in io::stdin().lock().lines() {
                match line.as_ref().map(|s| s.as_str()) {
                    Ok("p") => {
                        player_sender
                            .send(PlayerEvent::Command(PlayerCommand::Pause))
                            .unwrap();
                    }
                    Ok("r") => {
                        player_sender
                            .send(PlayerEvent::Command(PlayerCommand::Resume))
                            .unwrap();
                    }
                    Ok("s") => {
                        player_sender
                            .send(PlayerEvent::Command(PlayerCommand::Stop))
                            .unwrap();
                    }
                    Ok("<") => {
                        player_sender
                            .send(PlayerEvent::Command(PlayerCommand::Previous))
                            .unwrap();
                    }
                    Ok(">") => {
                        player_sender
                            .send(PlayerEvent::Command(PlayerCommand::Next))
                            .unwrap();
                    }
                    _ => log::warn!("unknown command"),
                }
            }
        }
    });

    for event in player.receiver() {
        player.handle(event);
    }
    output.sink().close();

    Ok(())
}


================================================
FILE: psst-core/Cargo.toml
================================================
[package]
name = "psst-core"
version = "0.1.0"
authors = ["Jan Pochyla <jpochyla@gmail.com>"]
edition = "2021"


[build-dependencies]
gix-config = "0.45.1"
time = { version = "0.3.36", features = ["local-offset"] }

[dependencies]

# Common
byteorder = { version = "1.5.0" }
crossbeam-channel = { version = "0.5.13" }
git-version = { version = "0.3.9" }
log = { version = "0.4.22" }
num-bigint = { version = "0.4.6", features = ["rand"] }
num-traits = { version = "0.2.19" }
oauth2 = { version = "4.4.2" }
parking_lot = { version = "0.12.3" }
librespot-protocol = "0.7.1"
protobuf = "3"
sysinfo = "0.35.0"
data-encoding = "2.9"
rand = { version = "0.9.1" }
rangemap = { version = "1.5.1" }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
socks = { version = "0.3.4" }
tempfile = { version = "3.13.0" }
rustfm-scrobble = "1.1.1"
ureq = { version = "3.0.11", features = ["json"] }
url = { version = "2.5.2" }

# Cryptography
aes = { version = "0.8.4" }
ctr = { version = "0.9.2" }
hmac = { version = "0.12.1" }
sha-1 = { version = "0.10.1" }
shannon = { version = "0.2.0" }

# Audio
audio_thread_priority = "0.33.0"
cpal = { version = "0.15.3", optional = true }
cubeb = { git = "https://github.com/mozilla/cubeb-rs", optional = true }
libsamplerate = { version = "0.1.0" }
rb = { version = "0.4.1" }
symphonia = { version = "0.5.4", default-features = false, features = [
  "ogg",
  "vorbis",
  "mp3",
] }

[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.61.1", features = ["Win32_System_Com"], default-features = false }


================================================
FILE: psst-core/build.rs
================================================
use gix_config::File;
use std::{env, fs, io::Write};
use time::OffsetDateTime;

fn main() {
    let outdir = env::var("OUT_DIR").unwrap();
    let outfile = format!("{outdir}/build-time.txt");

    let mut fh = fs::File::create(outfile).unwrap();
    let now = OffsetDateTime::now_local().unwrap_or_else(|_| OffsetDateTime::now_utc());
    write!(fh, r#""{now}""#).ok();

    let git_config = File::from_git_dir("../.git/".into()).expect("Git Config not found!");
    // Get Git's 'Origin' URL
    let mut remote_url = git_config
        .raw_value("remote.origin.url")
        .expect("Couldn't extract origin url!")
        .to_string();

    // Check whether origin is accessed via ssh
    if remote_url.contains('@') {
        // If yes, strip the `git@` prefix and split the domain and path
        let mut split = remote_url
            .strip_prefix("git@")
            .unwrap_or(&remote_url)
            .split(':');
        let domain = split
            .next()
            .expect("Couldn't extract domain from ssh-style origin");
        let path = split
            .next()
            .expect("Couldn't expect path from ssh-style origin");

        // And construct the http-style url
        remote_url = format!("https://{domain}/{path}");
    }
    let trimmed_url = remote_url.trim_end_matches(".git");
    remote_url.clone_from(&String::from(trimmed_url));

    let outfile = format!("{outdir}/remote-url.txt");
    let mut file = fs::File::create(outfile).unwrap();
    write!(file, r#""{remote_url}""#).ok();
}


================================================
FILE: psst-core/src/actor.rs
================================================
use std::{
    fmt::Display,
    thread::{self, JoinHandle},
    time::Duration,
};

use crossbeam_channel::{
    bounded, unbounded, Receiver, RecvTimeoutError, SendError, Sender, TrySendError,
};

pub enum Act<T: Actor> {
    Continue,
    WaitOr {
        timeout: Duration,
        timeout_msg: T::Message,
    },
    Shutdown,
}

pub trait Actor: Sized {
    type Message: Send + 'static;
    type Error: Display;

    fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Error>;

    fn process(mut self, recv: Receiver<Self::Message>) {
        let mut act = Act::Continue;
        loop {
            let msg = match act {
                Act::Continue => match recv.recv() {
                    Ok(msg) => msg,
                    Err(_) => {
                        break;
                    }
                },
                Act::WaitOr {
                    timeout,
                    timeout_msg,
                } => match recv.recv_timeout(timeout) {
                    Ok(msg) => msg,
                    Err(RecvTimeoutError::Timeout) => timeout_msg,
                    Err(RecvTimeoutError::Disconnected) => {
                        break;
                    }
                },
                Act::Shutdown => {
                    break;
                }
            };
            act = match self.handle(msg) {
                Ok(act) => act,
                Err(err) => {
                    log::error!("error: {err}");
                    break;
                }
            };
        }
    }

    fn spawn<F>(cap: Capacity, name: &str, factory: F) -> ActorHandle<Self::Message>
    where
        F: FnOnce(Sender<Self::Message>) -> Self + Send + 'static,
    {
        let (send, recv) = cap.to_channel();
        ActorHandle {
            sender: send.clone(),
            thread: thread::Builder::new()
                .name(name.to_string())
                .spawn(move || {
                    factory(send).process(recv);
                })
                .unwrap(),
        }
    }

    fn spawn_with_default_cap<F>(name: &str, factory: F) -> ActorHandle<Self::Message>
    where
        F: FnOnce(Sender<Self::Message>) -> Self + Send + 'static,
    {
        Self::spawn(Capacity::Bounded(128), name, factory)
    }
}

pub struct ActorHandle<M> {
    thread: JoinHandle<()>,
    sender: Sender<M>,
}

impl<M> ActorHandle<M> {
    pub fn sender(&self) -> Sender<M> {
        self.sender.clone()
    }

    pub fn join(self) {
        let _ = self.thread.join();
    }

    pub fn send(&self, msg: M) -> Result<(), SendError<M>> {
        self.sender.send(msg)
    }

    pub fn try_send(&self, msg: M) -> Result<(), TrySendError<M>> {
        self.sender.try_send(msg)
    }
}

pub enum Capacity {
    Sync,
    Bounded(usize),
    Unbounded,
}

impl Capacity {
    pub fn to_channel<T>(&self) -> (Sender<T>, Receiver<T>) {
        match self {
            Capacity::Sync => bounded(0),
            Capacity::Bounded(cap) => bounded(*cap),
            Capacity::Unbounded => unbounded(),
        }
    }
}


================================================
FILE: psst-core/src/audio/decode.rs
================================================
use std::{io, time::Duration};

use symphonia::{
    core::{
        audio::{SampleBuffer, SignalSpec},
        codecs::{CodecParameters, Decoder, DecoderOptions},
        conv::ConvertibleSample,
        errors::Error as SymphoniaError,
        formats::{FormatOptions, FormatReader, SeekMode, SeekTo},
        io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
        units::TimeStamp,
    },
    default::{
        codecs::{MpaDecoder, VorbisDecoder},
        formats::{MpaReader, OggReader},
    },
};

use crate::{error::Error, util::FileWithConstSize};

pub enum AudioCodecFormat {
    Mp3,
    OggVorbis,
}

impl AudioCodecFormat {
    fn format_reader(
        &self,
        mss: MediaSourceStream,
    ) -> Result<Box<dyn FormatReader>, SymphoniaError> {
        match self {
            Self::Mp3 => Ok(Box::new(MpaReader::try_new(
                mss,
                &FormatOptions::default(),
            )?)),
            Self::OggVorbis => Ok(Box::new(OggReader::try_new(
                mss,
                &FormatOptions::default(),
            )?)),
        }
    }

    fn decoder(&self, codec_params: &CodecParameters) -> Result<Box<dyn Decoder>, SymphoniaError> {
        match self {
            Self::Mp3 => Ok(Box::new(MpaDecoder::try_new(
                codec_params,
                &DecoderOptions::default(),
            )?)),
            Self::OggVorbis => Ok(Box::new(VorbisDecoder::try_new(
                codec_params,
                &DecoderOptions::default(),
            )?)),
        }
    }
}

pub struct AudioDecoder {
    track_id: u32, // Internal track index.
    decoder: Box<dyn Decoder>,
    format: Box<dyn FormatReader>,
}

impl AudioDecoder {
    pub fn new<T>(input: T, codec: AudioCodecFormat) -> Result<Self, Error>
    where
        T: io::Read + io::Seek + Send + Sync + 'static,
    {
        let mss = MediaSourceStream::new(
            Box::new(FileWithConstSize::new(input)),
            MediaSourceStreamOptions::default(),
        );
        let format = codec.format_reader(mss)?;
        let track = format.default_track().unwrap();
        let decoder = codec.decoder(&track.codec_params)?;

        Ok(Self {
            track_id: track.id,
            decoder,
            format,
        })
    }

    pub fn codec_params(&self) -> &CodecParameters {
        self.decoder.codec_params()
    }

    pub fn signal_spec(&self) -> SignalSpec {
        SignalSpec {
            rate: self.codec_params().sample_rate.unwrap(),
            channels: self.codec_params().channels.unwrap(),
        }
    }

    pub fn seek(&mut self, time: Duration) -> Result<TimeStamp, Error> {
        let seeked_to = self.format.seek(
            SeekMode::Accurate,
            SeekTo::Time {
                time: time.as_secs_f64().into(),
                track_id: Some(self.track_id),
            },
        )?;
        Ok(seeked_to.actual_ts)
    }

    /// Read a next packet of audio from this decoder.  Returns `None` in case
    /// of EOF or internal error.
    pub fn read_packet<S>(&mut self, samples: &mut SampleBuffer<S>) -> Option<TimeStamp>
    where
        S: ConvertibleSample,
    {
        loop {
            // Demux an encoded packet from the media format.
            let packet = match self.format.next_packet() {
                Ok(packet) => packet,
                Err(SymphoniaError::IoError(io)) if io.kind() == io::ErrorKind::UnexpectedEof => {
                    return None; // End of this stream.
                }
                Err(err) => {
                    log::error!("format error: {err}");
                    return None; // We cannot recover from format errors, quit.
                }
            };
            while !self.format.metadata().is_latest() {
                // Consume any new metadata that has been read since the last
                // packet.
            }
            // If the packet does not belong to the selected track, skip over it.
            if packet.track_id() != self.track_id {
                continue;
            }
            // Decode the packet into an audio buffer.
            match self.decoder.decode(&packet) {
                Ok(decoded) => {
                    // Interleave the samples into the buffer.
                    samples.copy_interleaved_ref(decoded);
                    return Some(packet.ts());
                }
                Err(SymphoniaError::IoError(err)) => {
                    // The packet failed to decode due to an IO error, skip the packet.
                    log::error!("io decode error: {err}");
                    continue;
                }
                Err(SymphoniaError::DecodeError(err)) => {
                    // The packet failed to decode due to invalid data, skip the packet.
                    log::error!("decode error: {err}");
                    continue;
                }
                Err(err) => {
                    log::error!("fatal decode error: {err}");
                    return None;
                }
            };
        }
    }
}

impl<T> MediaSource for FileWithConstSize<T>
where
    T: io::Read + io::Seek + Send + Sync,
{
    fn is_seekable(&self) -> bool {
        true
    }

    fn byte_len(&self) -> Option<u64> {
        Some(self.len())
    }
}

impl From<SymphoniaError> for Error {
    fn from(err: SymphoniaError) -> Error {
        Error::AudioDecodingError(Box::new(err))
    }
}


================================================
FILE: psst-core/src/audio/decrypt.rs
================================================
use std::{convert::TryInto, io};

use aes::{
    cipher::{generic_array::GenericArray, KeyIvInit, StreamCipher, StreamCipherSeek},
    Aes128,
};
use ctr::Ctr128BE;

const AUDIO_AESIV: [u8; 16] = [
    0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93,
];

#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct AudioKey(pub [u8; 16]);

impl AudioKey {
    pub fn from_raw(data: &[u8]) -> Option<Self> {
        Some(AudioKey(data.try_into().ok()?))
    }
}

pub struct AudioDecrypt<T> {
    cipher: Ctr128BE<Aes128>,
    reader: T,
}

impl<T: io::Read> AudioDecrypt<T> {
    pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
        let cipher = Ctr128BE::<Aes128>::new(
            GenericArray::from_slice(&key.0),
            GenericArray::from_slice(&AUDIO_AESIV),
        );
        AudioDecrypt { cipher, reader }
    }
}

impl<T: io::Read> io::Read for AudioDecrypt<T> {
    fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
        let len = self.reader.read(output)?;

        self.cipher.apply_keystream(&mut output[..len]);

        Ok(len)
    }
}

impl<T: io::Read + io::Seek> io::Seek for AudioDecrypt<T> {
    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
        let newpos = self.reader.seek(pos)?;

        self.cipher.seek(newpos);

        Ok(newpos)
    }
}


================================================
FILE: psst-core/src/audio/mod.rs
================================================
pub mod decode;
pub mod decrypt;
pub mod normalize;
pub mod output;
pub mod probe;
pub mod resample;
pub mod source;


================================================
FILE: psst-core/src/audio/normalize.rs
================================================
use std::{
    io,
    io::{Read, Seek, SeekFrom},
};

use byteorder::{ReadBytesExt, LE};

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum NormalizationLevel {
    None,
    Track,
    Album,
}

#[derive(Clone, Copy)]
pub struct NormalizationData {
    track_gain_db: f32,
    track_peak: f32,
    album_gain_db: f32,
    album_peak: f32,
}

impl NormalizationData {
    pub fn parse(mut file: impl Read + Seek) -> io::Result<Self> {
        const NORMALIZATION_OFFSET: u64 = 144;

        file.seek(SeekFrom::Start(NORMALIZATION_OFFSET))?;

        let track_gain_db = file.read_f32::<LE>()?;
        let track_peak = file.read_f32::<LE>()?;
        let album_gain_db = file.read_f32::<LE>()?;
        let album_peak = file.read_f32::<LE>()?;

        Ok(Self {
            track_gain_db,
            track_peak,
            album_gain_db,
            album_peak,
        })
    }

    pub fn factor_for_level(&self, level: NormalizationLevel, pregain: f32) -> f32 {
        match level {
            NormalizationLevel::None => 1.0,
            NormalizationLevel::Track => Self::factor(pregain, self.track_gain_db, self.track_peak),
            NormalizationLevel::Album => Self::factor(pregain, self.album_gain_db, self.album_peak),
        }
    }

    fn factor(pregain: f32, gain: f32, peak: f32) -> f32 {
        let mut nf = f32::powf(10.0, (pregain + gain) / 20.0);
        if nf * peak > 1.0 {
            nf = 1.0 / peak;
        }
        nf
    }
}


================================================
FILE: psst-core/src/audio/output/cpal.rs
================================================
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crossbeam_channel::{bounded, Receiver, Sender};
use num_traits::Pow;

use crate::{
    actor::{Act, Actor, ActorHandle},
    audio::{
        output::{AudioOutput, AudioSink},
        source::{AudioSource, Empty},
    },
    error::Error,
};

pub struct CpalOutput {
    _handle: ActorHandle<StreamMsg>,
    sink: CpalSink,
}

impl CpalOutput {
    pub fn open() -> Result<Self, Error> {
        // Open the default output device.
        let device = cpal::default_host()
            .default_output_device()
            .ok_or(cpal::DefaultStreamConfigError::DeviceNotAvailable)?;

        if let Ok(name) = device.name() {
            log::info!("using audio device: {name:?}");
        }

        // Get the default device config, so we know what sample format and sample rate
        // the device supports.
        let supported = Self::preferred_output_config(&device)?;

        let (callback_send, callback_recv) = bounded(16);

        let handle = Stream::spawn_with_default_cap("audio_output", {
            let config = supported.config();
            // TODO: Support additional sample formats.
            move |this| Stream::open(device, config, callback_recv, this).unwrap()
        });
        let sink = CpalSink {
            channel_count: supported.channels(),
            sample_rate: supported.sample_rate(),
            stream_send: handle.sender(),
            callback_send,
        };

        Ok(Self {
            _handle: handle,
            sink,
        })
    }

    fn preferred_output_config(
        device: &cpal::Device,
    ) -> Result<cpal::SupportedStreamConfig, Error> {
        const PREFERRED_SAMPLE_FORMAT: cpal::SampleFormat = cpal::SampleFormat::F32;
        const PREFERRED_SAMPLE_RATE: cpal::SampleRate = cpal::SampleRate(44_100);
        const PREFERRED_CHANNELS: cpal::ChannelCount = 2;

        for s in device.supported_output_configs()? {
            let rates = s.min_sample_rate()..=s.max_sample_rate();
            if s.channels() == PREFERRED_CHANNELS
                && s.sample_format() == PREFERRED_SAMPLE_FORMAT
                && rates.contains(&PREFERRED_SAMPLE_RATE)
            {
                return Ok(s.with_sample_rate(PREFERRED_SAMPLE_RATE));
            }
        }

        Ok(device.default_output_config()?)
    }
}

impl AudioOutput for CpalOutput {
    type Sink = CpalSink;

    fn sink(&self) -> Self::Sink {
        self.sink.clone()
    }
}

#[derive(Clone)]
pub struct CpalSink {
    channel_count: cpal::ChannelCount,
    sample_rate: cpal::SampleRate,
    callback_send: Sender<CallbackMsg>,
    stream_send: Sender<StreamMsg>,
}

impl CpalSink {
    fn send_to_callback(&self, msg: CallbackMsg) {
        if self.callback_send.send(msg).is_err() {
            log::error!("output stream actor is dead");
        }
    }

    fn send_to_stream(&self, msg: StreamMsg) {
        if self.stream_send.send(msg).is_err() {
            log::error!("output stream actor is dead");
        }
    }
}

impl AudioSink for CpalSink {
    fn channel_count(&self) -> usize {
        self.channel_count as usize
    }

    fn sample_rate(&self) -> u32 {
        self.sample_rate.0
    }

    fn set_volume(&self, volume: f32) {
        self.send_to_callback(CallbackMsg::SetVolume(volume));
    }

    fn play(&self, source: impl AudioSource) {
        self.send_to_callback(CallbackMsg::PlaySource(Box::new(source)));
    }

    fn pause(&self) {
        self.send_to_stream(StreamMsg::Pause);
        self.send_to_callback(CallbackMsg::Pause);
    }

    fn resume(&self) {
        self.send_to_stream(StreamMsg::Resume);
        self.send_to_callback(CallbackMsg::Resume);
    }

    fn stop(&self) {
        self.play(Empty);
        self.pause();
    }

    fn close(&self) {
        self.send_to_stream(StreamMsg::Close);
    }
}

struct Stream {
    stream: cpal::Stream,
    _device: cpal::Device,
}

impl Stream {
    fn open(
        device: cpal::Device,
        config: cpal::StreamConfig,
        callback_recv: Receiver<CallbackMsg>,
        stream_send: Sender<StreamMsg>,
    ) -> Result<Self, Error> {
        let mut callback = StreamCallback {
            callback_recv,
            stream_send,
            source: Box::new(Empty),
            volume: 1.0, // We start with the full volume.
            state: CallbackState::Paused,
        };

        log::info!("opening output stream: {config:?}");
        let stream = device.build_output_stream(
            &config,
            move |output, _| {
                callback.write_samples(output);
            },
            |err| {
                log::error!("audio output error: {err}");
            },
            None,
        )?;

        Ok(Self {
            _device: device,
            stream,
        })
    }
}

impl Actor for Stream {
    type Message = StreamMsg;
    type Error = Error;

    fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Error> {
        match msg {
            StreamMsg::Pause => {
                log::debug!("pausing audio output stream");
                if let Err(err) = self.stream.pause() {
                    log::error!("failed to stop stream: {err}");
                }
                Ok(Act::Continue)
            }
            StreamMsg::Resume => {
                log::debug!("resuming audio output stream");
                if let Err(err) = self.stream.play() {
                    log::error!("failed to start stream: {err}");
                }
                Ok(Act::Continue)
            }
            StreamMsg::Close => {
                log::debug!("closing audio output stream");
                let _ = self.stream.pause();
                Ok(Act::Shutdown)
            }
        }
    }
}

enum StreamMsg {
    Pause,
    Resume,
    Close,
}

enum CallbackMsg {
    PlaySource(Box<dyn AudioSource>),
    SetVolume(f32),
    Pause,
    Resume,
}

enum CallbackState {
    Playing,
    Paused,
}

struct StreamCallback {
    #[allow(unused)]
    stream_send: Sender<StreamMsg>,
    callback_recv: Receiver<CallbackMsg>,
    source: Box<dyn AudioSource>,
    state: CallbackState,
    volume: f32,
}

impl StreamCallback {
    fn write_samples(&mut self, output: &mut [f32]) {
        // Process any pending data messages.
        while let Ok(msg) = self.callback_recv.try_recv() {
            match msg {
                CallbackMsg::PlaySource(src) => {
                    self.source = src;
                }
                CallbackMsg::SetVolume(volume) => {
                    self.volume = volume;
                }
                CallbackMsg::Pause => {
                    self.state = CallbackState::Paused;
                }
                CallbackMsg::Resume => {
                    self.state = CallbackState::Playing;
                }
            }
        }

        let written = if matches!(self.state, CallbackState::Playing) {
            // Write out as many samples as possible from the audio source to the
            // output buffer.
            let written = self.source.write(output);

            // Apply scaled global volume level.
            let scaled_volume = self.volume.pow(4);
            output[..written]
                .iter_mut()
                .for_each(|s| *s *= scaled_volume);

            written
        } else {
            0
        };

        // Mute any remaining samples.
        output[written..].iter_mut().for_each(|s| *s = 0.0);
    }
}

impl From<cpal::DefaultStreamConfigError> for Error {
    fn from(err: cpal::DefaultStreamConfigError) -> Error {
        Error::AudioOutputError(Box::new(err))
    }
}

impl From<cpal::SupportedStreamConfigsError> for Error {
    fn from(err: cpal::SupportedStreamConfigsError) -> Error {
        Error::AudioOutputError(Box::new(err))
    }
}

impl From<cpal::BuildStreamError> for Error {
    fn from(err: cpal::BuildStreamError) -> Error {
        Error::AudioOutputError(Box::new(err))
    }
}

impl From<cpal::PlayStreamError> for Error {
    fn from(err: cpal::PlayStreamError) -> Error {
        Error::AudioOutputError(Box::new(err))
    }
}

impl From<cpal::PauseStreamError> for Error {
    fn from(err: cpal::PauseStreamError) -> Error {
        Error::AudioOutputError(Box::new(err))
    }
}


================================================
FILE: psst-core/src/audio/output/cubeb.rs
================================================
use std::{env, ffi::CString, ops::Deref};

use crossbeam_channel::{bounded, Receiver, Sender};

use crate::{
    actor::{Act, Actor, ActorHandle},
    audio::{
        output::{AudioOutput, AudioSink},
        source::{AudioSource, Empty},
    },
    error::Error,
};

pub struct CubebOutput {
    #[allow(unused)]
    handle: ActorHandle<StreamMsg>,
    sink: CubebSink,
}

impl CubebOutput {
    pub fn open() -> Result<Self, Error> {
        let (callback_send, callback_recv) = bounded(16);

        let handle = Stream::spawn_with_default_cap("audio_output", {
            move |_| Stream::open(callback_recv).unwrap()
        });
        let sink = CubebSink {
            callback_send,
            stream_send: handle.sender(),
        };

        Ok(Self { handle, sink })
    }
}

impl AudioOutput for CubebOutput {
    type Sink = CubebSink;

    fn sink(&self) -> Self::Sink {
        self.sink.clone()
    }
}

type Frame = cubeb::StereoFrame<f32>;

const STREAM_CHANNELS: usize = 2;
const SAMPLE_RATE: u32 = 44_100;
const STREAM_LATENCY: u32 = 0x1000;

struct Stream {
    #[allow(unused)]
    ctx: cubeb::Context,
    stream: cubeb::Stream<Frame>,
}

impl Stream {
    fn open(callback_recv: Receiver<CallbackMsg>) -> Result<Self, Error> {
        // Call CoInitialize() before any other calls to the API.
        #[cfg(target_os = "windows")]
        unsafe {
            let _ = windows::Win32::System::Com::CoInitialize(0 as *mut _);
        };

        let backend_name = env::var("CUBEB_BACKEND")
            .ok()
            .and_then(|s| CString::new(s).ok());
        let ctx_name = CString::new("Psst").ok();
        let ctx = cubeb::Context::init(ctx_name.as_deref(), backend_name.as_deref())?;

        let mut callback = StreamCallback {
            callback_recv,
            source: Box::new(Empty),
            state: CallbackState::Paused,
            buffer: vec![0.0; 1024 * 1024],
        };

        let params = cubeb::StreamParamsBuilder::new()
            .format(cubeb::SampleFormat::Float32NE)
            .rate(SAMPLE_RATE)
            .channels(STREAM_CHANNELS as u32)
            .layout(cubeb::ChannelLayout::STEREO)
            .take();

        let mut builder = cubeb::StreamBuilder::new();
        builder
            .name("Psst")
            .default_output(&params)
            .latency(STREAM_LATENCY)
            .data_callback(move |_, output| {
                callback.write_samples(output);
                output.len() as isize
            })
            .state_callback(|state| {
                log::debug!("stream state: {:?}", state);
            });
        let stream = builder.init(&ctx)?;

        Ok(Self { ctx, stream })
    }
}

enum StreamMsg {
    Pause,
    Resume,
    Close,
    SetVolume(f32),
}

impl Actor for Stream {
    type Message = StreamMsg;
    type Error = Error;

    fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Error> {
        match msg {
            StreamMsg::Pause => {
                log::debug!("pausing audio output stream");
                if let Err(err) = self.stream.stop() {
                    log::error!("failed to stop stream: {}", err);
                }
                Ok(Act::Continue)
            }
            StreamMsg::Resume => {
                log::debug!("resuming audio output stream");
                if let Err(err) = self.stream.start() {
                    log::error!("failed to start stream: {}", err);
                }
                Ok(Act::Continue)
            }
            StreamMsg::Close => {
                log::debug!("closing audio output stream");
                let _ = self.stream.stop();
                Ok(Act::Shutdown)
            }
            StreamMsg::SetVolume(volume) => {
                log::debug!("setting volume");
                if let Err(err) = self.stream.set_volume(volume) {
                    log::error!("failed to set volume: {}", err);
                }
                Ok(Act::Continue)
            }
        }
    }
}

#[derive(Clone)]
pub struct CubebSink {
    callback_send: Sender<CallbackMsg>,
    stream_send: Sender<StreamMsg>,
}

impl AudioSink for CubebSink {
    fn channel_count(&self) -> usize {
        STREAM_CHANNELS
    }

    fn sample_rate(&self) -> u32 {
        SAMPLE_RATE
    }

    fn set_volume(&self, volume: f32) {
        self.stream_send.send(StreamMsg::SetVolume(volume)).unwrap();
    }

    fn play(&self, source: impl AudioSource) {
        self.callback_send
            .send(CallbackMsg::PlaySource(Box::new(source)))
            .unwrap()
    }

    fn pause(&self) {
        self.callback_send.send(CallbackMsg::Pause).unwrap();
        self.stream_send.send(StreamMsg::Pause).unwrap();
    }

    fn resume(&self) {
        self.callback_send.send(CallbackMsg::Resume).unwrap();
        self.stream_send.send(StreamMsg::Resume).unwrap();
    }

    fn stop(&self) {
        self.pause();
    }

    fn close(&self) {
        self.stop();
    }
}

enum CallbackMsg {
    PlaySource(Box<dyn AudioSource>),
    Pause,
    Resume,
}

enum CallbackState {
    Playing,
    Paused,
}

struct StreamCallback {
    callback_recv: Receiver<CallbackMsg>,
    source: Box<dyn AudioSource>,
    state: CallbackState,
    buffer: Vec<f32>,
}

impl StreamCallback {
    fn write_samples(&mut self, output: &mut [Frame]) {
        // Process any pending data messages.
        while let Ok(msg) = self.callback_recv.try_recv() {
            match msg {
                CallbackMsg::PlaySource(src) => {
                    self.source = src;
                }
                CallbackMsg::Pause => {
                    self.state = CallbackState::Paused;
                }
                CallbackMsg::Resume => {
                    self.state = CallbackState::Playing;
                }
            }
        }

        let written = if matches!(self.state, CallbackState::Playing) {
            // Write out as many samples as possible from the audio source to the
            // output buffer.
            let n_output_frames = output.len();
            let n_output_samples = n_output_frames * STREAM_CHANNELS;
            let n_samples = self.source.write(&mut self.buffer[..n_output_samples]);
            let mut n_frames = 0;
            for (i, o) in self.buffer[..n_samples]
                .chunks(STREAM_CHANNELS)
                .zip(output.iter_mut())
            {
                o.l = i[0];
                o.r = i[1];
                n_frames += 1;
            }
            n_frames
        } else {
            0
        };

        // Mute any remaining samples.
        output[written..].iter_mut().for_each(|s| {
            s.l = 0.0;
            s.r = 0.0;
        });
    }
}

unsafe impl Sync for StreamCallback {}

impl From<cubeb::Error> for Error {
    fn from(err: cubeb::Error) -> Self {
        Error::AudioOutputError(Box::new(err))
    }
}


================================================
FILE: psst-core/src/audio/output/mod.rs
================================================
use crate::audio::source::AudioSource;

#[cfg(feature = "cpal")]
pub mod cpal;
#[cfg(feature = "cubeb")]
pub mod cubeb;

#[cfg(feature = "cubeb")]
pub type DefaultAudioOutput = cubeb::CubebOutput;
#[cfg(feature = "cpal")]
pub type DefaultAudioOutput = cpal::CpalOutput;

pub type DefaultAudioSink = <DefaultAudioOutput as AudioOutput>::Sink;

pub trait AudioOutput {
    type Sink: AudioSink;

    fn sink(&self) -> Self::Sink;
}

pub trait AudioSink {
    fn channel_count(&self) -> usize;
    fn sample_rate(&self) -> u32;
    fn set_volume(&self, volume: f32);
    fn play(&self, source: impl AudioSource);
    fn pause(&self);
    fn resume(&self);
    fn stop(&self);
    fn close(&self);
}


================================================
FILE: psst-core/src/audio/probe.rs
================================================
use std::fs::File;
use std::path::PathBuf;
use std::time::Duration;

use symphonia::core::codecs::CodecType;
use symphonia::core::formats::FormatOptions;
use symphonia::core::io::{MediaSourceStream, MediaSourceStreamOptions};
use symphonia::core::meta::MetadataOptions;
use symphonia::core::probe::{Hint, Probe};
use symphonia::default::formats::{MpaReader, OggReader};

use crate::error::Error;

pub struct TrackProbe {
    pub codec: CodecType,
    pub duration: Option<Duration>,
}

macro_rules! probe_err {
    ($message:tt) => {
        // This is necessary to work around the fact that the two impls for From<&str> are:
        //   Box<dyn std::error::Error>
        //   Box<dyn std::error::Error + Send + Sync>
        // And the trait bound on our error is:
        //   Box<dyn std::error::Error + Send>
        // Normally we could just do `$message.into()`, but no impl exists for exactly
        // `Error + Send`, so we have to be explicit about which we want to use.
        Error::AudioProbeError(Box::<dyn std::error::Error + Send + Sync>::from($message))
    };
}

impl TrackProbe {
    pub fn new(path: &PathBuf) -> Result<Self, Error> {
        // Register all supported file formats for detection.
        let mut probe = Probe::default();
        probe.register_all::<MpaReader>();
        probe.register_all::<OggReader>();

        let mut hint = Hint::new();
        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
            hint.with_extension(ext);
        }

        let file = File::open(path)?;
        let mss_opts = MediaSourceStreamOptions::default();
        let mss = MediaSourceStream::new(Box::new(file), mss_opts);

        let fmt_opts = FormatOptions::default();
        let meta_opts = MetadataOptions::default();
        let probe_result = probe
            .format(&hint, mss, &fmt_opts, &meta_opts)
            .map_err(|_| probe_err!("failed to probe file"))?;
        let track = probe_result
            .format
            .default_track()
            .ok_or_else(|| probe_err!("file contained no tracks"))?;
        let params = &track.codec_params;

        let duration =
            if let (Some(time_base), Some(n_frames)) = (params.time_base, params.n_frames) {
                let time = time_base.calc_time(n_frames);
                let secs = time.seconds;
                let ms = (time.frac * 1_000.0).round() as u64;
                Some(Duration::from_millis(secs * 1_000 + ms))
            } else {
                None
            };

        Ok(Self {
            codec: params.codec,
            duration,
        })
    }
}


================================================
FILE: psst-core/src/audio/resample.rs
================================================
use crate::error::Error;

#[derive(Copy, Clone)]
pub enum ResamplingQuality {
    SincBestQuality = libsamplerate::SRC_SINC_BEST_QUALITY as isize,
    SincMediumQuality = libsamplerate::SRC_SINC_MEDIUM_QUALITY as isize,
    SincFastest = libsamplerate::SRC_SINC_FASTEST as isize,
    ZeroOrderHold = libsamplerate::SRC_ZERO_ORDER_HOLD as isize,
    Linear = libsamplerate::SRC_LINEAR as isize,
}

#[derive(Copy, Clone)]
pub struct ResamplingSpec {
    pub input_rate: u32,
    pub output_rate: u32,
    pub channels: usize,
}

impl ResamplingSpec {
    pub fn output_size(&self, input_size: usize) -> usize {
        (self.output_rate as f64 / self.input_rate as f64 * input_size as f64) as usize
    }

    pub fn input_size(&self, output_size: usize) -> usize {
        (self.input_rate as f64 / self.output_rate as f64 * output_size as f64) as usize
    }

    pub fn ratio(&self) -> f64 {
        self.output_rate as f64 / self.input_rate as f64
    }
}

pub struct AudioResampler {
    pub spec: ResamplingSpec,
    state: *mut libsamplerate::SRC_STATE,
}

impl AudioResampler {
    pub fn new(quality: ResamplingQuality, spec: ResamplingSpec) -> Result<Self, Error> {
        let mut error_int = 0i32;
        let state = unsafe {
            libsamplerate::src_new(
                quality as i32,
                spec.channels as i32,
                &mut error_int as *mut i32,
            )
        };
        if error_int != 0 {
            Err(Error::ResamplingError(error_int))
        } else {
            Ok(Self { state, spec })
        }
    }

    pub fn process(&mut self, input: &[f32], output: &mut [f32]) -> Result<(usize, usize), Error> {
        if self.spec.input_rate == self.spec.output_rate {
            // Bypass conversion completely in case the sample rates are equal.
            let output = &mut output[..input.len()];
            output.copy_from_slice(input);
            return Ok((input.len(), output.len()));
        }
        let mut src = libsamplerate::SRC_DATA {
            data_in: input.as_ptr(),
            data_out: output.as_mut_ptr(),
            input_frames: (input.len() / self.spec.channels) as _,
            output_frames: (output.len() / self.spec.channels) as _,
            src_ratio: self.spec.ratio(),
            end_of_input: 0, // TODO: Use this.
            input_frames_used: 0,
            output_frames_gen: 0,
        };
        let error_int = unsafe { libsamplerate::src_process(self.state, &mut src as *mut _) };
        if error_int != 0 {
            Err(Error::ResamplingError(error_int))
        } else {
            let processed_len = src.input_frames_used as usize * self.spec.channels;
            let output_len = src.output_frames_gen as usize * self.spec.channels;
            Ok((processed_len, output_len))
        }
    }
}

impl Drop for AudioResampler {
    fn drop(&mut self) {
        unsafe { libsamplerate::src_delete(self.state) };
    }
}

unsafe impl Send for AudioResampler {}


================================================
FILE: psst-core/src/audio/source.rs
================================================
use crate::audio::resample::ResamplingSpec;

use super::resample::{AudioResampler, ResamplingQuality};

/// Types that can produce audio samples in `f32` format. `Send`able across
/// threads.
pub trait AudioSource: Send + 'static {
    /// Write at most of `output.len()` samples into the `output`. Returns the
    /// number of written samples. Should take care to always output a full
    /// frame, and should _never_ block.
    fn write(&mut self, output: &mut [f32]) -> usize;
    fn channel_count(&self) -> usize;
    fn sample_rate(&self) -> u32;
}

/// Empty audio source. Does not produce any samples.
pub struct Empty;

impl AudioSource for Empty {
    fn write(&mut self, _output: &mut [f32]) -> usize {
        0
    }

    fn channel_count(&self) -> usize {
        0
    }

    fn sample_rate(&self) -> u32 {
        0
    }
}

pub struct StereoMappedSource<S> {
    source: S,
    input_channels: usize,
    output_channels: usize,
    buffer: Vec<f32>,
}

impl<S> StereoMappedSource<S>
where
    S: AudioSource,
{
    pub fn new(source: S, output_channels: usize) -> Self {
        const BUFFER_SIZE: usize = 16 * 1024;

        let input_channels = source.channel_count();
        Self {
            source,
            input_channels,
            output_channels,
            buffer: vec![0.0; BUFFER_SIZE],
        }
    }
}

impl<S> AudioSource for StereoMappedSource<S>
where
    S: AudioSource,
{
    fn write(&mut self, output: &mut [f32]) -> usize {
        let input_max = (output.len() / self.output_channels) * self.input_channels;
        let buffer_max = input_max.min(self.buffer.len());
        let written = self.source.write(&mut self.buffer[..buffer_max]);
        let input = &self.buffer[..written];
        let input_frames = input.chunks_exact(self.input_channels);
        let output_frames = output.chunks_exact_mut(self.output_channels);
        for (i, o) in input_frames.zip(output_frames) {
            o[0] = i[0];
            o[1] = i[1];
            // Assume the rest is is implicitly silence.
        }
        output.len()
    }

    fn channel_count(&self) -> usize {
        self.output_channels
    }

    fn sample_rate(&self) -> u32 {
        self.source.sample_rate()
    }
}

pub struct ResampledSource<S> {
    source: S,
    resampler: AudioResampler,
    inp: Buf,
    out: Buf,
}

impl<S> ResampledSource<S> {
    pub fn new(source: S, output_sample_rate: u32, quality: ResamplingQuality) -> Self
    where
        S: AudioSource,
    {
        const BUFFER_SIZE: usize = 1024;

        let spec = ResamplingSpec {
            channels: source.channel_count(),
            input_rate: source.sample_rate(),
            output_rate: output_sample_rate,
        };
        let inp_buf = vec![0.0; BUFFER_SIZE];
        let out_buf = vec![0.0; spec.output_size(BUFFER_SIZE)];
        Self {
            resampler: AudioResampler::new(quality, spec).unwrap(),
            source,
            inp: Buf {
                buf: inp_buf,
                start: 0,
                end: 0,
            },
            out: Buf {
                buf: out_buf,
                start: 0,
                end: 0,
            },
        }
    }
}

impl<S> AudioSource for ResampledSource<S>
where
    S: AudioSource,
{
    fn write(&mut self, output: &mut [f32]) -> usize {
        let mut total = 0;

        while total < output.len() {
            if self.out.is_empty() {
                if self.inp.is_empty() {
                    let n = self.source.write(&mut self.inp.buf);
                    self.inp.buf[n..].iter_mut().for_each(|s| *s = 0.0);
                    self.inp.start = 0;
                    self.inp.end = self.inp.buf.len();
                }
                let (inp_consumed, out_written) = self
                    .resampler
                    .process(&self.inp.buf[self.inp.start..], &mut self.out.buf)
                    .unwrap();
                self.inp.start += inp_consumed;
                self.out.start = 0;
                self.out.end = out_written;
            }
            let source = self.out.get();
            let target = &mut output[total..];
            let to_write = self.out.len().min(target.len());
            target[..to_write].copy_from_slice(&source[..to_write]);
            total += to_write;
            self.out.start += to_write;
        }

        total
    }

    fn channel_count(&self) -> usize {
        self.resampler.spec.channels
    }

    fn sample_rate(&self) -> u32 {
        self.resampler.spec.output_rate
    }
}

struct Buf {
    buf: Vec<f32>,
    start: usize,
    end: usize,
}

impl Buf {
    fn get(&self) -> &[f32] {
        &self.buf[self.start..self.end]
    }

    fn len(&self) -> usize {
        self.end - self.start
    }

    fn is_empty(&self) -> bool {
        self.start >= self.end
    }
}


================================================
FILE: psst-core/src/cache.rs
================================================
use std::{
    fs, io,
    path::{Path, PathBuf},
    sync::Arc,
};

use crate::{
    audio::decrypt::AudioKey,
    error::Error,
    item_id::{FileId, ItemId},
};

use librespot_protocol::metadata::{Episode, Track};
use protobuf::Message;

pub type CacheHandle = Arc<Cache>;

#[derive(Debug)]
pub struct Cache {
    base: PathBuf,
}

fn create_cache_dirs(base: &Path) -> io::Result<()> {
    mkdir_if_not_exists(base)?;
    mkdir_if_not_exists(&base.join("track"))?;
    mkdir_if_not_exists(&base.join("episode"))?;
    mkdir_if_not_exists(&base.join("audio"))?;
    mkdir_if_not_exists(&base.join("key"))?;
    Ok(())
}

impl Cache {
    pub fn new(base: PathBuf) -> Result<CacheHandle, Error> {
        log::info!("using cache: {base:?}");

        // Create the cache structure.
        create_cache_dirs(&base)?;

        let cache = Self { base };
        Ok(Arc::new(cache))
    }

    pub fn clear(&self) -> io::Result<()> {
        log::info!("clearing cache: {:?}", self.base);

        for entry in fs::read_dir(&self.base)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                fs::remove_dir_all(path)?;
            } else {
                fs::remove_file(path)?;
            }
        }

        // Re-create the essential directory structure.
        create_cache_dirs(&self.base)
    }
}

// Cache of `Track` protobuf structures.
impl Cache {
    pub fn get_track(&self, item_id: ItemId) -> Option<Track> {
        let buf = fs::read(self.track_path(item_id)).ok()?;
        Track::parse_from_bytes(&buf).ok()
    }

    pub fn save_track(&self, item_id: ItemId, track: &Track) -> Result<(), Error> {
        log::debug!("saving track to cache: {item_id:?}");
        fs::write(self.track_path(item_id), track.write_to_bytes()?)?;
        Ok(())
    }

    fn track_path(&self, item_id: ItemId) -> PathBuf {
        self.base.join("track").join(item_id.to_base62())
    }
}

// Cache of `Episode` protobuf structures.
impl Cache {
    pub fn get_episode(&self, item_id: ItemId) -> Option<Episode> {
        let buf = fs::read(self.episode_path(item_id)).ok()?;
        Episode::parse_from_bytes(&buf).ok()
    }

    pub fn save_episode(&self, item_id: ItemId, episode: &Episode) -> Result<(), Error> {
        log::debug!("saving episode to cache: {item_id:?}");
        fs::write(self.episode_path(item_id), episode.write_to_bytes()?)?;
        Ok(())
    }

    fn episode_path(&self, item_id: ItemId) -> PathBuf {
        self.base.join("episode").join(item_id.to_base62())
    }
}

// Cache of `AudioKey`s.
impl Cache {
    pub fn get_audio_key(&self, item_id: ItemId, file_id: FileId) -> Option<AudioKey> {
        let buf = fs::read(self.audio_key_path(item_id, file_id)).ok()?;
        AudioKey::from_raw(&buf)
    }

    pub fn save_audio_key(
        &self,
        item_id: ItemId,
        file_id: FileId,
        key: &AudioKey,
    ) -> Result<(), Error> {
        log::debug!("saving audio key to cache: {item_id:?}:{file_id:?}");
        fs::write(self.audio_key_path(item_id, file_id), key.0)?;
        Ok(())
    }

    fn audio_key_path(&self, item_id: ItemId, file_id: FileId) -> PathBuf {
        let mut key_id = String::new();
        key_id += &item_id.to_base62()[..16];
        key_id += &file_id.to_base16()[..16];
        self.base.join("key").join(key_id)
    }
}

// Cache of encrypted audio file content.
impl Cache {
    pub fn audio_file_path(&self, file_id: FileId) -> PathBuf {
        self.base.join("audio").join(file_id.to_base16())
    }

    pub fn save_audio_file(&self, file_id: FileId, from_path: PathBuf) -> Result<(), Error> {
        log::debug!("saving audio file to cache: {file_id:?}");
        fs::copy(from_path, self.audio_file_path(file_id))?;
        Ok(())
    }
}

// Cache of user country code.
impl Cache {
    pub fn get_country_code(&self) -> Option<String> {
        fs::read_to_string(self.country_code_path()).ok()
    }

    pub fn save_country_code(&self, country_code: &str) -> Result<(), Error> {
        fs::write(self.country_code_path(), country_code)?;
        Ok(())
    }

    fn country_code_path(&self) -> PathBuf {
        self.base.join("country_code")
    }
}

pub fn mkdir_if_not_exists(path: &Path) -> io::Result<()> {
    fs::create_dir(path).or_else(|err| {
        if err.kind() == io::ErrorKind::AlreadyExists {
            Ok(())
        } else {
            Err(err)
        }
    })
}


================================================
FILE: psst-core/src/cdn.rs
================================================
use std::{
    io::Read,
    sync::Arc,
    time::{Duration, Instant},
};

use serde::Deserialize;

use crate::{
    error::Error,
    item_id::FileId,
    session::{SessionService},
    util::default_ureq_agent_builder,
};
use crate::session::login5::Login5;

pub type CdnHandle = Arc<Cdn>;

pub struct Cdn {
    session: SessionService,
    agent: ureq::Agent,
    login5: Login5,
}

impl Cdn {
    pub fn new(session: SessionService, proxy_url: Option<&str>) -> Result<CdnHandle, Error> {
        let agent = default_ureq_agent_builder(proxy_url).build();
        Ok(Arc::new(Self {
            session,
            agent: agent.into(),
            login5: Login5::new(None, proxy_url),
        }))
    }

    pub fn resolve_audio_file_url(&self, id: FileId) -> Result<CdnUrl, Error> {
        let locations_uri = format!(
            "https://api.spotify.com/v1/storage-resolve/files/audio/interactive/{}",
            id.to_base16()
        );
        let access_token = self.login5.get_access_token(&self.session)?;
        let response = self
            .agent
            .get(&locations_uri)
            .query("version", "10000000")
            .query("product", "9")
            .query("platform", "39")
            .query("alt", "json")
            .header("Authorization", &format!("Bearer {}", access_token.access_token))
            .call()?;

        #[derive(Deserialize)]
        struct AudioFileLocations {
            cdnurl: Vec<String>,
        }

        // Deserialize the response and pick a file URL from the returned CDN list.
        let locations: AudioFileLocations = response.into_body().read_json()?;
        let file_uri = locations
            .cdnurl
            .into_iter()
            // TODO:
            //  Now, we always pick the first URL in the list, figure out a better strategy.
            //  Choosing by random seems wrong.
            .next()
            // TODO: Avoid panicking here.
            .expect("No file URI found");

        let uri = CdnUrl::new(file_uri);
        Ok(uri)
    }

    pub fn fetch_file_range(
        &self,
        uri: &str,
        offset: u64,
        length: u64,
    ) -> Result<(u64, impl Read), Error> {
        let response = self
            .agent
            .get(uri)
            .header("Range", &range_header(offset, length))
            .call()?;
        let total_length = parse_total_content_length(&response);
        let data_reader = response.into_body().into_reader();
        Ok((total_length, data_reader))
    }
}

#[derive(Clone)]
pub struct CdnUrl {
    pub url: String,
    pub expires: Instant,
}

impl CdnUrl {
    // In case we fail to parse the expiration time from URL, this default is used.
    const DEFAULT_EXPIRATION: Duration = Duration::from_secs(60 * 30);

    // Consider URL expired even before the official expiration time.
    const EXPIRATION_TIME_THRESHOLD: Duration = Duration::from_secs(5);

    fn new(url: String) -> Self {
        let expires_in = parse_expiration(&url).unwrap_or_else(|| {
            log::warn!("failed to parse expiration time from URL {:?}", &url);
            Self::DEFAULT_EXPIRATION
        });
        let expires = Instant::now() + expires_in;
        Self { url, expires }
    }

    pub fn is_expired(&self) -> bool {
        self.expires.saturating_duration_since(Instant::now()) < Self::EXPIRATION_TIME_THRESHOLD
    }
}

impl From<ureq::Error> for Error {
    fn from(err: ureq::Error) -> Self {
        Error::AudioFetchingError(Box::new(err))
    }
}

/// Constructs a Range header value for given offset and length.
fn range_header(offfset: u64, length: u64) -> String {
    let last_byte = offfset + length - 1; // Offset of the last byte of the range is inclusive.
    format!("bytes={offfset}-{last_byte}")
}

/// Parses a total content length from a Content-Range response header.
///
/// For example, returns 146515 for a response with header
/// "Content-Range: bytes 0-1023/146515".
fn parse_total_content_length(response: &ureq::http::response::Response<ureq::Body>) -> u64 {
    response
        .headers()
        .get("Content-Range")
        .expect("Content-Range header not found")
        .to_str()
        .expect("Failed to parse Content-Range Header")
        .split('/')
        .next_back()
        .expect("Failed to parse Content-Range Header")
        .parse()
        .expect("Failed to parse Content-Range Header")
}

/// Parses an expiration of an audio file URL.
fn parse_expiration(url: &str) -> Option<Duration> {
    let token_exp = url.split("__token__=exp=").nth(1);
    let expires_millis = if let Some(token_exp) = token_exp {
        // Parse from the expiration token param
        token_exp.split('~').next()?
    } else if let Some(verify_exp) = url.split("verify=").nth(1) {
        // Parse from verify parameter (new spotifycdn.com format)
        verify_exp.split('-').next()?
    } else {
        // Parse from the first param
        let first_param = url.split('?').nth(1)?;
        first_param.split('_').next()?
    };
    let expires_millis = expires_millis.parse().ok()?;
    let expires = Duration::from_millis(expires_millis);
    Some(expires)
}


================================================
FILE: psst-core/src/connection/diffie_hellman.rs
================================================
use num_bigint::{BigUint, ToBigUint};
use rand::Rng;

pub struct DHLocalKeys {
    private_key: BigUint,
    public_key: BigUint,
}

impl DHLocalKeys {
    pub fn random() -> DHLocalKeys {
        let private_key = rand::rng().random::<u32>().to_biguint().unwrap();
        let public_key = dh_generator().modpow(&private_key, &dh_prime());
        DHLocalKeys {
            private_key,
            public_key,
        }
    }

    pub fn public_key(&self) -> Vec<u8> {
        self.public_key.to_bytes_be()
    }

    pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
        let remote_key = BigUint::from_bytes_be(remote_key);
        let shared_key = remote_key.modpow(&self.private_key, &dh_prime());
        shared_key.to_bytes_be()
    }
}

fn dh_generator() -> BigUint {
    BigUint::from(0x2_u64)
}

fn dh_prime() -> BigUint {
    BigUint::from_bytes_be(&[
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
        0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
        0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
        0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
        0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
        0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
    ])
}


================================================
FILE: psst-core/src/connection/mod.rs
================================================
pub mod diffie_hellman;
pub mod shannon_codec;

use std::{
    convert::TryInto,
    io,
    io::{Read, Write},
    net::{TcpStream, ToSocketAddrs},
};

use byteorder::{ReadBytesExt, BE};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use socks::Socks5Stream;
use url::Url;

use crate::{
    connection::{
        diffie_hellman::DHLocalKeys,
        shannon_codec::{ShannonDecoder, ShannonEncoder, ShannonMsg},
    },
    error::Error,
    util::{default_ureq_agent_builder, NET_CONNECT_TIMEOUT, NET_IO_TIMEOUT},
};

use librespot_protocol::authentication::AuthenticationType;
use protobuf::{Enum, Message, MessageField, SpecialFields};

// Device ID used for authentication message.
const DEVICE_ID: &str = "Psst";

// URI of access-point resolve endpoint.
const AP_RESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com";

// Access-point used in case the resolving fails.
const AP_FALLBACK: &str = "ap.spotify.com:443";

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(from = "SerializedCredentials")]
#[serde(into = "SerializedCredentials")]
pub struct Credentials {
    pub username: Option<String>,
    pub auth_data: Vec<u8>,
    pub auth_type: AuthenticationType,
}

impl Credentials {
    pub fn from_username_and_password(username: String, password: String) -> Self {
        Self {
            username: Some(username),
            auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,
            auth_data: password.into_bytes(),
        }
    }

    pub fn from_access_token(token: String) -> Self {
        Self {
            username: None,
            auth_type: AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN,
            auth_data: token.into_bytes(),
        }
    }
}

#[derive(Serialize, Deserialize)]
struct SerializedCredentials {
    username: String,
    auth_data: String,
    auth_type: i32,
}

impl From<SerializedCredentials> for Credentials {
    fn from(value: SerializedCredentials) -> Self {
        Self {
            username: Some(value.username),
            auth_data: value.auth_data.into_bytes(),
            auth_type: AuthenticationType::from_i32(value.auth_type).unwrap_or_default(),
        }
    }
}

impl From<Credentials> for SerializedCredentials {
    fn from(value: Credentials) -> Self {
        Self {
            username: value.username.unwrap_or_default(),
            auth_data: String::from_utf8(value.auth_data)
                .expect("Invalid UTF-8 in serialized credentials"),
            auth_type: value.auth_type as _,
        }
    }
}

pub struct Transport {
    pub stream: TcpStream,
    pub encoder: ShannonEncoder<TcpStream>,
    pub decoder: ShannonDecoder<TcpStream>,
}

impl Transport {
    pub fn resolve_ap_with_fallback(proxy_url: Option<&str>) -> Vec<String> {
        match Self::resolve_ap(proxy_url) {
            Ok(ap_list) => {
                log::info!("successfully resolved {} access points", ap_list.len());
                ap_list
            }
            Err(err) => {
                log::error!("error while resolving APs, using fallback: {err:?}");
                vec![AP_FALLBACK.into()]
            }
        }
    }

    pub fn resolve_ap(proxy_url: Option<&str>) -> Result<Vec<String>, Error> {
        #[derive(Clone, Debug, Deserialize)]
        struct APResolveData {
            ap_list: Vec<String>,
        }

        let agent: ureq::Agent = default_ureq_agent_builder(proxy_url).build().into();
        log::info!("requesting AP list from {AP_RESOLVE_ENDPOINT}");
        let data: APResolveData = agent
            .get(AP_RESOLVE_ENDPOINT)
            .call()?
            .into_body()
            .read_json()?;
        if data.ap_list.is_empty() {
            log::warn!("received empty AP list from server");
            Err(Error::UnexpectedResponse)
        } else {
            log::info!("received {} APs from server", data.ap_list.len());
            Ok(data.ap_list)
        }
    }

    pub fn connect(ap_list: &[String], proxy_url: Option<&str>) -> Result<Self, Error> {
        log::info!(
            "attempting to connect using {} access points",
            ap_list.len()
        );
        for (index, ap) in ap_list.iter().enumerate() {
            log::info!("trying AP {} of {}: {}", index + 1, ap_list.len(), ap);
            let stream = if let Some(url) = proxy_url {
                match Self::stream_through_proxy(ap, url) {
                    Ok(s) => s,
                    Err(e) => {
                        log::warn!("failed to connect to AP {ap} through proxy: {e:?}");
                        continue;
                    }
                }
            } else {
                match Self::stream_without_proxy(ap) {
                    Ok(s) => s,
                    Err(e) => {
                        log::warn!("failed to connect to AP {ap} without proxy: {e:?}");
                        continue;
                    }
                }
            };
            if let Err(err) = stream.set_write_timeout(Some(NET_IO_TIMEOUT)) {
                log::warn!("failed to set TCP write timeout: {err:?}");
            }
            log::info!("successfully connected to AP: {ap}");
            return Self::exchange_keys(stream);
        }
        log::error!("failed to connect to any access point");
        Err(Error::ConnectionFailed)
    }

    fn stream_without_proxy(ap: &str) -> Result<TcpStream, io::Error> {
        let mut last_err = None;
        for addr in ap.to_socket_addrs()? {
            match TcpStream::connect_timeout(&addr, NET_CONNECT_TIMEOUT) {
                Ok(stream) => {
                    return Ok(stream);
                }
                Err(err) => {
                    last_err.replace(err);
                }
            }
        }
        Err(last_err.unwrap_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidInput,
                "could not resolve to any addresses",
            )
        }))
    }

    fn stream_through_proxy(ap: &str, url: &str) -> Result<TcpStream, Error> {
        match Url::parse(url) {
            Ok(url) if url.scheme() == "socks" || url.scheme() == "socks5" => {
                // Currently we only support SOCKS5 proxies.
                Self::stream_through_socks5_proxy(ap, &url)
            }
            _ => {
                // Proxy URL failed to parse or has unsupported scheme.
                Err(Error::ProxyUrlInvalid)
            }
        }
    }

    fn stream_through_socks5_proxy(ap: &str, url: &Url) -> Result<TcpStream, Error> {
        let addrs = url.socket_addrs(|| None)?;
        let username = url.username();
        let password = url.password().unwrap_or("");
        // TODO: `socks` crate does not support connection timeouts.
        let proxy = if username.is_empty() {
            Socks5Stream::connect(&addrs[..], ap)?
        } else {
            Socks5Stream::connect_with_password(&addrs[..], ap, username, password)?
        };
        Ok(proxy.into_inner())
    }

    pub fn exchange_keys(mut stream: TcpStream) -> Result<Self, Error> {
        use librespot_protocol::keyexchange::APResponseMessage;

        let local_keys = DHLocalKeys::random();

        // Start by sending the hello message with our public key and nonce.
        log::trace!("sending client hello");
        let client_nonce: [u8; 16] = rand::random();
        let hello = client_hello(local_keys.public_key(), client_nonce.into());
        let hello_packet = make_packet(&[0, 4], &hello);
        stream.write_all(&hello_packet)?;
        log::trace!("sent client hello");

        // Wait for the response packet with the remote public key.  Note that we are
        // keeping both the hello packet and the response packet for later (they get
        // hashed together with the shared secret to make a key pair).
        log::trace!("waiting for AP response");
        let apresp_packet = read_packet(&mut stream)?;
        let apresp = APResponseMessage::parse_from_bytes(&apresp_packet[4..])?;
        log::trace!("received AP response");

        // Compute the challenge response and the sending/receiving keys.
        let remote_key = apresp
            .challenge
            .login_crypto_challenge
            .diffie_hellman
            .gs
            .as_ref()
            .expect("Missing data");

        let (challenge, send_key, recv_key) = compute_keys(
            &local_keys.shared_secret(remote_key),
            &hello_packet,
            &apresp_packet,
        );

        // Respond with the computed HMAC and finish the handshake.
        log::trace!("sending client response");
        let response = client_response_plaintext(challenge);
        let response_packet = make_packet(&[], &response);
        stream.write_all(&response_packet)?;
        log::trace!("sent client response");

        // Use the derived keys to make a codec, wrapping the TCP stream.
        let encoder = ShannonEncoder::new(stream.try_clone()?, &send_key);
        let decoder = ShannonDecoder::new(stream.try_clone()?, &recv_key);

        Ok(Self {
            stream,
            encoder,
            decoder,
        })
    }

    pub fn authenticate(&mut self, credentials: Credentials) -> Result<Credentials, Error> {
        use librespot_protocol::{authentication::APWelcome, keyexchange::APLoginFailed};

        // Send a login request with the client credentials.
        let request = client_response_encrypted(credentials);
        self.encoder.encode(request)?;

        // Expect an immediate response with the authentication result.
        let response = self.decoder.decode()?;

        match response.cmd {
            ShannonMsg::AP_WELCOME => {
                let welcome_data =
                    APWelcome::parse_from_bytes(&response.payload).expect("Missing data");

                Ok(Credentials {
                    username: Some(welcome_data.canonical_username().to_string()),
                    auth_data: welcome_data.reusable_auth_credentials().to_vec(),
                    auth_type: welcome_data.reusable_auth_credentials_type(),
                })
            }
            ShannonMsg::AUTH_FAILURE => {
                let error_data =
                    APLoginFailed::parse_from_bytes(&response.payload).expect("Missing data");
                Err(Error::AuthFailed {
                    code: error_data.error_code() as _,
                })
            }
            _ => {
                unreachable!("unexpected message");
            }
        }
    }
}

fn read_packet(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
    let size = stream.read_u32::<BE>()?;
    let mut buf = vec![0_u8; size as usize];
    let (size_buf, data_buf) = buf.split_at_mut(4);
    size_buf.copy_from_slice(&size.to_be_bytes());
    stream.read_exact(data_buf)?;
    Ok(buf)
}

fn make_packet(prefix: &[u8], data: &[u8]) -> Vec<u8> {
    let size = prefix.len() + 4 + data.len();
    let mut buf = Vec::with_capacity(size);
    let size_u32: u32 = size.try_into().unwrap();
    buf.extend(prefix);
    buf.extend(size_u32.to_be_bytes());
    buf.extend(data);
    buf
}

fn client_hello(public_key: Vec<u8>, nonce: Vec<u8>) -> Vec<u8> {
    use librespot_protocol::keyexchange::*;

    let hello = ClientHello {
        build_info: MessageField::some(BuildInfo {
            platform: Some(Platform::PLATFORM_LINUX_X86.into()),
            product: Some(Product::PRODUCT_PARTNER.into()),
            product_flags: vec![],
            version: Some(109_800_078),
            special_fields: SpecialFields::new(),
        }),
        cryptosuites_supported: vec![Cryptosuite::CRYPTO_SUITE_SHANNON.into()],
        fingerprints_supported: vec![],
        powschemes_supported: vec![],
        login_crypto_hello: MessageField::some(LoginCryptoHelloUnion {
            diffie_hellman: MessageField::some(LoginCryptoDiffieHellmanHello {
                gc: Some(public_key),
                server_keys_known: Some(1),
                special_fields: SpecialFields::new(),
            }),
            special_fields: SpecialFields::new(),
        }),
        client_nonce: Some(nonce),
        padding: Some(vec![0x1e]),
        feature_set: None.into(),
        special_fields: SpecialFields::new(),
    };

    hello
        .write_to_bytes()
        .expect("Failed to serialize client hello")
}

fn client_response_plaintext(challenge: Vec<u8>) -> Vec<u8> {
    use librespot_protocol::keyexchange::*;

    let response = ClientResponsePlaintext {
        login_crypto_response: MessageField::some(LoginCryptoResponseUnion {
            diffie_hellman: MessageField::some(LoginCryptoDiffieHellmanResponse {
                hmac: Some(challenge),
                special_fields: SpecialFields::new(),
            }),
            special_fields: SpecialFields::new(),
        }),
        pow_response: MessageField::some(PoWResponseUnion::default()),
        crypto_response: MessageField::some(CryptoResponseUnion::default()),
        special_fields: SpecialFields::new(),
    };

    response
        .write_to_bytes()
        .expect("Failed to serialize client response")
}

fn compute_keys(
    shared_secret: &[u8],
    hello_packet: &[u8],
    apresp_packet: &[u8],
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
    let mut data = Vec::with_capacity(5 * 20);
    for i in 1..6 {
        let mut mac: Hmac<Sha1> =
            Hmac::new_from_slice(shared_secret).expect("HMAC can take key of any size");
        mac.update(hello_packet);
        mac.update(apresp_packet);
        mac.update(&[i]);
        data.extend(mac.finalize().into_bytes());
    }
    let mut mac: Hmac<Sha1> =
        Hmac::new_from_slice(&data[..20]).expect("HMAC can take key of any size");
    mac.update(hello_packet);
    mac.update(apresp_packet);
    let digest = mac.finalize().into_bytes();

    (
        (*digest).to_vec(),
        data[20..52].to_vec(),
        data[52..84].to_vec(),
    )
}

fn client_response_encrypted(credentials: Credentials) -> ShannonMsg {
    use librespot_protocol::authentication::{
        ClientResponseEncrypted, LoginCredentials, SystemInfo, Os, CpuFamily
    };

    let response = ClientResponseEncrypted {
        login_credentials: MessageField::some(LoginCredentials {
            username: credentials.username,
            auth_data: Some(credentials.auth_data),
            typ: Some(credentials.auth_type.into()),
            special_fields: SpecialFields::new(),
        }),
        system_info: MessageField::some(SystemInfo {
            device_id: Some(DEVICE_ID.to_string()),
            system_information_string: Some("librespot_but_actually_psst".to_string()),
            os: Some(Os::default().into()),
            cpu_family: Some(CpuFamily::default().into()),
            ..SystemInfo::default()
        }),
        ..ClientResponseEncrypted::default()
    };

    let buf = response.write_to_bytes().expect("Failed to serialize");
    ShannonMsg::new(ShannonMsg::LOGIN, buf)
}


================================================
FILE: psst-core/src/connection/shannon_codec.rs
================================================
use std::{convert::TryInto, io};

use shannon::Shannon;

#[derive(Debug)]
pub struct ShannonMsg {
    pub cmd: u8,
    pub payload: Vec<u8>,
}

impl ShannonMsg {
    pub const SECRET_BLOCK: u8 = 0x02;
    pub const PING: u8 = 0x04;
    pub const STREAM_CHUNK: u8 = 0x08;
    pub const STREAM_CHUNK_RES: u8 = 0x09;
    pub const CHANNEL_ERROR: u8 = 0x0a;
    pub const CHANNEL_ABORT: u8 = 0x0b;
    pub const REQUEST_KEY: u8 = 0x0c;
    pub const AES_KEY: u8 = 0x0d;
    pub const AES_KEY_ERROR: u8 = 0x0e;
    pub const IMAGE: u8 = 0x19;
    pub const COUNTRY_CODE: u8 = 0x1b;
    pub const PONG: u8 = 0x49;
    pub const PONG_ACK: u8 = 0x4a;
    pub const PAUSE: u8 = 0x4b;
    pub const PRODUCT_INFO: u8 = 0x50;
    pub const LEGACY_WELCOME: u8 = 0x69;
    pub const LICENSE_VERSION: u8 = 0x76;
    pub const LOGIN: u8 = 0xab;
    pub const AP_WELCOME: u8 = 0xac;
    pub const AUTH_FAILURE: u8 = 0xad;
    pub const MERCURY_REQ: u8 = 0xb2;
    pub const MERCURY_SUB: u8 = 0xb3;
    pub const MERCURY_UNSUB: u8 = 0xb4;
    pub const MERCURY_PUB: u8 = 0xb5;

    pub fn new(cmd: u8, payload: impl Into<Vec<u8>>) -> Self {
        Self {
            cmd,
            payload: payload.into(),
        }
    }
}

const MAC_SIZE: usize = 4;
const HEADER_SIZE: usize = 3;

pub struct ShannonEncoder<T> {
    inner: T,
    nonce: u32,
    cipher: Shannon,
}

impl<T> ShannonEncoder<T>
where
    T: io::Write,
{
    pub fn new(inner: T, send_key: &[u8]) -> Self {
        Self {
            inner,
            nonce: 0,
            cipher: Shannon::new(send_key),
        }
    }

    pub fn encode(&mut self, item: ShannonMsg) -> io::Result<()> {
        // Buffer up the whole message.
        let mut buf = Vec::with_capacity(HEADER_SIZE + item.payload.len() + MAC_SIZE);
        let len_u16: u16 = item.payload.len().try_into().unwrap();
        buf.push(item.cmd);
        buf.extend(len_u16.to_be_bytes());
        buf.extend(item.payload);

        // Seed the cipher, rotate the nonce, and encrypt the header and payload.
        self.cipher.nonce_u32(self.nonce);
        self.nonce += 1;
        self.cipher.encrypt(&mut buf);

        // Compute the MAC and append it.
        let mut mac = [0_u8; MAC_SIZE];
        self.cipher.finish(&mut mac);
        buf.extend(mac);

        self.inner.write_all(&buf)
    }

    pub fn as_inner_mut(&mut self) -> &mut T {
        &mut self.inner
    }
}

pub struct ShannonDecoder<T> {
    inner: T,
    nonce: u32,
    cipher: Shannon,
}

impl<T> ShannonDecoder<T>
where
    T: io::Read,
{
    pub fn new(inner: T, recv_key: &[u8]) -> Self {
        Self {
            inner,
            nonce: 0,
            cipher: Shannon::new(recv_key),
        }
    }

    pub fn decode(&mut self) -> io::Result<ShannonMsg> {
        // Seed the cipher and rotate the nonce.
        self.cipher.nonce_u32(self.nonce);
        self.nonce += 1;

        // Read the whole header.  Reading and decrypting byte by byte is not really
        // reliable, because of a bug in `shannon` crate.
        let mut header = [0_u8; HEADER_SIZE];
        self.inner.read_exact(&mut header)?;
        self.cipher.decrypt(&mut header);

        // Parse the header fields.
        let cmd = header[0];
        let size = u16::from_be_bytes([header[1], header[2]]) as usize;

        // Read and decrypt the payload.
        let mut payload = vec![0_u8; size];
        self.inner.read_exact(&mut payload)?;
        self.cipher.decrypt(&mut payload);

        // Read and check the MAC.
        let mut mac = [0_u8; MAC_SIZE];
        self.inner.read_exact(&mut mac)?;
        self.cipher.check_mac(&mac)?;

        Ok(ShannonMsg::new(cmd, payload))
    }

    pub fn as_inner(&self) -> &T {
        &self.inner
    }
}


================================================
FILE: psst-core/src/error.rs
================================================
use std::sync::mpsc::RecvTimeoutError;
use std::{error, fmt, io};

#[derive(Debug)]
pub enum Error {
    SessionDisconnected,
    UnexpectedResponse,
    MediaFileNotFound,
    ProxyUrlInvalid,
    AuthFailed { code: i32 },
    ConnectionFailed,
    JsonError(Box<dyn error::Error + Send>),
    InvalidStateError(Box<dyn error::Error + Send + Sync>),
    UnimplementedError(Box<dyn error::Error + Send + Sync>),
    AudioFetchingError(Box<dyn error::Error + Send>),
    AudioDecodingError(Box<dyn error::Error + Send>),
    AudioOutputError(Box<dyn error::Error + Send>),
    AudioProbeError(Box<dyn error::Error + Send>),
    ScrobblerError(Box<dyn error::Error + Send>),
    ResamplingError(i32),
    ConfigError(String),
    IoError(io::Error),
    SendError,
    RecvTimeoutError(RecvTimeoutError),
    JoinError,
    OAuthError(String),
}

impl error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::SessionDisconnected => write!(f, "Session disconnected"),
            Self::UnexpectedResponse => write!(f, "Unknown server response"),
            Self::MediaFileNotFound => write!(f, "Audio file not found"),
            Self::ProxyUrlInvalid => write!(f, "Invalid proxy URL"),
            Self::AuthFailed { code } => match code {
                0 => write!(f, "Authentication failed: protocol error"),
                2 => write!(f, "Authentication failed: try another AP"),
                5 => write!(f, "Authentication failed: bad connection id"),
                9 => write!(f, "Authentication failed: travel restriction"),
                11 => write!(f, "Authentication failed: premium account required"),
                12 => write!(f, "Authentication failed: bad credentials"),
                13 => write!(f, "Authentication failed: could not validate credentials"),
                14 => write!(f, "Authentication failed: account exists"),
                15 => write!(f, "Authentication failed: extra verification required"),
                16 => write!(f, "Authentication failed: invalid app key"),
                17 => write!(f, "Authentication failed: application banned"),
                _ => write!(f, "Authentication failed with error code {code}"),
            },
            Self::ConnectionFailed => write!(f, "Failed to connect to any access point"),
            Self::ResamplingError(code) => {
                write!(f, "Resampling failed with error code {code}")
            }
            Self::ConfigError(msg) => write!(f, "Configuration error: {msg}"),
            Self::JsonError(err)
            | Self::AudioFetchingError(err)
            | Self::AudioDecodingError(err)
            | Self::AudioOutputError(err)
            | Self::ScrobblerError(err)
            | Self::AudioProbeError(err) => err.fmt(f),
            Self::InvalidStateError(err)
            | Self::UnimplementedError(err) => err.fmt(f),
            Self::IoError(err) => err.fmt(f),
            Self::SendError => write!(f, "Failed to send into a channel"),
            Self::RecvTimeoutError(err) => write!(f, "Channel receive timeout: {err}"),
            Self::JoinError => write!(f, "Failed to join thread"),
            Self::OAuthError(msg) => write!(f, "OAuth error: {msg}"),
        }
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Error {
        Error::IoError(err)
    }
}

impl<T> From<crossbeam_channel::SendError<T>> for Error {
    fn from(_: crossbeam_channel::SendError<T>) -> Self {
        Error::SendError
    }
}

impl From<RecvTimeoutError> for Error {
    fn from(err: RecvTimeoutError) -> Self {
        Error::RecvTimeoutError(err)
    }
}

impl From<protobuf::Error> for Error {
    fn from(err: protobuf::Error) -> Self { Error::InvalidStateError(err.into()) }
}

================================================
FILE: psst-core/src/item_id.rs
================================================
use std::{
    collections::HashMap,
    convert::TryInto,
    fmt,
    ops::Deref,
    path::PathBuf,
    sync::{LazyLock, Mutex},
};

static LOCAL_REGISTRY: LazyLock<Mutex<LocalItemRegistry>> =
    LazyLock::new(|| Mutex::new(LocalItemRegistry::new()));

// LocalItemRegistry allows generating IDs for local music files, so they can be
// treated similarly to files hosted on Spotify's remote servers. IDs are
// easier to pass around since they implement `Copy`, as opposed to passing
// around a `PathBuf` or `File` pointing to the file.
//
// The registry stores two complementary maps for bi-directional lookup. This
// allows for quick registration of new tracks and quick lookup of existing
// tracks by ID, at the cost of increased memory usage. The ID-to-path lookup
// should be prioritized, as that is required to begin playback. Path-to-ID
// lookup is helpful to avoid registering the same path under multiple IDs,
// but is okay to be a bit slower since it's only done once per track when
// (when loading the list of local files from Spotify's config).
pub struct LocalItemRegistry {
    next_id: u128,
    path_to_id: HashMap<PathBuf, u128>,
    id_to_path: HashMap<u128, PathBuf>,
}

impl LocalItemRegistry {
    fn new() -> Self {
        Self {
            next_id: 1,
            path_to_id: HashMap::new(),
            id_to_path: HashMap::new(),
        }
    }

    pub fn get_or_insert(path: PathBuf) -> u128 {
        let mut registry = LOCAL_REGISTRY.lock().unwrap();
        registry.path_to_id.get(&path).copied().unwrap_or_else(|| {
            let id = registry.next_id;
            registry.next_id += 1;
            registry.id_to_path.insert(id, path.clone());
            id
        })
    }

    pub fn get(id: u128) -> Option<PathBuf> {
        let registry = LOCAL_REGISTRY.lock().unwrap();
        registry.id_to_path.get(&id).cloned()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ItemIdType {
    Track,
    Podcast,
    LocalFile,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemId {
    pub id: u128,
    pub id_type: ItemIdType,
}

const BASE62_DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const BASE16_DIGITS: &[u8] = b"0123456789abcdef";

impl ItemId {
    pub const INVALID: Self = Self::new(0u128, ItemIdType::Unknown);

    pub const fn new(id: u128, id_type: ItemIdType) -> Self {
        Self { id, id_type }
    }

    pub fn from_base16(id: &str, id_type: ItemIdType) -> Option<Self> {
        let mut n = 0_u128;
        for c in id.as_bytes() {
            let d = BASE16_DIGITS.iter().position(|e| e == c)? as u128;
            n *= 16;
            n += d;
        }
        Some(Self::new(n, id_type))
    }

    pub fn from_base62(id: &str, id_type: ItemIdType) -> Option<Self> {
        let mut n = 0_u128;
        for c in id.as_bytes() {
            let d = BASE62_DIGITS.iter().position(|e| e == c)? as u128;
            n *= 62;
            n += d;
        }
        Some(Self::new(n, id_type))
    }

    pub fn from_raw(data: &[u8], id_type: ItemIdType) -> Option<Self> {
        let n = u128::from_be_bytes(data.try_into().ok()?);
        Some(Self::new(n, id_type))
    }

    pub fn from_uri(uri: &str) -> Option<Self> {
        let gid = uri.split(':').next_back()?;
        if uri.contains(":episode:") {
            Self::from_base62(gid, ItemIdType::Podcast)
        } else if uri.contains(":track:") {
            Self::from_base62(gid, ItemIdType::Track)
        } else {
            Self::from_base62(gid, ItemIdType::Unknown)
        }
    }

    /// Converts an ID to an URI as described in: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
    pub fn to_uri(&self) -> Option<String> {
        let b64 = self.to_base62();
        match self.id_type {
            ItemIdType::Track => Some(format!("spotify:track:{b64}")),
            ItemIdType::Podcast => Some(format!("spotify:podcast:{b64}")),
            // TODO: support adding local files to playlists
            ItemIdType::LocalFile => None,
            ItemIdType::Unknown => None,
        }
    }

    pub fn to_base16(&self) -> String {
        format!("{:032x}", self.id)
    }

    pub fn to_base62(&self) -> String {
        let mut n = self.id;
        let mut data = [0_u8; 22];
        for i in 0..22 {
            data[21 - i] = BASE62_DIGITS[(n % 62) as usize];
            n /= 62;
        }
        std::str::from_utf8(&data).unwrap().to_string()
    }

    pub fn to_raw(&self) -> [u8; 16] {
        self.id.to_be_bytes()
    }

    pub fn from_local(path: PathBuf) -> Self {
        Self::new(
            LocalItemRegistry::get_or_insert(path),
            ItemIdType::LocalFile,
        )
    }

    pub fn to_local(&self) -> PathBuf {
        match self.id_type {
            // local items should only be constructed with `from_local`
            ItemIdType::LocalFile => LocalItemRegistry::get(self.id).expect("valid item ID"),
            _ => panic!("expected local file"),
        }
    }
}

impl Default for ItemId {
    fn default() -> Self {
        Self::INVALID
    }
}

impl From<ItemId> for String {
    fn from(id: ItemId) -> Self {
        id.to_base62()
    }
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct FileId(pub [u8; 20]);

impl FileId {
    pub fn from_raw(data: &[u8]) -> Option<Self> {
        Some(FileId(data.try_into().ok()?))
    }

    pub fn to_base16(&self) -> String {
        self.0
            .iter()
            .map(|b| format!("{b:02x}"))
            .collect::<Vec<String>>()
            .concat()
    }
}

impl Deref for FileId {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl fmt::Debug for FileId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_tuple("FileId").field(&self.to_base16()).finish()
    }
}

impl fmt::Display for FileId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.write_str(&self.to_base16())
    }
}


================================================
FILE: psst-core/src/lastfm.rs
================================================
use crate::error::Error;
use crate::oauth::listen_for_callback_parameter;
use rustfm_scrobble::{responses::SessionResponse, Scrobble, Scrobbler, ScrobblerError};
use std::{net::SocketAddr, time::Duration};
use url::Url;

pub struct LastFmClient;

impl LastFmClient {
    /// Report a track as "now playing" to Last.fm using an existing Scrobbler instance.
    pub fn now_playing_song(
        scrobbler: &Scrobbler, // Requires an authenticated Scrobbler
        artist: &str,
        title: &str,
        album: Option<&str>,
    ) -> Result<(), Error> {
        let song = Scrobble::new(artist, title, album.unwrap_or(""));
        scrobbler
            .now_playing(&song)
            .map(|_| ())
            .map_err(Error::from)
    }

    /// Scrobble a finished track to Last.fm using an existing Scrobbler instance.
    pub fn scrobble_song(
        scrobbler: &Scrobbler, // Requires an authenticated Scrobbler
        artist: &str,
        title: &str,
        album: Option<&str>,
    ) -> Result<(), Error> {
        let song = Scrobble::new(artist, title, album.unwrap_or(""));
        scrobbler.scrobble(&song).map(|_| ()).map_err(Error::from)
    }

    /// Creates an authenticated Last.fm Scrobbler instance with provided credentials.
    /// Note: This assumes the session_key is valid. Validity is checked on first API call.
    pub fn create_scrobbler(
        api_key: Option<&str>,
        api_secret: Option<&str>,
        session_key: Option<&str>,
    ) -> Result<Scrobbler, Error> {
        let (Some(api_key), Some(api_secret), Some(session_key)) =
            (api_key, api_secret, session_key)
        else {
            log::warn!("missing Last.fm API key, secret, or session key for scrobbler creation.");
            return Err(Error::ConfigError(
                "Missing Last.fm API key, secret, or session key.".to_string(),
            ));
        };

        let mut scrobbler = Scrobbler::new(api_key, api_secret);
        // Associate the session key with the scrobbler instance.
        scrobbler.authenticate_with_session_key(session_key);
        log::info!("scrobbler instance created with session key (validity checked on first use).");
        Ok(scrobbler)
    }
}

impl From<ScrobblerError> for Error {
    fn from(value: ScrobblerError) -> Self {
        Self::ScrobblerError(Box::new(value))
    }
}

/// Generate a Last.fm authentication URL
pub fn generate_lastfm_auth_url(
    api_key: &str,
    callback_url: &str,
) -> Result<String, url::ParseError> {
    let base = "http://www.last.fm/api/auth/";
    let url = Url::parse_with_params(base, &[("api_key", api_key), ("cb", callback_url)])?;
    Ok(url.to_string())
}

/// Exchange a token for a Last.fm session key
pub fn exchange_token_for_session(
    api_key: &str,
    api_secret: &str,
    token: &str,
) -> Result<String, Error> {
    let mut scrobbler = Scrobbler::new(api_key, api_secret);
    scrobbler
        .authenticate_with_token(token) // Uses auth.getSession API call internally
        .map(|response: SessionResponse| response.key) // Extract the session key string
        .map_err(Error::from) // Map ScrobblerError to crate::error::Error
}

/// Listen for a Last.fm token from the callback
pub fn get_lastfm_token_listener(
    socket_address: SocketAddr,
    timeout: Duration,
) -> Result<String, Error> {
    // Use the shared listener function, specifying "token" as the parameter
    listen_for_callback_parameter(socket_address, timeout, "token")
}


================================================
FILE: psst-core/src/lib.rs
================================================
#![allow(clippy::new_without_default)]

use git_version::git_version;

pub const GIT_VERSION: &str = git_version!();
pub const BUILD_TIME: &str = include!(concat!(env!("OUT_DIR"), "/build-time.txt"));
pub const REMOTE_URL: &str = include!(concat!(env!("OUT_DIR"), "/remote-url.txt"));

pub mod actor;
pub mod audio;
pub mod cache;
pub mod cdn;
pub mod connection;
pub mod error;
pub mod item_id;
pub mod lastfm;
pub mod metadata;
pub mod oauth;
pub mod player;
pub mod session;
pub mod system_info;
pub mod util;


================================================
FILE: psst-core/src/metadata.rs
================================================
use std::time::Duration;

use crate::{
    error::Error,
    item_id::{FileId, ItemId, ItemIdType},
    player::file::{AudioFormat, MediaFile, MediaPath},
    session::SessionService,
};

use librespot_protocol::metadata::restriction::Country_restriction;
use librespot_protocol::metadata::{AudioFile, Episode, Restriction, Track};

pub trait Fetch: protobuf::Message {
    fn uri(id: ItemId) -> String;
    fn fetch(session: &SessionService, id: ItemId) -> Result<Self, Error> {
        session.connected()?.get_mercury_protobuf(Self::uri(id))
    }
}

impl Fetch for Track {
    fn uri(id: ItemId) -> String {
        format!("hm://metadata/3/track/{}", id.to_base16())
    }
}

impl Fetch for Episode {
    fn uri(id: ItemId) -> String {
        format!("hm://metadata/3/episode/{}", id.to_base16())
    }
}

pub trait ToMediaPath {
    fn is_restricted_in_region(&self, country: &str) -> bool;
    fn find_allowed_alternative(&self, country: &str) -> Option<ItemId>;
    fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath>;
}

impl ToMediaPath for Track {
    fn is_restricted_in_region(&self, country: &str) -> bool {
        self.restriction
            .iter()
            .any(|rest| is_restricted_in_region(rest, country))
    }

    fn find_allowed_alternative(&self, country: &str) -> Option<ItemId> {
        let alt_track = self
            .alternative
            .iter()
            .find(|alt_track| !alt_track.is_restricted_in_region(country))?;
        ItemId::from_raw(alt_track.gid.as_ref()?, ItemIdType::Track)
    }

    fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath> {
        let file = select_preferred_file(&self.file, preferred_bitrate)?;
        Some(MediaPath {
            item_id: ItemId::from_raw(self.gid.as_ref()?, ItemIdType::Track)?,
            file_id: FileId::from_raw(file.file_id.as_ref()?)?,
            file_format: AudioFormat::from_protocol(file.format()),
            duration: Duration::from_millis(self.duration? as u64),
        })
    }
}

impl ToMediaPath for Episode {
    fn is_restricted_in_region(&self, country: &str) -> bool {
        self.restriction
            .iter()
            .any(|rest| is_restricted_in_region(rest, country))
    }

    fn find_allowed_alternative(&self, _country: &str) -> Option<ItemId> {
        None
    }

    fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath> {
        let file = select_preferred_file(&self.audio, preferred_bitrate)?;
        Some(MediaPath {
            item_id: ItemId::from_raw(self.gid.as_ref()?, ItemIdType::Podcast)?,
            file_id: FileId::from_raw(file.file_id.as_ref()?)?,
            file_format: AudioFormat::from_protocol(file.format()),
            duration: Duration::from_millis(self.duration? as u64),
        })
    }
}

fn select_preferred_file(files: &[AudioFile], preferred_bitrate: usize) -> Option<&AudioFile> {
    MediaFile::supported_audio_formats_for_bitrate(preferred_bitrate)
        .iter()
        .find_map(|&preferred_format| {
            files
                .iter()
                .find(|file| file.format == Some(preferred_format.into()))
        })
}

fn is_restricted_in_region(restriction: &Restriction, country: &str) -> bool {
    if let Some(list) = &restriction.country_restriction {
        return match list {
            Country_restriction::CountriesAllowed(allowed) => {
                !is_country_in_list(allowed.as_bytes(), country.as_bytes())
            }
            Country_restriction::CountriesForbidden(forbidden) => {
                is_country_in_list(forbidden.as_bytes(), country.as_bytes())
            }
            _ => false,
        };
    }
    false
}

fn is_country_in_list(countries: &[u8], country: &[u8]) -> bool {
    countries.chunks(2).any(|code| code == country)
}


================================================
FILE: psst-core/src/oauth.rs
================================================
use crate::error::Error;
use oauth2::{
    basic::BasicClient, reqwest::http_client, AuthUrl, AuthorizationCode, ClientId, CsrfToken,
    PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use std::{
    io::{BufRead, BufReader, Write},
    net::TcpStream,
    net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener},
    sync::mpsc,
    time::Duration,
};
use url::Url;

pub fn listen_for_callback_parameter(
    socket_address: SocketAddr,
    timeout: Duration,
    parameter_name: &'static str,
) -> Result<String, Error> {
    log::info!(
        "starting callback listener for '{parameter_name}' on {socket_address:?}",
    );

    // Create a simpler, linear flow
    // 1. Bind the listener
    let listener = match TcpListener::bind(socket_address) {
        Ok(l) => {
            log::info!("listener bound successfully");
            l
        }
        Err(e) => {
            log::error!("Failed to bind listener: {e}");
            return Err(Error::IoError(e));
        }
    };

    // 2. Set up the channel for communication
    let (tx, rx) = mpsc::channel::<Result<String, Error>>();

    // 3. Spawn the thread
    let handle = std::thread::spawn(move || {
        if let Ok((mut stream, _)) = listener.accept() {
            handle_callback_connection(&mut stream, &tx, parameter_name);
        } else {
            log::error!("Failed to accept connection on callback listener");
            let _ = tx.send(Err(Error::IoError(std::io::Error::other(
                "Failed to accept connection",
            ))));
        }
    });

    // 4. Wait for the result with timeout
    let result = match rx.recv_timeout(timeout) {
        Ok(r) => r,
        Err(e) => {
            log::error!("Timed out or channel error: {e}");
            return Err(Error::from(e));
        }
    };

    // 5. Wait for thread completion
    if handle.join().is_err() {
        log::warn!("thread join failed, but continuing with result");
    }

    // 6. Return the result
    result
}

/// Handles an incoming TCP connection for a generic OAuth callback.
fn handle_callback_connection(
    stream: &mut TcpStream,
    tx: &mpsc::Sender<Result<String, Error>>,
    parameter_name: &'static str,
) {
    let mut reader = BufReader::new(&mut *stream);
    let mut request_line = String::new();

    if reader.read_line(&mut request_line).is_ok() {
        match extract_parameter_from_request(&request_line, parameter_name) {
            Some(value) => {
                log::info!("received callback parameter '{parameter_name}'.");
                send_success_response(stream);
                let _ = tx.send(Ok(value));
            }
            None => {
                let err_msg = format!(
                    "Failed to extract parameter '{parameter_name}' from request: {request_line}",
                );
                log::error!("{err_msg}");
                let _ = tx.send(Err(Error::OAuthError(err_msg)));
            }
        }
    } else {
        log::error!("Failed to read request line from callback.");
        let _ = tx.send(Err(Error::IoError(std::io::Error::other(
            "Failed to read request line",
        ))));
    }
}

/// Extracts a specified query parameter from an HTTP request line.
fn extract_parameter_from_request(request_line: &str, parameter_name: &str) -> Option<String> {
    request_line
        .split_whitespace()
        .nth(1)
        .and_then(|path| Url::parse(&format!("http://localhost{path}")).ok())
        .and_then(|url| {
            url.query_pairs()
                .find(|(key, _)| key == parameter_name)
                .map(|(_, value)| value.into_owned())
        })
}

pub fn get_authcode_listener(
    socket_address: SocketAddr,
    timeout: Duration,
) -> Result<AuthorizationCode, Error> {
    listen_for_callback_parameter(socket_address, timeout, "code").map(AuthorizationCode::new)
}

pub fn send_success_response(stream: &mut TcpStream) {
    let response = "HTTP/1.1 200 OK\r\n\r\n\
        <html>\
        <head>\
            <style>\
                body {\
                    background-color: #121212;\
                    color: #ffffff;\
                    font-family: sans-serif;\
                    display: flex;\
                    justify-content: center;\
                    align-items: center;\
                    height: 100vh;\
                    margin: 0;\
                }\
                a {\
                    color: #aaaaaa;\
                    text-decoration: underline;\
                    cursor: pointer;\
                }\
            </style>\
        </head>\
        <body>\
            <div>Successfully authenticated! You can close this window now.</div>\
        </body>\
        </html>";
    let _ = stream.write_all(response.as_bytes());
}

fn create_spotify_oauth_client(redirect_port: u16) -> BasicClient {
    let redirect_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), redirect_port);
    let redirect_uri = format!("http://{redirect_address}/login");

    BasicClient::new(
        ClientId::new(crate::session::access_token::CLIENT_ID.to_string()),
        None,
        AuthUrl::new("https://accounts.spotify.com/authorize".to_string()).unwrap(),
        Some(TokenUrl::new("https://accounts.spotify.com/api/token".to_string()).unwrap()),
    )
    .set_redirect_uri(RedirectUrl::new(redirect_uri).expect("Invalid redirect URL"))
}

pub fn generate_auth_url(redirect_port: u16) -> (String, PkceCodeVerifier) {
    let client = create_spotify_oauth_client(redirect_port);
    let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

    let (auth_url, _) = client
        .authorize_url(CsrfToken::new_random)
        .add_scopes(get_scopes())
        .set_pkce_challenge(pkce_challenge)
        .url();

    (auth_url.to_string(), pkce_verifier)
}

pub fn exchange_code_for_token(
    redirect_port: u16,
    code: AuthorizationCode,
    pkce_verifier: PkceCodeVerifier,
) -> String {
    let client = create_spotify_oauth_client(redirect_port);

    let token_response = client
        .exchange_code(code)
        .set_pkce_verifier(pkce_verifier)
        .request(http_client)
        .expect("Failed to exchange code for token");

    token_response.access_token().secret().to_string()
}

fn get_scopes() -> Vec<Scope> {
    crate::session::access_token::ACCESS_SCOPES
        .split(',')
        .map(|s| Scope::new(s.trim().to_string()))
        .collect()
}


================================================
FILE: psst-core/src/player/file.rs
================================================
use std::{
    fs, io,
    io::{Seek, SeekFrom},
    path::PathBuf,
    sync::Arc,
    thread,
    thread::JoinHandle,
    time::Duration,
};

use symphonia::core::codecs::CodecType;

use crate::{
    audio::{
        decode::{AudioCodecFormat, AudioDecoder},
        decrypt::{AudioDecrypt, AudioKey},
        normalize::NormalizationData,
    },
    cache::CacheHandle,
    cdn::{CdnHandle, CdnUrl},
    error::Error,
    item_id::{FileId, ItemId},
    util::OffsetFile,
};

use librespot_protocol::metadata::audio_file::Format;

use super::storage::{StreamRequest, StreamStorage, StreamWriter};

#[derive(Debug, Clone, Copy)]
pub struct MediaPath {
    pub item_id: ItemId,
    pub file_id: FileId,
    pub file_format: AudioFormat,
    pub duration: Duration,
}

// possibly should be combined with AudioCodecFormat?
#[derive(Debug, Clone, Copy)]
pub enum AudioFormat {
    Mp3,
    OggVorbis,
    Unsupported,
}

impl AudioFormat {
    pub fn from_protocol(format: Format) -> Self {
        use Format::*;
        match format {
            MP3_256 | MP3_320 | MP3_160 | MP3_96 | MP3_160_ENC => Self::Mp3,
            OGG_VORBIS_96 | OGG_VORBIS_160 | OGG_VORBIS_320 => Self::OggVorbis,
            _ => Self::Unsupported,
        }
    }

    pub fn from_codec(codec: CodecType) -> Self {
        use symphonia::core::codecs::*;
        if codec == CODEC_TYPE_MP3 {
            Self::Mp3
        } else if codec == CODEC_TYPE_VORBIS {
            Self::OggVorbis
        } else {
            Self::Unsupported
        }
    }
}

pub enum MediaFile {
    Streamed {
        streamed_file: Arc<StreamedFile>,
        servicing_handle: JoinHandle<()>,
    },
    Cached {
        cached_file: CachedFile,
    },
    Local {
        path: MediaPath,
    },
}

impl MediaFile {
    pub fn supported_audio_formats_for_bitrate(bitrate: usize) -> &'static [Format] {
        match bitrate {
            96 => &[
                Format::OGG_VORBIS_96,
                Format::MP3_96,
                Format::OGG_VORBIS_160,
                Format::MP3_160,
                Format::MP3_160_ENC,
                Format::MP3_256,
                Format::OGG_VORBIS_320,
                Format::MP3_320,
            ],
            160 => &[
                Format::OGG_VORBIS_160,
                Format::MP3_160,
                Format::MP3_160_ENC,
                Format::MP3_256,
                Format::OGG_VORBIS_320,
                Format::MP3_320,
                Format::OGG_VORBIS_96,
                Format::MP3_96,
            ],
            320 => &[
                Format::OGG_VORBIS_320,
                Format::MP3_320,
                Format::MP3_256,
                Format::OGG_VORBIS_160,
                Format::MP3_160,
                Format::MP3_160_ENC,
                Format::OGG_VORBIS_96,
                Format::MP3_96,
            ],
            _ => unreachable!(),
        }
    }

    pub fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Result<Self, Error> {
        let cached_path = cache.audio_file_path(path.file_id);
        if cached_path.exists() {
            let cached_file = CachedFile::open(path, cached_path)?;
            Ok(Self::Cached { cached_file })
        } else {
            let streamed_file = Arc::new(StreamedFile::open(path, cdn, cache)?);
            let servicing_handle = thread::spawn({
                let streamed_file = Arc::clone(&streamed_file);
                move || {
                    streamed_file
                        .service_streaming()
                        .expect("Streaming thread failed");
                }
            });
            Ok(Self::Streamed {
                streamed_file,
                servicing_handle,
            })
        }
    }

    pub fn local(path: MediaPath) -> Self {
        Self::Local { path }
    }

    pub fn path(&self) -> MediaPath {
        match self {
            Self::Streamed { streamed_file, .. } => streamed_file.path,
            Self::Cached { cached_file, .. } => cached_file.path,
            Self::Local { path } => *path,
        }
    }

    pub fn storage(&self) -> Option<&StreamStorage> {
        match self {
            Self::Streamed { streamed_file, .. } => Some(&streamed_file.storage),
            Self::Cached { cached_file, .. } => Some(&cached_file.storage),
            Self::Local { .. } => None,
        }
    }

    pub fn remote_audio_source(
        &self,
        key: AudioKey,
    ) -> Result<(AudioDecoder, NormalizationData), Error> {
        let reader = self
            .storage()
            .expect("storage always set for remote files")
            .reader()?;
        let mut decrypted = AudioDecrypt::new(key, reader);
        let normalization = NormalizationData::parse(&mut decrypted)?;
        let encoded = OffsetFile::new(decrypted, self.header_length())?;
        let decoded = AudioDecoder::new(encoded, self.codec_format())?;
        Ok((decoded, normalization))
    }

    pub fn local_audio_source(&self) -> Result<(AudioDecoder, NormalizationData), Error> {
        let mut reader = fs::File::open(self.path().item_id.to_local())?;
        let normalization = NormalizationData::parse(&mut reader)?;
        let encoded = OffsetFile::new(reader, self.header_length())?;
        let decoded = AudioDecoder::new(encoded, self.codec_format())?;
        Ok((decoded, normalization))
    }

    fn header_length(&self) -> u64 {
        match self.path().file_format {
            AudioFormat::OggVorbis => 167,
            _ => 0,
        }
    }

    fn codec_format(&self) -> AudioCodecFormat {
        match self.path().file_format {
            AudioFormat::OggVorbis => AudioCodecFormat::OggVorbis,
            AudioFormat::Mp3 => AudioCodecFormat::Mp3,
            AudioFormat::Unsupported => unreachable!("unsupported codec"),
        }
    }
}

pub struct StreamedFile {
    path: MediaPath,
    storage: StreamStorage,
    url: CdnUrl,
    cdn: CdnHandle,
    cache: CacheHandle,
}

impl StreamedFile {
    fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Result<StreamedFile, Error> {
        // First, we need to resolve URL of the file contents.
        let url = cdn.resolve_audio_file_url(path.file_id)?;
        log::debug!("resolved file URL: {:?}", url.url);

        // How many bytes we request in the first chunk.
        const INITIAL_REQUEST_LENGTH: u64 = 1024 * 6;

        // Send the initial request, that gives us the total file length and the
        // beginning of the contents.  Use the total length for creating the backing
        // data storage.
        let (total_length, mut initial_data) =
            cdn.fetch_file_range(&url.url, 0, INITIAL_REQUEST_LENGTH)?;
        let storage = StreamStorage::new(total_length)?;

        // Pipe the initial data from the request body into storage.
        io::copy(&mut initial_data, &mut storage.writer()?)?;

        Ok(StreamedFile {
            path,
            storage,
            url,
            cdn,
            cache,
        })
    }

    fn service_streaming(&self) -> Result<(), Error> {
        let mut last_url = self.url.clone();
        let mut fresh_url = || -> Result<CdnUrl, Error> {
            if last_url.is_expired() {
                last_url = self.cdn.resolve_audio_file_url(self.path.file_id)?;
            }
            Ok(last_url.clone())
        };
        let mut download_range = |offset, length| -> Result<(), Error> {
            let thread_name = format!(
                "cdn-{}-{}..{}",
                self.path.file_id.to_base16(),
                offset,
                offset + length
            );
            // TODO: We spawn threads here without any accounting.  Seems wrong.
            thread::Builder::new().name(thread_name).spawn({
                let url = fresh_url()?.url;
                let cdn = self.cdn.clone();
                let cache = self.cache.clone();
                let mut writer = self.storage.writer()?;
                let file_path = self.storage.path().to_path_buf();
                let file_id = self.path.file_id;
                move || {
                    match load_range(&mut writer, &cdn, &url, offset, length) {
                        Ok(_) => {
                            // If the file is completely downloaded, copy it to cache.
                            if writer.is_complete() && !cache.audio_file_path(file_id).exists() {
                                // TODO: We should do this atomically.
                                if let Err(err) = cache.save_audio_file(file_id, file_path) {
                                    log::warn!("failed to save audio file to cache: {err:?}");
                                }
                            }
                        }
                        Err(err) => {
                            log::error!("failed to download: {err}");
                            // Range failed to download, remove it from the requested set.
                            writer.mark_as_not_requested(offset, length);
                        }
                    }
                }
            })?;

            Ok(())
        };

        while let Ok(req) = self.storage.receiver().recv() {
            match req {
                StreamRequest::Preload { offset, length } => {
                    if let Err(err) = download_range(offset, length) {
                        log::error!("failed to request audio range: {err:?}");
                    }
                }
                StreamRequest::Blocked { offset } => {
                    log::info!("blocked at {offset}");
                }
            }
        }
        Ok(())
    }
}

pub struct CachedFile {
    path: MediaPath,
    storage: StreamStorage,
}

impl CachedFile {
    fn open(path: MediaPath, file_path: PathBuf) -> Result<Self, Error> {
        Ok(Self {
            path,
            storage: StreamStorage::from_complete_file(file_path)?,
        })
    }
}

fn load_range(
    writer: &mut StreamWriter,
    cdn: &CdnHandle,
    url: &str,
    offset: u64,
    length: u64,
) -> Result<(), Error> {
    log::trace!("downloading {}..{}", offset, offset + length);

    // Download range of data from the CDN.  Block until we a have reader of the
    // request body.
    let (_total_length, mut reader) = cdn.fetch_file_range(url, offset, length)?;

    // Pipe it into storage. Blocks until fully written, but readers sleeping on
    // this file should be notified as soon as their offset is covered.
    writer.seek(SeekFrom::Start(offset))?;
    io::copy(&mut reader, writer)?;

    Ok(())
}


================================================
FILE: psst-core/src/player/item.rs
================================================
use std::time::Duration;

use crate::{
    audio::{
        decode::AudioDecoder, decrypt::AudioKey, normalize::NormalizationLevel, probe::TrackProbe,
    },
    cache::CacheHandle,
    cdn::CdnHandle,
    error::Error,
    item_id::{ItemId, ItemIdType, LocalItemRegistry},
    metadata::{Fetch, ToMediaPath},
    session::SessionService,
};

use librespot_protocol::metadata::{Episode, Track};

use super::{
    file::{AudioFormat, MediaFile, MediaPath},
    PlaybackConfig,
};

pub struct LoadedPlaybackItem {
    pub file: MediaFile,
    pub source: AudioDecoder,
    pub norm_factor: f32,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct PlaybackItem {
    pub item_id: ItemId,
    pub norm_level: NormalizationLevel,
}

impl PlaybackItem {
    pub fn load(
        &self,
        session: &SessionService,
        cdn: CdnHandle,
        cache: CacheHandle,
        config: &PlaybackConfig,
    ) -> Result<LoadedPlaybackItem, Error> {
        let path = load_media_path(self.item_id, session, &cache, config)?;
        let (file, source, norm_data) = match self.item_id.id_type {
            ItemIdType::LocalFile => {
                let file = MediaFile::local(path);
                let (source, norm_data) = file.local_audio_source()?;
                (file, source, norm_data)
            }
            _ => {
                let key = load_audio_key(&path, session, &cache)?;
                let file = MediaFile::open(path, cdn, cache)?;
                let (source, norm_data) = file.remote_audio_source(key)?;
                (file, source, norm_data)
            }
        };
        let norm_factor = norm_data.factor_for_level(self.norm_level, config.pregain);
        Ok(LoadedPlaybackItem {
            file,
            source,
            norm_factor,
        })
    }
}

fn load_media_path(
    item_id: ItemId,
    session: &SessionService,
    cache: &CacheHandle,
    config: &PlaybackConfig,
) -> Result<MediaPath, Error> {
    match item_id.id_type {
        ItemIdType::Track => {
            load_media_path_from_track_or_alternative(item_id, session, cache, config)
        }
        ItemIdType::Podcast => load_media_path_from_episode(item_id, session, cache, config),
        ItemIdType::LocalFile => load_media_path_from_local(item_id),
        ItemIdType::Unknown => unimplemented!(),
    }
}

fn load_media_path_from_track_or_alternative(
    item_id: ItemId,
    session: &SessionService,
    cache: &CacheHandle,
    config: &PlaybackConfig,
) -> Result<MediaPath, Error> {
    let track = load_track(item_id, session, cache)?;
    let country = get_country_code(session, cache);
    let path = match country {
        Some(user_country) if track.is_restricted_in_region(&user_country) => {
            // The track is regionally restricted and is unavailable.  Let's try to find an
            // alternative track.
            let alt_id = track
                .find_allowed_alternative(&user_country)
                .ok_or(Error::MediaFileNotFound)?;
            let alt_track = load_track(alt_id, session, cache)?;
            let alt_path = alt_track
                .to_media_path(config.bitrate)
                .ok_or(Error::MediaFileNotFound)?;
            // We've found an alternative track with a fitting audio file.  Let's cheat a
            // little and pretend we've obtained it from the requested track.
            // TODO: We should be honest and display the real track information.
            MediaPath {
                item_id,
                ..alt_path
            }
        }
        _ => {
            // Either we do not have a country code loaded or the track is available, return
            // it.
            track
                .to_media_path(config.bitrate)
                .ok_or(Error::MediaFileNotFound)?
        }
    };
    Ok(path)
}

fn load_media_path_from_episode(
    item_id: ItemId,
    session: &SessionService,
    cache: &CacheHandle,
    config: &PlaybackConfig,
) -> Result<MediaPath, Error> {
    let episode = load_episode(item_id, session, cache)?;
    let country = get_country_code(session, cache);
    let path = match country {
        Some(user_country) if episode.is_restricted_in_region(&user_country) => {
            // Episode is restricted, and doesn't have any alternatives.
            return Err(Error::MediaFileNotFound);
        }
        _ => episode
            .to_media_path(config.bitrate)
            .ok_or(Error::MediaFileNotFound)?,
    };
    Ok(path)
}

fn load_media_path_from_local(item_id: ItemId) -> Result<MediaPath, Error> {
    let path = LocalItemRegistry::get(item_id.id).expect("valid local item ID");
    let probe = TrackProbe::new(&path)?;
    Ok(MediaPath {
        item_id,
        file_id: Default::default(),
        file_format: AudioFormat::from_codec(probe.codec),
        // It's possible (though unlikely) that we're unable to determine the track
        // duration from the codec params; in that case, default to 0 and let it
        // be calculated at runtime as we play the track.
        duration: probe.duration.unwrap_or(Duration::from_millis(0)),
    })
}

fn get_country_code(session: &SessionService, cache: &CacheHandle) -> Option<String> {
    if let Some(cached_country_code) = cache.get_country_code() {
        Some(cached_country_code)
    } else {
        let country_code = session.connected().ok()?.get_country_code()?;
        if let Err(err) = cache.save_country_code(&country_code) {
            log::warn!("failed to save country code to cache: {err:?}");
        }
        Some(country_code)
    }
}

fn load_track(
    item_id: ItemId,
    session: &SessionService,
    cache: &CacheHandle,
) -> Result<Track, Error> {
    if let Some(cached_track) = cache.get_track(item_id) {
        Ok(cached_track)
    } else {
        let track = Track::fetch(session, item_id)?;
        if let Err(err) = cache.save_track(item_id, &track) {
            log::warn!("failed to save track to cache: {err:?}");
        }
        Ok(track)
    }
}

fn load_episode(
    item_id: ItemId,
    session: &SessionService,
    cache: &CacheHandle,
) -> Result<Episode, Error> {
    if let Some(cached_episode) = cache.get_episode(item_id) {
        Ok(cached_episode)
    } else {
        let episode = Episode::fetch(session, item_id)?;
        if let Err(err) = cache.save_episode(item_id, &episode) {
            log::warn!("failed to save episode to cache: {err:?}");
        }
        Ok(episode)
    }
}

fn load_audio_key(
    path: &MediaPath,
    session: &SessionService,
    cache: &CacheHandle,
) -> Result<AudioKey, Error> {
    if let Some(cached_key) = cache.get_audio_key(path.item_id, path.file_id) {
        Ok(cached_key)
    } else {
        let key = session
            .connected()?
            .get_audio_key(path.item_id, path.file_id)?;
        if let Err(err) = cache.save_audio_key(path.item_id, path.file_id, &key) {
            log::warn!("failed to save audio key to cache: {err:?}");
        }
        Ok(key)
    }
}


================================================
FILE: psst-core/src/player/mod.rs
================================================
pub mod file;
pub mod item;
pub mod queue;
mod storage;
mod worker;

use std::{mem, thread, thread::JoinHandle, time::Duration};

use crossbeam_channel::{unbounded, Receiver, Sender};

use crate::{
    audio::output::{AudioOutput, AudioSink, DefaultAudioOutput, DefaultAudioSink},
    cache::CacheHandle,
    cdn::CdnHandle,
    error::Error,
    session::SessionService,
};

use self::{
    file::MediaPath,
    item::{LoadedPlaybackItem, PlaybackItem},
    queue::{Queue, QueueBehavior},
    worker::PlaybackManager,
};

const PREVIOUS_TRACK_THRESHOLD: Duration = Duration::from_secs(3);
const STOP_AFTER_CONSECUTIVE_LOADING_FAILURES: usize = 3;

#[derive(Clone)]
pub struct PlaybackConfig {
    pub bitrate: usize,
    pub pregain: f32,
}

impl Default for PlaybackConfig {
    fn default() -> Self {
        Self {
            bitrate: 320,
            pregain: 3.0,
        }
    }
}

pub struct Player {
    state: PlayerState,
    preload: PreloadState,
    session: SessionService,
    cdn: CdnHandle,
    cache: CacheHandle,
    config: PlaybackConfig,
    queue: Queue,
    sender: Sender<PlayerEvent>,
    receiver: Receiver<PlayerEvent>,
    audio_output_sink: DefaultAudioSink,
    playback_mgr: PlaybackManager,
    consecutive_loading_failures: usize,
}

impl Player {
    pub fn new(
        session: SessionService,
        cdn: CdnHandle,
        cache: CacheHandle,
        config: PlaybackConfig,
        audio_output: &DefaultAudioOutput,
    ) -> Self {
        let (sender, receiver) = unbounded();
        Self {
            playback_mgr: PlaybackManager::new(audio_output.sink(), sender.clone()),
            session,
            cdn,
            cache,
            config,
            sender,
            receiver,
            audio_output_sink: audio_output.sink(),
            state: PlayerState::Stopped,
            preload: PreloadState::None,
            queue: Queue::new(),
            consecutive_loading_failures: 0,
        }
    }

    pub fn sender(&self) -> Sender<PlayerEvent> {
        self.sender.clone()
    }

    pub fn receiver(&self) -> Receiver<PlayerEvent> {
        self.receiver.clone()
    }

    pub fn handle(&mut self, event: PlayerEvent) {
        match event {
            PlayerEvent::Command(cmd) => self.handle_command(cmd),
            PlayerEvent::Loaded { item, result } => self.handle_loaded(item, result),
            PlayerEvent::Preloaded { item, result } => self.handle_preloaded(item, result),
            PlayerEvent::Position { position, path } => self.handle_position(position, path),
            PlayerEvent::EndOfTrack => self.handle_end_of_track(),
            PlayerEvent::Loading { .. }
            | PlayerEvent::Playing { .. }
            | PlayerEvent::Pausing { .. }
            | PlayerEvent::Resuming { .. }
            | PlayerEvent::Stopped
            | PlayerEvent::Blocked { .. } => {}
        };
    }

    fn handle_command(&mut self, cmd: PlayerCommand) {
        match cmd {
            PlayerCommand::LoadQueue { items, position } => self.load_queue(items, position),
            PlayerCommand::LoadAndPlay { item } => self.load_and_play(item),
            PlayerCommand::Preload { item } => self.preload(item),
            PlayerCommand::Pause => self.pause(),
            PlayerCommand::Resume => self.resume(),
            PlayerCommand::PauseOrResume => self.pause_or_resume(),
            PlayerCommand::Previous => self.previous(),
            PlayerCommand::Next => self.next(),
            PlayerCommand::Stop => self.stop(),
            PlayerCommand::Seek { position } => self.seek(position),
            PlayerCommand::Configure { config } => self.configure(config),
            PlayerCommand::SetQueueBehavior { behavior } => self.queue.set_behaviour(behavior),
            PlayerCommand::AddToQueue { item } => self.queue.add(item),
            PlayerCommand::SetVolume { volume } => self.set_volume(volume),
        }
    }

    fn handle_loaded(&mut self, item: PlaybackItem, result: Result<LoadedPlaybackItem, Error>) {
        match self.state {
            PlayerState::Loading {
                item: requested_item,
                ..
            } if item == requested_item => match result {
                Ok(loaded_item) => {
                    self.consecutive_loading_failures = 0;
                    self.play_loaded(loaded_item);
                }
                Err(err) => {
                    self.consecutive_loading_failures += 1;
                    if self.consecutive_loading_failures < STOP_AFTER_CONSECUTIVE_LOADING_FAILURES {
                        log::error!("skipping, error while loading: {err}");
                        self.next();
                    } else {
                        log::error!("stopping, error while loading: {err}");
                        self.stop();
                    }
                }
            },
            _ => {
                log::info!("stale load result received, ignoring");
            }
        }
    }

    fn handle_preloaded(&mut self, item: PlaybackItem, result: Result<LoadedPlaybackItem, Error>) {
        match self.preload {
            PreloadState::Preloading {
                item: requested_item,
                ..
            } if item == requested_item => match result {
                Ok(loaded_item) => {
                    log::info!("preloaded audio file");
                    self.preload = PreloadState::Preloaded { item, loaded_item };
                }
                Err(err) => {
                    log::error!("failed to preload audio file, error while opening: {err}");
                    self.preload = PreloadState::None;
                }
            },
            _ => {
                log::info!("stale preload result received, ignoring");

                // We are not preloading this item, but because we sometimes extract the
                // preloading thread and use it for loading, let's check if the item is not
                // being loaded now.
                self.handle_loaded(item, result);
            }
        }
    }

    fn handle_position(&mut self, new_position: Duration, path: MediaPath) {
        match &mut self.state {
            PlayerState::Playing { position, .. } | PlayerState::Paused { position, .. } => {
                *position = new_position;
            }
            _ => {
                log::warn!("received unexpected position report");
            }
        }
        const PRELOAD_BEFORE_END_OF_TRACK: Duration = Duration::from_secs(30);
        let time_until_end_of_track = path.duration.checked_sub(new_position).unwrap_or_default();
        if time_until_end_of_track <= PRELOAD_BEFORE_END_OF_TRACK {
            if let Some(&item_to_preload) = self.queue.get_following() {
                self.preload(item_to_preload);
            }
        }
    }

    fn handle_end_of_track(&mut self) {
        self.queue.skip_to_following();
        if let Some(&item) = self.queue.get_current() {
            self.load_and_play(item);
        } else {
            self.stop();
        }
    }

    fn load_queue(&mut self, items: Vec<PlaybackItem>, position: usize) {
        self.queue.fill(items, position);
        if let Some(&item) = self.queue.get_current() {
            self.load_and_play(item);
        } else {
            self.stop();
        }
    }

    fn load_and_play(&mut self, item: PlaybackItem) {
        // Make sure to stop the sink, so any current audio source is cleared and the
        // playback stopped.
        self.audio_output_sink.stop();

        // Check if the item is already in the preloader state.
        let loading_handle = match mem::replace(&mut self.preload, PreloadState::None) {
            PreloadState::Preloaded {
                item: preloaded_item,
                loaded_item,
            } if preloaded_item == item => {
                // This item is already loaded in the preloader state.
                self.play_loaded(loaded_item);
                return;
            }

            PreloadState::Preloading {
                item: preloaded_item,
                loading_handle,
            } if preloaded_item == item => {
                // This item is being preloaded. Take it out of the preloader state.
                loading_handle
            }

            preloading_other_file_or_none => {
                self.preload = preloading_other_file_or_none;
                // Item is not preloaded yet, load it in a background thread.
                thread::spawn({
                    let sender = self.sender.clone();
                    let session = self.session.clone();
                    let cdn = self.cdn.clone();
                    let cache = self.cache.clone();
                    let config = self.config.clone();
                    move || {
                        let result = item.load(&session, cdn, cache, &config);
                        sender.send(PlayerEvent::Loaded { item, result }).unwrap();
                    }
                })
            }
        };

        self.sender.send(PlayerEvent::Loading { item }).unwrap();
        self.state = PlayerState::Loading {
            item,
            _loading_handle: loading_handle,
        };
    }

    fn preload(&mut self, item: PlaybackItem) {
        if self.is_in_preload(item) {
            return;
        }
        let loading_handle = thread::spawn({
            let sender = self.sender.clone();
            let session = self.session.clone();
            let cdn = self.cdn.clone();
            let cache = self.cache.clone();
            let config = self.config.clone();
            move || {
                let result = item.load(&session, cdn, cache, &config);
                sender
                    .send(PlayerEvent::Preloaded { item, result })
                    .unwrap();
            }
        });
        self.preload = PreloadState::Preloading {
            item,
            loading_handle,
        };
    }

    fn set_volume(&mut self, volume: f64) {
        self.audio_output_sink.set_volume(volume as f32);
    }

    fn play_loaded(&mut self, loaded_item: LoadedPlaybackItem) {
        log::info!("starting playback");
        let path = loaded_item.file.path();
        let position = Duration::default();
        self.playback_mgr.play(loaded_item);
        self.state = PlayerState::Playing { path, position };
        self.sender
            .send(PlayerEvent::Playing { path, position })
            .unwrap();
    }

    fn pause(&mut self) {
        match mem::replace(&mut self.state, PlayerState::Invalid) {
            PlayerState::Playing { path, position } | PlayerState::Paused { path, position } => {
                log::info!("pausing playback");
                self.audio_output_sink.pause();
                self.sender
                    .send(PlayerEvent::Pausing { path, position })
                    .unwrap();
                self.state = PlayerState::Paused { path, position };
            }
            _ => {
                log::warn!("invalid state transition");
            }
        }
    }

    fn resume(&mut self) {
        match mem::replace(&mut self.state, PlayerState::Invalid) {
            PlayerState::Playing { path, position } | PlayerState::Paused { path, position } => {
                log::info!("resuming playback");
                self.audio_output_sink.resume();
                self.sender
                    .send(PlayerEvent::Resuming { path, position })
                    .unwrap();
                self.state = PlayerState::Playing { path, position };
            }
            _ => {
                log::warn!("invalid state transition");
            }
        }
    }

    fn pause_or_resume(&mut self) {
        match &self.state {
            PlayerState::Playing { .. } => self.pause(),
            PlayerState::Paused { .. } => self.resume(),
            _ => {
                // Do nothing.
            }
        }
    }

    fn previous(&mut self) {
        if self.is_near_playback_start() {
            self.queue.skip_to_previous();
            if let Some(&item) = self.queue.get_current() {
                self.load_and_play(item);
            } else {
                self.stop();
            }
        } else {
            self.seek(Duration::default());
        }
    }

    fn next(&mut self) {
        self.queue.skip_to_next();
        if let Some(&item) = self.queue.get_current() {
            self.load_and_play(item);
        } else {
            self.stop();
        }
    }

    fn stop(&mut self) {
        self.sender.send(PlayerEvent::Stopped).unwrap();
        self.audio_output_sink.stop();
        self.state = PlayerState::Stopped;
        self.queue.clear();
        self.consecutive_loading_failures = 0;
    }

    fn seek(&mut self, position: Duration) {
        self.playback_mgr.seek(position);
    }

    fn configure(&mut self, config: PlaybackConfig) {
        self.config = config;
    }

    fn is_near_playback_start(&self) -> bool {
        match self.state {
            PlayerState::Playing { position, .. } | PlayerState::Paused { position, .. } => {
                position < PREVIOUS_TRACK_THRESHOLD
            }
            _ => false,
        }
    }

    fn is_in_preload(&self, item: PlaybackItem) -> bool {
        match self.preload {
            PreloadState::Preloading { item: p_item, .. }
            | PreloadState::Preloaded { item: p_item, .. } => p_item == item,
            _ => false,
        }
    }
}

pub enum PlayerCommand {
    LoadQueue {
        items: Vec<PlaybackItem>,
        position: usize,
    },
    LoadAndPlay {
        item: PlaybackItem,
    },
    Preload {
        item: PlaybackItem,
    },
    Pause,
    Resume,
    PauseOrResume,
    Previous,
    Next,
    Stop,
    Seek {
        position: Duration,
    },
    Configure {
        config: PlaybackConfig,
    },
    SetQueueBehavior {
        behavior: QueueBehavior,
    },
    AddToQueue {
        item: PlaybackItem,
    },
    /// Change playback volume to a value in 0.0..=1.0 range.
    SetVolume {
        volume: f64,
    },
}

pub enum PlayerEvent {
    Command(PlayerCommand),
    /// Track has started loading.  `Loaded` follows.
    Loading {
        item: PlaybackItem,
    },
    /// Track loading either succeeded or failed.  `Playing` follows in case of
    /// success.
    Loaded {
        item: PlaybackItem,
        result: Result<LoadedPlaybackItem, Error>,
    },
    /// Next item in queue has been either successfully preloaded or failed to
    /// preload.
    Preloaded {
        item: PlaybackItem,
        result: Result<LoadedPlaybackItem, Error>,
    },
    /// Player has started playing new track.  `Position` events will follow.
    Playing {
        path: MediaPath,
        position: Duration,
    },
    /// Player is in a paused state.  `Resuming` might follow.
    Pausing {
        path: MediaPath,
        position: Duration,
    },
    /// Player is resuming playback of a track.  `Position` events will follow.
    Resuming {
        path: MediaPath,
        position: Duration,
    },
    /// Position of the playback head has changed.
    Position {
        path: MediaPath,
        position: Duration,
    },
    /// Player would like to continue playing, but is blocked, waiting for I/O.
    Blocked {
        path: MediaPath,
        position: Duration,
    },
    /// Player has finished playing a track.  `Loading` or `Playing` might
    /// follow if the queue is not empty, `Stopped` will follow if it is.
    EndOfTrack,
    /// The queue is empty.
    Stopped,
}

enum PlayerState {
    Loading {
        item: PlaybackItem,
        _loading_handle: JoinHandle<()>,
    },
    Playing {
        path: MediaPath,
        position: Duration,
    },
    Paused {
        path: MediaPath,
        position: Duration,
    },
    Stopped,
    Invalid,
}

enum PreloadState {
    Preloading {
        item: PlaybackItem,
        loading_handle: JoinHandle<()>,
    },
    Preloaded {
        item: PlaybackItem,
        loaded_item: LoadedPlaybackItem,
    },
    None,
}


================================================
FILE: psst-core/src/player/queue.rs
================================================
use rand::prelude::SliceRandom;

use super::PlaybackItem;

#[derive(Default, Debug)]
pub enum QueueBehavior {
    #[default]
    Sequential,
    Random,
    LoopTrack,
    LoopAll,
}

pub struct Queue {
    items: Vec<PlaybackItem>,
    user_items: Vec<PlaybackItem>,
    position: usize,
    user_items_position: usize,
    positions: Vec<usize>,
    behavior: QueueBehavior,
}

impl Queue {
    pub fn new() -> Self {
        Self {
            items: Vec::new(),
            user_items: Vec::new(),
            position: 0,
            user_items_position: 0,
            positions: Vec::new(),
            behavior: QueueBehavior::default(),
        }
    }

    pub fn clear(&mut self) {
        self.items.clear();
        self.positions.clear();
        self.position = 0;
    }

    pub fn fill(&mut self, items: Vec<PlaybackItem>, position: usize) {
        self.positions.clear();
        self.items = items;
        self.position = position;
        self.compute_positions();
    }

    pub fn add(&mut self, item: PlaybackItem) {
        self.user_items.push(item);
    }

    fn handle_added_queue(&mut self) {
        if self.user_items.len() > self.user_items_position {
            self.items.insert(
                self.positions.len(),
                self.user_items[self.user_items_position],
            );
            self.positions
                .insert(self.position + 1, self.positions.len());
            self.user_items_position += 1;
        }
    }

    pub fn set_behaviour(&mut self, behavior: QueueBehavior) {
        self.behavior = behavior;
        self.compute_positions();
    }

    fn compute_positions(&mut self) {
        // In the case of switching away from shuffle, the position should be set back to
        // where it appears in the actual playlist order.
        let playlist_position = if self.positions.len() > 1 {
            self.positions[self.position]
        } else {
            self.position
        };
        // Start with an ordered 1:1 mapping.
        self.positions = (0..self.items.len()).collect();

        if let QueueBehavior::Random = self.behavior {
            // Swap the current position with the first item, so we will start from the
            // beginning, with the full queue ahead of us.  Then shuffle the rest of the
            // items and set the position to 0.
            if self.positions.len() > 1 {
                self.positions.swap(0, self.position);
                self.positions[1..].shuffle(&mut rand::rng());
            }
            self.position = 0;
        } else {
            self.position = playlist_position;
        }
    }

    pub fn skip_to_previous(&mut self) {
        self.position = self.previous_position();
    }

    pub fn skip_to_next(&mut self) {
        self.handle_added_queue();
        self.position = self.next_position();
    }

    pub fn skip_to_following(&mut self) {
        self.handle_added_queue();
        self.position = self.following_position();
    }

    pub fn get_current(&self) -> Option<&PlaybackItem> {
        let position = self.positions.get(self.position).copied()?;
        self.items.get(position)
    }

    pub fn get_following(&self) -> Option<&PlaybackItem> {
        if let Some(position) = self.positions.get(self.position).copied() {
            if let Some(item) = self.items.get(position) {
                return Some(item);
            }
        } else {
            return self.user_items.first();
        }
        None
    }

    fn previous_position(&self) -> usize {
        match self.behavior {
            QueueBehavior::Sequential
            | QueueBehavior::Random
            | QueueBehavior::LoopTrack
            | QueueBehavior::LoopAll => self.position.saturating_sub(1),
        }
    }

    fn next_position(&self) -> usize {
        match self.behavior {
            QueueBehavior::Sequential | QueueBehavior::Random | QueueBehavior::LoopTrack => {
                self.position + 1
            }
            QueueBehavior::LoopAll => (self.position + 1) % self.items.len(),
        }
    }

    fn following_position(&self) -> usize {
        match self.behavior {
            QueueBehavior::Sequential | QueueBehavior::Random => self.position + 1,
            QueueBehavior::LoopTrack => self.position,
            QueueBehavior::LoopAll => (self.position + 1) % self.items.len(),
        }
    }
}


================================================
FILE: psst-core/src/player/storage.rs
================================================
use std::{
    fs::File,
    io,
    io::{Read, Seek, SeekFrom, Write},
    ops::Range,
    path::{Path, PathBuf},
    sync::Arc,
};

use crossbeam_channel::{unbounded, Receiver, Sender};
use parking_lot::{Condvar, Mutex};
use rangemap::RangeSet;
use tempfile::NamedTempFile;

pub enum StreamRequest {
    Preload { offset: u64, length: u64 },
    Blocked { offset: u64 },
}

pub struct StreamStorage {
    file: StreamFile,
    data_map: Arc<StreamDataMap>,
    req_receiver: Receiver<StreamRequest>,
    req_sender: Sender<StreamRequest>,
}

pub struct StreamReader {
    reader: File,
    data_map: Arc<StreamDataMap>,
    req_sender: Sender<StreamRequest>,
}

pub struct StreamWriter {
    writer: File,
    data_map: Arc<StreamDataMap>,
}

impl StreamStorage {
    pub fn new(total_size: u64) -> io::Result<StreamStorage> {
        // Use a temporary file for the backing storage, stretched to the full size, so
        // we can seek freely.
        let tmp_file = NamedTempFile::new()?;
        tmp_file.as_file().set_len(total_size)?;

        // Create a channel for requesting downloads of data.
        let (data_req_sender, data_req_receiver) = unbounded();

        Ok(StreamStorage {
            file: StreamFile::Temporary(tmp_file),
            req_receiver: data_req_receiver,
            req_sender: data_req_sender,
            data_map: Arc::new(StreamDataMap {
                total_size,
                downloaded: Mutex::new(RangeSet::new()),
                requested: Mutex::new(RangeSet::new()),
                condvar: Condvar::new(),
            }),
        })
    }

    pub fn from_complete_file(path: PathBuf) -> io::Result<StreamStorage> {
        // Query for the total file size.
        let total_size = path.metadata()?.len();

        // Create the data channel even though it will not be used, as the file should
        // be complete.  We could also turn these into `Option`s.
        let (data_req_sender, data_req_receiver) = unbounded();

        // Because the file is complete, let's mark the full range of data as
        // downloaded.  We mark it as requested as well, because the downloaded set is
        // always ⊆ the requested set.
        let mut downloaded_set = RangeSet::new();
        downloaded_set.insert(0..total_size);
        let requested_set = downloaded_set.clone();

        Ok(StreamStorage {
            file: StreamFile::Persisted(path),
            req_receiver: data_req_receiver,
            req_sender: data_req_sender,
            data_map: Arc::new(StreamDataMap {
                total_size,
                downloaded: Mutex::new(downloaded_set),
                requested: Mutex::new(requested_set),
                condvar: Condvar::new(),
            }),
        })
    }

    pub fn reader(&self) -> io::Result<StreamReader> {
        Ok(StreamReader {
            reader: self.file.reopen()?, // Re-opened files have a starting seek position.
            data_map: self.data_map.clone(),
            req_sender: self.req_sender.clone(),
        })
    }

    pub fn writer(&self) -> io::Result<StreamWriter> {
        Ok(StreamWriter {
            writer: self.file.reopen()?, // Re-opened files have a starting seek position.
            data_map: self.data_map.clone(),
        })
    }

    pub fn receiver(&self) -> &Receiver<StreamRequest> {
        &self.req_receiver
    }

    pub fn path(&self) -> &Path {
        self.file.path()
    }
}

enum StreamFile {
    Temporary(NamedTempFile),
    Persisted(PathBuf),
}

impl StreamFile {
    fn reopen(&self) -> io::Result<File> {
        match self {
            StreamFile::Temporary(tmp_file) => tmp_file.reopen(),
            StreamFile::Persisted(path) => File::open(path),
        }
    }

    fn path(&self) -> &Path {
        match self {
            StreamFile::Temporary(tmp_file) => tmp_file.path(),
            StreamFile::Persisted(path) => path,
        }
    }
}

impl StreamWriter {
    pub fn is_complete(&self) -> bool {
        self.data_map.is_complete()
    }

    pub fn mark_as_not_requested(&self, offset: u64, length: u64) {
        self.data_map.mark_as_not_requested(offset, length);
    }
}

impl Write for StreamWriter {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let position = self.writer.stream_position()?;
        let written = self.writer.write(buf)?;
        self.data_map.mark_as_downloaded(position, written as u64);
        Ok(written)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.writer.flush()
    }
}

impl Seek for StreamWriter {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        self.writer.seek(pos)
    }
}

const MINIMUM_READ_LENGTH: u64 = 1024 * 64;
const PREFETCH_READ_LENGTH: u64 = 1024 * 256;

impl Read for StreamReader {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let position = self.reader.stream_position()?;
        let remaining_len = self.data_map.remaining(position);
        if remaining_len == 0 {
            return Ok(0); // We're at the end of the file.
        }
        let needed_len = remaining_len.min(buf.len() as u64);

        // Make sure that at least `PREFETCH_READ_LENGTH` bytes in front of the reading
        // head is requested.
        let prefetch_len = needed_len.max(PREFETCH_READ_LENGTH).min(remaining_len);
        for (pos, len) in self.data_map.not_yet_requested(position, prefetch_len) {
            let req_len = len.max(MINIMUM_READ_LENGTH);
            self.data_map.mark_as_requested(pos, req_len);
            self.req_sender
                .send(StreamRequest::Preload {
                    offset: pos,
                    length: req_len,
                })
                .expect("Data request channel was closed");
        }

        // Block and wait until at least a part of the range is available, and read it.
        let ready_to_read_len = self.data_map.wait_for(position, |offset| {
            // Notify the servicing thread we are blocked, so it can possibly prioritize the
            // blocked offset.
            self.req_sender
                .send(StreamRequest::Blocked { offset })
                .expect("Data request channel was closed");
        });
        assert!(ready_to_read_len > 0);
        self.reader
            .read(&mut buf[..ready_to_read_len.min(needed_len) as usize])
    }
}

impl Seek for StreamReader {
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
        self.reader.seek(pos)
    }
}

#[derive(Debug)]
struct StreamDataMap {
    total_size: u64,
    // Contains ranges of data requested from the server.  Downloaded ranges are not removed from
    // this set.
    requested: Mutex<RangeSet<u64>>,
    // Contains ranges of data sure to be present in the backing storage.  Always a subset of the
    // requested ranges.
    downloaded: Mutex<RangeSet<u64>>,
    condvar: Condvar,
}

impl StreamDataMap {
    /// Return the number of bytes from offset until the end of the file.
    fn remaining(&self, offset: u64) -> u64 {
        self.total_size.saturating_sub(offset)
    }

    /// Return a vector of sub-ranges of `offset..offset+length` that have not
    /// yet been requested from the backend.
    fn not_yet_requested(&self, offset: u64, length: u64) -> Vec<(u64, u64)> {
        self.requested
            .lock()
            .gaps(&(offset..offset + length))
            .map(|r| range_to_offset_and_length(&r))
            .collect()
    }

    /// Mark given range as requested from the backend, so we can avoid
    /// requesting it more than once.
    fn mark_as_requested(&self, offset: u64, length: u64) {
        self.requested.lock().insert(offset..offset + length);
    }

    /// Remove range previously marked as requested.
    fn mark_as_not_requested(&self, offset: u64, length: u64) {
        self.requested.lock().remove(offset..offset + length);
    }

    /// Mark the range as downloaded and notify the `self.condvar`, so tasks
    /// currently blocked in `self.wait_for` are woken up.
    fn mark_as_downloaded(&self, offset: u64, length: u64) {
        self.downloaded.lock().insert(offset..offset + length);
        self.condvar.notify_all();
    }

    /// Block, waiting until at least some data at given offset is downloaded.
    /// Returns length that is available.  See `self.mark_as_downloaded`.
    fn wait_for(&self, offset: u64, blocking_callback: impl Fn(u64)) -> u64 {
        let mut downloaded = self.downloaded.lock();
        let mut called_callback = false;
        loop {
            if let Some(range) = downloaded.get(&offset) {
                let (over_ofs, over_len) = range_to_offset_and_length(range);
                let offset_from_overlapping = offset - over_ofs;
                let available_len = over_len - offset_from_overlapping;
                // There is `available_len` bytes of data downloaded, stop waiting.
                break available_len;
            } else {
                // Call the blocking callback, but only the first time we are waiting.
                if !called_callback {
                    called_callback = true;
                    blocking_callback(offset);
                }
                // There are no overlaps, wait.
                self.condvar.wait(&mut downloaded);
            }
        }
    }

    // Returns true if data is completely downloaded.
    fn is_complete(&self) -> bool {
        self.downloaded
            .lock()
            .gaps(&(0..self.total_size))
            .next()
            .is_none()
    }
}

fn range_to_offset_and_length(range: &Range<u64>) -> (u64, u64) {
    (range.start, range.end - range.start)
}


================================================
FILE: psst-core/src/player/worker.rs
================================================
use std::{
    ops::Range,
    sync::{
        atomic::{AtomicU64, Ordering},
        Arc,
    },
    time::Duration,
};

use crossbeam_channel::Sender;
use rb::{Consumer, Producer, RbConsumer, RbProducer, SpscRb, RB};
use symphonia::core::{
    audio::{SampleBuffer, SignalSpec},
    units::TimeBase,
};

use crate::{
    actor::{Act, Actor, ActorHandle},
    audio::{
        decode::AudioDecoder,
        output::{AudioSink, DefaultAudioSink},
        resample::ResamplingQuality,
        source::{AudioSource, ResampledSource, StereoMappedSource},
    },
    error::Error,
};

use super::{
    file::{MediaFile, MediaPath},
    LoadedPlaybackItem, PlayerEvent,
};

pub struct PlaybackManager {
    sink: DefaultAudioSink,
    event_send: Sender<PlayerEvent>,
    current: Option<(MediaPath, Sender<Msg>)>,
}

impl PlaybackManager {
    pub fn new(sink: DefaultAudioSink, event_send: Sender<PlayerEvent>) -> Self {
        Self {
            sink,
            event_send,
            current: None,
        }
    }

    pub fn play(&mut self, loaded: LoadedPlaybackItem) {
        let path = loaded.file.path();
        let source = DecoderSource::new(
            loaded.file,
            loaded.source,
            loaded.norm_factor,
            self.event_send.clone(),
        );
        self.current = Some((path, source.actor.sender()));
        if source.sample_rate() == self.sink.sample_rate()
            && source.channel_count() == self.sink.channel_count()
        {
            // We can start playing the source right away.
            self.sink.play(source);
        } else {
            // Some output streams have different sample rate than the source, so we need to
            // resample before pushing to the sink.
            let source = ResampledSource::new(
                source,
                self.sink.sample_rate(),
                ResamplingQuality::SincMediumQuality,
            );
            // Source output streams also have a different channel count. Map the stereo
            // channels and silence the others.
            let source = StereoMappedSource::new(source, self.sink.channel_count());
            self.sink.play(source);
        }
        self.sink.resume();
    }

    pub fn seek(&self, position: Duration) {
        if let Some((path, worker)) = &self.current {
            let _ = worker.send(Msg::Seek(position));

            // Because the position events are sent in the `DecoderSource`, doing this here
            // is slightly hacky. The alternative would be propagating `event_send` into the
            // worker.
            let _ = self.event_send.send(PlayerEvent::Position {
                path: path.to_owned(),
                position,
            });
        }
    }
}

pub struct DecoderSource {
    file: MediaFile,
    actor: ActorHandle<Msg>,
    consumer: Consumer<f32>,
    event_send: Sender<PlayerEvent>,
    total_samples: Arc<AtomicU64>,
    position: Arc<AtomicU64>,
    precision: u64,
    reported: u64,
    end_of_track: bool,
    norm_factor: f32,
    signal_spec: SignalSpec,
    time_base: TimeBase,
}

impl DecoderSource {
    pub fn new(
        file: MediaFile,
        decoder: AudioDecoder,
        norm_factor: f32,
        event_send: Sender<PlayerEvent>,
    ) -> Self {
        const REPORT_PRECISION: Duration = Duration::from_millis(900);

        // Gather the source signal parameters and compute how often we should report
        // the play-head position.
        let signal_spec = decoder.signal_spec();
        let time_base = decoder.codec_params().time_base.unwrap();
        let precision = (signal_spec.rate as f64
            * signal_spec.channels.count() as f64
            * REPORT_PRECISION.as_secs_f64()) as u64;

        // Create a ring-buffer for the decoded samples.  Worker thread is producing,
        // we are consuming in the `AudioSource` impl.
        let buffer = Worker::default_buffer();
        let consumer = buffer.consumer();

        // We keep track of the current play-head position by sharing an atomic sample
        // counter with the decoding worker.  Worker is setting this on seek, we are
        // incrementing on reading from the ring-buffer.
        let position = Arc::new(AtomicU64::new(0));

        // Because the `n_frames` count that Symphonia gives us can be a bit unreliable,
        // we track the total number of samples in this stream in this atomic, set when
        // the underlying decoder returns EOF.
        let total_samples = Arc::new(AtomicU64::new(u64::MAX));

        // Spawn the worker and kick-start the decoding.  The buffer will start filling
        // now.
        let actor = Worker::spawn_with_default_cap("audio_decoding", {
            let position = Arc::clone(&position);
            let total_samples = Arc::clone(&total_samples);
            move |this| Worker::new(this, decoder, buffer, position, total_samples)
        });
        let _ = actor.send(Msg::Read);

        Self {
            file,
            actor,
            consumer,
            event_send,
            norm_factor,
            signal_spec,
            time_base,
            total_samples,
            end_of_track: false,
            position,
            precision,
            reported: u64::MAX, // Something sufficiently distinct from any position.
        }
    }

    fn written_samples(&self, position: u64) -> u64 {
        self.position.fetch_add(position, Ordering::Relaxed) + position
    }

    fn should_report(&self, pos: u64) -> bool {
        self.reported > pos || pos - self.reported >= self.precision
    }

    fn samples_to_duration(&self, samples: u64) -> Duration {
        let frames = samples / self.signal_spec.channels.count() as u64;
        let time = self.time_base.calc_time(frames);
        Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac)
    }
}

impl AudioSource for DecoderSource {
    fn write(&mut self, output: &mut [f32]) -> usize {
        if self.end_of_track {
            return 0;
        }
        let written = self.consumer.read(output).unwrap_or(0);

        // Apply the normalization factor.
        output[..written]
            .iter_mut()
            .for_each(|s| *s *= self.norm_factor);

        let position = self.written_samples(written as u64);
        if self.should_report(position) {
            // Send a position report, so the upper layers can visualize the playback
            // progress and preload the next track.  We cannot block here, so if the channel
            // is full, we just try the next time instead of waiting.
            if self
                .event_send
                .try_send(PlayerEvent::Position {
                    path: self.file.path(),
                    position: self.samples_to_duration(position),
                })
                .is_ok()
            {
                self.reported = position;
            }
        }

        let total_samples = self.total_samples.load(Ordering::Relaxed);
        if position >= total_samples {
            // After reading the total number of samples, we stop. Signal to the upper layer
            // this track is over and short-circuit all further reads from this source.
            if self.event_send.try_send(PlayerEvent::EndOfTrack).is_ok() {
                self.end_of_track = true;
            }
        }

        written
    }

    fn channel_count(&self) -> usize {
        self.signal_spec.channels.count()
    }

    fn sample_rate(&self) -> u32 {
        self.signal_spec.rate
    }
}

impl Drop for DecoderSource {
    fn drop(&mut self) {
        let _ = self.actor.send(Msg::Stop);
    }
}

enum Msg {
    Seek(Duration),
    Read,
    Stop,
}

struct Worker {
    /// Sending part of our own actor channel.
    this: Sender<Msg>,
    /// Decoder we are reading packets/samples from.
    input: AudioDecoder,
    /// Audio properties of the decoded signal.
    input_spec: SignalSpec,
    /// Sample buffer containing samples read in the last packet.
    input_packet: SampleBuffer<f32>,
    /// Ring-buffer for the output signal.
    output: SpscRb<f32>,
    /// Producing part of the output ring-buffer.
    output_producer: Producer<f32>,
    /// Shared atomic position.  We update this on seek only.
    position: Arc<AtomicU64>,
    /// Shared atomic for total number of samples.  We set this on EOF.
    total_samples: Arc<AtomicU64>,
    /// Range of samples in `resampled` that are awaiting flush into `output`.
    samples_to_write: Range<usize>,
    /// Number of samples written into the output channel.
    samples_written: u64,
    /// Are we in the middle of automatic read loop?
    is_reading: bool,
}

impl Worker {
    fn default_buffer() -> SpscRb<f32> {
        const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;

        SpscRb::new(DEFAULT_BUFFER_SIZE)
    }

    fn new(
        this: Sender<Msg>,
        input: AudioDecoder,
        output: SpscRb<f32>,
        position: Arc<AtomicU64>,
        total_samples: Arc<AtomicU64>,
    ) -> Self {
        const DEFAULT_MAX_FRAMES: u64 = 8 * 1024;

        let max_input_frames = input
            .codec_params()
            .max_frames_per_packet
            .unwrap_or(DEFAULT_MAX_FRAMES);

        // Promote the worker thread to audio priority to prevent buffer under-runs on
        // high CPU usage.
        if let Err(err) =
            audio_thread_priority::promote_current_thread_to_real_time(0, input.signal_spec().rate)
        {
            log::warn!("failed to promote thread to audio priority: {err}");
        }

        Self {
            output_producer: output.producer(),
            input_packet: SampleBuffer::new(max_input_frames, input.signal_spec()),
            input_spec: input.signal_spec(),
            input,
            this,
            output,
            position,
            total_samples,
            samples_written: 0,
            samples_to_write: 0..0, // Arbitrary empty range.
            is_reading: false,
        }
    }
}

impl Actor for Worker {
    type Message = Msg;
    type Error = Error;

    fn handle(&mut self, msg: Msg) -> Result<Act<Self>, Self::Error> {
        match msg {
            Msg::Seek(time) => self.on_seek(time),
            Msg::Read => self.on_read(),
            Msg::Stop => Ok(Act::Shutdown),
        }
    }
}

impl Worker {
    fn on_seek(&mut self, time: Duration) -> Result<Act<Self>, Error> {
        match self.input.seek(time) {
            Ok(timestamp) => {
                if self.is_reading {
                    self.samples_to_write = 0..0;
                } else {
                    self.this.send(Msg::Read)?;
                }
                let position = timestamp * self.input_spec.channels.count() as u64;
                self.samples_written = position;
                self.position.store(position, Ordering::Relaxed);
                self.output.clear();
            }
            Err(err) => {
                log::error!("failed to seek: {err}");
            }
        }
        Ok(Act::Continue)
    }

    fn on_read(&mut self) -> Result<Act<Self>, Error> {
        if !self.samples_to_write.is_empty() {
            let writable = &self.input_packet.samples()[self.samples_to_write.clone()];
            if let Ok(written) = self.output_producer.write(writable) {
                self.samples_written += written as u64;
                self.samples_to_write.start += written;
                self.is_reading = true;
                self.this.send(Msg::Read)?;
                Ok(Act::Continue)
            } else {
                // Buffer is full.  Wait a bit a try again.  We also have to indicate that the
                // read loop is not running at the moment (if we receive a `Seek` while waiting,
                // we need it to explicitly kickstart reading again).
                self.is_reading = false;
                Ok(Act::WaitOr {
                    timeout: Duration::from_millis(500),
                    timeout_msg: Msg::Read,
                })
            }
        } else {
            match self.input.read_packet(&mut self.input_packet) {
                Some(_) => {
                    self.samples_to_write = 0..self.input_packet.samples().len();
                    self.is_reading = true;
                    self.this.send(Msg::Read)?;
                }
                None => {
                    self.is_reading = false;
                    self.total_samples
                        .store(self.samples_written, Ordering::Relaxed);
                }
            }
            Ok(Act::Continue)
        }
    }
}


================================================
FILE: psst-core/src/session/access_token.rs
================================================
use std::time::{Duration, Instant};

use parking_lot::Mutex;
use serde::Deserialize;

use crate::error::Error;

use super::SessionService;

// Client ID of the official Web Spotify front-end.
pub const CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";

// All scopes we could possibly require.
pub const ACCESS_SCOPES: &str = "streaming,user-read-email,user-read-private,playlist-read-private,playlist-read-collaborative,playlist-modify-public,playlist-modify-private,user-follow-modify,user-follow-read,user-library-read,user-library-modify,user-top-read,user-read-recently-played";

// Consider token expired even before the official expiration time.  Spotify
// seems to be reporting excessive token TTLs so let's cut it down by 30
// minutes.
const EXPIRATION_TIME_THRESHOLD: Duration = Duration::from_secs(60 * 30);

#[derive(Clone)]
pub struct AccessToken {
    pub token: String,
    pub expires: Instant,
}

impl AccessToken {
    fn expired() -> Self {
        Self {
            token: String::new(),
            expires: Instant::now(),
        }
    }

    pub fn request(session: &SessionService) -> Result<Self, Error> {
        #[derive(Deserialize)]
        struct MercuryAccessToken {
            #[serde(rename = "expiresIn")]
            expires_in: u64,
            #[serde(rename = "accessToken")]
            access_token: String,
        }

        let token: MercuryAccessToken = session.connected()?.get_mercury_json(format!(
            "hm://keymaster/token/authenticated?client_id={CLIENT_ID}&scope={ACCESS_SCOPES}",
        ))?;

        Ok(Self {
            token: token.access_token,
            expires: Instant::now() + Duration::from_secs(token.expires_in),
        })
    }

    fn is_expired(&self) -> bool {
        self.expires.saturating_duration_since(Instant::now()) < EXPIRATION_TIME_THRESHOLD
    }
}

pub struct TokenProvider {
    token: Mutex<AccessToken>,
}

impl TokenProvider {
    pub fn new() -> Self {
        Self {
            token: Mutex::new(AccessToken::expired()),
        }
    }

    pub fn get(&self, session: &SessionService) -> Result<AccessToken, Error> {
        let mut token = self.token.lock();
        if token.is_expired() {
            log::info!("access token expired, requesting");
            *token = AccessToken::request(session)?;
        }
        Ok(token.clone())
    }
}


================================================
FILE: psst-core/src/session/audio_key.rs
================================================
use std::{
    collections::HashMap,
    io::{Cursor, Read},
};

use byteorder::{ReadBytesExt, BE};
use crossbeam_channel::Sender;

use crate::{
    audio::decrypt::AudioKey,
    connection::shannon_codec::ShannonMsg,
    error::Error,
    item_id::{FileId, ItemId},
    util::Sequence,
};

pub struct AudioKeyDispatcher {
    sequence: Sequence<u32>,
    pending: HashMap<u32, Sender<Result<AudioKey, Error>>>,
}

impl AudioKeyDispatcher {
    pub fn new() -> Self {
        Self {
            sequence: Sequence::new(0),
            pending: HashMap::new(),
        }
    }

    pub fn enqueue_request(
        &mut self,
        track: ItemId,
        file: FileId,
        callback: Sender<Result<AudioKey, Error>>,
    ) -> ShannonMsg {
        let seq = self.sequence.advance();
        self.pending.insert(seq, callback);
        Self::make_key_request(seq, track, file)
    }

    fn make_key_request(seq: u32, track: ItemId, file: FileId) -> ShannonMsg {
        let mut buf = Vec::new();
        buf.extend(file.0);
        buf.extend(track.to_raw());
        buf.extend(seq.to_be_bytes());
        buf.extend(0_u16.to_be_bytes());
        ShannonMsg::new(ShannonMsg::REQUEST_KEY, buf)
    }

    pub fn handle_aes_key(&mut self, msg: ShannonMsg) {
        let mut payload = Cursor::new(msg.payload);
        let seq = payload.read_u32::<BE>().unwrap();

        if let Some(tx) = self.pending.remove(&seq) {
            let mut key = [0_u8; 16];
            payload.read_exact(&mut key).unwrap();

            if tx.send(Ok(AudioKey(key))).is_err() {
                log::warn!("missing receiver for audio key, seq: {seq}");
            }
        } else {
            log::warn!("received unexpected audio key msg, seq: {seq}");
        }
    }

    pub fn handle_aes_key_error(&mut self, msg: ShannonMsg) {
        let mut payload = Cursor::new(msg.payload);
        let seq = payload.read_u32::<BE>().unwrap();

        if let Some(tx) = self.pending.remove(&seq) {
            log::error!("audio key error");
            if tx.send(Err(Error::UnexpectedResponse)).is_err() {
                log::warn!("missing receiver for audio key error, seq: {seq}");
            }
        } else {
            log::warn!("received unknown audio key, seq: {seq}");
        }
    }
}


================================================
FILE: psst-core/src/session/client_token.rs
================================================
// Ported from librespot

use crate::error::Error;
use crate::session::token::{Token};
use crate::util::{default_ureq_agent_builder, solve_hash_cash};
use data_encoding::HEXUPPER_PERMISSIVE;
use librespot_protocol::clienttoken_http::{
    ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
    ClientTokenResponse, ClientTokenResponseType,
};
use parking_lot::Mutex;
use protobuf::{Enum, Message};
use std::time::{Duration, Instant};
use crate::system_info::{CLIENT_ID, DEVICE_ID, OS, SPOTIFY_SEMANTIC_VERSION};

pub struct ClientTokenProvider {
    token: Mutex<Option<Token>>,
    agent: ureq::Agent,
}

impl ClientTokenProvider {
    pub fn new(proxy_url: Option<&str>) -> Self {
        Self {
            token: Mutex::new(None),
            agent: default_ureq_agent_builder(proxy_url).build().into(),
        }
    }

    fn request<M: Message>(&self, message: &M) -> Result<Vec<u8>, Error> {
        let body = message.write_to_bytes()?;

        let mut response = self
            .agent
            .post("https://clienttoken.spotify.com/v1/clienttoken")
            .header("Accept", "application/x-protobuf")
            .send(body)?;

        let vec = response.body_mut().read_to_vec();
        Ok(vec?)
    }

    fn request_new_token(&self) -> Result<Token, Error> {
        log::debug!("Requesting new token...");

        let mut request = ClientTokenRequest::new();
        request.request_type = ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST.into();

        let client_data = request.mut_client_data();

        client_data.client_version = SPOTIFY_SEMANTIC_VERSION.into();
        client_data.client_id = CLIENT_ID.into();

        let connectivity_data = client_data.mut_connectivity_sdk_data();
        connectivity_data.device_id = DEVICE_ID.to_string();

        let platform_data = connectivity_data
            .platform_specific_data
            .mut_or_insert_default();

        let os_version = sysinfo::System::os_version().unwrap_or("0".into());
        let kernel_version = sysinfo::System::kernel_version().unwrap_or_else(|| String::from("0"));

        match OS {
            "windows" => {
                let os_version = os_version.parse::<f32>().unwrap_or(10.) as i32;
                let kernel_version = kernel_version.parse::<i32>().unwrap_or(21370);

                let (pe, image_file) = match std::env::consts::ARCH {
                    "arm" => (448, 452),
                    "aarch64" => (43620, 452),
                    "x86_64" => (34404, 34404),
                    _ => (332, 332), // x86
                };

                let windows_data = platform_data.mut_desktop_windows();
                windows_data.os_version = os_version;
                windows_data.os_build = kernel_version;
                windows_data.platform_id = 2;
                windows_data.unknown_value_6 = 9;
                windows_data.image_file_machine = image_file;
                windows_data.pe_machine = pe;
                windows_data.unknown_value_10 = true;
            }
            "ios" => {
                let ios_data = platform_data.mut_ios();
                ios_data.user_interface_idiom = 0;
                ios_data.target_iphone_simulator = false;
                ios_data.hw_machine = "iPhone14,5".to_string();
                ios_data.system_version = os_version;
            }
            "android" => {
                let android_data = platform_data.mut_android();
                android_data.android_version = os_version;
                android_data.api_version = 31;
                "Pixel".clone_into(&mut android_data.device_name);
                "GF5KQ".clone_into(&mut android_data.model_str);
                "Google".clone_into(&mut android_data.vendor);
            }
            "macos" => {
                let macos_data = platform_data.mut_desktop_macos();
                macos_data.system_version = os_version;
                macos_data.hw_model = "iMac21,1".to_string();
                macos_data.compiled_cpu_type = std::env::consts::ARCH.to_string();
            }
            _ => {
                let linux_data = platform_data.mut_desktop_linux();
                linux_data.system_name = "Linux".to_string();
                linux_data.system_release = kernel_version;
                linux_data.system_version = os_version;
                linux_data.hardware = std::env::consts::ARCH.to_string();
            }
        }

        let mut response = self.request(&request)?;
        let mut count = 0;
        const MAX_TRIES: u8 = 3;

        let token_response = loop {
            count += 1;

            let message = ClientTokenResponse::parse_from_bytes(&response)?;

            match ClientTokenResponseType::from_i32(message.response_type.value()) {
                // depending on the platform, you're either given a token immediately
                // or are presented a hash cash challenge to solve first
                Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => {
                    log::debug!("Received a granted token");
                    break message;
                }
                Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
                    log::debug!("Received a hash cash challenge, solving...");

                    let challenges = message.challenges().clone();
                    let state = challenges.state;
                    if let Some(challenge) = challenges.challenges.first() {
                        let hash_cash_challenge = challenge.evaluate_hashcash_parameters();

                        let ctx = vec![];
                        let prefix = HEXUPPER_PERMISSIVE
                            .decode(hash_cash_challenge.prefix.as_bytes
Download .txt
gitextract_nr4iye45/

├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── build.yml
├── .gitignore
├── .homebrew/
│   └── generate_formula.sh
├── .pkg/
│   ├── APPIMAGE/
│   │   └── pkg2appimage-ingredients.yml
│   ├── DEBIAN/
│   │   └── control
│   ├── copyright
│   └── psst.desktop
├── .rustfmt.toml
├── Cargo.toml
├── Cross.toml
├── LICENSE.md
├── README.md
├── psst-cli/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs
├── psst-core/
│   ├── Cargo.toml
│   ├── build.rs
│   └── src/
│       ├── actor.rs
│       ├── audio/
│       │   ├── decode.rs
│       │   ├── decrypt.rs
│       │   ├── mod.rs
│       │   ├── normalize.rs
│       │   ├── output/
│       │   │   ├── cpal.rs
│       │   │   ├── cubeb.rs
│       │   │   └── mod.rs
│       │   ├── probe.rs
│       │   ├── resample.rs
│       │   └── source.rs
│       ├── cache.rs
│       ├── cdn.rs
│       ├── connection/
│       │   ├── diffie_hellman.rs
│       │   ├── mod.rs
│       │   └── shannon_codec.rs
│       ├── error.rs
│       ├── item_id.rs
│       ├── lastfm.rs
│       ├── lib.rs
│       ├── metadata.rs
│       ├── oauth.rs
│       ├── player/
│       │   ├── file.rs
│       │   ├── item.rs
│       │   ├── mod.rs
│       │   ├── queue.rs
│       │   ├── storage.rs
│       │   └── worker.rs
│       ├── session/
│       │   ├── access_token.rs
│       │   ├── audio_key.rs
│       │   ├── client_token.rs
│       │   ├── login5.rs
│       │   ├── mercury.rs
│       │   ├── mod.rs
│       │   └── token.rs
│       ├── system_info.rs
│       └── util.rs
└── psst-gui/
    ├── Cargo.toml
    ├── assets/
    │   ├── logo.afdesign
    │   └── logo.icns
    ├── build-icons.sh
    ├── build.rs
    └── src/
        ├── cmd.rs
        ├── controller/
        │   ├── after_delay.rs
        │   ├── alert_cleanup.rs
        │   ├── ex_click.rs
        │   ├── ex_cursor.rs
        │   ├── ex_scroll.rs
        │   ├── input.rs
        │   ├── mod.rs
        │   ├── nav.rs
        │   ├── on_command.rs
        │   ├── on_command_async.rs
        │   ├── on_debounce.rs
        │   ├── on_update.rs
        │   ├── playback.rs
        │   ├── session.rs
        │   └── sort.rs
        ├── data/
        │   ├── album.rs
        │   ├── artist.rs
        │   ├── config.rs
        │   ├── ctx.rs
        │   ├── find.rs
        │   ├── id.rs
        │   ├── mod.rs
        │   ├── nav.rs
        │   ├── playback.rs
        │   ├── playlist.rs
        │   ├── promise.rs
        │   ├── recommend.rs
        │   ├── search.rs
        │   ├── show.rs
        │   ├── slider_scroll_scale.rs
        │   ├── track.rs
        │   ├── user.rs
        │   └── utils.rs
        ├── delegate.rs
        ├── error.rs
        ├── main.rs
        ├── ui/
        │   ├── album.rs
        │   ├── artist.rs
        │   ├── credits.rs
        │   ├── episode.rs
        │   ├── find.rs
        │   ├── home.rs
        │   ├── library.rs
        │   ├── lyrics.rs
        │   ├── menu.rs
        │   ├── mod.rs
        │   ├── playable.rs
        │   ├── playback.rs
        │   ├── playlist.rs
        │   ├── preferences.rs
        │   ├── recommend.rs
        │   ├── search.rs
        │   ├── show.rs
        │   ├── theme.rs
        │   ├── track.rs
        │   ├── user.rs
        │   └── utils.rs
        ├── webapi/
        │   ├── cache.rs
        │   ├── client.rs
        │   ├── local.rs
        │   └── mod.rs
        └── widget/
            ├── checkbox.rs
            ├── dispatcher.rs
            ├── empty.rs
            ├── fill_between.rs
            ├── icons.rs
            ├── link.rs
            ├── maybe.rs
            ├── mod.rs
            ├── overlay.rs
            ├── promise.rs
            ├── remote_image.rs
            ├── theme.rs
            └── utils.rs
Download .txt
SYMBOL INDEX (1637 symbols across 112 files)

FILE: psst-cli/src/main.rs
  function main (line 16) | fn main() {
  function start (line 35) | fn start(track_id: &str, session: SessionService) -> Result<(), Error> {
  function play_item (line 50) | fn play_item(

FILE: psst-core/build.rs
  function main (line 5) | fn main() {

FILE: psst-core/src/actor.rs
  type Act (line 11) | pub enum Act<T: Actor> {
  type Actor (line 20) | pub trait Actor: Sized {
    method handle (line 24) | fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Er...
    method process (line 26) | fn process(mut self, recv: Receiver<Self::Message>) {
    method spawn (line 60) | fn spawn<F>(cap: Capacity, name: &str, factory: F) -> ActorHandle<Self...
    method spawn_with_default_cap (line 76) | fn spawn_with_default_cap<F>(name: &str, factory: F) -> ActorHandle<Se...
  type ActorHandle (line 84) | pub struct ActorHandle<M> {
  function sender (line 90) | pub fn sender(&self) -> Sender<M> {
  function join (line 94) | pub fn join(self) {
  function send (line 98) | pub fn send(&self, msg: M) -> Result<(), SendError<M>> {
  function try_send (line 102) | pub fn try_send(&self, msg: M) -> Result<(), TrySendError<M>> {
  type Capacity (line 107) | pub enum Capacity {
    method to_channel (line 114) | pub fn to_channel<T>(&self) -> (Sender<T>, Receiver<T>) {

FILE: psst-core/src/audio/decode.rs
  type AudioCodecFormat (line 21) | pub enum AudioCodecFormat {
    method format_reader (line 27) | fn format_reader(
    method decoder (line 43) | fn decoder(&self, codec_params: &CodecParameters) -> Result<Box<dyn De...
  type AudioDecoder (line 57) | pub struct AudioDecoder {
    method new (line 64) | pub fn new<T>(input: T, codec: AudioCodecFormat) -> Result<Self, Error>
    method codec_params (line 83) | pub fn codec_params(&self) -> &CodecParameters {
    method signal_spec (line 87) | pub fn signal_spec(&self) -> SignalSpec {
    method seek (line 94) | pub fn seek(&mut self, time: Duration) -> Result<TimeStamp, Error> {
    method read_packet (line 107) | pub fn read_packet<S>(&mut self, samples: &mut SampleBuffer<S>) -> Opt...
  method is_seekable (line 161) | fn is_seekable(&self) -> bool {
  method byte_len (line 165) | fn byte_len(&self) -> Option<u64> {
  method from (line 171) | fn from(err: SymphoniaError) -> Error {

FILE: psst-core/src/audio/decrypt.rs
  constant AUDIO_AESIV (line 9) | const AUDIO_AESIV: [u8; 16] = [
  type AudioKey (line 14) | pub struct AudioKey(pub [u8; 16]);
    method from_raw (line 17) | pub fn from_raw(data: &[u8]) -> Option<Self> {
  type AudioDecrypt (line 22) | pub struct AudioDecrypt<T> {
  function new (line 28) | pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
  function read (line 38) | fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
  function seek (line 48) | fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {

FILE: psst-core/src/audio/normalize.rs
  type NormalizationLevel (line 9) | pub enum NormalizationLevel {
  type NormalizationData (line 16) | pub struct NormalizationData {
    method parse (line 24) | pub fn parse(mut file: impl Read + Seek) -> io::Result<Self> {
    method factor_for_level (line 42) | pub fn factor_for_level(&self, level: NormalizationLevel, pregain: f32...
    method factor (line 50) | fn factor(pregain: f32, gain: f32, peak: f32) -> f32 {

FILE: psst-core/src/audio/output/cpal.rs
  type CpalOutput (line 14) | pub struct CpalOutput {
    method open (line 20) | pub fn open() -> Result<Self, Error> {
    method preferred_output_config (line 54) | fn preferred_output_config(
  type Sink (line 76) | type Sink = CpalSink;
  method sink (line 78) | fn sink(&self) -> Self::Sink {
  type CpalSink (line 84) | pub struct CpalSink {
    method send_to_callback (line 92) | fn send_to_callback(&self, msg: CallbackMsg) {
    method send_to_stream (line 98) | fn send_to_stream(&self, msg: StreamMsg) {
  method channel_count (line 106) | fn channel_count(&self) -> usize {
  method sample_rate (line 110) | fn sample_rate(&self) -> u32 {
  method set_volume (line 114) | fn set_volume(&self, volume: f32) {
  method play (line 118) | fn play(&self, source: impl AudioSource) {
  method pause (line 122) | fn pause(&self) {
  method resume (line 127) | fn resume(&self) {
  method stop (line 132) | fn stop(&self) {
  method close (line 137) | fn close(&self) {
  type Stream (line 142) | struct Stream {
    method open (line 148) | fn open(
  type Message (line 182) | type Message = StreamMsg;
  type Error (line 183) | type Error = Error;
  method handle (line 185) | fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Erro...
  type StreamMsg (line 210) | enum StreamMsg {
  type CallbackMsg (line 216) | enum CallbackMsg {
  type CallbackState (line 223) | enum CallbackState {
  type StreamCallback (line 228) | struct StreamCallback {
    method write_samples (line 238) | fn write_samples(&mut self, output: &mut [f32]) {
  method from (line 279) | fn from(err: cpal::DefaultStreamConfigError) -> Error {
  method from (line 285) | fn from(err: cpal::SupportedStreamConfigsError) -> Error {
  method from (line 291) | fn from(err: cpal::BuildStreamError) -> Error {
  method from (line 297) | fn from(err: cpal::PlayStreamError) -> Error {
  method from (line 303) | fn from(err: cpal::PauseStreamError) -> Error {

FILE: psst-core/src/audio/output/cubeb.rs
  type CubebOutput (line 14) | pub struct CubebOutput {
    method open (line 21) | pub fn open() -> Result<Self, Error> {
  type Sink (line 37) | type Sink = CubebSink;
  method sink (line 39) | fn sink(&self) -> Self::Sink {
  type Frame (line 44) | type Frame = cubeb::StereoFrame<f32>;
  constant STREAM_CHANNELS (line 46) | const STREAM_CHANNELS: usize = 2;
  constant SAMPLE_RATE (line 47) | const SAMPLE_RATE: u32 = 44_100;
  constant STREAM_LATENCY (line 48) | const STREAM_LATENCY: u32 = 0x1000;
  type Stream (line 50) | struct Stream {
    method open (line 57) | fn open(callback_recv: Receiver<CallbackMsg>) -> Result<Self, Error> {
  type StreamMsg (line 102) | enum StreamMsg {
  type Message (line 110) | type Message = StreamMsg;
  type Error (line 111) | type Error = Error;
  method handle (line 113) | fn handle(&mut self, msg: Self::Message) -> Result<Act<Self>, Self::Erro...
  type CubebSink (line 146) | pub struct CubebSink {
  method channel_count (line 152) | fn channel_count(&self) -> usize {
  method sample_rate (line 156) | fn sample_rate(&self) -> u32 {
  method set_volume (line 160) | fn set_volume(&self, volume: f32) {
  method play (line 164) | fn play(&self, source: impl AudioSource) {
  method pause (line 170) | fn pause(&self) {
  method resume (line 175) | fn resume(&self) {
  method stop (line 180) | fn stop(&self) {
  method close (line 184) | fn close(&self) {
  type CallbackMsg (line 189) | enum CallbackMsg {
  type CallbackState (line 195) | enum CallbackState {
  type StreamCallback (line 200) | struct StreamCallback {
    method write_samples (line 208) | fn write_samples(&mut self, output: &mut [Frame]) {
  method from (line 255) | fn from(err: cubeb::Error) -> Self {

FILE: psst-core/src/audio/output/mod.rs
  type DefaultAudioOutput (line 9) | pub type DefaultAudioOutput = cubeb::CubebOutput;
  type DefaultAudioOutput (line 11) | pub type DefaultAudioOutput = cpal::CpalOutput;
  type DefaultAudioSink (line 13) | pub type DefaultAudioSink = <DefaultAudioOutput as AudioOutput>::Sink;
  type AudioOutput (line 15) | pub trait AudioOutput {
    method sink (line 18) | fn sink(&self) -> Self::Sink;
  type AudioSink (line 21) | pub trait AudioSink {
    method channel_count (line 22) | fn channel_count(&self) -> usize;
    method sample_rate (line 23) | fn sample_rate(&self) -> u32;
    method set_volume (line 24) | fn set_volume(&self, volume: f32);
    method play (line 25) | fn play(&self, source: impl AudioSource);
    method pause (line 26) | fn pause(&self);
    method resume (line 27) | fn resume(&self);
    method stop (line 28) | fn stop(&self);
    method close (line 29) | fn close(&self);

FILE: psst-core/src/audio/probe.rs
  type TrackProbe (line 14) | pub struct TrackProbe {
    method new (line 33) | pub fn new(path: &PathBuf) -> Result<Self, Error> {

FILE: psst-core/src/audio/resample.rs
  type ResamplingQuality (line 4) | pub enum ResamplingQuality {
  type ResamplingSpec (line 13) | pub struct ResamplingSpec {
    method output_size (line 20) | pub fn output_size(&self, input_size: usize) -> usize {
    method input_size (line 24) | pub fn input_size(&self, output_size: usize) -> usize {
    method ratio (line 28) | pub fn ratio(&self) -> f64 {
  type AudioResampler (line 33) | pub struct AudioResampler {
    method new (line 39) | pub fn new(quality: ResamplingQuality, spec: ResamplingSpec) -> Result...
    method process (line 55) | pub fn process(&mut self, input: &[f32], output: &mut [f32]) -> Result...
  method drop (line 84) | fn drop(&mut self) {

FILE: psst-core/src/audio/source.rs
  type AudioSource (line 7) | pub trait AudioSource: Send + 'static {
    method write (line 11) | fn write(&mut self, output: &mut [f32]) -> usize;
    method channel_count (line 12) | fn channel_count(&self) -> usize;
    method sample_rate (line 13) | fn sample_rate(&self) -> u32;
    method write (line 20) | fn write(&mut self, _output: &mut [f32]) -> usize {
    method channel_count (line 24) | fn channel_count(&self) -> usize {
    method sample_rate (line 28) | fn sample_rate(&self) -> u32 {
    method write (line 61) | fn write(&mut self, output: &mut [f32]) -> usize {
    method channel_count (line 76) | fn channel_count(&self) -> usize {
    method sample_rate (line 80) | fn sample_rate(&self) -> u32 {
    method write (line 127) | fn write(&mut self, output: &mut [f32]) -> usize {
    method channel_count (line 157) | fn channel_count(&self) -> usize {
    method sample_rate (line 161) | fn sample_rate(&self) -> u32 {
  type Empty (line 17) | pub struct Empty;
  type StereoMappedSource (line 33) | pub struct StereoMappedSource<S> {
  function new (line 44) | pub fn new(source: S, output_channels: usize) -> Self {
  type ResampledSource (line 85) | pub struct ResampledSource<S> {
  function new (line 93) | pub fn new(source: S, output_sample_rate: u32, quality: ResamplingQualit...
  type Buf (line 166) | struct Buf {
    method get (line 173) | fn get(&self) -> &[f32] {
    method len (line 177) | fn len(&self) -> usize {
    method is_empty (line 181) | fn is_empty(&self) -> bool {

FILE: psst-core/src/cache.rs
  type CacheHandle (line 16) | pub type CacheHandle = Arc<Cache>;
  type Cache (line 19) | pub struct Cache {
    method new (line 33) | pub fn new(base: PathBuf) -> Result<CacheHandle, Error> {
    method clear (line 43) | pub fn clear(&self) -> io::Result<()> {
    method get_track (line 63) | pub fn get_track(&self, item_id: ItemId) -> Option<Track> {
    method save_track (line 68) | pub fn save_track(&self, item_id: ItemId, track: &Track) -> Result<(),...
    method track_path (line 74) | fn track_path(&self, item_id: ItemId) -> PathBuf {
    method get_episode (line 81) | pub fn get_episode(&self, item_id: ItemId) -> Option<Episode> {
    method save_episode (line 86) | pub fn save_episode(&self, item_id: ItemId, episode: &Episode) -> Resu...
    method episode_path (line 92) | fn episode_path(&self, item_id: ItemId) -> PathBuf {
    method get_audio_key (line 99) | pub fn get_audio_key(&self, item_id: ItemId, file_id: FileId) -> Optio...
    method save_audio_key (line 104) | pub fn save_audio_key(
    method audio_key_path (line 115) | fn audio_key_path(&self, item_id: ItemId, file_id: FileId) -> PathBuf {
    method audio_file_path (line 125) | pub fn audio_file_path(&self, file_id: FileId) -> PathBuf {
    method save_audio_file (line 129) | pub fn save_audio_file(&self, file_id: FileId, from_path: PathBuf) -> ...
    method get_country_code (line 138) | pub fn get_country_code(&self) -> Option<String> {
    method save_country_code (line 142) | pub fn save_country_code(&self, country_code: &str) -> Result<(), Erro...
    method country_code_path (line 147) | fn country_code_path(&self) -> PathBuf {
  function create_cache_dirs (line 23) | fn create_cache_dirs(base: &Path) -> io::Result<()> {
  function mkdir_if_not_exists (line 152) | pub fn mkdir_if_not_exists(path: &Path) -> io::Result<()> {

FILE: psst-core/src/cdn.rs
  type CdnHandle (line 17) | pub type CdnHandle = Arc<Cdn>;
  type Cdn (line 19) | pub struct Cdn {
    method new (line 26) | pub fn new(session: SessionService, proxy_url: Option<&str>) -> Result...
    method resolve_audio_file_url (line 35) | pub fn resolve_audio_file_url(&self, id: FileId) -> Result<CdnUrl, Err...
    method fetch_file_range (line 72) | pub fn fetch_file_range(
  type CdnUrl (line 90) | pub struct CdnUrl {
    constant DEFAULT_EXPIRATION (line 97) | const DEFAULT_EXPIRATION: Duration = Duration::from_secs(60 * 30);
    constant EXPIRATION_TIME_THRESHOLD (line 100) | const EXPIRATION_TIME_THRESHOLD: Duration = Duration::from_secs(5);
    method new (line 102) | fn new(url: String) -> Self {
    method is_expired (line 111) | pub fn is_expired(&self) -> bool {
  method from (line 117) | fn from(err: ureq::Error) -> Self {
  function range_header (line 123) | fn range_header(offfset: u64, length: u64) -> String {
  function parse_total_content_length (line 132) | fn parse_total_content_length(response: &ureq::http::response::Response<...
  function parse_expiration (line 147) | fn parse_expiration(url: &str) -> Option<Duration> {

FILE: psst-core/src/connection/diffie_hellman.rs
  type DHLocalKeys (line 4) | pub struct DHLocalKeys {
    method random (line 10) | pub fn random() -> DHLocalKeys {
    method public_key (line 19) | pub fn public_key(&self) -> Vec<u8> {
    method shared_secret (line 23) | pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
  function dh_generator (line 30) | fn dh_generator() -> BigUint {
  function dh_prime (line 34) | fn dh_prime() -> BigUint {

FILE: psst-core/src/connection/mod.rs
  constant DEVICE_ID (line 31) | const DEVICE_ID: &str = "Psst";
  constant AP_RESOLVE_ENDPOINT (line 34) | const AP_RESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com";
  constant AP_FALLBACK (line 37) | const AP_FALLBACK: &str = "ap.spotify.com:443";
  type Credentials (line 42) | pub struct Credentials {
    method from_username_and_password (line 49) | pub fn from_username_and_password(username: String, password: String) ...
    method from_access_token (line 57) | pub fn from_access_token(token: String) -> Self {
    method from (line 74) | fn from(value: SerializedCredentials) -> Self {
  type SerializedCredentials (line 67) | struct SerializedCredentials {
    method from (line 84) | fn from(value: Credentials) -> Self {
  type Transport (line 94) | pub struct Transport {
    method resolve_ap_with_fallback (line 101) | pub fn resolve_ap_with_fallback(proxy_url: Option<&str>) -> Vec<String> {
    method resolve_ap (line 114) | pub fn resolve_ap(proxy_url: Option<&str>) -> Result<Vec<String>, Erro...
    method connect (line 136) | pub fn connect(ap_list: &[String], proxy_url: Option<&str>) -> Result<...
    method stream_without_proxy (line 170) | fn stream_without_proxy(ap: &str) -> Result<TcpStream, io::Error> {
    method stream_through_proxy (line 190) | fn stream_through_proxy(ap: &str, url: &str) -> Result<TcpStream, Erro...
    method stream_through_socks5_proxy (line 203) | fn stream_through_socks5_proxy(ap: &str, url: &Url) -> Result<TcpStrea...
    method exchange_keys (line 216) | pub fn exchange_keys(mut stream: TcpStream) -> Result<Self, Error> {
    method authenticate (line 270) | pub fn authenticate(&mut self, credentials: Credentials) -> Result<Cre...
  function read_packet (line 305) | fn read_packet(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
  function make_packet (line 314) | fn make_packet(prefix: &[u8], data: &[u8]) -> Vec<u8> {
  function client_hello (line 324) | fn client_hello(public_key: Vec<u8>, nonce: Vec<u8>) -> Vec<u8> {
  function client_response_plaintext (line 357) | fn client_response_plaintext(challenge: Vec<u8>) -> Vec<u8> {
  function compute_keys (line 378) | fn compute_keys(
  function client_response_encrypted (line 405) | fn client_response_encrypted(credentials: Credentials) -> ShannonMsg {

FILE: psst-core/src/connection/shannon_codec.rs
  type ShannonMsg (line 6) | pub struct ShannonMsg {
    constant SECRET_BLOCK (line 12) | pub const SECRET_BLOCK: u8 = 0x02;
    constant PING (line 13) | pub const PING: u8 = 0x04;
    constant STREAM_CHUNK (line 14) | pub const STREAM_CHUNK: u8 = 0x08;
    constant STREAM_CHUNK_RES (line 15) | pub const STREAM_CHUNK_RES: u8 = 0x09;
    constant CHANNEL_ERROR (line 16) | pub const CHANNEL_ERROR: u8 = 0x0a;
    constant CHANNEL_ABORT (line 17) | pub const CHANNEL_ABORT: u8 = 0x0b;
    constant REQUEST_KEY (line 18) | pub const REQUEST_KEY: u8 = 0x0c;
    constant AES_KEY (line 19) | pub const AES_KEY: u8 = 0x0d;
    constant AES_KEY_ERROR (line 20) | pub const AES_KEY_ERROR: u8 = 0x0e;
    constant IMAGE (line 21) | pub const IMAGE: u8 = 0x19;
    constant COUNTRY_CODE (line 22) | pub const COUNTRY_CODE: u8 = 0x1b;
    constant PONG (line 23) | pub const PONG: u8 = 0x49;
    constant PONG_ACK (line 24) | pub const PONG_ACK: u8 = 0x4a;
    constant PAUSE (line 25) | pub const PAUSE: u8 = 0x4b;
    constant PRODUCT_INFO (line 26) | pub const PRODUCT_INFO: u8 = 0x50;
    constant LEGACY_WELCOME (line 27) | pub const LEGACY_WELCOME: u8 = 0x69;
    constant LICENSE_VERSION (line 28) | pub const LICENSE_VERSION: u8 = 0x76;
    constant LOGIN (line 29) | pub const LOGIN: u8 = 0xab;
    constant AP_WELCOME (line 30) | pub const AP_WELCOME: u8 = 0xac;
    constant AUTH_FAILURE (line 31) | pub const AUTH_FAILURE: u8 = 0xad;
    constant MERCURY_REQ (line 32) | pub const MERCURY_REQ: u8 = 0xb2;
    constant MERCURY_SUB (line 33) | pub const MERCURY_SUB: u8 = 0xb3;
    constant MERCURY_UNSUB (line 34) | pub const MERCURY_UNSUB: u8 = 0xb4;
    constant MERCURY_PUB (line 35) | pub const MERCURY_PUB: u8 = 0xb5;
    method new (line 37) | pub fn new(cmd: u8, payload: impl Into<Vec<u8>>) -> Self {
  constant MAC_SIZE (line 45) | const MAC_SIZE: usize = 4;
  constant HEADER_SIZE (line 46) | const HEADER_SIZE: usize = 3;
  type ShannonEncoder (line 48) | pub struct ShannonEncoder<T> {
  function new (line 58) | pub fn new(inner: T, send_key: &[u8]) -> Self {
  function encode (line 66) | pub fn encode(&mut self, item: ShannonMsg) -> io::Result<()> {
  function as_inner_mut (line 87) | pub fn as_inner_mut(&mut self) -> &mut T {
  type ShannonDecoder (line 92) | pub struct ShannonDecoder<T> {
  function new (line 102) | pub fn new(inner: T, recv_key: &[u8]) -> Self {
  function decode (line 110) | pub fn decode(&mut self) -> io::Result<ShannonMsg> {
  function as_inner (line 138) | pub fn as_inner(&self) -> &T {

FILE: psst-core/src/error.rs
  type Error (line 5) | pub enum Error {
    method fmt (line 32) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    method from (line 75) | fn from(err: io::Error) -> Error {
    method from (line 81) | fn from(_: crossbeam_channel::SendError<T>) -> Self {
    method from (line 87) | fn from(err: RecvTimeoutError) -> Self {
    method from (line 93) | fn from(err: protobuf::Error) -> Self { Error::InvalidStateError(err.i...

FILE: psst-core/src/item_id.rs
  type LocalItemRegistry (line 25) | pub struct LocalItemRegistry {
    method new (line 32) | fn new() -> Self {
    method get_or_insert (line 40) | pub fn get_or_insert(path: PathBuf) -> u128 {
    method get (line 50) | pub fn get(id: u128) -> Option<PathBuf> {
  type ItemIdType (line 57) | pub enum ItemIdType {
  type ItemId (line 65) | pub struct ItemId {
    constant INVALID (line 74) | pub const INVALID: Self = Self::new(0u128, ItemIdType::Unknown);
    method new (line 76) | pub const fn new(id: u128, id_type: ItemIdType) -> Self {
    method from_base16 (line 80) | pub fn from_base16(id: &str, id_type: ItemIdType) -> Option<Self> {
    method from_base62 (line 90) | pub fn from_base62(id: &str, id_type: ItemIdType) -> Option<Self> {
    method from_raw (line 100) | pub fn from_raw(data: &[u8], id_type: ItemIdType) -> Option<Self> {
    method from_uri (line 105) | pub fn from_uri(uri: &str) -> Option<Self> {
    method to_uri (line 117) | pub fn to_uri(&self) -> Option<String> {
    method to_base16 (line 128) | pub fn to_base16(&self) -> String {
    method to_base62 (line 132) | pub fn to_base62(&self) -> String {
    method to_raw (line 142) | pub fn to_raw(&self) -> [u8; 16] {
    method from_local (line 146) | pub fn from_local(path: PathBuf) -> Self {
    method to_local (line 153) | pub fn to_local(&self) -> PathBuf {
  constant BASE62_DIGITS (line 70) | const BASE62_DIGITS: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDE...
  constant BASE16_DIGITS (line 71) | const BASE16_DIGITS: &[u8] = b"0123456789abcdef";
  method default (line 163) | fn default() -> Self {
  method from (line 169) | fn from(id: ItemId) -> Self {
  type FileId (line 175) | pub struct FileId(pub [u8; 20]);
    method from_raw (line 178) | pub fn from_raw(data: &[u8]) -> Option<Self> {
    method to_base16 (line 182) | pub fn to_base16(&self) -> String {
    method fmt (line 200) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    method fmt (line 206) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
  type Target (line 192) | type Target = [u8];
  method deref (line 194) | fn deref(&self) -> &Self::Target {

FILE: psst-core/src/lastfm.rs
  type LastFmClient (line 7) | pub struct LastFmClient;
    method now_playing_song (line 11) | pub fn now_playing_song(
    method scrobble_song (line 25) | pub fn scrobble_song(
    method create_scrobbler (line 37) | pub fn create_scrobbler(
  method from (line 60) | fn from(value: ScrobblerError) -> Self {
  function generate_lastfm_auth_url (line 66) | pub fn generate_lastfm_auth_url(
  function exchange_token_for_session (line 76) | pub fn exchange_token_for_session(
  function get_lastfm_token_listener (line 89) | pub fn get_lastfm_token_listener(

FILE: psst-core/src/lib.rs
  constant GIT_VERSION (line 5) | pub const GIT_VERSION: &str = git_version!();
  constant BUILD_TIME (line 6) | pub const BUILD_TIME: &str = include!(concat!(env!("OUT_DIR"), "/build-t...
  constant REMOTE_URL (line 7) | pub const REMOTE_URL: &str = include!(concat!(env!("OUT_DIR"), "/remote-...

FILE: psst-core/src/metadata.rs
  type Fetch (line 13) | pub trait Fetch: protobuf::Message {
    method uri (line 14) | fn uri(id: ItemId) -> String;
    method fetch (line 15) | fn fetch(session: &SessionService, id: ItemId) -> Result<Self, Error> {
    method uri (line 21) | fn uri(id: ItemId) -> String {
    method uri (line 27) | fn uri(id: ItemId) -> String {
  type ToMediaPath (line 32) | pub trait ToMediaPath {
    method is_restricted_in_region (line 33) | fn is_restricted_in_region(&self, country: &str) -> bool;
    method find_allowed_alternative (line 34) | fn find_allowed_alternative(&self, country: &str) -> Option<ItemId>;
    method to_media_path (line 35) | fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath>;
    method is_restricted_in_region (line 39) | fn is_restricted_in_region(&self, country: &str) -> bool {
    method find_allowed_alternative (line 45) | fn find_allowed_alternative(&self, country: &str) -> Option<ItemId> {
    method to_media_path (line 53) | fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath> {
    method is_restricted_in_region (line 65) | fn is_restricted_in_region(&self, country: &str) -> bool {
    method find_allowed_alternative (line 71) | fn find_allowed_alternative(&self, _country: &str) -> Option<ItemId> {
    method to_media_path (line 75) | fn to_media_path(&self, preferred_bitrate: usize) -> Option<MediaPath> {
  function select_preferred_file (line 86) | fn select_preferred_file(files: &[AudioFile], preferred_bitrate: usize) ...
  function is_restricted_in_region (line 96) | fn is_restricted_in_region(restriction: &Restriction, country: &str) -> ...
  function is_country_in_list (line 111) | fn is_country_in_list(countries: &[u8], country: &[u8]) -> bool {

FILE: psst-core/src/oauth.rs
  function listen_for_callback_parameter (line 15) | pub fn listen_for_callback_parameter(
  function handle_callback_connection (line 71) | fn handle_callback_connection(
  function extract_parameter_from_request (line 103) | fn extract_parameter_from_request(request_line: &str, parameter_name: &s...
  function get_authcode_listener (line 115) | pub fn get_authcode_listener(
  function send_success_response (line 122) | pub fn send_success_response(stream: &mut TcpStream) {
  function create_spotify_oauth_client (line 151) | fn create_spotify_oauth_client(redirect_port: u16) -> BasicClient {
  function generate_auth_url (line 164) | pub fn generate_auth_url(redirect_port: u16) -> (String, PkceCodeVerifie...
  function exchange_code_for_token (line 177) | pub fn exchange_code_for_token(
  function get_scopes (line 193) | fn get_scopes() -> Vec<Scope> {

FILE: psst-core/src/player/file.rs
  type MediaPath (line 31) | pub struct MediaPath {
  type AudioFormat (line 40) | pub enum AudioFormat {
    method from_protocol (line 47) | pub fn from_protocol(format: Format) -> Self {
    method from_codec (line 56) | pub fn from_codec(codec: CodecType) -> Self {
  type MediaFile (line 68) | pub enum MediaFile {
    method supported_audio_formats_for_bitrate (line 82) | pub fn supported_audio_formats_for_bitrate(bitrate: usize) -> &'static...
    method open (line 118) | pub fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Re...
    method local (line 140) | pub fn local(path: MediaPath) -> Self {
    method path (line 144) | pub fn path(&self) -> MediaPath {
    method storage (line 152) | pub fn storage(&self) -> Option<&StreamStorage> {
    method remote_audio_source (line 160) | pub fn remote_audio_source(
    method local_audio_source (line 175) | pub fn local_audio_source(&self) -> Result<(AudioDecoder, Normalizatio...
    method header_length (line 183) | fn header_length(&self) -> u64 {
    method codec_format (line 190) | fn codec_format(&self) -> AudioCodecFormat {
  type StreamedFile (line 199) | pub struct StreamedFile {
    method open (line 208) | fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Result...
    method service_streaming (line 235) | fn service_streaming(&self) -> Result<(), Error> {
  type CachedFile (line 297) | pub struct CachedFile {
    method open (line 303) | fn open(path: MediaPath, file_path: PathBuf) -> Result<Self, Error> {
  function load_range (line 311) | fn load_range(

FILE: psst-core/src/player/item.rs
  type LoadedPlaybackItem (line 22) | pub struct LoadedPlaybackItem {
  type PlaybackItem (line 29) | pub struct PlaybackItem {
    method load (line 35) | pub fn load(
  function load_media_path (line 65) | fn load_media_path(
  function load_media_path_from_track_or_alternative (line 81) | fn load_media_path_from_track_or_alternative(
  function load_media_path_from_episode (line 119) | fn load_media_path_from_episode(
  function load_media_path_from_local (line 139) | fn load_media_path_from_local(item_id: ItemId) -> Result<MediaPath, Erro...
  function get_country_code (line 153) | fn get_country_code(session: &SessionService, cache: &CacheHandle) -> Op...
  function load_track (line 165) | fn load_track(
  function load_episode (line 181) | fn load_episode(
  function load_audio_key (line 197) | fn load_audio_key(

FILE: psst-core/src/player/mod.rs
  constant PREVIOUS_TRACK_THRESHOLD (line 26) | const PREVIOUS_TRACK_THRESHOLD: Duration = Duration::from_secs(3);
  constant STOP_AFTER_CONSECUTIVE_LOADING_FAILURES (line 27) | const STOP_AFTER_CONSECUTIVE_LOADING_FAILURES: usize = 3;
  type PlaybackConfig (line 30) | pub struct PlaybackConfig {
  method default (line 36) | fn default() -> Self {
  type Player (line 44) | pub struct Player {
    method new (line 60) | pub fn new(
    method sender (line 84) | pub fn sender(&self) -> Sender<PlayerEvent> {
    method receiver (line 88) | pub fn receiver(&self) -> Receiver<PlayerEvent> {
    method handle (line 92) | pub fn handle(&mut self, event: PlayerEvent) {
    method handle_command (line 108) | fn handle_command(&mut self, cmd: PlayerCommand) {
    method handle_loaded (line 127) | fn handle_loaded(&mut self, item: PlaybackItem, result: Result<LoadedP...
    method handle_preloaded (line 154) | fn handle_preloaded(&mut self, item: PlaybackItem, result: Result<Load...
    method handle_position (line 180) | fn handle_position(&mut self, new_position: Duration, path: MediaPath) {
    method handle_end_of_track (line 198) | fn handle_end_of_track(&mut self) {
    method load_queue (line 207) | fn load_queue(&mut self, items: Vec<PlaybackItem>, position: usize) {
    method load_and_play (line 216) | fn load_and_play(&mut self, item: PlaybackItem) {
    method preload (line 264) | fn preload(&mut self, item: PlaybackItem) {
    method set_volume (line 287) | fn set_volume(&mut self, volume: f64) {
    method play_loaded (line 291) | fn play_loaded(&mut self, loaded_item: LoadedPlaybackItem) {
    method pause (line 302) | fn pause(&mut self) {
    method resume (line 318) | fn resume(&mut self) {
    method pause_or_resume (line 334) | fn pause_or_resume(&mut self) {
    method previous (line 344) | fn previous(&mut self) {
    method next (line 357) | fn next(&mut self) {
    method stop (line 366) | fn stop(&mut self) {
    method seek (line 374) | fn seek(&mut self, position: Duration) {
    method configure (line 378) | fn configure(&mut self, config: PlaybackConfig) {
    method is_near_playback_start (line 382) | fn is_near_playback_start(&self) -> bool {
    method is_in_preload (line 391) | fn is_in_preload(&self, item: PlaybackItem) -> bool {
  type PlayerCommand (line 400) | pub enum PlayerCommand {
  type PlayerEvent (line 435) | pub enum PlayerEvent {
  type PlayerState (line 485) | enum PlayerState {
  type PreloadState (line 502) | enum PreloadState {

FILE: psst-core/src/player/queue.rs
  type QueueBehavior (line 6) | pub enum QueueBehavior {
  type Queue (line 14) | pub struct Queue {
    method new (line 24) | pub fn new() -> Self {
    method clear (line 35) | pub fn clear(&mut self) {
    method fill (line 41) | pub fn fill(&mut self, items: Vec<PlaybackItem>, position: usize) {
    method add (line 48) | pub fn add(&mut self, item: PlaybackItem) {
    method handle_added_queue (line 52) | fn handle_added_queue(&mut self) {
    method set_behaviour (line 64) | pub fn set_behaviour(&mut self, behavior: QueueBehavior) {
    method compute_positions (line 69) | fn compute_positions(&mut self) {
    method skip_to_previous (line 94) | pub fn skip_to_previous(&mut self) {
    method skip_to_next (line 98) | pub fn skip_to_next(&mut self) {
    method skip_to_following (line 103) | pub fn skip_to_following(&mut self) {
    method get_current (line 108) | pub fn get_current(&self) -> Option<&PlaybackItem> {
    method get_following (line 113) | pub fn get_following(&self) -> Option<&PlaybackItem> {
    method previous_position (line 124) | fn previous_position(&self) -> usize {
    method next_position (line 133) | fn next_position(&self) -> usize {
    method following_position (line 142) | fn following_position(&self) -> usize {

FILE: psst-core/src/player/storage.rs
  type StreamRequest (line 15) | pub enum StreamRequest {
  type StreamStorage (line 20) | pub struct StreamStorage {
    method new (line 39) | pub fn new(total_size: u64) -> io::Result<StreamStorage> {
    method from_complete_file (line 61) | pub fn from_complete_file(path: PathBuf) -> io::Result<StreamStorage> {
    method reader (line 89) | pub fn reader(&self) -> io::Result<StreamReader> {
    method writer (line 97) | pub fn writer(&self) -> io::Result<StreamWriter> {
    method receiver (line 104) | pub fn receiver(&self) -> &Receiver<StreamRequest> {
    method path (line 108) | pub fn path(&self) -> &Path {
  type StreamReader (line 27) | pub struct StreamReader {
  type StreamWriter (line 33) | pub struct StreamWriter {
    method is_complete (line 135) | pub fn is_complete(&self) -> bool {
    method mark_as_not_requested (line 139) | pub fn mark_as_not_requested(&self, offset: u64, length: u64) {
  type StreamFile (line 113) | enum StreamFile {
    method reopen (line 119) | fn reopen(&self) -> io::Result<File> {
    method path (line 126) | fn path(&self) -> &Path {
  method write (line 145) | fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
  method flush (line 152) | fn flush(&mut self) -> io::Result<()> {
  method seek (line 158) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
  constant MINIMUM_READ_LENGTH (line 163) | const MINIMUM_READ_LENGTH: u64 = 1024 * 64;
  constant PREFETCH_READ_LENGTH (line 164) | const PREFETCH_READ_LENGTH: u64 = 1024 * 256;
  method read (line 167) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  method seek (line 204) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
  type StreamDataMap (line 210) | struct StreamDataMap {
    method remaining (line 223) | fn remaining(&self, offset: u64) -> u64 {
    method not_yet_requested (line 229) | fn not_yet_requested(&self, offset: u64, length: u64) -> Vec<(u64, u64...
    method mark_as_requested (line 239) | fn mark_as_requested(&self, offset: u64, length: u64) {
    method mark_as_not_requested (line 244) | fn mark_as_not_requested(&self, offset: u64, length: u64) {
    method mark_as_downloaded (line 250) | fn mark_as_downloaded(&self, offset: u64, length: u64) {
    method wait_for (line 257) | fn wait_for(&self, offset: u64, blocking_callback: impl Fn(u64)) -> u64 {
    method is_complete (line 280) | fn is_complete(&self) -> bool {
  function range_to_offset_and_length (line 289) | fn range_to_offset_and_length(range: &Range<u64>) -> (u64, u64) {

FILE: psst-core/src/player/worker.rs
  type PlaybackManager (line 33) | pub struct PlaybackManager {
    method new (line 40) | pub fn new(sink: DefaultAudioSink, event_send: Sender<PlayerEvent>) ->...
    method play (line 48) | pub fn play(&mut self, loaded: LoadedPlaybackItem) {
    method seek (line 78) | pub fn seek(&self, position: Duration) {
  type DecoderSource (line 93) | pub struct DecoderSource {
    method new (line 109) | pub fn new(
    method written_samples (line 165) | fn written_samples(&self, position: u64) -> u64 {
    method should_report (line 169) | fn should_report(&self, pos: u64) -> bool {
    method samples_to_duration (line 173) | fn samples_to_duration(&self, samples: u64) -> Duration {
  method write (line 181) | fn write(&mut self, output: &mut [f32]) -> usize {
  method channel_count (line 221) | fn channel_count(&self) -> usize {
  method sample_rate (line 225) | fn sample_rate(&self) -> u32 {
  method drop (line 231) | fn drop(&mut self) {
  type Msg (line 236) | enum Msg {
  type Worker (line 242) | struct Worker {
    method default_buffer (line 268) | fn default_buffer() -> SpscRb<f32> {
    method new (line 274) | fn new(
    method on_seek (line 326) | fn on_seek(&mut self, time: Duration) -> Result<Act<Self>, Error> {
    method on_read (line 346) | fn on_read(&mut self) -> Result<Act<Self>, Error> {
  type Message (line 313) | type Message = Msg;
  type Error (line 314) | type Error = Error;
  method handle (line 316) | fn handle(&mut self, msg: Msg) -> Result<Act<Self>, Self::Error> {

FILE: psst-core/src/session/access_token.rs
  constant CLIENT_ID (line 11) | pub const CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
  constant ACCESS_SCOPES (line 14) | pub const ACCESS_SCOPES: &str = "streaming,user-read-email,user-read-pri...
  constant EXPIRATION_TIME_THRESHOLD (line 19) | const EXPIRATION_TIME_THRESHOLD: Duration = Duration::from_secs(60 * 30);
  type AccessToken (line 22) | pub struct AccessToken {
    method expired (line 28) | fn expired() -> Self {
    method request (line 35) | pub fn request(session: &SessionService) -> Result<Self, Error> {
    method is_expired (line 54) | fn is_expired(&self) -> bool {
  type TokenProvider (line 59) | pub struct TokenProvider {
    method new (line 64) | pub fn new() -> Self {
    method get (line 70) | pub fn get(&self, session: &SessionService) -> Result<AccessToken, Err...

FILE: psst-core/src/session/audio_key.rs
  type AudioKeyDispatcher (line 17) | pub struct AudioKeyDispatcher {
    method new (line 23) | pub fn new() -> Self {
    method enqueue_request (line 30) | pub fn enqueue_request(
    method make_key_request (line 41) | fn make_key_request(seq: u32, track: ItemId, file: FileId) -> ShannonM...
    method handle_aes_key (line 50) | pub fn handle_aes_key(&mut self, msg: ShannonMsg) {
    method handle_aes_key_error (line 66) | pub fn handle_aes_key_error(&mut self, msg: ShannonMsg) {

FILE: psst-core/src/session/client_token.rs
  type ClientTokenProvider (line 16) | pub struct ClientTokenProvider {
    method new (line 22) | pub fn new(proxy_url: Option<&str>) -> Self {
    method request (line 29) | fn request<M: Message>(&self, message: &M) -> Result<Vec<u8>, Error> {
    method request_new_token (line 42) | fn request_new_token(&self) -> Result<Token, Error> {
    method get (line 234) | pub fn get(&self) -> Result<String, Error> {

FILE: psst-core/src/session/login5.rs
  constant MAX_LOGIN_TRIES (line 25) | const MAX_LOGIN_TRIES: u8 = 3;
  constant LOGIN_TIMEOUT (line 26) | const LOGIN_TIMEOUT: Duration = Duration::from_secs(3);
  type ChallengeError (line 29) | pub enum ChallengeError {
  type Login5Error (line 35) | enum Login5Error {
    method fmt (line 51) | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
  method from (line 68) | fn from(err: Login5Error) -> Self {
  type Login5 (line 79) | pub struct Login5 {
    method new (line 94) | pub fn new(
    method request (line 106) | fn request(&self, message: &LoginRequest) -> Result<Vec<u8>, Error> {
    method request_new_token (line 121) | fn request_new_token(&self, login: Login_method) -> Result<Token, Erro...
    method handle_challenges (line 183) | fn handle_challenges(
    method get_access_token (line 246) | pub fn get_access_token(&self, session: &SessionService) -> Result<Tok...

FILE: psst-core/src/session/mercury.rs
  type MercuryDispatcher (line 14) | pub struct MercuryDispatcher {
    method new (line 20) | pub fn new() -> Self {
    method enqueue_request (line 27) | pub fn enqueue_request(
    method handle_mercury_req (line 43) | pub fn handle_mercury_req(&mut self, shannon_msg: ShannonMsg) {
  type MercuryRequest (line 66) | pub struct MercuryRequest {
    method get (line 73) | pub fn get(uri: String) -> Self {
    method send (line 81) | pub fn send(uri: String, data: Vec<u8>) -> Self {
    method encode_to_mercury_message (line 89) | fn encode_to_mercury_message(self, seq: u64) -> Vec<u8> {
    method encode_to_parts (line 95) | fn encode_to_parts(self) -> Vec<Vec<u8>> {
  type MercuryResponse (line 112) | pub struct MercuryResponse {
    method decode_from_parts (line 119) | fn decode_from_parts(mut parts: Vec<Vec<u8>>) -> Self {
  type Pending (line 133) | struct Pending {
  type Msg (line 139) | struct Msg {
    constant FINAL (line 147) | const FINAL: u8 = 0x01;
    constant PARTIAL (line 148) | const PARTIAL: u8 = 0x02;
    method new (line 150) | fn new(seq: u64, flags: u8, parts: Vec<Vec<u8>>) -> Self {
    method decode (line 160) | fn decode(buf: Vec<u8>) -> Self {
    method encode (line 181) | fn encode(&self) -> Vec<u8> {
    method aggregate (line 195) | fn aggregate(msgs: impl IntoIterator<Item = Self>) -> Vec<Vec<u8>> {

FILE: psst-core/src/session/mod.rs
  type SessionConfig (line 39) | pub struct SessionConfig {
  type SessionService (line 50) | pub struct SessionService {
    method empty (line 58) | pub fn empty() -> Self {
    method with_config (line 66) | pub fn with_config(config: SessionConfig) -> Self {
    method update_config (line 75) | pub fn update_config(&self, config: SessionConfig) {
    method is_connected (line 83) | pub fn is_connected(&self) -> bool {
    method connected (line 92) | pub fn connected(&self) -> Result<SessionHandle, Error> {
    method shutdown (line 114) | pub fn shutdown(&self) {
  type SessionConnection (line 123) | pub struct SessionConnection {
    method open (line 134) | pub fn open(config: SessionConfig) -> Result<Self, Error> {
  type SessionWorker (line 147) | pub struct SessionWorker {
    method run (line 156) | pub fn run(transport: Transport) -> Self {
    method handle (line 184) | pub fn handle(&self) -> SessionHandle {
    method join (line 190) | pub fn join(self) {
    method has_terminated (line 202) | pub fn has_terminated(&self) -> bool {
  type SessionHandle (line 208) | pub struct SessionHandle {
    method get_mercury_protobuf (line 213) | pub fn get_mercury_protobuf<T>(&self, uri: String) -> Result<T, Error>
    method get_mercury_json (line 222) | pub fn get_mercury_json<T>(&self, uri: String) -> Result<T, Error>
    method get_mercury_bytes (line 231) | pub fn get_mercury_bytes(&self, uri: String) -> Result<Vec<u8>, Error> {
    method get_audio_key (line 247) | pub fn get_audio_key(&self, track: ItemId, file: FileId) -> Result<Aud...
    method get_country_code (line 260) | pub fn get_country_code(&self) -> Option<String> {
    method request_shutdown (line 268) | pub fn request_shutdown(&self) {
  function decode_shannon_messages (line 278) | fn decode_shannon_messages(mut decoder: ShannonDecoder<TcpStream>, dispa...
  function encode_shannon_messages (line 299) | fn encode_shannon_messages(
  type DispatchCmd (line 317) | enum DispatchCmd {
  function dispatch_messages (line 336) | fn dispatch_messages(
  function pong_message (line 399) | fn pong_message() -> ShannonMsg {
  function parse_country_code (line 403) | fn parse_country_code(msg: ShannonMsg) -> Result<String, Error> {
  method from (line 410) | fn from(error: serde_json::Error) -> Self {

FILE: psst-core/src/session/token.rs
  constant EXPIRY_THRESHOLD (line 5) | const EXPIRY_THRESHOLD: Duration = Duration::from_secs(10);
  type Token (line 8) | pub struct Token {
    method is_expired (line 17) | pub fn is_expired(&self) -> bool {

FILE: psst-core/src/system_info.rs
  constant OS (line 2) | pub const OS: &str = std::env::consts::OS;
  constant DEVICE_ID (line 6) | pub const DEVICE_ID: &str = "Psst";
  constant CLIENT_ID (line 9) | pub const CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
  constant SPOTIFY_SEMANTIC_VERSION (line 12) | pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.52.442";

FILE: psst-core/src/util.rs
  constant NET_CONNECT_TIMEOUT (line 8) | pub const NET_CONNECT_TIMEOUT: Duration = Duration::from_millis(8 * 1000);
  constant NET_IO_TIMEOUT (line 10) | pub const NET_IO_TIMEOUT: Duration = Duration::from_millis(16 * 1000);
  function default_ureq_agent_builder (line 12) | pub fn default_ureq_agent_builder(
  function solve_hash_cash (line 29) | pub fn solve_hash_cash(
  type Sequence (line 70) | pub struct Sequence<T>(T);
  function new (line 73) | pub fn new(value: T) -> Self {
  function advance (line 77) | pub fn advance(&mut self) -> T {
  type OffsetFile (line 83) | pub struct OffsetFile<T> {
  function new (line 89) | pub fn new(mut stream: T, offset: u64) -> io::Result<OffsetFile<T>> {
  function read (line 96) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  function write (line 102) | fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
  function flush (line 106) | fn flush(&mut self) -> io::Result<()> {
  function seek (line 112) | fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
  type FileWithConstSize (line 123) | pub struct FileWithConstSize<T> {
  function len (line 129) | pub fn len(&self) -> u64 {
  function is_empty (line 133) | pub fn is_empty(&self) -> bool {
  function new (line 142) | pub fn new(mut stream: T) -> Self {
  function read (line 154) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  function seek (line 163) | fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {

FILE: psst-gui/build.rs
  function main (line 1) | fn main() {
  function add_windows_icon (line 7) | fn add_windows_icon() {

FILE: psst-gui/src/cmd.rs
  constant WIDGET_SEARCH_INPUT (line 13) | pub const WIDGET_SEARCH_INPUT: WidgetId = WidgetId::reserved(1);
  constant SHOW_MAIN (line 16) | pub const SHOW_MAIN: Selector = Selector::new("app.show-main");
  constant SHOW_ACCOUNT_SETUP (line 17) | pub const SHOW_ACCOUNT_SETUP: Selector = Selector::new("app.show-initial");
  constant CLOSE_ALL_WINDOWS (line 18) | pub const CLOSE_ALL_WINDOWS: Selector = Selector::new("app.close-all-win...
  constant QUIT_APP_WITH_SAVE (line 19) | pub const QUIT_APP_WITH_SAVE: Selector = Selector::new("app.quit-with-sa...
  constant SET_FOCUS (line 20) | pub const SET_FOCUS: Selector = Selector::new("app.set-focus");
  constant COPY (line 21) | pub const COPY: Selector<String> = Selector::new("app.copy-to-clipboard");
  constant GO_TO_URL (line 22) | pub const GO_TO_URL: Selector<String> = Selector::new("app.go-to-url");
  constant TOGGLE_FINDER (line 25) | pub const TOGGLE_FINDER: Selector = Selector::new("app.show-finder");
  constant FIND_IN_PLAYLIST (line 26) | pub const FIND_IN_PLAYLIST: Selector<Find> = Selector::new("find-in-play...
  constant FIND_IN_SAVED_TRACKS (line 27) | pub const FIND_IN_SAVED_TRACKS: Selector<Find> = Selector::new("find-in-...
  constant SESSION_CONNECT (line 30) | pub const SESSION_CONNECT: Selector = Selector::new("app.session-connect");
  constant LOG_OUT (line 31) | pub const LOG_OUT: Selector = Selector::new("app.log-out");
  constant NAVIGATE (line 34) | pub const NAVIGATE: Selector<Nav> = Selector::new("app.navigates");
  constant NAVIGATE_BACK (line 35) | pub const NAVIGATE_BACK: Selector<usize> = Selector::new("app.navigate-b...
  constant NAVIGATE_REFRESH (line 36) | pub const NAVIGATE_REFRESH: Selector = Selector::new("app.navigate-refre...
  constant TOGGLE_LYRICS (line 37) | pub const TOGGLE_LYRICS: Selector = Selector::new("app.toggle-lyrics");
  constant PLAYBACK_LOADING (line 40) | pub const PLAYBACK_LOADING: Selector<ItemId> = Selector::new("app.playba...
  constant PLAYBACK_PLAYING (line 41) | pub const PLAYBACK_PLAYING: Selector<(ItemId, Duration)> = Selector::new...
  constant PLAYBACK_PROGRESS (line 42) | pub const PLAYBACK_PROGRESS: Selector<Duration> = Selector::new("app.pla...
  constant PLAYBACK_PAUSING (line 43) | pub const PLAYBACK_PAUSING: Selector = Selector::new("app.playback-pausi...
  constant PLAYBACK_RESUMING (line 44) | pub const PLAYBACK_RESUMING: Selector = Selector::new("app.playback-resu...
  constant PLAYBACK_BLOCKED (line 45) | pub const PLAYBACK_BLOCKED: Selector = Selector::new("app.playback-block...
  constant PLAYBACK_STOPPED (line 46) | pub const PLAYBACK_STOPPED: Selector = Selector::new("app.playback-stopp...
  constant PLAY (line 49) | pub const PLAY: Selector<usize> = Selector::new("app.play-index");
  constant PLAY_TRACKS (line 50) | pub const PLAY_TRACKS: Selector<PlaybackPayload> = Selector::new("app.pl...
  constant PLAY_PREVIOUS (line 51) | pub const PLAY_PREVIOUS: Selector = Selector::new("app.play-previous");
  constant PLAY_PAUSE (line 52) | pub const PLAY_PAUSE: Selector = Selector::new("app.play-pause");
  constant PLAY_RESUME (line 53) | pub const PLAY_RESUME: Selector = Selector::new("app.play-resume");
  constant PLAY_NEXT (line 54) | pub const PLAY_NEXT: Selector = Selector::new("app.play-next");
  constant PLAY_STOP (line 55) | pub const PLAY_STOP: Selector = Selector::new("app.play-stop");
  constant ADD_TO_QUEUE (line 56) | pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector:...
  constant PLAY_QUEUE_BEHAVIOR (line 57) | pub const PLAY_QUEUE_BEHAVIOR: Selector<QueueBehavior> = Selector::new("...
  constant PLAY_SEEK (line 58) | pub const PLAY_SEEK: Selector<f64> = Selector::new("app.play-seek");
  constant SKIP_TO_POSITION (line 59) | pub const SKIP_TO_POSITION: Selector<u64> = Selector::new("app.skip-to-p...
  constant SORT_BY_DATE_ADDED (line 62) | pub const SORT_BY_DATE_ADDED: Selector = Selector::new("app.sort-by-date...
  constant SORT_BY_TITLE (line 63) | pub const SORT_BY_TITLE: Selector = Selector::new("app.sort-by-title");
  constant SORT_BY_ARTIST (line 64) | pub const SORT_BY_ARTIST: Selector = Selector::new("app.sort-by-artist");
  constant SORT_BY_ALBUM (line 65) | pub const SORT_BY_ALBUM: Selector = Selector::new("app.sort-by-album");
  constant SORT_BY_DURATION (line 66) | pub const SORT_BY_DURATION: Selector = Selector::new("app.sort-by-durati...
  constant TOGGLE_SORT_ORDER (line 69) | pub const TOGGLE_SORT_ORDER: Selector = Selector::new("app.toggle-sort-o...
  constant SHOW_CREDITS_WINDOW (line 72) | pub const SHOW_CREDITS_WINDOW: Selector<Arc<Track>> = Selector::new("app...
  constant LOAD_TRACK_CREDITS (line 73) | pub const LOAD_TRACK_CREDITS: Selector<Arc<Track>> = Selector::new("app....
  constant SHOW_ARTWORK (line 76) | pub const SHOW_ARTWORK: Selector = Selector::new("app.show-artwork");

FILE: psst-gui/src/controller/after_delay.rs
  type DelayFunc (line 7) | type DelayFunc<T> = Box<dyn FnOnce(&mut EventCtx, &mut T, &Env)>;
  type AfterDelay (line 9) | pub struct AfterDelay<T> {
  function new (line 16) | pub fn new(
  function event (line 33) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...
  function lifecycle (line 45) | fn lifecycle(

FILE: psst-gui/src/controller/alert_cleanup.rs
  type AlertCleanupController (line 5) | pub struct AlertCleanupController;
    method event (line 10) | fn event(
  constant CLEANUP_INTERVAL (line 7) | const CLEANUP_INTERVAL: Duration = Duration::from_secs(1);

FILE: psst-gui/src/controller/ex_click.rs
  type ExClick (line 6) | pub struct ExClick<T> {
  function new (line 12) | pub fn new(
  function event (line 24) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...
  function lifecycle (line 49) | fn lifecycle(

FILE: psst-gui/src/controller/ex_cursor.rs
  type ExCursor (line 6) | pub struct ExCursor<T> {
  function new (line 12) | pub fn new(cursor: Cursor) -> Self {
  function event (line 21) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...

FILE: psst-gui/src/controller/ex_scroll.rs
  type ExScroll (line 4) | pub struct ExScroll<T> {
  function new (line 10) | pub fn new(
  function event (line 22) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...
  function lifecycle (line 45) | fn lifecycle(

FILE: psst-gui/src/controller/input.rs
  type SubmitHandler (line 9) | type SubmitHandler = Box<dyn Fn(&mut EventCtx, &mut String, &Env)>;
  type InputController (line 11) | pub struct InputController {
    method new (line 16) | pub fn new() -> Self {
    method on_submit (line 20) | pub fn on_submit(
    method event (line 30) | fn event(

FILE: psst-gui/src/controller/nav.rs
  type NavController (line 9) | pub struct NavController;
    method load_route_data (line 12) | fn load_route_data(&self, ctx: &mut EventCtx, data: &mut AppState) {
    method event (line 79) | fn event(
    method lifecycle (line 136) | fn lifecycle(

FILE: psst-gui/src/controller/on_command.rs
  type OnCommand (line 3) | pub struct OnCommand<U, F> {
  function new (line 9) | pub fn new<T>(selector: Selector<U>, handler: F) -> Self
  function event (line 24) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...

FILE: psst-gui/src/controller/on_command_async.rs
  type AsyncCmdPre (line 11) | type AsyncCmdPre<T, U> = Box<dyn Fn(&mut EventCtx, &mut T, U)>;
  type AsyncCmdReq (line 12) | type AsyncCmdReq<U, V> = Arc<dyn Fn(U) -> V + Sync + Send + 'static>;
  type AsyncCmdRes (line 13) | type AsyncCmdRes<T, U, V> = Box<dyn Fn(&mut EventCtx, &mut T, (U, V))>;
  type OnCommandAsync (line 15) | pub struct OnCommandAsync<W, T, U, V> {
  constant RESPONSE (line 28) | const RESPONSE: Selector<SingleUse<(U, V)>> = Selector::new("on_cmd_asyn...
  function new (line 30) | pub fn new(
  function event (line 55) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 94) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 98) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 102) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 106) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/controller/on_debounce.rs
  type OnDebounce (line 5) | pub struct OnDebounce<T> {
  function trailing (line 12) | pub fn trailing(
  function event (line 29) | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, da...
  function update (line 40) | fn update(&mut self, child: &mut W, ctx: &mut UpdateCtx, old_data: &T, d...

FILE: psst-gui/src/controller/on_update.rs
  type OnUpdate (line 3) | pub struct OnUpdate<F> {
  function new (line 8) | pub fn new<T>(handler: F) -> Self
  function update (line 22) | fn update(&mut self, child: &mut W, ctx: &mut UpdateCtx, old_data: &T, d...

FILE: psst-gui/src/controller/playback.rs
  type PlaybackController (line 36) | pub struct PlaybackController {
    method new (line 72) | pub fn new() -> Self {
    method open_audio_output_and_start_threads (line 84) | fn open_audio_output_and_start_threads(
    method service_events (line 114) | fn service_events(mut player: Player, event_sink: ExtEventSink, widget...
    method create_media_controls (line 163) | fn create_media_controls(
    method handle_media_control_event (line 194) | fn handle_media_control_event(event: MediaControlEvent, sender: &Sende...
    method update_media_control_playback (line 211) | fn update_media_control_playback(&mut self, playback: &Playback) {
    method update_media_control_metadata (line 227) | fn update_media_control_metadata(&mut self, playback: &Playback) {
    method send (line 257) | fn send(&mut self, event: PlayerEvent) {
    method report_now_playing (line 265) | fn report_now_playing(&mut self, playback: &Playback) {
    method report_scrobble (line 290) | fn report_scrobble(&mut self, playback: &Playback) {
    method play (line 318) | fn play(&mut self, items: &Vector<QueueEntry>, position: usize) {
    method pause (line 341) | fn pause(&mut self) {
    method resume (line 345) | fn resume(&mut self) {
    method pause_or_resume (line 349) | fn pause_or_resume(&mut self) {
    method previous (line 353) | fn previous(&mut self) {
    method next (line 357) | fn next(&mut self) {
    method stop (line 361) | fn stop(&mut self) {
    method seek (line 365) | fn seek(&mut self, position: Duration) {
    method seek_relative (line 369) | fn seek_relative(&mut self, data: &AppState, forward: bool) {
    method set_volume (line 385) | fn set_volume(&mut self, volume: f64) {
    method add_to_queue (line 389) | fn add_to_queue(&mut self, item: &PlaybackItem) {
    method set_queue_behavior (line 395) | fn set_queue_behavior(&mut self, behavior: QueueBehavior) {
    method update_lyrics (line 406) | fn update_lyrics(&mut self, ctx: &mut EventCtx, data: &AppState, now_p...
    method event (line 417) | fn event(
    method lifecycle (line 583) | fn lifecycle(
    method update (line 622) | fn update(
  function init_scrobbler_instance (line 45) | fn init_scrobbler_instance(data: &AppState) -> Option<Scrobbler> {
  function random_lowercase_string (line 648) | fn random_lowercase_string(len: usize) -> String {

FILE: psst-gui/src/controller/session.rs
  type SessionController (line 9) | pub struct SessionController;
    method connect (line 12) | fn connect(&self, ctx: &mut EventCtx, data: &mut AppState) {
    method event (line 27) | fn event(
    method lifecycle (line 48) | fn lifecycle(

FILE: psst-gui/src/controller/sort.rs
  type SortController (line 8) | pub struct SortController;
    method event (line 14) | fn event(

FILE: psst-gui/src/data/album.rs
  type AlbumDetail (line 10) | pub struct AlbumDetail {
  type Album (line 15) | pub struct Album {
    method release (line 39) | pub fn release(&self) -> String {
    method release_year (line 47) | pub fn release_year(&self) -> String {
    method release_year_int (line 51) | pub fn release_year_int(&self) -> usize {
    method release_with_format (line 58) | fn release_with_format(&self, format: &(impl Formattable + ?Sized)) ->...
    method image (line 65) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
    method url (line 69) | pub fn url(&self) -> String {
    method link (line 73) | pub fn link(&self) -> AlbumLink {
    method has_explicit (line 81) | pub fn has_explicit(&self) -> bool {
    method into_tracks_with_context (line 85) | pub fn into_tracks_with_context(self: Arc<Self>) -> Vector<Arc<Track>> {
  type AlbumLink (line 99) | pub struct AlbumLink {
    method image (line 107) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
  type AlbumType (line 114) | pub enum AlbumType {
  type DatePrecision (line 124) | pub enum DatePrecision {
  type Copyright (line 131) | pub struct Copyright {
  type CopyrightType (line 138) | pub enum CopyrightType {

FILE: psst-gui/src/data/artist.rs
  type ArtistDetail (line 9) | pub struct ArtistDetail {
  type Artist (line 18) | pub struct Artist {
    method image (line 25) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
    method link (line 29) | pub fn link(&self) -> ArtistLink {
  type ArtistAlbums (line 38) | pub struct ArtistAlbums {
  type ArtistInfo (line 45) | pub struct ArtistInfo {
  type ArtistStats (line 53) | pub struct ArtistStats {
  type ArtistTracks (line 60) | pub struct ArtistTracks {
    method link (line 67) | pub fn link(&self) -> ArtistLink {
  type ArtistLink (line 76) | pub struct ArtistLink {
    method url (line 82) | pub fn url(&self) -> String {

FILE: psst-gui/src/data/config.rs
  type Preferences (line 25) | pub struct Preferences {
    method reset (line 35) | pub fn reset(&mut self) {
    method measure_cache_usage (line 42) | pub fn measure_cache_usage() -> Option<u64> {
  type PreferencesTab (line 48) | pub enum PreferencesTab {
  type Authentication (line 56) | pub struct Authentication {
    method new (line 68) | pub fn new() -> Self {
    method session_config (line 79) | pub fn session_config(&self) -> SessionConfig {
    method authenticate_and_get_credentials (line 93) | pub fn authenticate_and_get_credentials(config: SessionConfig) -> Resu...
    method clear (line 98) | pub fn clear(&mut self) {
  constant APP_NAME (line 104) | const APP_NAME: &str = "Psst";
  constant CONFIG_FILENAME (line 105) | const CONFIG_FILENAME: &str = "config.json";
  constant PROXY_ENV_VAR (line 106) | const PROXY_ENV_VAR: &str = "SOCKS_PROXY";
  type Config (line 110) | pub struct Config {
    method app_dirs (line 156) | fn app_dirs() -> Option<AppDirs> {
    method spotify_local_files_file (line 162) | pub fn spotify_local_files_file(username: &str) -> Option<PathBuf> {
    method cache_dir (line 169) | pub fn cache_dir() -> Option<PathBuf> {
    method config_dir (line 173) | pub fn config_dir() -> Option<PathBuf> {
    method config_path (line 177) | fn config_path() -> Option<PathBuf> {
    method load (line 181) | pub fn load() -> Option<Config> {
    method save (line 192) | pub fn save(&self) {
    method has_credentials (line 209) | pub fn has_credentials(&self) -> bool {
    method store_credentials (line 213) | pub fn store_credentials(&mut self, credentials: Credentials) {
    method clear_credentials (line 217) | pub fn clear_credentials(&mut self) {
    method username (line 221) | pub fn username(&self) -> Option<&str> {
    method session (line 227) | pub fn session(&self) -> SessionConfig {
    method playback (line 234) | pub fn playback(&self) -> PlaybackConfig {
    method proxy (line 241) | pub fn proxy() -> Option<String> {
  method default (line 132) | fn default() -> Self {
  type AudioQuality (line 256) | pub enum AudioQuality {
    method as_bitrate (line 264) | fn as_bitrate(self) -> usize {
  type Theme (line 274) | pub enum Theme {
  type SortOrder (line 281) | pub enum SortOrder {
  type SortCriteria (line 288) | pub enum SortCriteria {
  function get_dir_size (line 297) | fn get_dir_size(path: &Path) -> Option<u64> {

FILE: psst-gui/src/data/ctx.rs
  type Ctx (line 12) | pub struct Ctx<C, T> {
  function new (line 22) | pub fn new(c: C, t: T) -> Self {
  function make (line 26) | pub fn make<S: Data>(cl: impl Lens<S, C>, tl: impl Lens<S, T>) -> impl L...
  function data (line 30) | pub fn data() -> impl Lens<Self, T> {
  function map (line 34) | pub fn map<U>(map: impl Lens<T, U>) -> impl Lens<Self, Ctx<C, U>>
  type CtxMake (line 42) | struct CtxMake<CL, TL> {
  function with (line 55) | fn with<V, F>(&self, data: &S, f: F) -> V
  function with_mut (line 65) | fn with_mut<V, F>(&self, data: &mut S, f: F) -> V
  type CtxMap (line 79) | struct CtxMap<Map> {
  function with (line 90) | fn with<V, F>(&self, c: &Ctx<C, T>, f: F) -> V
  function with_mut (line 100) | fn with_mut<V, F>(&self, c: &mut Ctx<C, T>, f: F) -> V
  function in_promise (line 123) | pub fn in_promise() -> impl Lens<Self, Promise<Ctx<C, PT>, PD, PE>> {
  function for_each (line 164) | fn for_each(&self, mut cb: impl FnMut(&Ctx<C, T>, usize)) {
  function for_each_mut (line 171) | fn for_each_mut(&mut self, mut cb: impl FnMut(&mut Ctx<C, T>, usize)) {
  function data_len (line 186) | fn data_len(&self) -> usize {
  function fmt (line 195) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  method eq (line 204) | fn eq(&self, other: &Self) -> bool {

FILE: psst-gui/src/data/find.rs
  type Finder (line 5) | pub struct Finder {
    method new (line 13) | pub fn new() -> Self {
    method reset (line 17) | pub fn reset(&mut self) {
    method reset_matches (line 23) | pub fn reset_matches(&mut self) {
    method report_match (line 27) | pub fn report_match(&mut self) -> usize {
    method focus_previous (line 32) | pub fn focus_previous(&mut self) {
    method focus_next (line 40) | pub fn focus_next(&mut self) {
  type FindQuery (line 50) | pub struct FindQuery {
    method new (line 55) | pub fn new(query: &str) -> Self {
    method build_regex (line 61) | fn build_regex(query: &str) -> Regex {
    method is_empty (line 68) | pub fn is_empty(&self) -> bool {
    method matches_str (line 72) | pub fn matches_str(&self, s: &str) -> bool {
  type MatchFindQuery (line 77) | pub trait MatchFindQuery {
    method matches_query (line 78) | fn matches_query(&self, query: &FindQuery) -> bool;

FILE: psst-gui/src/data/id.rs
  type Id (line 2) | pub trait Id {
    method id (line 5) | fn id(&self) -> Self::Id;
    method has_id (line 7) | fn has_id(&self, id: &Self::Id) -> bool {

FILE: psst-gui/src/data/mod.rs
  constant ALERT_DURATION (line 66) | pub const ALERT_DURATION: Duration = Duration::from_secs(5);
  type AppState (line 69) | pub struct AppState {
    method default_with_config (line 94) | pub fn default_with_config(config: Config) -> Self {
    method navigate (line 179) | pub fn navigate(&mut self, nav: &Nav) {
    method navigate_back (line 188) | pub fn navigate_back(&mut self) {
    method refresh_all (line 206) | pub fn refresh_all(&mut self) {
    method refresh_playlist (line 219) | pub fn refresh_playlist(&mut self) {
    method queued_entry (line 226) | pub fn queued_entry(&self, item_id: ItemId) -> Option<QueueEntry> {
    method add_queued_entry (line 243) | pub fn add_queued_entry(&mut self, queue_entry: QueueEntry) {
    method loading_playback (line 247) | pub fn loading_playback(&mut self, item: Playable, origin: PlaybackOri...
    method start_playback (line 258) | pub fn start_playback(&mut self, item: Playable, origin: PlaybackOrigi...
    method progress_playback (line 269) | pub fn progress_playback(&mut self, progress: Duration) {
    method pause_playback (line 275) | pub fn pause_playback(&mut self) {
    method resume_playback (line 279) | pub fn resume_playback(&mut self) {
    method block_playback (line 283) | pub fn block_playback(&mut self) {
    method stop_playback (line 287) | pub fn stop_playback(&mut self) {
    method set_queue_behavior (line 293) | pub fn set_queue_behavior(&mut self, queue_behavior: QueueBehavior) {
    method common_ctx_mut (line 301) | pub fn common_ctx_mut(&mut self) -> &mut CommonCtx {
    method with_library_mut (line 305) | pub fn with_library_mut(&mut self, func: impl FnOnce(&mut Library)) {
    method library_updated (line 310) | fn library_updated(&mut self) {
    method add_alert (line 319) | pub fn add_alert(&mut self, message: impl Display, style: AlertStyle) {
    method info_alert (line 329) | pub fn info_alert(&mut self, message: impl Display) {
    method error_alert (line 333) | pub fn error_alert(&mut self, message: impl Display) {
    method dismiss_alert (line 337) | pub fn dismiss_alert(&mut self, id: usize) {
    method cleanup_alerts (line 341) | pub fn cleanup_alerts(&mut self) {
  type Library (line 349) | pub struct Library {
    method add_track (line 358) | pub fn add_track(&mut self, track: Arc<Track>) {
    method remove_track (line 365) | pub fn remove_track(&mut self, track_id: &TrackId) {
    method contains_track (line 372) | pub fn contains_track(&self, track: &Track) -> bool {
    method add_album (line 380) | pub fn add_album(&mut self, album: Arc<Album>) {
    method remove_album (line 387) | pub fn remove_album(&mut self, album_id: &str) {
    method contains_album (line 394) | pub fn contains_album(&self, album: &Album) -> bool {
    method add_show (line 402) | pub fn add_show(&mut self, show: Arc<Show>) {
    method remove_show (line 409) | pub fn remove_show(&mut self, show_id: &str) {
    method contains_show (line 416) | pub fn contains_show(&self, show: &Show) -> bool {
    method writable_playlists (line 424) | pub fn writable_playlists(&self) -> Vec<&Playlist> {
    method add_playlist (line 441) | pub fn add_playlist(&mut self, playlist: Playlist) {
    method remove_from_playlist (line 447) | pub fn remove_from_playlist(&mut self, id: &str) {
    method rename_playlist (line 453) | pub fn rename_playlist(&mut self, link: PlaylistLink) {
    method is_created_by_user (line 464) | pub fn is_created_by_user(&self, playlist: &Playlist) -> bool {
    method contains_playlist (line 472) | pub fn contains_playlist(&self, playlist: &Playlist) -> bool {
    method increment_playlist_track_count (line 480) | pub fn increment_playlist_track_count(&mut self, link: &PlaylistLink) {
    method decrement_playlist_track_count (line 488) | pub fn decrement_playlist_track_count(&mut self, link: &PlaylistLink) {
  method default (line 498) | fn default() -> Self {
  type SavedTracks (line 510) | pub struct SavedTracks {
    method new (line 516) | pub fn new(tracks: Vector<Arc<Track>>) -> Self {
  type SavedAlbums (line 523) | pub struct SavedAlbums {
    method new (line 529) | pub fn new(albums: Vector<Arc<Album>>) -> Self {
  type Shows (line 536) | pub struct Shows {
    method new (line 542) | pub fn new(shows: Vector<Arc<Show>>) -> Self {
  type CommonCtx (line 549) | pub struct CommonCtx {
    method is_playing (line 557) | pub fn is_playing(&self, item: &Playable) -> bool {
  type WithCtx (line 562) | pub type WithCtx<T> = Ctx<Arc<CommonCtx>, T>;
  type HomeDetail (line 565) | pub struct HomeDetail {
  type MixedView (line 579) | pub struct MixedView {
  type Alert (line 590) | pub struct Alert {
    method fresh_id (line 598) | fn fresh_id() -> usize {
  type AlertStyle (line 604) | pub enum AlertStyle {

FILE: psst-gui/src/data/nav.rs
  type Route (line 13) | pub enum Route {
  type Nav (line 28) | pub enum Nav {
    method route (line 44) | pub fn route(&self) -> Route {
    method title (line 60) | pub fn title(&self) -> String {
    method full_title (line 76) | pub fn full_title(&self) -> String {
  type SpotifyUrl (line 94) | pub enum SpotifyUrl {
    method parse (line 103) | pub fn parse(url: &str) -> Option<Self> {
    method id (line 118) | pub fn id(&self) -> Arc<str> {

FILE: psst-gui/src/data/playback.rs
  type Playback (line 14) | pub struct Playback {
  type QueueEntry (line 23) | pub struct QueueEntry {
  type Playable (line 29) | pub enum Playable {
    method track (line 35) | pub fn track(&self) -> Option<&Arc<Track>> {
    method id (line 43) | pub fn id(&self) -> ItemId {
    method name (line 50) | pub fn name(&self) -> &Arc<str> {
    method duration (line 57) | pub fn duration(&self) -> Duration {
    method same (line 64) | pub fn same(&self, other: &Self) -> bool {
  method same (line 70) | fn same(&self, other: &Self) -> bool {
  type QueueBehavior (line 76) | pub enum QueueBehavior {
  type PlaybackState (line 85) | pub enum PlaybackState {
  type NowPlaying (line 93) | pub struct NowPlaying {
    method cover_image_url (line 104) | pub fn cover_image_url(&self, width: f64, height: f64) -> Option<&str> {
    method cover_image_metadata (line 117) | pub fn cover_image_metadata(&self) -> Option<(&str, (u32, u32))> {
  type PlaybackOrigin (line 144) | pub enum PlaybackOrigin {
    method to_nav (line 156) | pub fn to_nav(&self) -> Nav {
    method fmt (line 171) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  type PlaybackPayload (line 186) | pub struct PlaybackPayload {

FILE: psst-gui/src/data/playlist.rs
  type PlaylistDetail (line 10) | pub struct PlaylistDetail {
  type PlaylistAddTrack (line 16) | pub struct PlaylistAddTrack {
  type PlaylistRemoveTrack (line 22) | pub struct PlaylistRemoveTrack {
  type Playlist (line 28) | pub struct Playlist {
    method link (line 45) | pub fn link(&self) -> PlaylistLink {
    method image (line 52) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
    method url (line 58) | pub fn url(&self) -> String {
  type PlaylistTracks (line 64) | pub struct PlaylistTracks {
    method link (line 71) | pub fn link(&self) -> PlaylistLink {
  type PlaylistLink (line 80) | pub struct PlaylistLink {
  function deserialize_track_count (line 85) | fn deserialize_track_count<'de, D>(deserializer: D) -> Result<Option<usi...
  function deserialize_description (line 97) | fn deserialize_description<'de, D>(deserializer: D) -> Result<Arc<str>, ...

FILE: psst-gui/src/data/promise.rs
  type Promise (line 6) | pub enum Promise<T: Data, D: Data = (), E: Data = Error> {
  type PromiseState (line 15) | pub enum PromiseState {
  function state (line 23) | pub fn state(&self) -> PromiseState {
  function is_resolved (line 32) | pub fn is_resolved(&self) -> bool {
  function is_deferred (line 36) | pub fn is_deferred(&self, d: &D) -> bool
  function contains (line 43) | pub fn contains(&self, d: &D) -> bool
  function deferred (line 50) | pub fn deferred(&self) -> Option<&D> {
  function resolved (line 59) | pub fn resolved(&self) -> Option<&T> {
  function resolved_mut (line 67) | pub fn resolved_mut(&mut self) -> Option<&mut T> {
  function clear (line 75) | pub fn clear(&mut self) {
  function defer (line 79) | pub fn defer(&mut self, def: D) {
  function resolve (line 83) | pub fn resolve(&mut self, def: D, val: T) {
  function reject (line 87) | pub fn reject(&mut self, def: D, err: E) {
  function resolve_or_reject (line 91) | pub fn resolve_or_reject(&mut self, def: D, res: Result<T, E>) {
  function update (line 98) | pub fn update(&mut self, (def, res): (D, Result<T, E>))
  function defer_default (line 111) | pub fn defer_default(&mut self) {

FILE: psst-gui/src/data/recommend.rs
  type Recommend (line 12) | pub struct Recommend {
  type RecommendationsRequest (line 18) | pub struct RecommendationsRequest {
    method for_track (line 26) | pub fn for_track(id: TrackId) -> Self {
    method with_params (line 33) | pub fn with_params(mut self, params: RecommendationsParams) -> Self {
  type RecommendationsKnobs (line 40) | pub struct RecommendationsKnobs {
    method as_params (line 59) | pub fn as_params(&self) -> RecommendationsParams {
  type Toggled (line 80) | pub struct Toggled<T> {
  function from (line 86) | fn from(t: Toggled<u64>) -> Self {
  function from (line 96) | fn from(t: Toggled<f64>) -> Self {
  type RecommendationsParams (line 106) | pub struct RecommendationsParams {
  type Range (line 125) | pub struct Range<T> {
  function new (line 132) | pub fn new(min: Option<T>, max: Option<T>, target: Option<T>) -> Self {
  type Recommendations (line 138) | pub struct Recommendations {
  type RecommendationsSeed (line 146) | pub struct RecommendationsSeed {
  type RecommendationsSeedType (line 161) | pub enum RecommendationsSeedType {

FILE: psst-gui/src/data/search.rs
  type Search (line 8) | pub struct Search {
  type SearchTopic (line 15) | pub enum SearchTopic {
    method as_str (line 24) | pub fn as_str(&self) -> &'static str {
    method display_name (line 34) | pub fn display_name(&self) -> &'static str {
    method all (line 44) | pub fn all() -> &'static [Self] {
  type SearchResults (line 56) | pub struct SearchResults {
    method is_empty (line 67) | pub fn is_empty(&self) -> bool {

FILE: psst-gui/src/data/show.rs
  type ShowDetail (line 13) | pub struct ShowDetail {
  type Show (line 19) | pub struct Show {
    method image (line 29) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
    method link (line 33) | pub fn link(&self) -> ShowLink {
  type ShowEpisodes (line 42) | pub struct ShowEpisodes {
  type ShowLink (line 48) | pub struct ShowLink {
    method url (line 54) | pub fn url(&self) -> String {
  type Episode (line 60) | pub struct Episode {
    method image (line 79) | pub fn image(&self, width: f64, height: f64) -> Option<&Image> {
    method url (line 83) | pub fn url(&self) -> String {
    method release (line 90) | pub fn release(&self) -> String {
  type EpisodeLink (line 100) | pub struct EpisodeLink {
  type ResumePoint (line 106) | pub struct ResumePoint {
  type EpisodeId (line 116) | pub struct EpisodeId(pub ItemId);
    type Error (line 125) | type Error = &'static str;
    method try_from (line 127) | fn try_from(value: String) -> Result<Self, Self::Error> {
  method same (line 119) | fn same(&self, other: &Self) -> bool {
  method from (line 135) | fn from(id: EpisodeId) -> Self {

FILE: psst-gui/src/data/slider_scroll_scale.rs
  type SliderScrollScale (line 11) | pub struct SliderScrollScale {
  method default (line 23) | fn default() -> Self {

FILE: psst-gui/src/data/track.rs
  type Track (line 11) | pub struct Track {
    method lens_artist_name (line 34) | pub fn lens_artist_name() -> impl Lens<Self, Arc<str>> {
    method lens_album_name (line 43) | pub fn lens_album_name() -> impl Lens<Self, Arc<str>> {
    method artist_name (line 52) | pub fn artist_name(&self) -> Arc<str> {
    method artist_names (line 59) | pub fn artist_names(&self) -> String {
    method album_name (line 66) | pub fn album_name(&self) -> Arc<str> {
    method url (line 73) | pub fn url(&self) -> String {
  type TrackLines (line 80) | pub struct TrackLines {
  type TrackId (line 89) | pub struct TrackId(pub ItemId);
    type Error (line 98) | type Error = &'static str;
    method try_from (line 100) | fn try_from(value: String) -> Result<Self, Self::Error> {
  method same (line 92) | fn same(&self, other: &Self) -> bool {
  method from (line 108) | fn from(id: TrackId) -> Self {
  type AudioAnalysis (line 115) | pub struct AudioAnalysis {
  type AudioSegment (line 121) | pub struct AudioSegment {
  type TimeInterval (line 131) | pub struct TimeInterval {

FILE: psst-gui/src/data/user.rs
  type UserProfile (line 7) | pub struct UserProfile {
  type PublicUser (line 14) | pub struct PublicUser {

FILE: psst-gui/src/data/utils.rs
  type Cached (line 15) | pub struct Cached<T: Data> {
  function new (line 22) | pub fn new(data: T, at: SystemTime) -> Self {
  function fresh (line 29) | pub fn fresh(data: T) -> Self {
  function map (line 36) | pub fn map<U: Data>(self, f: impl Fn(T) -> U) -> Cached<U> {
  type Page (line 45) | pub struct Page<T: Clone> {
  type Image (line 53) | pub struct Image {
    method fits (line 60) | pub fn fits(&self, width: f64, height: f64) -> bool {
    method at_least_of_size (line 68) | pub fn at_least_of_size(images: &Vector<Self>, width: f64, height: f64...
  function default_str (line 77) | pub fn default_str() -> Arc<str> {
  type Float64 (line 82) | pub struct Float64(pub f64);
    method hash (line 93) | fn hash<H: hash::Hasher>(&self, state: &mut H) {
    method fmt (line 99) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    method from (line 105) | fn from(f: f64) -> Self {
  method eq (line 85) | fn eq(&self, other: &Self) -> bool {
  function from (line 111) | fn from(f: Float64) -> Self {
  function deserialize_secs (line 117) | pub fn deserialize_secs<'de, D>(deserializer: D) -> Result<Duration, D::...
  function deserialize_millis (line 126) | pub fn deserialize_millis<'de, D>(deserializer: D) -> Result<Duration, D...
  function deserialize_date (line 136) | pub fn deserialize_date<'de, D>(deserializer: D) -> Result<Date, D::Error>
  function deserialize_date_option (line 151) | pub fn deserialize_date_option<'de, D>(deserializer: D) -> Result<Option...
  function deserialize_first_page (line 161) | pub fn deserialize_first_page<'de, D, T>(deserializer: D) -> Result<Vect...
  function deserialize_null_arc_str (line 171) | pub fn deserialize_null_arc_str<'de, D>(deserializer: D) -> Result<Arc<s...
  function sanitize_html_string (line 179) | pub fn sanitize_html_string(text: &str) -> Arc<str> {

FILE: psst-gui/src/delegate.rs
  type Delegate (line 22) | pub struct Delegate {
    method new (line 32) | pub fn new() -> Self {
    method with_main (line 45) | pub fn with_main(main_window: WindowId) -> Self {
    method with_preferences (line 51) | pub fn with_preferences(preferences_window: WindowId) -> Self {
    method show_or_create_window (line 57) | fn show_or_create_window<F>(
    method show_main (line 73) | fn show_main(&mut self, config: &Config, ctx: &mut DelegateCtx) {
    method show_account_setup (line 82) | fn show_account_setup(&mut self, ctx: &mut DelegateCtx) {
    method show_preferences (line 86) | fn show_preferences(&mut self, ctx: &mut DelegateCtx) {
    method close_all_windows (line 90) | fn close_all_windows(&mut self, ctx: &mut DelegateCtx) {
    method close_preferences (line 97) | fn close_preferences(&mut self, ctx: &mut DelegateCtx) {
    method close_credits (line 103) | fn close_credits(&mut self, ctx: &mut DelegateCtx) {
    method show_credits (line 109) | fn show_credits(&mut self, ctx: &mut DelegateCtx) -> WindowId {
    method show_artwork (line 128) | fn show_artwork(&mut self, ctx: &mut DelegateCtx) {
    method command (line 134) | fn command(
    method window_removed (line 226) | fn window_removed(
    method event (line 253) | fn event(
    method command_image (line 288) | fn command_image(

FILE: psst-gui/src/error.rs
  type Error (line 6) | pub enum Error {
    method fmt (line 13) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {

FILE: psst-gui/src/main.rs
  constant ENV_LOG (line 24) | const ENV_LOG: &str = "PSST_LOG";
  constant ENV_LOG_STYLE (line 25) | const ENV_LOG_STYLE: &str = "PSST_LOG_STYLE";
  function main (line 27) | fn main() {

FILE: psst-gui/src/ui/album.rs
  constant LOAD_DETAIL (line 21) | pub const LOAD_DETAIL: Selector<AlbumLink> = Selector::new("app.album.lo...
  function detail_widget (line 23) | pub fn detail_widget() -> impl Widget<AppState> {
  function loaded_detail_widget (line 44) | fn loaded_detail_widget() -> impl Widget<WithCtx<Cached<Arc<Album>>>> {
  function cover_widget (line 93) | fn cover_widget(size: f64) -> impl Widget<Arc<Album>> {
  function rounded_cover_widget (line 100) | fn rounded_cover_widget(size: f64) -> impl Widget<Arc<Album>> {
  function album_widget (line 104) | pub fn album_widget(horizontal: bool) -> impl Widget<WithCtx<Arc<Album>>> {
  function album_ctx_menu (line 188) | fn album_ctx_menu(album: &WithCtx<Arc<Album>>) -> Menu<AppState> {
  function album_menu (line 192) | fn album_menu(album: &Arc<Album>, library: &Arc<Library>) -> Menu<AppSta...
  method origin (line 240) | fn origin(&self) -> PlaybackOrigin {
  method for_each (line 244) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
  method count (line 250) | fn count(&self) -> usize {

FILE: psst-gui/src/ui/artist.rs
  constant LOAD_DETAIL (line 25) | pub const LOAD_DETAIL: Selector<ArtistLink> = Selector::new("app.artist....
  function detail_widget (line 27) | pub fn detail_widget() -> impl Widget<AppState> {
  function async_top_tracks_widget (line 35) | fn async_top_tracks_widget() -> impl Widget<AppState> {
  function async_albums_widget (line 63) | fn async_albums_widget() -> impl Widget<AppState> {
  function async_artist_info (line 80) | fn async_artist_info() -> impl Widget<AppState> {
  function async_related_widget (line 97) | fn async_related_widget() -> impl Widget<AppState> {
  function artist_widget (line 108) | pub fn artist_widget(horizontal: bool) -> impl Widget<Artist> {
  function link_widget (line 149) | pub fn link_widget() -> impl Widget<ArtistLink> {
  function cover_widget (line 161) | pub fn cover_widget(size: f64) -> impl Widget<Artist> {
  function artist_info_widget (line 170) | fn artist_info_widget() -> impl Widget<WithCtx<ArtistInfo>> {
  function top_tracks_widget (line 220) | fn top_tracks_widget() -> impl Widget<WithCtx<ArtistTracks>> {
  function albums_widget (line 232) | fn albums_widget() -> impl Widget<WithCtx<ArtistAlbums>> {
  function related_widget (line 249) | fn related_widget() -> impl Widget<Cached<Vector<Artist>>> {
  function header_widget (line 257) | fn header_widget<T: Data>(text: impl Into<LabelText<T>>) -> impl Widget<...
  function artist_info_menu (line 265) | fn artist_info_menu(artist: &ArtistInfo) -> Menu<AppState> {
  function artist_menu (line 298) | fn artist_menu(artist: &ArtistLink) -> Menu<AppState> {

FILE: psst-gui/src/ui/credits.rs
  type TrackCredits (line 18) | pub struct TrackCredits {
  type RoleCredit (line 30) | pub struct RoleCredit {
  type CreditArtist (line 37) | pub struct CreditArtist {
  function credits_widget (line 52) | pub fn credits_widget() -> impl Widget<AppState> {
  function role_credit_widget (line 91) | fn role_credit_widget() -> impl Widget<RoleCredit> {
  function credit_artist_widget (line 110) | fn credit_artist_widget() -> impl Widget<CreditArtist> {
  type CursorController (line 165) | struct CursorController;
    method event (line 168) | fn event(
  function proper_case (line 187) | fn proper_case(s: &str) -> String {
  function capitalize_first (line 202) | fn capitalize_first(s: &str) -> String {
  type CreditsController (line 211) | pub struct CreditsController;
    method update (line 214) | fn update(

FILE: psst-gui/src/ui/episode.rs
  function playable_widget (line 19) | pub fn playable_widget() -> impl Widget<PlayRow<Arc<Episode>>> {
  function cover_widget (line 79) | fn cover_widget(size: f64) -> impl Widget<Arc<Episode>> {
  function rounded_cover_widget (line 87) | fn rounded_cover_widget(size: f64) -> impl Widget<Arc<Episode>> {
  function episode_row_menu (line 92) | fn episode_row_menu(row: &PlayRow<Arc<Episode>>) -> Menu<AppState> {
  function episode_menu (line 96) | pub fn episode_menu(episode: &Episode, _library: &Arc<Library>) -> Menu<...

FILE: psst-gui/src/ui/find.rs
  type Find (line 15) | pub struct Find {
  type Report (line 21) | struct Report {
  constant FIND (line 25) | const FIND: Selector = Selector::new("find");
  constant REPORT_MATCH (line 26) | const REPORT_MATCH: Selector<Report> = Selector::new("report-match");
  constant FOCUS_MATCH (line 27) | const FOCUS_MATCH: Selector = Selector::new("focus-match");
  type Findable (line 29) | pub struct Findable<W> {
  function new (line 36) | pub fn new(inner: W, selector: Selector<Find>) -> Self {
  function set_state (line 44) | fn set_state(&mut self, ctx: &mut EventCtx, matches: bool) {
  function event (line 57) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 84) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 88) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &...
  function layout (line 92) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 96) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
  function finder_widget (line 109) | pub fn finder_widget(selector: Selector<Find>, label: &'static str) -> i...
  type FinderController (line 164) | struct FinderController {
    method event (line 173) | fn event(
    method update (line 212) | fn update(

FILE: psst-gui/src/ui/home.rs
  constant LOAD_MADE_FOR_YOU (line 22) | pub const LOAD_MADE_FOR_YOU: Selector = Selector::new("app.home.load-mad...
  function home_widget (line 24) | pub fn home_widget() -> impl Widget<AppState> {
  function simple_title_label (line 40) | fn simple_title_label(title: &str) -> impl Widget<AppState> {
  function made_for_you (line 49) | fn made_for_you() -> impl Widget<AppState> {
  function recommended_stations (line 66) | fn recommended_stations() -> impl Widget<AppState> {
  function uniquely_yours_results_widget (line 83) | fn uniquely_yours_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function uniquely_yours (line 101) | fn uniquely_yours() -> impl Widget<AppState> {
  function user_top_mixes (line 118) | fn user_top_mixes() -> impl Widget<AppState> {
  function best_of_artists (line 135) | fn best_of_artists() -> impl Widget<AppState> {
  function your_shows (line 152) | pub fn your_shows() -> impl Widget<AppState> {
  function jump_back_in (line 226) | fn jump_back_in() -> impl Widget<AppState> {
  function shows_that_you_might_like (line 243) | pub fn shows_that_you_might_like() -> impl Widget<AppState> {
  function loaded_results_widget (line 260) | pub fn loaded_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function title_label (line 282) | fn title_label() -> impl Widget<WithCtx<MixedView>> {
  function artist_results_widget (line 300) | fn artist_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function album_results_widget (line 311) | fn album_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function playlist_results_widget (line 324) | fn playlist_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function show_results_widget (line 337) | fn show_results_widget() -> impl Widget<WithCtx<MixedView>> {
  function user_top_artists_widget (line 348) | fn user_top_artists_widget() -> impl Widget<AppState> {
  function top_tracks_widget (line 363) | fn top_tracks_widget() -> impl Widget<WithCtx<Vector<Arc<Track>>>> {
  function user_top_tracks_widget (line 375) | fn user_top_tracks_widget() -> impl Widget<AppState> {

FILE: psst-gui/src/ui/library.rs
  constant LOAD_TRACKS (line 21) | pub const LOAD_TRACKS: Selector = Selector::new("app.library.load-tracks");
  constant LOAD_ALBUMS (line 22) | pub const LOAD_ALBUMS: Selector = Selector::new("app.library.load-albums");
  constant LOAD_SHOWS (line 23) | pub const LOAD_SHOWS: Selector = Selector::new("app.library.load-shows");
  constant SAVE_TRACK (line 25) | pub const SAVE_TRACK: Selector<Arc<Track>> = Selector::new("app.library....
  constant UNSAVE_TRACK (line 26) | pub const UNSAVE_TRACK: Selector<TrackId> = Selector::new("app.library.u...
  constant SAVE_ALBUM (line 28) | pub const SAVE_ALBUM: Selector<Arc<Album>> = Selector::new("app.library....
  constant UNSAVE_ALBUM (line 29) | pub const UNSAVE_ALBUM: Selector<AlbumLink> = Selector::new("app.library...
  constant SAVE_SHOW (line 31) | pub const SAVE_SHOW: Selector<Arc<Show>> = Selector::new("app.library.sa...
  constant UNSAVE_SHOW (line 32) | pub const UNSAVE_SHOW: Selector<ShowLink> = Selector::new("app.library.u...
  function saved_tracks_widget (line 34) | pub fn saved_tracks_widget() -> impl Widget<AppState> {
  function saved_albums_widget (line 108) | pub fn saved_albums_widget() -> impl Widget<AppState> {
  function saved_shows_widget (line 169) | pub fn saved_shows_widget() -> impl Widget<AppState> {

FILE: psst-gui/src/ui/lyrics.rs
  constant SHOW_LYRICS (line 12) | pub const SHOW_LYRICS: Selector<NowPlaying> = Selector::new("app.home.sh...
  function lyrics_widget (line 14) | pub fn lyrics_widget() -> impl Widget<AppState> {
  function track_info_widget (line 30) | fn track_info_widget() -> impl Widget<AppState> {
  function track_lyrics_widget (line 64) | fn track_lyrics_widget() -> impl Widget<AppState> {

FILE: psst-gui/src/ui/menu.rs
  function main_menu (line 8) | pub fn main_menu(_window: Option<WindowId>, _data: &AppState, _env: &Env...
  function mac_app_menu (line 18) | fn mac_app_menu() -> Menu<AppState> {
  function edit_menu (line 46) | fn edit_menu() -> Menu<AppState> {
  function view_menu (line 53) | fn view_menu() -> Menu<AppState> {

FILE: psst-gui/src/ui/mod.rs
  constant DOWNLOAD_ARTWORK (line 55) | pub const DOWNLOAD_ARTWORK: Selector<(String, String)> = Selector::new("...
  function main_window (line 57) | pub fn main_window(config: &Config) -> WindowDesc<AppState> {
  function preferences_window (line 71) | pub fn preferences_window() -> WindowDesc<AppState> {
  function account_setup_window (line 95) | pub fn account_setup_window() -> WindowDesc<AppState> {
  function artwork_window (line 109) | pub fn artwork_window() -> WindowDesc<AppState> {
  function preferences_widget (line 145) | fn preferences_widget() -> impl Widget<AppState> {
  function account_setup_widget (line 153) | fn account_setup_widget() -> impl Widget<AppState> {
  type ArtworkController (line 161) | struct ArtworkController;
    method event (line 164) | fn event(
  function artwork_widget (line 202) | pub fn artwork_widget() -> impl Widget<AppState> {
  function root_widget (line 215) | fn root_widget() -> impl Widget<AppState> {
  function alert_widget (line 295) | fn alert_widget() -> impl Widget<AppState> {
  function route_widget (line 335) | fn route_widget() -> impl Widget<AppState> {
  function sidebar_menu_widget (line 394) | fn sidebar_menu_widget() -> impl Widget<AppState> {
  function sidebar_link_widget (line 416) | fn sidebar_link_widget(
  function volume_slider (line 462) | fn volume_slider() -> impl Widget<AppState> {
  function topbar_sort_widget (line 497) | fn topbar_sort_widget() -> impl Widget<AppState> {
  function topbar_back_button_widget (line 541) | fn topbar_back_button_widget() -> impl Widget<AppState> {
  function history_menu (line 564) | fn history_menu(history: &Vector<Nav>) -> Menu<AppState> {
  function sorting_menu (line 578) | fn sorting_menu(app_state: &AppState) -> Menu<AppState> {
  function topbar_title_widget (line 606) | fn topbar_title_widget() -> impl Widget<AppState> {
  function route_icon_widget (line 615) | fn route_icon_widget() -> impl Widget<Nav> {
  function route_title_widget (line 634) | fn route_title_widget() -> impl Widget<Nav> {
  function compute_main_window_title (line 641) | fn compute_main_window_title(data: &AppState, _env: &Env) -> String {

FILE: psst-gui/src/ui/playable.rs
  type Display (line 29) | pub struct Display {
  function list_widget (line 33) | pub fn list_widget<T>(display: Display) -> impl Widget<WithCtx<T>>
  function list_widget_with_find (line 40) | pub fn list_widget_with_find<T>(
  function playable_widget (line 53) | fn playable_widget(display: Display) -> impl Widget<PlayRow<Playable>> {
  function is_playing_marker_widget (line 80) | pub fn is_playing_marker_widget() -> impl Widget<bool> {
  type PlayRow (line 96) | pub struct PlayRow<T> {
  function with (line 105) | fn with<U>(&self, item: U) -> PlayRow<U> {
  method matches_query (line 117) | fn matches_query(&self, q: &FindQuery) -> bool {
  type PlayableIter (line 133) | pub trait PlayableIter {
    method origin (line 134) | fn origin(&self) -> PlaybackOrigin;
    method count (line 135) | fn count(&self) -> usize;
    method for_each (line 136) | fn for_each(&self, cb: impl FnMut(Playable, usize));
    method origin (line 142) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 146) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 152) | fn count(&self) -> usize {
    method origin (line 158) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 162) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 168) | fn count(&self) -> usize {
    method origin (line 174) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 178) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 184) | fn count(&self) -> usize {
    method origin (line 190) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 194) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 200) | fn count(&self) -> usize {
    method origin (line 206) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 210) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 216) | fn count(&self) -> usize {
    method origin (line 222) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 226) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 232) | fn count(&self) -> usize {
    method origin (line 238) | fn origin(&self) -> PlaybackOrigin {
    method for_each (line 242) | fn for_each(&self, mut cb: impl FnMut(Playable, usize)) {
    method count (line 248) | fn count(&self) -> usize {
  function for_each (line 257) | fn for_each(&self, mut cb: impl FnMut(&PlayRow<Playable>, usize)) {
  function for_each_mut (line 273) | fn for_each_mut(&mut self, mut cb: impl FnMut(&mut PlayRow<Playable>, us...
  function data_len (line 289) | fn data_len(&self) -> usize {
  type PlayController (line 294) | struct PlayController;
    method event (line 301) | fn event(

FILE: psst-gui/src/ui/playback.rs
  function panel_widget (line 27) | pub fn panel_widget() -> impl Widget<AppState> {
  function playing_item_widget (line 45) | fn playing_item_widget() -> impl Widget<NowPlaying> {
  function cover_widget (line 165) | fn cover_widget(size: f64) -> impl Widget<NowPlaying> {
  function playback_origin_icon (line 176) | fn playback_origin_icon(origin: &PlaybackOrigin) -> &'static SvgIcon {
  function player_widget (line 189) | fn player_widget() -> impl Widget<Playback> {
  function player_play_pause_widget (line 218) | fn player_play_pause_widget() -> impl Widget<Playback> {
  function queue_behavior_widget (line 252) | fn queue_behavior_widget() -> impl Widget<Playback> {
  function cycle_queue_behavior (line 268) | fn cycle_queue_behavior(qb: &QueueBehavior) -> QueueBehavior {
  function queue_behavior_icon (line 277) | fn queue_behavior_icon(qb: &QueueBehavior) -> &'static SvgIcon {
  function small_button_widget (line 286) | fn small_button_widget<T: Data>(svg: &SvgIcon) -> impl Widget<T> {
  function faded_button_widget (line 293) | fn faded_button_widget<T: Data>(svg: &SvgIcon) -> impl Widget<T> {
  function durations_widget (line 301) | fn durations_widget() -> impl Widget<NowPlaying> {
  type BarLayout (line 314) | struct BarLayout<T, I, P> {
  function new (line 325) | fn new(item: I, player: P) -> Self {
  function event (line 339) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 344) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 349) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 354) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 395) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
  type SeekBar (line 401) | struct SeekBar {
    method new (line 406) | fn new() -> Self {
    method event (line 414) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut Now...
    method lifecycle (line 437) | fn lifecycle(
    method update (line 455) | fn update(
    method layout (line 467) | fn layout(
    method paint (line 477) | fn paint(&mut self, ctx: &mut PaintCtx, data: &NowPlaying, env: &Env) {
  function _compute_loudness_path_from_analysis (line 486) | fn _compute_loudness_path_from_analysis(
  function paint_audio_analysis (line 547) | fn paint_audio_analysis(ctx: &mut PaintCtx, data: &NowPlaying, path: &Be...
  function paint_progress_bar (line 569) | fn paint_progress_bar(ctx: &mut PaintCtx, data: &NowPlaying, env: &Env) {

FILE: psst-gui/src/ui/playlist.rs
  constant LOAD_LIST (line 26) | pub const LOAD_LIST: Selector = Selector::new("app.playlist.load-list");
  constant LOAD_DETAIL (line 27) | pub const LOAD_DETAIL: Selector<(PlaylistLink, AppState)> =
  constant ADD_TRACK (line 29) | pub const ADD_TRACK: Selector<PlaylistAddTrack> = Selector::new("app.pla...
  constant REMOVE_TRACK (line 30) | pub const REMOVE_TRACK: Selector<PlaylistRemoveTrack> = Selector::new("a...
  constant FOLLOW_PLAYLIST (line 32) | pub const FOLLOW_PLAYLIST: Selector<Playlist> = Selector::new("app.playl...
  constant UNFOLLOW_PLAYLIST (line 33) | pub const UNFOLLOW_PLAYLIST: Selector<PlaylistLink> = Selector::new("app...
  constant UNFOLLOW_PLAYLIST_CONFIRM (line 34) | pub const UNFOLLOW_PLAYLIST_CONFIRM: Selector<PlaylistLink> =
  constant RENAME_PLAYLIST (line 37) | pub const RENAME_PLAYLIST: Selector<PlaylistLink> = Selector::new("app.p...
  constant RENAME_PLAYLIST_CONFIRM (line 38) | pub const RENAME_PLAYLIST_CONFIRM: Selector<PlaylistLink> =
  constant SHOW_RENAME_PLAYLIST_CONFIRM (line 41) | const SHOW_RENAME_PLAYLIST_CONFIRM: Selector<PlaylistLink> =
  constant SHOW_UNFOLLOW_PLAYLIST_CONFIRM (line 43) | const SHOW_UNFOLLOW_PLAYLIST_CONFIRM: Selector<UnfollowPlaylist> =
  function list_widget (line 46) | pub fn list_widget() -> impl Widget<AppState> {
  function unfollow_confirm_window (line 165) | fn unfollow_confirm_window(msg: UnfollowPlaylist) -> WindowDesc<AppState> {
  function unfollow_playlist_confirm_widget (line 179) | fn unfollow_playlist_confirm_widget(msg: UnfollowPlaylist) -> impl Widge...
  function rename_playlist_window (line 210) | fn rename_playlist_window(link: PlaylistLink) -> WindowDesc<AppState> {
  type TextInput (line 225) | struct TextInput {
    method with (line 230) | fn with<V, F: FnOnce(&String) -> V>(&self, _data: &AppState, f: F) -> V {
    method with_mut (line 234) | fn with_mut<V, F: FnOnce(&mut String) -> V>(&self, _data: &mut AppStat...
  function rename_playlist_widget (line 239) | fn rename_playlist_widget(link: PlaylistLink) -> impl Widget<AppState> {
  function button_section (line 274) | fn button_section(
  function information_section (line 299) | fn information_section(title_msg: &str, description_msg: &str) -> impl W...
  function playlist_widget (line 316) | pub fn playlist_widget(horizontal: bool) -> impl Widget<WithCtx<Playlist...
  function cover_widget (line 386) | fn cover_widget(size: f64) -> impl Widget<Playlist> {
  function rounded_cover_widget (line 394) | fn rounded_cover_widget(size: f64) -> impl Widget<Playlist> {
  function detail_widget (line 399) | pub fn detail_widget() -> impl Widget<AppState> {
  function async_playlist_info_widget (line 414) | fn async_playlist_info_widget() -> impl Widget<AppState> {
  function playlist_info_widget (line 431) | fn playlist_info_widget() -> impl Widget<WithCtx<Playlist>> {
  function async_tracks_widget (line 499) | fn async_tracks_widget() -> impl Widget<AppState> {
  function tracks_widget (line 527) | fn tracks_widget() -> impl Widget<WithCtx<PlaylistTracks>> {
  function sort_playlist (line 542) | fn sort_playlist(data: &AppState, result: Result<Vector<Arc<Track>>, Err...
  function playlist_menu_ctx (line 574) | fn playlist_menu_ctx(playlist: &WithCtx<Playlist>) -> Menu<AppState> {
  type UnfollowPlaylist (line 635) | struct UnfollowPlaylist {

FILE: psst-gui/src/ui/preferences.rs
  constant CLEAR_CACHE (line 26) | const CLEAR_CACHE: Selector = Selector::new("app.preferences.clear-cache");
  function make_input_row (line 29) | fn make_input_row<L>(
  function account_setup_widget (line 53) | pub fn account_setup_widget() -> impl Widget<AppState> {
  function preferences_widget (line 76) | pub fn preferences_widget() -> impl Widget<AppState> {
  function tabs_widget (line 131) | fn tabs_widget() -> impl Widget<AppState> {
  function tab_link_widget (line 160) | fn tab_link_widget(
  function general_tab_widget (line 181) | fn general_tab_widget() -> impl Widget<AppState> {
  type CacheController (line 279) | struct CacheController {
    constant RESULT (line 284) | const RESULT: Selector<Option<u64>> = Selector::new("app.preferences.m...
    method new (line 286) | fn new() -> Self {
    method start_measuring (line 290) | fn start_measuring(&mut self, sink: druid::ExtEventSink, widget_id: dr...
    method event (line 303) | fn event(
    method lifecycle (line 335) | fn lifecycle(
  type AccountTab (line 351) | enum AccountTab {
  function account_tab_widget (line 356) | fn account_tab_widget(tab: AccountTab) -> impl Widget<AppState> {
  function lastfm_connected_view (line 435) | fn lastfm_connected_view() -> impl Widget<AppState> {
  function lastfm_disconnected_view (line 459) | fn lastfm_disconnected_view() -> impl Widget<AppState> {
  type Authenticate (line 525) | pub struct Authenticate {
    method new (line 532) | fn new(tab: AccountTab) -> Self {
    method spawn_auth_thread (line 541) | fn spawn_auth_thread<T: Send + 'static>(
    method start_spotify_auth (line 565) | fn start_spotify_auth(&mut self, ctx: &mut EventCtx, data: &mut AppSta...
    constant SPOTIFY_REQUEST (line 621) | pub const SPOTIFY_REQUEST: Selector =
    constant SPOTIFY_RESPONSE (line 623) | pub const SPOTIFY_RESPONSE: Selector<Result<Credentials, String>> =
    constant INITIALIZE_LASTFM_FIELDS (line 627) | pub const INITIALIZE_LASTFM_FIELDS: Selector =
    constant LASTFM_REQUEST (line 631) | pub const LASTFM_REQUEST: Selector =
    constant LASTFM_RESPONSE (line 633) | pub const LASTFM_RESPONSE: Selector<Result<String, String>> =
    method event (line 638) | fn event(
    method lifecycle (line 769) | fn lifecycle(
  function cache_tab_widget (line 784) | fn cache_tab_widget() -> impl Widget<AppState> {
  function about_tab_widget (line 826) | fn about_tab_widget() -> impl Widget<AppState> {

FILE: psst-gui/src/ui/recommend.rs
  constant KNOBS_DEBOUNCE_DELAY (line 19) | const KNOBS_DEBOUNCE_DELAY: Duration = Duration::from_millis(500);
  constant UPDATE_PARAMS (line 21) | pub const UPDATE_PARAMS: Selector<RecommendationsParams> =
  constant LOAD_RESULTS (line 23) | pub const LOAD_RESULTS: Selector<Arc<RecommendationsRequest>> =
  function results_widget (line 26) | pub fn results_widget() -> impl Widget<AppState> {
  function params_widget (line 67) | fn params_widget() -> impl Widget<Arc<RecommendationsKnobs>> {
  function track_results_widget (line 128) | fn track_results_widget() -> impl Widget<WithCtx<Recommendations>> {

FILE: psst-gui/src/ui/search.rs
  constant NUMBER_OF_RESULTS_PER_TOPIC (line 22) | const NUMBER_OF_RESULTS_PER_TOPIC: usize = 5;
  constant INDIVIDUAL_TOPIC_RESULTS_LIMIT (line 23) | const INDIVIDUAL_TOPIC_RESULTS_LIMIT: usize = 50;
  constant LOAD_RESULTS (line 25) | pub const LOAD_RESULTS: Selector<(Arc<str>, Option<SearchTopic>)> =
  constant OPEN_LINK (line 27) | pub const OPEN_LINK: Selector<SpotifyUrl> = Selector::new("app.search.op...
  constant SET_TOPIC (line 28) | pub const SET_TOPIC: Selector<Option<SearchTopic>> = Selector::new("app....
  function input_widget (line 30) | pub fn input_widget() -> impl Widget<AppState> {
  function results_widget (line 44) | pub fn results_widget() -> impl Widget<AppState> {
  function topic_widget (line 52) | fn topic_widget() -> impl Widget<AppState> {
  function topic_button (line 70) | fn topic_button(label: &str, topic: Option<SearchTopic>) -> impl Widget<...
  function async_results_widget (line 96) | fn async_results_widget() -> impl Widget<AppState> {
  function loaded_results_widget (line 146) | fn loaded_results_widget() -> impl Widget<WithCtx<SearchResults>> {
  function results_list (line 162) | fn results_list(include_headers: bool) -> Flex<WithCtx<SearchResults>> {
  function section_widget (line 171) | fn section_widget<T: Data, W: Widget<T> + 'static>(
  function artist_results_widget (line 189) | fn artist_results_widget(include_header: bool) -> impl Widget<WithCtx<Se...
  function album_results_widget (line 199) | fn album_results_widget(include_header: bool) -> impl Widget<WithCtx<Sea...
  function track_results_widget (line 209) | fn track_results_widget(include_header: bool) -> impl Widget<WithCtx<Sea...
  function playlist_results_widget (line 229) | fn playlist_results_widget(include_header: bool) -> impl Widget<WithCtx<...
  function show_results_widget (line 239) | fn show_results_widget(include_header: bool) -> impl Widget<WithCtx<Sear...
  function header_widget (line 249) | fn header_widget<T: Data>(text: impl Into<LabelText<T>>) -> impl Widget<...

FILE: psst-gui/src/ui/show.rs
  constant LOAD_DETAIL (line 18) | pub const LOAD_DETAIL: Selector<ShowLink> = Selector::new("app.show.load...
  function detail_widget (line 20) | pub fn detail_widget() -> impl Widget<AppState> {
  function async_info_widget (line 28) | fn async_info_widget() -> impl Widget<AppState> {
  function info_widget (line 49) | fn info_widget() -> impl Widget<WithCtx<Arc<Show>>> {
  function async_episodes_widget (line 91) | fn async_episodes_widget() -> impl Widget<AppState> {
  function show_widget (line 122) | pub fn show_widget(horizontal: bool) -> impl Widget<WithCtx<Arc<Show>>> {
  function cover_widget (line 192) | fn cover_widget(size: f64) -> impl Widget<Arc<Show>> {
  function rounded_cover_widget (line 199) | fn rounded_cover_widget(size: f64) -> impl Widget<Arc<Show>> {
  function show_ctx_menu (line 204) | fn show_ctx_menu(show: &WithCtx<Arc<Show>>) -> Menu<AppState> {
  function show_menu (line 208) | fn show_menu(show: &Arc<Show>, library: &Arc<Library>) -> Menu<AppState> {

FILE: psst-gui/src/ui/theme.rs
  function grid (line 7) | pub fn grid(m: f64) -> f64 {
  constant GRID (line 11) | pub const GRID: f64 = 8.0;
  constant GREY_000 (line 13) | pub const GREY_000: Key<Color> = Key::new("app.grey_000");
  constant GREY_100 (line 14) | pub const GREY_100: Key<Color> = Key::new("app.grey_100");
  constant GREY_200 (line 15) | pub const GREY_200: Key<Color> = Key::new("app.grey_200");
  constant GREY_300 (line 16) | pub const GREY_300: Key<Color> = Key::new("app.grey_300");
  constant GREY_400 (line 17) | pub const GREY_400: Key<Color> = Key::new("app.grey_400");
  constant GREY_500 (line 18) | pub const GREY_500: Key<Color> = Key::new("app.grey_500");
  constant GREY_600 (line 19) | pub const GREY_600: Key<Color> = Key::new("app.grey_600");
  constant GREY_700 (line 20) | pub const GREY_700: Key<Color> = Key::new("app.grey_700");
  constant BLUE_100 (line 21) | pub const BLUE_100: Key<Color> = Key::new("app.blue_100");
  constant BLUE_200 (line 22) | pub const BLUE_200: Key<Color> = Key::new("app.blue_200");
  constant RED (line 24) | pub const RED: Key<Color> = Key::new("app.red");
  constant MENU_BUTTON_BG_ACTIVE (line 26) | pub const MENU_BUTTON_BG_ACTIVE: Key<Color> = Key::new("app.menu-bg-acti...
  constant MENU_BUTTON_BG_INACTIVE (line 27) | pub const MENU_BUTTON_BG_INACTIVE: Key<Color> = Key::new("app.menu-bg-in...
  constant MENU_BUTTON_FG_ACTIVE (line 28) | pub const MENU_BUTTON_FG_ACTIVE: Key<Color> = Key::new("app.menu-fg-acti...
  constant MENU_BUTTON_FG_INACTIVE (line 29) | pub const MENU_BUTTON_FG_INACTIVE: Key<Color> = Key::new("app.menu-fg-in...
  constant UI_FONT_MEDIUM (line 31) | pub const UI_FONT_MEDIUM: Key<FontDescriptor> = Key::new("app.ui-font-me...
  constant UI_FONT_MONO (line 32) | pub const UI_FONT_MONO: Key<FontDescriptor> = Key::new("app.ui-font-mono");
  constant TEXT_SIZE_SMALL (line 33) | pub const TEXT_SIZE_SMALL: Key<f64> = Key::new("app.text-size-small");
  constant ICON_COLOR (line 35) | pub const ICON_COLOR: Key<Color> = Key::new("app.icon-color");
  constant ICON_SIZE_TINY (line 36) | pub const ICON_SIZE_TINY: Size = Size::new(12.0, 12.0);
  constant ICON_SIZE_SMALL (line 37) | pub const ICON_SIZE_SMALL: Size = Size::new(14.0, 14.0);
  constant ICON_SIZE_MEDIUM (line 38) | pub const ICON_SIZE_MEDIUM: Size = Size::new(16.0, 16.0);
  constant ICON_SIZE_LARGE (line 39) | pub const ICON_SIZE_LARGE: Size = Size::new(22.0, 22.0);
  constant LINK_HOT_COLOR (line 41) | pub const LINK_HOT_COLOR: Key<Color> = Key::new("app.link-hot-color");
  constant LINK_ACTIVE_COLOR (line 42) | pub const LINK_ACTIVE_COLOR: Key<Color> = Key::new("app.link-active-colo...
  constant LINK_COLD_COLOR (line 43) | pub const LINK_COLD_COLOR: Key<Color> = Key::new("app.link-cold-color");
  function setup (line 45) | pub fn setup(env: &mut Env, state: &AppState) {
  function setup_light_theme (line 131) | fn setup_light_theme(env: &mut Env) {
  function setup_dark_theme (line 150) | fn setup_dark_theme(env: &mut Env) {

FILE: psst-gui/src/ui/track.rs
  type Display (line 31) | pub struct Display {
    method empty (line 41) | pub fn empty() -> Self {
  function playable_widget (line 53) | pub fn playable_widget(track: &Track, display: Display) -> impl Widget<P...
  function cover_widget (line 203) | fn cover_widget(size: f64) -> impl Widget<Arc<Track>> {
  function rounded_cover_widget (line 213) | fn rounded_cover_widget(size: f64) -> impl Widget<Arc<Track>> {
  function popularity_stars (line 217) | fn popularity_stars(popularity: u32) -> String {
  function track_row_menu (line 234) | fn track_row_menu(row: &PlayRow<Arc<Track>>) -> Menu<AppState> {
  function track_menu (line 238) | pub fn track_menu(

FILE: psst-gui/src/ui/user.rs
  constant LOAD_PROFILE (line 15) | pub const LOAD_PROFILE: Selector = Selector::new("app.user.load-profile");
  function user_widget (line 17) | pub fn user_widget() -> impl Widget<AppState> {
  function preferences_widget (line 57) | fn preferences_widget<T: Data>(svg: &SvgIcon) -> impl Widget<T> {

FILE: psst-gui/src/ui/utils.rs
  type Spinner (line 14) | struct Spinner {
    method new (line 19) | pub fn new() -> Self {
    method event (line 25) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, ...
    method lifecycle (line 36) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, _da...
    method update (line 43) | fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _...
    method layout (line 45) | fn layout(
    method paint (line 55) | fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {
  function stat_row (line 74) | pub fn stat_row<T: Data>(
  function placeholder_widget (line 92) | pub fn placeholder_widget<T: Data>() -> impl Widget<T> {
  function spinner_widget (line 96) | pub fn spinner_widget<T: Data>() -> impl Widget<T> {
  function error_widget (line 100) | pub fn error_widget() -> impl Widget<Error> {
  function as_minutes_and_seconds (line 124) | pub fn as_minutes_and_seconds(dur: Duration) -> String {
  function as_human (line 130) | pub fn as_human(dur: Duration) -> String {
  function format_number_with_commas (line 137) | pub fn format_number_with_commas(n: i64) -> String {
  type InfoLayout (line 155) | pub struct InfoLayout<T, B, S> {
  function new (line 166) | pub fn new(biography: B, stats: S) -> Self {
  function event (line 175) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 180) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 185) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 190) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 232) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/webapi/cache.rs
  type WebApiCache (line 16) | pub struct WebApiCache {
    method new (line 22) | pub fn new(base: Option<PathBuf>) -> Self {
    method get_image (line 30) | pub fn get_image(&self, uri: &Arc<str>) -> Option<ImageBuf> {
    method set_image (line 34) | pub fn set_image(&self, uri: Arc<str>, image: ImageBuf) {
    method get_image_from_disk (line 38) | pub fn get_image_from_disk(&self, uri: &Arc<str>) -> Option<ImageBuf> {
    method save_image_to_disk (line 46) | pub fn save_image_to_disk(&self, uri: &Arc<str>, data: &[u8]) {
    method hash_uri (line 56) | fn hash_uri(uri: &str) -> u64 {
    method get (line 62) | pub fn get(&self, bucket: &str, key: &str) -> Option<File> {
    method set (line 66) | pub fn set(&self, bucket: &str, key: &str, value: &[u8]) {
    method bucket (line 79) | fn bucket(&self, bucket: &str) -> Option<PathBuf> {
    method key (line 83) | fn key(&self, bucket: &str, key: &str) -> Option<PathBuf> {

FILE: psst-gui/src/webapi/client.rs
  type WebApi (line 46) | pub struct WebApi {
    method new (line 56) | pub fn new(
    method user_agent (line 78) | fn user_agent() -> String {
    method access_token (line 92) | fn access_token(&self) -> Result<String, Error> {
    method request (line 99) | fn request(&self, request: &RequestBuilder) -> Result<Response<Body>, ...
    method with_retry (line 133) | fn with_retry(f: impl Fn() -> Result<Response<Body>, Error>) -> Result...
    method send_empty_json (line 154) | fn send_empty_json(&self, request: &RequestBuilder) -> Result<(), Erro...
    method load (line 160) | fn load<T: DeserializeOwned>(&self, request: &RequestBuilder) -> Resul...
    method load_cached (line 170) | fn load_cached<T: Data + DeserializeOwned>(
    method for_all_pages (line 196) | fn for_all_pages<T: DeserializeOwned + Clone>(
    method for_some_pages (line 227) | fn for_some_pages<T: DeserializeOwned + Clone>(
    method load_all_pages (line 271) | fn load_all_pages<T: DeserializeOwned + Clone>(
    method load_some_pages (line 286) | fn load_some_pages<T: DeserializeOwned + Clone>(
    method load_local_tracks (line 302) | pub fn load_local_tracks(&self, username: &str) {
    method load_and_return_home_section (line 312) | fn load_and_return_home_section(&self, request: &RequestBuilder) -> Re...
    method install_as_global (line 679) | pub fn install_as_global(self) {
    method global (line 686) | pub fn global() -> Arc<Self> {
    method get_user_profile (line 694) | pub fn get_user_profile(&self) -> Result<UserProfile, Error> {
    method get_user_top_tracks (line 700) | pub fn get_user_top_tracks(&self) -> Result<Vector<Arc<Track>>, Error> {
    method get_user_top_artist (line 708) | pub fn get_user_top_artist(&self) -> Result<Vector<Artist>, Error> {
    method get_artist (line 727) | pub fn get_artist(&self, id: &str) -> Result<Artist, Error> {
    method get_artist_albums (line 734) | pub fn get_artist_albums(&self, id: &str) -> Result<ArtistAlbums, Erro...
    method get_artist_top_tracks (line 779) | pub fn get_artist_top_tracks(&self, id: &str) -> Result<Vector<Arc<Tra...
    method get_related_artists (line 792) | pub fn get_related_artists(&self, id: &str) -> Result<Cached<Vector<Ar...
    method get_artist_info (line 806) | pub fn get_artist_info(&self, id: &str) -> Result<ArtistInfo, Error> {
    method get_album (line 924) | pub fn get_album(&self, id: &str) -> Result<Cached<Arc<Album>>, Error> {
    method get_show (line 935) | pub fn get_show(&self, id: &str) -> Result<Cached<Arc<Show>>, Error> {
    method get_episodes (line 945) | pub fn get_episodes(
    method get_show_episodes (line 962) | pub fn get_show_episodes(&self, id: &str) -> Result<Vector<Arc<Episode...
    method get_track (line 986) | pub fn get_track(&self, id: &str) -> Result<Arc<Track>, Error> {
    method get_track_credits (line 992) | pub fn get_track_credits(&self, track_id: &str) -> Result<TrackCredits...
    method get_lyrics (line 1003) | pub fn get_lyrics(&self, track_id: String) -> Result<Vector<TrackLines...
    method get_saved_albums (line 1037) | pub fn get_saved_albums(&self) -> Result<Vector<Arc<Album>>, Error> {
    method save_album (line 1054) | pub fn save_album(&self, id: &str) -> Result<(), Error> {
    method unsave_album (line 1060) | pub fn unsave_album(&self, id: &str) -> Result<(), Error> {
    method get_saved_tracks (line 1066) | pub fn get_saved_tracks(&self) -> Result<Vector<Arc<Track>>, Error> {
    method get_saved_shows (line 1081) | pub fn get_saved_shows(&self) -> Result<Vector<Arc<Show>>, Error> {
    method save_track (line 1098) | pub fn save_track(&self, id: &str) -> Result<(), Error> {
    method unsave_track (line 1104) | pub fn unsave_track(&self, id: &str) -> Result<(), Error> {
    method save_show (line 1110) | pub fn save_show(&self, id: &str) -> Result<(), Error> {
    method unsave_show (line 1116) | pub fn unsave_show(&self, id: &str) -> Result<(), Error> {
    method get_user_info (line 1124) | pub fn get_user_info(&self) -> Result<(String, String), Error> {
    method get_section (line 1143) | pub fn get_section(&self, section_uri: &str) -> Result<MixedView, Erro...
    method get_made_for_you (line 1174) | pub fn get_made_for_you(&self) -> Result<MixedView, Error> {
    method get_top_mixes (line 1179) | pub fn get_top_mixes(&self) -> Result<MixedView, Error> {
    method recommended_stations (line 1184) | pub fn recommended_stations(&self) -> Result<MixedView, Error> {
    method uniquely_yours (line 1189) | pub fn uniquely_yours(&self) -> Result<MixedView, Error> {
    method best_of_artists (line 1194) | pub fn best_of_artists(&self) -> Result<MixedView, Error> {
    method jump_back_in (line 1200) | pub fn jump_back_in(&self) -> Result<MixedView, Error> {
    method your_shows (line 1206) | pub fn your_shows(&self) -> Result<MixedView, Error> {
    method shows_that_you_might_like (line 1211) | pub fn shows_that_you_might_like(&self) -> Result<MixedView, Error> {
    method get_playlists (line 1220) | pub fn get_playlists(&self) -> Result<Vector<Playlist>, Error> {
    method follow_playlist (line 1226) | pub fn follow_playlist(&self, id: &str) -> Result<(), Error> {
    method unfollow_playlist (line 1234) | pub fn unfollow_playlist(&self, id: &str) -> Result<(), Error> {
    method get_playlist (line 1242) | pub fn get_playlist(&self, id: &str) -> Result<Playlist, Error> {
    method get_playlist_tracks (line 1249) | pub fn get_playlist_tracks(&self, id: &str) -> Result<Vector<Arc<Track...
    method change_playlist_details (line 1287) | pub fn change_playlist_details(&self, id: &str, name: &str) -> Result<...
    method add_track_to_playlist (line 1295) | pub fn add_track_to_playlist(&self, playlist_id: &str, track_uri: &str...
    method remove_track_from_playlist (line 1306) | pub fn remove_track_from_playlist(
    method search (line 1324) | pub fn search(
    method load_spotify_link (line 1367) | pub fn load_spotify_link(&self, link: &SpotifyUrl) -> Result<Nav, Erro...
    method get_recommendations (line 1388) | pub fn get_recommendations(
    method _get_audio_analysis (line 1447) | pub fn _get_audio_analysis(&self, track_id: &str) -> Result<AudioAnaly...
    method get_cached_image (line 1457) | pub fn get_cached_image(&self, uri: &Arc<str>) -> Option<ImageBuf> {
    method get_image (line 1461) | pub fn get_image(&self, uri: Arc<str>) -> Result<ImageBuf, Error> {
  method from (line 1514) | fn from(err: io::Error) -> Self {
  method from (line 1520) | fn from(err: ureq::Error) -> Self {
  method from (line 1526) | fn from(err: serde_json::Error) -> Self {
  method from (line 1532) | fn from(err: image::ImageError) -> Self {
  type Method (line 1538) | enum Method {
  type RequestBuilder (line 1547) | struct RequestBuilder {
    method new (line 1559) | fn new(path: impl Display, method: Method, body: Option<serde_json::Va...
    method query (line 1571) | fn query(mut self, key: impl Display, value: impl Display) -> Self {
    method header (line 1576) | fn header(mut self, key: impl Display, value: impl Display) -> Self {
    method set_protocol (line 1581) | fn set_protocol(mut self, protocol: impl Display) -> Self {
    method get_headers (line 1585) | fn get_headers(&self) -> &HashMap<String, String> {
    method get_body (line 1588) | fn get_body(&self) -> Option<&serde_json::Value> {
    method set_body (line 1591) | fn set_body(mut self, body: Option<serde_json::Value>) -> Self {
    method get_method (line 1595) | fn get_method(&self) -> &Method {
    method set_method (line 1599) | fn set_method(mut self, method: Method) -> Self {
    method set_base_uri (line 1603) | fn set_base_uri(mut self, url: impl Display) -> Self {
    method build (line 1607) | fn build(&self) -> String {

FILE: psst-gui/src/webapi/local.rs
  constant MAGIC_BYTES (line 60) | const MAGIC_BYTES: &[u8] = b"SPCO";
  constant FILE_TYPE (line 61) | const FILE_TYPE: &[u8] = b"LocalFilesStorage";
  constant ARRAY_SIGNATURE (line 63) | const ARRAY_SIGNATURE: u8 = 0x60;
  constant STRING_SIGNATURE (line 64) | const STRING_SIGNATURE: u8 = 0x09;
  constant TRAILER_END (line 65) | const TRAILER_END: [u8; 2] = [0x78u8, 0x04u8];
  type LocalTrack (line 68) | pub struct LocalTrack {
  type LocalTrackManager (line 75) | pub struct LocalTrackManager {
    method new (line 80) | pub fn new() -> Self {
    method load_tracks_for_user (line 86) | pub fn load_tracks_for_user(&mut self, username: &str) -> io::Result<(...
    method find_local_track (line 128) | pub fn find_local_track(&self, track_json: Value) -> Option<Arc<Track>> {
    method is_matching_in_addition_to_title (line 183) | fn is_matching_in_addition_to_title(t1: &LocalTrack, t2: &LocalTrackJs...
  type LocalAlbumLinkJson (line 204) | struct LocalAlbumLinkJson {
  type LocalArtistLinkJson (line 213) | struct LocalArtistLinkJson {
  type LocalTrackJson (line 220) | struct LocalTrackJson {
  type LocalTracksReader (line 235) | struct LocalTracksReader {
    method new (line 240) | fn new(file: File) -> io::Result<Self> {
    method parse_file (line 247) | fn parse_file(mut file: File) -> io::Result<ChunkedReader> {
    method advance (line 263) | fn advance(&mut self, len: usize) -> io::Result<()> {
    method advance_until (line 267) | fn advance_until(&mut self, bytes: &[u8]) -> io::Result<()> {
    method read_string (line 271) | fn read_string(&mut self) -> io::Result<String> {
    method read_array (line 281) | fn read_array(&mut self) -> io::Result<usize> {
  type ChunkedReader (line 292) | struct ChunkedReader {
    method new (line 298) | fn new(inner: File) -> Self {
    method read_next_chunk (line 305) | fn read_next_chunk(&mut self) -> io::Result<()> {
  method read (line 316) | fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
  function read_u8 (line 330) | fn read_u8(f: &mut impl io::Read) -> io::Result<u8> {
  function read_u16_le (line 337) | fn read_u16_le(f: &mut impl io::Read) -> io::Result<u16> {
  function read_uvarint (line 344) | fn read_uvarint(f: &mut impl io::Read) -> io::Result<u64> {
  function read_bytes (line 364) | fn read_bytes(f: &mut impl io::Read, len: usize) -> io::Result<Vec<u8>> {
  function read_utf8 (line 371) | fn read_utf8(f: &mut impl io::Read, len: usize) -> io::Result<String> {
  function advance (line 377) | fn advance(f: &mut impl io::Read, len: usize) -> io::Result<()> {
  function advance_until (line 386) | pub fn advance_until(f: &mut impl io::Read, bytes: &[u8]) -> io::Result<...

FILE: psst-gui/src/widget/checkbox.rs
  type Checkbox (line 9) | pub struct Checkbox {
    method new (line 14) | pub fn new(text: impl Into<LabelText<bool>>) -> Checkbox {
    method event (line 22) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut bool...
    method lifecycle (line 43) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, dat...
    method update (line 53) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &bool, data: &bool...
    method layout (line 58) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &...
    method paint (line 73) | fn paint(&mut self, ctx: &mut PaintCtx, data: &bool, env: &Env) {

FILE: psst-gui/src/widget/dispatcher.rs
  type ChildPicker (line 5) | type ChildPicker<T, U> = dyn Fn(&T, &Env) -> U;
  type ChildBuilder (line 6) | type ChildBuilder<T, U> = dyn Fn(&U, &T, &Env) -> Box<dyn Widget<T>>;
  type ViewDispatcher (line 8) | pub struct ViewDispatcher<T, U> {
  function new (line 16) | pub fn new(
  function active_child (line 28) | fn active_child(&mut self) -> Option<&mut WidgetPod<T, Box<dyn Widget<T>...
  function event (line 38) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 48) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 68) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 94) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 105) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/widget/empty.rs
  type Empty (line 3) | pub struct Empty;
    method event (line 6) | fn event(&mut self, _ctx: &mut EventCtx, _event: &Event, _data: &mut T...
    method lifecycle (line 7) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _...
    method update (line 8) | fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _...
    method layout (line 9) | fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints, _data:...
    method paint (line 12) | fn paint(&mut self, _ctx: &mut PaintCtx, _data: &T, _env: &Env) {}

FILE: psst-gui/src/widget/fill_between.rs
  type FillBetween (line 8) | pub struct FillBetween<T: Data> {
  function new (line 14) | pub fn new(left: impl Widget<T> + 'static, right: impl Widget<T> + 'stat...
  function event (line 23) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 28) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 33) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 38) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 65) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/widget/icons.rs
  type PaintOp (line 182) | pub enum PaintOp {
  type SvgIcon (line 187) | pub struct SvgIcon {
    method scale (line 194) | pub fn scale(&self, to_size: impl Into<Size>) -> Icon {
  type Icon (line 206) | pub struct Icon {
    method new (line 215) | pub fn new(op: PaintOp, bez_path: BezPath, size: Size, scale: Affine) ...
    method with_color (line 225) | pub fn with_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Se...
    method set_color (line 230) | pub fn set_color(&mut self, color: impl Into<KeyOrValue<Color>>) {
    method event (line 236) | fn event(&mut self, _ctx: &mut EventCtx, _ev: &Event, _data: &mut T, _...
    method lifecycle (line 238) | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _ev: &LifeCycle, _dat...
    method update (line 240) | fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &T, _data: &T, _...
    method layout (line 242) | fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints, _data:...
    method paint (line 246) | fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) {

FILE: psst-gui/src/widget/link.rs
  type Link (line 5) | pub struct Link<T> {
  function new (line 14) | pub fn new(inner: impl Widget<T> + 'static) -> Self {
  function border (line 24) | pub fn border(
  function rounded (line 34) | pub fn rounded(mut self, radius: impl Into<KeyOrValue<RoundedRectRadii>>...
  function circle (line 39) | pub fn circle(self) -> Self {
  function active (line 43) | pub fn active(mut self, predicate: impl Fn(&T, &Env) -> bool + 'static) ...
  function event (line 50) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 54) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 61) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 65) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 71) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/widget/maybe.rs
  type Maybe (line 26) | pub struct Maybe<T> {
  type MaybeWidget (line 33) | enum MaybeWidget<T> {
  function new (line 40) | pub fn new<W1, W2>(
  function or_empty (line 60) | pub fn or_empty<W1: Widget<T> + 'static>(some_maker: impl Fn() -> W1 + '...
  function rebuild_widget (line 64) | fn rebuild_widget(&mut self, is_some: bool) {
  function event (line 74) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option...
  function lifecycle (line 83) | fn lifecycle(
  function update (line 102) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &Option<T>, data: &O...
  function layout (line 114) | fn layout(
  function paint (line 136) | fn paint(&mut self, ctx: &mut PaintCtx, data: &Option<T>, env: &Env) {
  function is_some (line 145) | fn is_some(&self) -> bool {
  function with_some (line 152) | fn with_some<R, F: FnOnce(&mut WidgetPod<T, Box<dyn Widget<T>>>) -> R>(
  function with_none (line 165) | fn with_none<R, F: FnOnce(&mut WidgetPod<(), Box<dyn Widget<()>>>) -> R>(

FILE: psst-gui/src/widget/mod.rs
  type MyWidgetExt (line 38) | pub trait MyWidgetExt<T: Data>: Widget<T> + Sized + 'static {
    method log (line 40) | fn log(self, label: &'static str) -> Logger<Self> {
    method link (line 44) | fn link(self) -> Link<T> {
    method clip (line 48) | fn clip<S>(self, shape: S) -> Clip<S, Self> {
    method padding_left (line 52) | fn padding_left(self, p: f64) -> Padding<T, Self> {
    method padding_right (line 56) | fn padding_right(self, p: f64) -> Padding<T, Self> {
    method padding_horizontal (line 60) | fn padding_horizontal(self, p: f64) -> Padding<T, Self> {
    method on_debounce (line 64) | fn on_debounce(
    method on_update (line 72) | fn on_update<F>(self, handler: F) -> ControllerHost<Self, OnUpdate<F>>
    method on_left_click (line 79) | fn on_left_click(
    method on_right_click (line 87) | fn on_right_click(
    method on_mouse_click (line 94) | fn on_mouse_click(
    method on_scroll (line 102) | fn on_scroll(
    method with_cursor (line 110) | fn with_cursor(self, cursor: Cursor) -> ControllerHost<Self, ExCursor<...
    method on_command (line 114) | fn on_command<U, F>(
    method on_command_async (line 126) | fn on_command_async<U: Data + Send, V: Data + Send>(
    method context_menu (line 142) | fn context_menu(

FILE: psst-gui/src/widget/overlay.rs
  type OverlayPosition (line 3) | pub enum OverlayPosition {
  type Overlay (line 7) | pub struct Overlay<T, W, O> {
  function bottom (line 17) | pub fn bottom(inner: W, overlay: O) -> Self {
  function event (line 32) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 37) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 42) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &...
  function layout (line 47) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 59) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/widget/promise.rs
  type Async (line 5) | pub struct Async<T, D, E> {
  type PromiseWidget (line 13) | enum PromiseWidget<T, D, E> {
  function new (line 21) | pub fn new<WD, WT, WE>(
  function rebuild_widget (line 39) | fn rebuild_widget(&mut self, state: PromiseState) {
  function event (line 50) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Promis...
  function lifecycle (line 67) | fn lifecycle(
  function update (line 97) | fn update(
  function layout (line 123) | fn layout(
  function paint (line 151) | fn paint(&mut self, ctx: &mut PaintCtx, data: &Promise<T, D, E>, env: &E...
  function state (line 168) | fn state(&self) -> PromiseState {
  function with_deferred (line 177) | fn with_deferred<R, F: FnOnce(&mut WidgetPod<D, Box<dyn Widget<D>>>) -> R>(
  function with_resolved (line 188) | fn with_resolved<R, F: FnOnce(&mut WidgetPod<T, Box<dyn Widget<T>>>) -> R>(
  function with_rejected (line 199) | fn with_rejected<R, F: FnOnce(&mut WidgetPod<E, Box<dyn Widget<E>>>) -> R>(

FILE: psst-gui/src/widget/remote_image.rs
  constant REQUEST_DATA (line 8) | pub const REQUEST_DATA: Selector<Arc<str>> = Selector::new("remote-image...
  constant PROVIDE_DATA (line 9) | pub const PROVIDE_DATA: Selector<ImagePayload> = Selector::new("remote-i...
  type ImagePayload (line 12) | pub struct ImagePayload {
  type RemoteImage (line 17) | pub struct RemoteImage<T> {
  function new (line 25) | pub fn new(
  function event (line 39) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 58) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 74) | fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: ...
  function layout (line 91) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 103) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {

FILE: psst-gui/src/widget/theme.rs
  type ThemeScope (line 4) | pub struct ThemeScope<W> {
  function new (line 10) | pub fn new(inner: W) -> Self {
  function set_env (line 17) | fn set_env(&mut self, data: &AppState, outer_env: &Env) {
  function event (line 25) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut AppSta...
  function lifecycle (line 30) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 38) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &AppState, data: &Ap...
  function layout (line 48) | fn layout(
  function paint (line 59) | fn paint(&mut self, ctx: &mut PaintCtx, data: &AppState, env: &Env) {

FILE: psst-gui/src/widget/utils.rs
  type FadeOut (line 7) | pub struct FadeOut<W> {
  function new (line 18) | pub fn new(inner: W, axis: Axis, limit: KeyOrValue<f64>) -> Self {
  function bottom (line 28) | pub fn bottom(inner: W, height: impl Into<KeyOrValue<f64>>) -> Self {
  function with_color (line 32) | pub fn with_color(mut self, color: impl Into<KeyOrValue<Color>>) -> Self {
  function event (line 43) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 47) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 51) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &...
  function layout (line 55) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 79) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
  function id (line 123) | fn id(&self) -> Option<WidgetId> {
  type Clip (line 128) | pub struct Clip<S, W> {
  function new (line 134) | pub fn new(shape: S, inner: W) -> Self {
  function event (line 140) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 144) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 148) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &...
  function layout (line 152) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 158) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
  function id (line 165) | fn id(&self) -> Option<WidgetId> {
  type Border (line 170) | pub enum Border {
    method with_color (line 176) | pub fn with_color<T: Data>(
  type Logger (line 194) | pub struct Logger<W> {
  function new (line 206) | pub fn new(inner: W) -> Self {
  function with_label (line 218) | pub fn with_label(mut self, title: &'static str) -> Self {
  function with_event (line 223) | pub fn with_event(mut self) -> Self {
  function with_lifecycle (line 228) | pub fn with_lifecycle(mut self) -> Self {
  function with_update (line 233) | pub fn with_update(mut self) -> Self {
  function with_layout (line 238) | pub fn with_layout(mut self) -> Self {
  function with_paint (line 243) | pub fn with_paint(mut self) -> Self {
  function event (line 250) | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env...
  function lifecycle (line 257) | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data:...
  function update (line 264) | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &...
  function layout (line 271) | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T,...
  function paint (line 278) | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
  function id (line 285) | fn id(&self) -> Option<WidgetId> {
Condensed preview — 136 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (784K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "github: jpochyla\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 498,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: \"\"\nlabels: bug\nassignees: \"\"\n---\n\n**Describe the b"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 594,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n**Is your feat"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 11178,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\npermissions:\n  contents: read\n\nenv:\n  C"
  },
  {
    "path": ".gitignore",
    "chars": 79,
    "preview": "config.json\ntarget\ncache\n.cargo\n.idea\n.DS_Store\n.env\n*.iml\nrust-toolchain\n*.ico"
  },
  {
    "path": ".homebrew/generate_formula.sh",
    "chars": 701,
    "preview": "#!/bin/bash\n\nset -eo pipefail\n\nREPO_OWNER=\"jpochyla\"\nREPO_NAME=\"psst\"\n\ncat <<EOF\ncask \"psst\" do\n  version :latest\n  sha2"
  },
  {
    "path": ".pkg/APPIMAGE/pkg2appimage-ingredients.yml",
    "chars": 301,
    "preview": "ingredients:\n  dist: focal\n  sources:\n    - deb http://us.archive.ubuntu.com/ubuntu/ focal main universe\n  debs:\n    - ."
  },
  {
    "path": ".pkg/DEBIAN/control",
    "chars": 300,
    "preview": "Package: psst-gui\nVersion: $VERSION\nArchitecture: $ARCHITECTURE\nMaintainer: Jan Pochyla <jpochyla@gmail.com>\nSection: so"
  },
  {
    "path": ".pkg/copyright",
    "chars": 1229,
    "preview": "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/\nUpstream-Name: Psst\nSource: https://github.co"
  },
  {
    "path": ".pkg/psst.desktop",
    "chars": 264,
    "preview": "[Desktop Entry]\nType=Application\nName=Psst\nComment=Fast and native Spotify client\nGenericName=Music Player\nIcon=psst\nTry"
  },
  {
    "path": ".rustfmt.toml",
    "chars": 51,
    "preview": "imports_granularity = \"Crate\"\nwrap_comments = true\n"
  },
  {
    "path": "Cargo.toml",
    "chars": 302,
    "preview": "[workspace]\nresolver = \"2\"\nmembers = [\"psst-core\", \"psst-cli\", \"psst-gui\"]\n\n[profile.dev]\nopt-level = 1\ndebug = true\nlto"
  },
  {
    "path": "Cross.toml",
    "chars": 439,
    "preview": "[build]\npre-build = [\"\"\"\n    dpkg --add-architecture $CROSS_DEB_ARCH && \\\n    apt-get update && \\\n    apt-get --assume-y"
  },
  {
    "path": "LICENSE.md",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2020 Jan Pochyla\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 7033,
    "preview": "# Psst\n\nA fast Spotify client with a native GUI written in Rust, without Electron.\nPsst is still very early in developme"
  },
  {
    "path": "psst-cli/Cargo.toml",
    "chars": 287,
    "preview": "[package]\nname = \"psst-cli\"\nversion = \"0.1.0\"\nauthors = [\"Jan Pochyla <jpochyla@gmail.com>\"]\nedition = \"2021\"\n\n[features"
  },
  {
    "path": "psst-cli/src/main.rs",
    "chars": 3401,
    "preview": "use psst_core::{\n    audio::{\n        normalize::NormalizationLevel,\n        output::{AudioOutput, AudioSink, DefaultAud"
  },
  {
    "path": "psst-core/Cargo.toml",
    "chars": 1593,
    "preview": "[package]\nname = \"psst-core\"\nversion = \"0.1.0\"\nauthors = [\"Jan Pochyla <jpochyla@gmail.com>\"]\nedition = \"2021\"\n\n\n[build-"
  },
  {
    "path": "psst-core/build.rs",
    "chars": 1533,
    "preview": "use gix_config::File;\nuse std::{env, fs, io::Write};\nuse time::OffsetDateTime;\n\nfn main() {\n    let outdir = env::var(\"O"
  },
  {
    "path": "psst-core/src/actor.rs",
    "chars": 3069,
    "preview": "use std::{\n    fmt::Display,\n    thread::{self, JoinHandle},\n    time::Duration,\n};\n\nuse crossbeam_channel::{\n    bounde"
  },
  {
    "path": "psst-core/src/audio/decode.rs",
    "chars": 5428,
    "preview": "use std::{io, time::Duration};\n\nuse symphonia::{\n    core::{\n        audio::{SampleBuffer, SignalSpec},\n        codecs::"
  },
  {
    "path": "psst-core/src/audio/decrypt.rs",
    "chars": 1364,
    "preview": "use std::{convert::TryInto, io};\n\nuse aes::{\n    cipher::{generic_array::GenericArray, KeyIvInit, StreamCipher, StreamCi"
  },
  {
    "path": "psst-core/src/audio/mod.rs",
    "chars": 117,
    "preview": "pub mod decode;\npub mod decrypt;\npub mod normalize;\npub mod output;\npub mod probe;\npub mod resample;\npub mod source;\n"
  },
  {
    "path": "psst-core/src/audio/normalize.rs",
    "chars": 1469,
    "preview": "use std::{\n    io,\n    io::{Read, Seek, SeekFrom},\n};\n\nuse byteorder::{ReadBytesExt, LE};\n\n#[derive(Debug, Clone, Copy, "
  },
  {
    "path": "psst-core/src/audio/output/cpal.rs",
    "chars": 8297,
    "preview": "use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};\nuse crossbeam_channel::{bounded, Receiver, Sender};\nuse num_tra"
  },
  {
    "path": "psst-core/src/audio/output/cubeb.rs",
    "chars": 6899,
    "preview": "use std::{env, ffi::CString, ops::Deref};\n\nuse crossbeam_channel::{bounded, Receiver, Sender};\n\nuse crate::{\n    actor::"
  },
  {
    "path": "psst-core/src/audio/output/mod.rs",
    "chars": 696,
    "preview": "use crate::audio::source::AudioSource;\n\n#[cfg(feature = \"cpal\")]\npub mod cpal;\n#[cfg(feature = \"cubeb\")]\npub mod cubeb;\n"
  },
  {
    "path": "psst-core/src/audio/probe.rs",
    "chars": 2609,
    "preview": "use std::fs::File;\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse symphonia::core::codecs::CodecType;\nuse symphon"
  },
  {
    "path": "psst-core/src/audio/resample.rs",
    "chars": 2975,
    "preview": "use crate::error::Error;\n\n#[derive(Copy, Clone)]\npub enum ResamplingQuality {\n    SincBestQuality = libsamplerate::SRC_S"
  },
  {
    "path": "psst-core/src/audio/source.rs",
    "chars": 4837,
    "preview": "use crate::audio::resample::ResamplingSpec;\n\nuse super::resample::{AudioResampler, ResamplingQuality};\n\n/// Types that c"
  },
  {
    "path": "psst-core/src/cache.rs",
    "chars": 4453,
    "preview": "use std::{\n    fs, io,\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse crate::{\n    audio::decrypt::AudioKey,\n    erro"
  },
  {
    "path": "psst-core/src/cdn.rs",
    "chars": 5163,
    "preview": "use std::{\n    io::Read,\n    sync::Arc,\n    time::{Duration, Instant},\n};\n\nuse serde::Deserialize;\n\nuse crate::{\n    err"
  },
  {
    "path": "psst-core/src/connection/diffie_hellman.rs",
    "chars": 1518,
    "preview": "use num_bigint::{BigUint, ToBigUint};\nuse rand::Rng;\n\npub struct DHLocalKeys {\n    private_key: BigUint,\n    public_key:"
  },
  {
    "path": "psst-core/src/connection/mod.rs",
    "chars": 14963,
    "preview": "pub mod diffie_hellman;\npub mod shannon_codec;\n\nuse std::{\n    convert::TryInto,\n    io,\n    io::{Read, Write},\n    net:"
  },
  {
    "path": "psst-core/src/connection/shannon_codec.rs",
    "chars": 3738,
    "preview": "use std::{convert::TryInto, io};\n\nuse shannon::Shannon;\n\n#[derive(Debug)]\npub struct ShannonMsg {\n    pub cmd: u8,\n    p"
  },
  {
    "path": "psst-core/src/error.rs",
    "chars": 3834,
    "preview": "use std::sync::mpsc::RecvTimeoutError;\nuse std::{error, fmt, io};\n\n#[derive(Debug)]\npub enum Error {\n    SessionDisconne"
  },
  {
    "path": "psst-core/src/item_id.rs",
    "chars": 6070,
    "preview": "use std::{\n    collections::HashMap,\n    convert::TryInto,\n    fmt,\n    ops::Deref,\n    path::PathBuf,\n    sync::{LazyLo"
  },
  {
    "path": "psst-core/src/lastfm.rs",
    "chars": 3483,
    "preview": "use crate::error::Error;\nuse crate::oauth::listen_for_callback_parameter;\nuse rustfm_scrobble::{responses::SessionRespon"
  },
  {
    "path": "psst-core/src/lib.rs",
    "chars": 513,
    "preview": "#![allow(clippy::new_without_default)]\n\nuse git_version::git_version;\n\npub const GIT_VERSION: &str = git_version!();\npub"
  },
  {
    "path": "psst-core/src/metadata.rs",
    "chars": 3823,
    "preview": "use std::time::Duration;\n\nuse crate::{\n    error::Error,\n    item_id::{FileId, ItemId, ItemIdType},\n    player::file::{A"
  },
  {
    "path": "psst-core/src/oauth.rs",
    "chars": 6486,
    "preview": "use crate::error::Error;\nuse oauth2::{\n    basic::BasicClient, reqwest::http_client, AuthUrl, AuthorizationCode, ClientI"
  },
  {
    "path": "psst-core/src/player/file.rs",
    "chars": 10587,
    "preview": "use std::{\n    fs, io,\n    io::{Seek, SeekFrom},\n    path::PathBuf,\n    sync::Arc,\n    thread,\n    thread::JoinHandle,\n "
  },
  {
    "path": "psst-core/src/player/item.rs",
    "chars": 7011,
    "preview": "use std::time::Duration;\n\nuse crate::{\n    audio::{\n        decode::AudioDecoder, decrypt::AudioKey, normalize::Normaliz"
  },
  {
    "path": "psst-core/src/player/mod.rs",
    "chars": 16090,
    "preview": "pub mod file;\npub mod item;\npub mod queue;\nmod storage;\nmod worker;\n\nuse std::{mem, thread, thread::JoinHandle, time::Du"
  },
  {
    "path": "psst-core/src/player/queue.rs",
    "chars": 4384,
    "preview": "use rand::prelude::SliceRandom;\n\nuse super::PlaybackItem;\n\n#[derive(Default, Debug)]\npub enum QueueBehavior {\n    #[defa"
  },
  {
    "path": "psst-core/src/player/storage.rs",
    "chars": 9627,
    "preview": "use std::{\n    fs::File,\n    io,\n    io::{Read, Seek, SeekFrom, Write},\n    ops::Range,\n    path::{Path, PathBuf},\n    s"
  },
  {
    "path": "psst-core/src/player/worker.rs",
    "chars": 12592,
    "preview": "use std::{\n    ops::Range,\n    sync::{\n        atomic::{AtomicU64, Ordering},\n        Arc,\n    },\n    time::Duration,\n};"
  },
  {
    "path": "psst-core/src/session/access_token.rs",
    "chars": 2358,
    "preview": "use std::time::{Duration, Instant};\n\nuse parking_lot::Mutex;\nuse serde::Deserialize;\n\nuse crate::error::Error;\n\nuse supe"
  },
  {
    "path": "psst-core/src/session/audio_key.rs",
    "chars": 2284,
    "preview": "use std::{\n    collections::HashMap,\n    io::{Cursor, Read},\n};\n\nuse byteorder::{ReadBytesExt, BE};\nuse crossbeam_channe"
  },
  {
    "path": "psst-core/src/session/client_token.rs",
    "chars": 10346,
    "preview": "// Ported from librespot\n\nuse crate::error::Error;\nuse crate::session::token::{Token};\nuse crate::util::{default_ureq_ag"
  },
  {
    "path": "psst-core/src/session/login5.rs",
    "chars": 10269,
    "preview": "// Ported from librespot\n\nuse crate::error::Error;\nuse crate::session::client_token::ClientTokenProvider;\nuse crate::ses"
  },
  {
    "path": "psst-core/src/session/mercury.rs",
    "chars": 6046,
    "preview": "use std::{\n    collections::HashMap,\n    io::{Cursor, Read},\n};\n\nuse byteorder::{ReadBytesExt, BE};\nuse crossbeam_channe"
  },
  {
    "path": "psst-core/src/session/mod.rs",
    "chars": 13606,
    "preview": "pub mod access_token;\npub mod audio_key;\npub mod mercury;\npub mod login5;\npub mod client_token;\npub mod token;\n\nuse std:"
  },
  {
    "path": "psst-core/src/session/token.rs",
    "chars": 467,
    "preview": "// Ported from librespot\n\nuse std::time::{Duration, Instant};\n\nconst EXPIRY_THRESHOLD: Duration = Duration::from_secs(10"
  },
  {
    "path": "psst-core/src/system_info.rs",
    "chars": 448,
    "preview": "/// Operating System as given by the Rust standard library\npub const OS: &str = std::env::consts::OS;\n\n/// Device ID use"
  },
  {
    "path": "psst-core/src/util.rs",
    "chars": 4349,
    "preview": "use crate::error::Error;\nuse byteorder::{BigEndian, ByteOrder};\nuse num_traits::{One, WrappingAdd};\nuse sha1::{Digest, S"
  },
  {
    "path": "psst-gui/Cargo.toml",
    "chars": 2127,
    "preview": "[package]\nname = \"psst-gui\"\nversion = \"0.1.0\"\nauthors = [\"Jan Pochyla <jpochyla@gmail.com>\"]\nedition = \"2021\"\nbuild = \"b"
  },
  {
    "path": "psst-gui/build-icons.sh",
    "chars": 1650,
    "preview": "#!/bin/bash\nset -euo pipefail\n\n# Check for required tools\ncommand -v rsvg-convert >/dev/null 2>&1 || {\n\techo >&2 \"rsvg-c"
  },
  {
    "path": "psst-gui/build.rs",
    "chars": 1212,
    "preview": "fn main() {\n    #[cfg(windows)]\n    add_windows_icon();\n}\n\n#[cfg(windows)]\nfn add_windows_icon() {\n    use image::{\n    "
  },
  {
    "path": "psst-gui/src/cmd.rs",
    "chars": 3821,
    "preview": "use crate::data::Track;\nuse druid::{Selector, WidgetId};\nuse psst_core::{item_id::ItemId, player::item::PlaybackItem};\nu"
  },
  {
    "path": "psst-gui/src/controller/after_delay.rs",
    "chars": 1456,
    "preview": "use std::time::Duration;\n\nuse druid::{\n    widget::Controller, Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, Time"
  },
  {
    "path": "psst-gui/src/controller/alert_cleanup.rs",
    "chars": 803,
    "preview": "use crate::data::AppState;\nuse druid::{widget::Controller, Env, Event, EventCtx, Widget};\nuse std::time::Duration;\n\npub "
  },
  {
    "path": "psst-gui/src/controller/ex_click.rs",
    "chars": 1812,
    "preview": "use druid::{\n    widget::Controller, Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, MouseButton,\n    MouseEvent, W"
  },
  {
    "path": "psst-gui/src/controller/ex_cursor.rs",
    "chars": 674,
    "preview": "use std::marker::PhantomData;\n\nuse druid::{widget::Controller, Data, Env, Event, EventCtx, Widget};\nuse druid_shell::Cur"
  },
  {
    "path": "psst-gui/src/controller/ex_scroll.rs",
    "chars": 1809,
    "preview": "use crate::data::SliderScrollScale;\nuse druid::{widget::Controller, Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx,"
  },
  {
    "path": "psst-gui/src/controller/input.rs",
    "chars": 2139,
    "preview": "use druid::{\n    commands,\n    widget::{prelude::*, Controller, TextBox},\n    HotKey, KbKey, SysMods,\n};\n\nuse crate::cmd"
  },
  {
    "path": "psst-gui/src/controller/mod.rs",
    "chars": 672,
    "preview": "mod after_delay;\nmod alert_cleanup;\nmod ex_click;\nmod ex_cursor;\nmod ex_scroll;\nmod input;\nmod nav;\nmod on_command;\nmod "
  },
  {
    "path": "psst-gui/src/controller/nav.rs",
    "chars": 5480,
    "preview": "use crate::{\n    cmd,\n    data::{AppState, Nav, SpotifyUrl},\n    ui::{album, artist, library, lyrics, playlist, recommen"
  },
  {
    "path": "psst-gui/src/controller/on_command.rs",
    "chars": 852,
    "preview": "use druid::{widget::Controller, Data, Env, Event, EventCtx, Selector, Widget};\n\npub struct OnCommand<U, F> {\n    selecto"
  },
  {
    "path": "psst-gui/src/controller/on_command_async.rs",
    "chars": 3445,
    "preview": "use std::{\n    sync::Arc,\n    thread::{self, JoinHandle},\n};\n\nuse druid::{\n    BoxConstraints, Data, Env, Event, EventCt"
  },
  {
    "path": "psst-gui/src/controller/on_debounce.rs",
    "chars": 1283,
    "preview": "use std::time::Duration;\n\nuse druid::{widget::Controller, Data, Env, Event, EventCtx, TimerToken, UpdateCtx, Widget};\n\np"
  },
  {
    "path": "psst-gui/src/controller/on_update.rs",
    "chars": 594,
    "preview": "use druid::{widget::Controller, Data, Env, UpdateCtx, Widget};\n\npub struct OnUpdate<F> {\n    handler: F,\n}\n\nimpl<F> OnUp"
  },
  {
    "path": "psst-gui/src/controller/playback.rs",
    "chars": 24889,
    "preview": "use std::{\n    thread::{self, JoinHandle},\n    time::Duration,\n};\n\nuse crossbeam_channel::Sender;\nuse druid::{\n    im::V"
  },
  {
    "path": "psst-gui/src/controller/session.rs",
    "chars": 1544,
    "preview": "use druid::widget::{prelude::*, Controller};\n\nuse crate::{\n    cmd,\n    data::AppState,\n    ui::{home, playlist, user},\n"
  },
  {
    "path": "psst-gui/src/controller/sort.rs",
    "chars": 2739,
    "preview": "use druid::widget::{prelude::*, Controller};\nuse druid::{Event, EventCtx, Widget};\n\nuse crate::cmd;\nuse crate::data::con"
  },
  {
    "path": "psst-gui/src/data/album.rs",
    "chars": 4140,
    "preview": "use std::sync::Arc;\n\nuse druid::{im::Vector, Data, Lens};\nuse serde::{Deserialize, Serialize};\nuse time::{formatting::Fo"
  },
  {
    "path": "psst-gui/src/data/artist.rs",
    "chars": 2045,
    "preview": "use std::sync::Arc;\n\nuse druid::{im::Vector, Data, Lens};\nuse serde::{Deserialize, Serialize};\n\nuse crate::data::{Album,"
  },
  {
    "path": "psst-gui/src/data/config.rs",
    "chars": 8592,
    "preview": "use std::{\n    env::{self, VarError},\n    fs::{self, File, OpenOptions},\n    io::{BufReader, BufWriter},\n    path::{Path"
  },
  {
    "path": "psst-gui/src/data/ctx.rs",
    "chars": 4871,
    "preview": "use std::fmt;\n\nuse druid::{\n    lens::{Field, Map},\n    widget::ListIter,\n    Data, Lens, LensExt,\n};\n\nuse crate::data::"
  },
  {
    "path": "psst-gui/src/data/find.rs",
    "chars": 1637,
    "preview": "use druid::{Data, Lens};\nuse regex::{Regex, RegexBuilder};\n\n#[derive(Clone, Default, Debug, Data, Lens)]\npub struct Find"
  },
  {
    "path": "psst-gui/src/data/id.rs",
    "chars": 170,
    "preview": "#[allow(dead_code)]\npub trait Id {\n    type Id: PartialEq;\n\n    fn id(&self) -> Self::Id;\n\n    fn has_id(&self, id: &Sel"
  },
  {
    "path": "psst-gui/src/data/mod.rs",
    "chars": 18147,
    "preview": "mod album;\nmod artist;\npub mod config;\nmod ctx;\nmod find;\nmod id;\nmod nav;\nmod playback;\nmod playlist;\nmod promise;\nmod "
  },
  {
    "path": "psst-gui/src/data/nav.rs",
    "chars": 4105,
    "preview": "use std::sync::Arc;\n\nuse druid::Data;\nuse serde::{Deserialize, Serialize};\nuse url::Url;\n\nuse crate::data::track::TrackI"
  },
  {
    "path": "psst-gui/src/data/playback.rs",
    "chars": 5426,
    "preview": "use std::{fmt, sync::Arc, time::Duration};\n\nuse druid::{im::Vector, Data, Lens};\nuse druid_enums::Matcher;\nuse psst_core"
  },
  {
    "path": "psst-gui/src/data/playlist.rs",
    "chars": 2688,
    "preview": "use std::sync::Arc;\n\nuse druid::{im::Vector, Data, Lens};\nuse serde::{Deserialize, Deserializer, Serialize};\n\nuse crate:"
  },
  {
    "path": "psst-gui/src/data/promise.rs",
    "chars": 2690,
    "preview": "use druid::Data;\n\nuse crate::error::Error;\n\n#[derive(Clone, Debug, Data, Default)]\npub enum Promise<T: Data, D: Data = ("
  },
  {
    "path": "psst-gui/src/data/recommend.rs",
    "chars": 4710,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    im::{vector, Vector},\n    Data, Lens,\n};\nuse serde::{Deserialize, Serialize};\n\nuse"
  },
  {
    "path": "psst-gui/src/data/search.rs",
    "chars": 1820,
    "preview": "use std::sync::Arc;\n\nuse druid::{im::Vector, Data, Lens};\n\nuse crate::data::{Album, Artist, Playlist, Promise, Show, Tra"
  },
  {
    "path": "psst-gui/src/data/show.rs",
    "chars": 3793,
    "preview": "use std::{convert::TryFrom, sync::Arc, time::Duration};\r\n\r\nuse druid::{im::Vector, Data, Lens};\r\nuse psst_core::item_id:"
  },
  {
    "path": "psst-gui/src/data/slider_scroll_scale.rs",
    "chars": 731,
    "preview": "use std::f64;\n\nuse druid::Lens;\n\nuse {\n    druid::Data,\n    serde::{Deserialize, Serialize},\n};\n\n#[derive(Clone, Debug, "
  },
  {
    "path": "psst-gui/src/data/track.rs",
    "chars": 3527,
    "preview": "use std::{convert::TryFrom, sync::Arc, time::Duration};\n\nuse druid::{im::Vector, lens::Map, Data, Lens};\nuse itertools::"
  },
  {
    "path": "psst-gui/src/data/user.rs",
    "chars": 349,
    "preview": "use std::sync::Arc;\n\nuse druid::{Data, Lens};\nuse serde::Deserialize;\n\n#[derive(Clone, Data, Lens, Deserialize)]\npub str"
  },
  {
    "path": "psst-gui/src/data/utils.rs",
    "chars": 4537,
    "preview": "use std::{\n    convert::TryFrom,\n    fmt, hash,\n    sync::Arc,\n    time::{Duration, SystemTime},\n};\n\nuse druid::{im::Vec"
  },
  {
    "path": "psst-gui/src/delegate.rs",
    "chars": 11070,
    "preview": "use directories::UserDirs;\nuse druid::{\n    commands, AppDelegate, Application, Command, DelegateCtx, Env, Event, Handle"
  },
  {
    "path": "psst-gui/src/error.rs",
    "chars": 333,
    "preview": "use std::{error, fmt};\n\nuse druid::Data;\n\n#[derive(Clone, Debug, Data)]\npub enum Error {\n    WebApiError(String),\n}\n\nimp"
  },
  {
    "path": "psst-gui/src/main.rs",
    "chars": 2209,
    "preview": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n#![allow(clippy::new_without_default, clippy::type_co"
  },
  {
    "path": "psst-gui/src/ui/album.rs",
    "chars": 8241,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{CrossAxisAlignment, Flex, Label, LineBreaking, List, ViewSwitcher},\n    L"
  },
  {
    "path": "psst-gui/src/ui/artist.rs",
    "chars": 10163,
    "preview": "use druid::{\n    im::Vector,\n    kurbo::Circle,\n    widget::{CrossAxisAlignment, Either, Flex, Label, LabelText, LineBre"
  },
  {
    "path": "psst-gui/src/ui/credits.rs",
    "chars": 7743,
    "preview": "use std::sync::Arc;\n\nuse crate::widget::Empty;\nuse crate::{\n    cmd,\n    data::{AppState, ArtistLink, Nav},\n    ui::them"
  },
  {
    "path": "psst-gui/src/ui/episode.rs",
    "chars": 3764,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{CrossAxisAlignment, Flex, Label, LineBreaking},\n    LensExt, LocalizedStr"
  },
  {
    "path": "psst-gui/src/ui/find.rs",
    "chars": 6611,
    "preview": "use druid::{\n    widget::{prelude::*, Controller, Either, Flex, Label, TextBox},\n    KbKey, Selector, WidgetExt,\n};\n\nuse"
  },
  {
    "path": "psst-gui/src/ui/home.rs",
    "chars": 13064,
    "preview": "use std::sync::Arc;\n\nuse druid::im::Vector;\nuse druid::widget::{Either, Flex, Label, Scroll};\nuse druid::{widget::List, "
  },
  {
    "path": "psst-gui/src/ui/library.rs",
    "chars": 5146,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{Flex, List},\n    LensExt, Selector, Widget, WidgetExt,\n};\n\nuse crate::{\n "
  },
  {
    "path": "psst-gui/src/ui/lyrics.rs",
    "chars": 3527,
    "preview": "use druid::widget::{Container, CrossAxisAlignment, Flex, Label, LineBreaking, List, Scroll};\nuse druid::{Insets, LensExt"
  },
  {
    "path": "psst-gui/src/ui/menu.rs",
    "chars": 3396,
    "preview": "use druid::{commands, platform_menus, Env, LocalizedString, Menu, MenuItem, SysMods, WindowId};\n\nuse crate::{\n    cmd,\n "
  },
  {
    "path": "psst-gui/src/ui/mod.rs",
    "chars": 21947,
    "preview": "use crate::data::config::SortCriteria;\nuse crate::data::Track;\nuse crate::error::Error;\nuse crate::{\n    cmd,\n    contro"
  },
  {
    "path": "psst-gui/src/ui/playable.rs",
    "chars": 9011,
    "preview": "use std::{mem, sync::Arc};\n\nuse druid::{\n    im::Vector,\n    kurbo::Line,\n    lens::Map,\n    piet::StrokeStyle,\n    widg"
  },
  {
    "path": "psst-gui/src/ui/playback.rs",
    "chars": 20189,
    "preview": "use std::time::Duration;\n\nuse druid::{\n    kurbo::{Affine, BezPath},\n    widget::{CrossAxisAlignment, Either, Flex, Labe"
  },
  {
    "path": "psst-gui/src/ui/playlist.rs",
    "chars": 21455,
    "preview": "use std::{cell::RefCell, cmp::Ordering, rc::Rc, sync::Arc};\n\nuse druid::{\n    im::Vector,\n    widget::{Button, Either, F"
  },
  {
    "path": "psst-gui/src/ui/preferences.rs",
    "chars": 31716,
    "preview": "use std::net::{IpAddr, Ipv4Addr, SocketAddr};\nuse std::thread::{self, JoinHandle};\nuse std::time::Duration;\n\nuse crate::"
  },
  {
    "path": "psst-gui/src/ui/recommend.rs",
    "chars": 5220,
    "preview": "use std::{sync::Arc, time::Duration};\n\nuse druid::{\n    widget::{CrossAxisAlignment, Flex, Slider},\n    FontDescriptor, "
  },
  {
    "path": "psst-gui/src/ui/search.rs",
    "chars": 8618,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{\n        CrossAxisAlignment, Either, Flex, Label, LabelText, List, MainAx"
  },
  {
    "path": "psst-gui/src/ui/show.rs",
    "chars": 7419,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{CrossAxisAlignment, Flex, Label, LineBreaking, Scroll},\n    LensExt, Loca"
  },
  {
    "path": "psst-gui/src/ui/theme.rs",
    "chars": 6330,
    "preview": "use druid::{Color, Env, FontDescriptor, FontFamily, FontWeight, Insets, Key, Size};\n\npub use druid::theme::*;\n\nuse crate"
  },
  {
    "path": "psst-gui/src/ui/track.rs",
    "chars": 13265,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{CrossAxisAlignment, Either, Flex, Label, LineBreaking, ViewSwitcher},\n   "
  },
  {
    "path": "psst-gui/src/ui/user.rs",
    "chars": 1984,
    "preview": "use druid::{\n    commands,\n    widget::{Either, Flex, Label},\n    Data, LensExt, Selector, Widget, WidgetExt,\n};\n\nuse cr"
  },
  {
    "path": "psst-gui/src/ui/utils.rs",
    "chars": 7763,
    "preview": "use std::{f64::consts::PI, time::Duration};\n\nuse druid::{\n    kurbo::Circle,\n    widget::{prelude::*, CrossAxisAlignment"
  },
  {
    "path": "psst-gui/src/webapi/cache.rs",
    "chars": 2608,
    "preview": "use std::{\n    collections::hash_map::DefaultHasher,\n    fs::{self, File},\n    hash::{Hash, Hasher},\n    num::NonZeroUsi"
  },
  {
    "path": "psst-gui/src/webapi/client.rs",
    "chars": 58522,
    "preview": "use std::{\n    collections::HashMap,\n    fmt::Display,\n    io::{self, Read},\n    path::PathBuf,\n    sync::Arc,\n    threa"
  },
  {
    "path": "psst-gui/src/webapi/local.rs",
    "chars": 12983,
    "preview": "use std::{\n    collections::HashMap,\n    fs::File,\n    io::{self, Cursor, Read},\n    path::PathBuf,\n    str,\n    sync::A"
  },
  {
    "path": "psst-gui/src/webapi/mod.rs",
    "chars": 59,
    "preview": "mod cache;\nmod client;\nmod local;\n\npub use client::WebApi;\n"
  },
  {
    "path": "psst-gui/src/widget/checkbox.rs",
    "chars": 3983,
    "preview": "use druid::{\n    kurbo::{BezPath, Size},\n    piet::{LineCap, LineJoin, LinearGradient, RenderContext, StrokeStyle, UnitP"
  },
  {
    "path": "psst-gui/src/widget/dispatcher.rs",
    "chars": 4044,
    "preview": "use std::{collections::HashMap, hash::Hash};\n\nuse druid::{widget::prelude::*, Data, Point, WidgetPod};\n\ntype ChildPicker"
  },
  {
    "path": "psst-gui/src/widget/empty.rs",
    "chars": 556,
    "preview": "use druid::widget::prelude::*;\n\npub struct Empty;\n\nimpl<T> Widget<T> for Empty {\n    fn event(&mut self, _ctx: &mut Even"
  },
  {
    "path": "psst-gui/src/widget/fill_between.rs",
    "chars": 2651,
    "preview": "use druid::{\n    BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx,\n    Point, Si"
  },
  {
    "path": "psst-gui/src/widget/icons.rs",
    "chars": 33407,
    "preview": "use crate::ui::theme;\nuse druid::{kurbo::BezPath, widget::prelude::*, Affine, Color, KeyOrValue, Size};\n\n#[allow(dead_co"
  },
  {
    "path": "psst-gui/src/widget/link.rs",
    "chars": 3511,
    "preview": "use druid::{widget::prelude::*, Color, Data, KeyOrValue, Point, RoundedRectRadii, WidgetPod};\n\nuse crate::ui::theme;\n\npu"
  },
  {
    "path": "psst-gui/src/widget/maybe.rs",
    "chars": 5857,
    "preview": "// Copyright 2020 The xi-editor Authors.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you ma"
  },
  {
    "path": "psst-gui/src/widget/mod.rs",
    "chars": 4347,
    "preview": "mod checkbox;\nmod dispatcher;\nmod empty;\npub mod fill_between;\npub mod icons;\nmod link;\nmod maybe;\nmod overlay;\nmod prom"
  },
  {
    "path": "psst-gui/src/widget/overlay.rs",
    "chars": 1807,
    "preview": "use druid::{widget::prelude::*, Data, Point, Vec2, WidgetPod};\n\npub enum OverlayPosition {\n    Bottom,\n}\n\npub struct Ove"
  },
  {
    "path": "psst-gui/src/widget/promise.rs",
    "chars": 6821,
    "preview": "use druid::{widget::prelude::*, Data, Point, WidgetExt, WidgetPod};\n\nuse crate::data::{Promise, PromiseState};\n\npub stru"
  },
  {
    "path": "psst-gui/src/widget/remote_image.rs",
    "chars": 3723,
    "preview": "use std::sync::Arc;\n\nuse druid::{\n    widget::{prelude::*, FillStrat, Image},\n    Data, ImageBuf, Point, Selector, Widge"
  },
  {
    "path": "psst-gui/src/widget/theme.rs",
    "chars": 1867,
    "preview": "use crate::{data::AppState, ui::theme};\nuse druid::widget::prelude::*;\n\npub struct ThemeScope<W> {\n    inner: W,\n    cac"
  },
  {
    "path": "psst-gui/src/widget/utils.rs",
    "chars": 7943,
    "preview": "use druid::{\n    kurbo::{Line, Shape},\n    widget::{prelude::*, Axis, BackgroundBrush, Painter},\n    Color, Data, KeyOrV"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the jpochyla/psst GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 136 files (733.4 KB), approximately 189.8k tokens, and a symbol index with 1637 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!