Full Code of ccgauche/ytermusic for AI

master 0481657a61a4 cached
63 files
201.2 KB
47.8k tokens
350 symbols
1 requests
Download .txt
Showing preview only (218K chars total). Download the full file or copy to clipboard to get everything.
Repository: ccgauche/ytermusic
Branch: master
Commit: 0481657a61a4
Files: 63
Total size: 201.2 KB

Directory structure:
gitextract_sweneqjg/

├── .github/
│   └── workflows/
│       ├── check.yml
│       ├── ci.yml
│       ├── clippy.yml
│       ├── fmt.yml
│       ├── release.yml
│       ├── test-linux.yml
│       ├── test-macos.yml
│       └── test-windows.yml
├── .gitignore
├── Cargo.toml
├── LICENCE
├── README.md
└── crates/
    ├── common-structs/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── app_status.rs
    │       ├── lib.rs
    │       └── music_download_status.rs
    ├── database/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       ├── reader.rs
    │       └── writer.rs
    ├── download-manager/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       └── task.rs
    ├── player/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── error.rs
    │       ├── lib.rs
    │       ├── player.rs
    │       ├── player_data.rs
    │       └── player_options.rs
    ├── ytermusic/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── config.rs
    │       ├── consts.rs
    │       ├── database.rs
    │       ├── errors.rs
    │       ├── main.rs
    │       ├── shutdown.rs
    │       ├── structures/
    │       │   ├── app_status.rs
    │       │   ├── media.rs
    │       │   ├── mod.rs
    │       │   ├── performance.rs
    │       │   └── sound_action.rs
    │       ├── systems/
    │       │   ├── logger.rs
    │       │   ├── mod.rs
    │       │   └── player.rs
    │       ├── tasks/
    │       │   ├── api.rs
    │       │   ├── clean.rs
    │       │   ├── last_playlist.rs
    │       │   ├── local_musics.rs
    │       │   └── mod.rs
    │       ├── term/
    │       │   ├── device_lost.rs
    │       │   ├── item_list.rs
    │       │   ├── list_selector.rs
    │       │   ├── mod.rs
    │       │   ├── music_player.rs
    │       │   ├── playlist.rs
    │       │   ├── playlist_view.rs
    │       │   ├── search.rs
    │       │   └── vertical_gauge.rs
    │       └── utils.rs
    └── ytpapi2/
        ├── Cargo.toml
        └── src/
            ├── json_extractor.rs
            ├── lib.rs
            └── string_utils.rs

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

================================================
FILE: .github/workflows/check.yml
================================================
name: Check

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  check:
    name: Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libdbus-1-dev pkg-config

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Run cargo check
        run: cargo check


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

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

env:
  CARGO_TERM_COLOR: always

jobs:
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libdbus-1-dev pkg-config

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt, clippy

      - name: Run cargo check
        run: cargo check

      - name: Run cargo fmt
        run: cargo fmt --all -- --check

      - name: Run cargo clippy
        run: cargo clippy -- -D warnings


================================================
FILE: .github/workflows/clippy.yml
================================================
name: Clippy

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libdbus-1-dev pkg-config

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy

      - name: Run cargo clippy
        run: cargo clippy -- -D warnings


================================================
FILE: .github/workflows/fmt.yml
================================================
name: Format

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  fmt:
    name: Format Check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt

      - name: Run cargo fmt
        run: cargo fmt --all -- --check


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  release:
    types: [published]

jobs:
  release:
    name: Build and Release (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            archive: tar.gz
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            archive: zip
          - os: macos-latest
            target: aarch64-apple-darwin
            archive: tar.gz
          - os: macos-14
            target: x86_64-apple-darwin
            archive: tar.gz

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Install system dependencies (Linux)
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libdbus-1-dev pkg-config


      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}


      - name: Build release binary
        run: |
          cargo build --release --target ${{ matrix.target }}

      - name: Package binary (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          # Determine binary name and path
          if [[ "${{ matrix.target }}" == *"windows"* ]]; then
            BINARY_NAME="ytermusic.exe"
            BINARY_PATH="target/${{ matrix.target }}/release/ytermusic.exe"
          else
            BINARY_NAME="ytermusic"
            BINARY_PATH="target/${{ matrix.target }}/release/ytermusic"
            strip "$BINARY_PATH"
          fi

          # Determine archive name
          ARCHIVE_NAME="ytermusic-${{ github.ref_name }}-${{ matrix.target }}"

          # Create archive
          if [[ "${{ matrix.archive }}" == "zip" ]]; then
            zip "${ARCHIVE_NAME}.zip" "$BINARY_PATH"
          else
            tar czf "${ARCHIVE_NAME}.tar.gz" -C "target/${{ matrix.target }}/release" "$BINARY_NAME"
          fi

      - name: Package binary (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          # Determine binary name and path
          $binaryName = "ytermusic.exe"
          $binaryPath = "target/${{ matrix.target }}/release/ytermusic.exe"

          # Determine archive name
          $archiveName = "ytermusic-${{ github.ref_name }}-${{ matrix.target }}"

          # Create archive
          if ("${{ matrix.archive }}" -eq "zip") {
            Compress-Archive -Path $binaryPath -DestinationPath "${archiveName}.zip"
          } else {
            # For tar.gz on Windows, we could use tar if available, or just copy the binary
            Copy-Item $binaryPath "${archiveName}.tar.gz"
          }

      - name: Upload release asset
        uses: actions/upload-release-asset@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ github.event.release.upload_url }}
          asset_path: ytermusic-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }}
          asset_name: ytermusic-${{ github.ref_name }}-${{ matrix.target }}.${{ matrix.archive }}
          asset_content_type: application/octet-stream


================================================
FILE: .github/workflows/test-linux.yml
================================================
name: Test Linux

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  test-linux:
    name: Test Linux (${{ matrix.rust }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        rust: [stable, beta]
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y libasound2-dev libdbus-1-dev pkg-config

      - name: Install ${{ matrix.rust }} toolchain
        uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}

      - name: Run cargo test
        run: cargo test --verbose


================================================
FILE: .github/workflows/test-macos.yml
================================================
name: Test macOS

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  test-macos:
    name: Test macOS (stable)
    runs-on: macos-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Run cargo test
        run: cargo test --verbose


================================================
FILE: .github/workflows/test-windows.yml
================================================
name: Test Windows

on:
  workflow_dispatch:

env:
  CARGO_TERM_COLOR: always

jobs:
  test-windows:
    name: Test Windows (stable)
    runs-on: windows-latest
    steps:
      - name: Checkout sources
        uses: actions/checkout@v4

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Run cargo test
        run: cargo test --verbose


================================================
FILE: .gitignore
================================================
/target
/data 
ytermusic.exe
headers.txt
account_id.txt
/player/target
/ytpapi/target
/ytpapi2/target
/rustube/target
/log.txt
/music_renamer
last-playlist.json
muex
cssparser

================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "3"
members = ["crates/common-structs","crates/database", "crates/download-manager","crates/player", "crates/ytermusic", "crates/ytpapi2"]

[workspace.dependencies]
player = { path = "crates/player" }
ytpapi2 = { path = "crates/ytpapi2" }
database = { path = "crates/database" }
download-manager = { path = "crates/download-manager" }
common-structs = { path = "crates/common-structs" }

================================================
FILE: LICENCE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# YTerMusic

![YTeRMUSiC](./assets/banner/YTeRMUSiC.png "YTeRMUSiC")

YTerMusic is a TUI based Youtube Music Player that aims to be as fast and simple as possible.

## Screenshots
<p>
  <img
  src="./assets/screenshots/Choose-A-Playlist.png"
  alt="Choose a playlist"
  title="Choose a Playlist"
  />
  <img
  src="./assets/screenshots/Playlist-All.gif"
  alt="Playlist RGB"
  title="Playlist RGB"
  />
</p>


## Features and upcoming features

- Play your Youtube Music Playlist and Supermix.
- Memory efficient (Around 20MB of RAM while fully loaded)
- Cache all downloads and store them
- Work even without connection (If musics were already downloaded)
- Automatic background download manager
	### Check List 
	- [x] Playlist selector
	- [x] Error message display in the TUI
	- [x] Enable connection less music playing
	- [ ] Cache limit to not exceed some given disk space
	- [x] A download limit to stop downloading after the queue is full
	- [x] Mouse support
	- [x] Search
	- [x] Custom theming (You can use hex! #05313d = ![05313d](./assets/hex/05313d.png "#05313d") )

## Install
> [!TIP]
> 3rd party AUR packages are available [here](https://aur.archlinux.org/packages?O=0&K=ytermusic).

- Download the latest version from [releases](https://github.com/ccgauche/ytermusic/releases/latest).
	### Linux
	Install the following libraries: 
	```sh
	sudo apt install alsa-tools libasound2-dev libdbus-1-dev pkg-config
	```
- Use `cargo` to install the latest version
	```sh
	cargo install ytermusic --git https://github.com/ccgauche/ytermusic
	```
## Setup
> [!IMPORTANT] 
> If you're using Firefox enable the "Raw" switch so the cookie isn't mangled.
> ![Firefox Raw Switch](./assets/screenshots/Firefox-Raw-Switch.png "Firefox Raw Switch")

- Give `ytermusic` authentication to your account, by copying out the headers
	1. Open the https://music.youtube.com website in your browser
	2. Open the developer tools (<kbd>F12</kbd> or <kbd>Fn</kbd> + <kbd>F12</kbd>)
	3. Go to the Network tab
	4. Find the request to the `music.youtube.com` document `/` page
	5. Copy the `Cookie` header from the associated response request
	6. Create a `headers.txt` file in one of the checked [paths](https://docs.rs/directories/latest/directories/struct.ProjectDirs.html#method.config_dir).
	7. Create an entry like this :
	```
	Cookie: <cookie>
	User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36
	```
- Then you can start `ytermusic`

### (Optional) Using a brand account
- If you use a second account for youtube music
  	1. Go to https://myaccount.google.com/
  	2. Switch to your brand account
  	3. copy the number written in the url, after \b\
  	4. paste it into a new `account_id.txt` file in the same folder as `headers.txt`

## Building from source

- Clone the repository
- Install rust `https://rustup.rs` nightly
- Run `cargo build --release`
- The executable is in `target/release/ytermusic.exe` or `target/release/ytermusic`

## Usage

- Use your mouse to <kbd>click</kbd> in lists if your terminal has mouse support
- Press <kbd>Space</kbd> to play/pause
- Press <kbd>Enter</kbd> to select a playlist or a music
- Press <kbd>f</kbd> to search
- Press <kbd>s</kbd> to shuffle
- Press <kbd>r</kbd> to remove a music from the main playlist
- Press <kbd>Arrow Right</kbd> or <kbd>\></kbd> to skip 5 seconds
- Press <kbd>Arrow Left</kbd> or <kbd>\<</kbd> to go back 5 seconds
- Press <kbd>CTRL</kbd> + <kbd>Arrow Right</kbd> or <kbd>CTRL</kbd> + <kbd>\></kbd> to go to the next song
- Press <kbd>CTRL</kbd> + <kbd>Arrow Left</kbd> or <kbd>CTRL</kbd> + <kbd>\<</kbd> to go to the previous song
- Press <kbd>+</kbd> for volume up
- Press <kbd>-</kbd> for volume down
- Press <kbd>Arrow down</kbd> to scroll down
- Press <kbd>Arrow up</kbd> to scroll up
- Press <kbd>ESC</kbd> to exit the current menu
- Press <kbd>CTRL</kbd> + <kbd>C</kbd> or <kbd>CTRL</kbd> + <kbd>D</kbd> to exit

## How to fix common issues

If you have any issues start by running:
```sh
ytermusic --fix-db
```
This will try to fix any issues with the cache database.

If you still have issues, you can clear the cache by running:
```sh
ytermusic --clear-cache
```

If you need to report an issue or find the files related to ytermusic:
```sh
ytermusic --files
```

## Changelog

```
Beta b0.1.1
 - Added `hide_channels_on_homepage` with a default value of `true` to the config file
 - Added `hide_albums_on_homepage` with a default value of `false` to the config file
 - Fixed default style to support transparency
 - Added more color configuration options

Beta b0.1.0
 - Fixed keyboard handling on windows
 - Improved error handling
 - Fixed youtube downloads
 - Made volume bar optional in config
 - Improved performance and updated dependencies

Alpha a0.0.11

- Added scrollable music view
- Added shuffle functionality
- Fixed some crashes while resizing the app
- Added error messages for invalid headers or cookies
- Added error messages for expired cookies

Alpha a0.0.10

- Speed up the download process
- Fix the download limit
- Fix music artists getting smashed together
- Fix the download manager not downloading all musics
- Improved stability
- Improved logs and added timings to better debug

Alpha a0.0.9:

- Progress info for downloads
- Mouse support on time bar
- Vertical volume bar
- Vertical volume bar supports mouse click
- Scroll to change volume and skip in timeline
- Improved the scrolling action
- Fixed the bug where the time bar would not update
- Debouncing the search input
- Changed the location of the cache folder to follow the XDG Base Directory Specification (By @FerrahWolfeh #20)
- More configuration options (By @ccgauche and @FerrahWolfeh)

Alpha a0.0.8

- Fixed scrolling
- Fixed audio-glitches
- Removed nightly flag use

Alpha a0.0.7

- Major changes in the API
- Fixed log file bloat issue

Alpha a0.0.6

- Fix: Fix a bug where the app would crash when trying to play a song that was not downloaded
- Fix: Improve the logger to not print the same error twice
- Improved startup time
- Fixed linux build
- Changed how task are distributed to the thread pool

Alpha a0.0.5

- Added local database cache to improve IO accesses
- Added searching for musics in the local library
- Greatly improved render performance and RAM usage
- Error management and error display in specific screen

Alpha a0.0.4

- Added menu navigation
- Added searching for musics
- Added new terminal backend

Alpha a0.0.3

- Mouse support to select playlist and music
- Download limiter
- Connection less music playing

Alpha a0.0.2

- Playlist selector
- Improved error management
- Improved TUI
- Performance upgrade
- Switch to Rustls instead of openSSL
```


================================================
FILE: crates/common-structs/Cargo.toml
================================================
[package]
name = "common-structs"
version = "0.1.0"
edition = "2024"

[dependencies]


================================================
FILE: crates/common-structs/src/app_status.rs
================================================
#[derive(PartialEq, Debug, Clone)]
pub enum AppStatus {
    Paused,
    Playing,
    NoMusic,
}


================================================
FILE: crates/common-structs/src/lib.rs
================================================
mod app_status;
mod music_download_status;

pub use app_status::AppStatus;
pub use music_download_status::MusicDownloadStatus;


================================================
FILE: crates/common-structs/src/music_download_status.rs
================================================
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum MusicDownloadStatus {
    NotDownloaded,
    Downloaded,
    Downloading(usize),
    DownloadFailed,
}

impl MusicDownloadStatus {
    pub fn character(&self, playing: Option<bool>) -> String {
        match self {
            Self::NotDownloaded => {
                if let Some(e) = playing {
                    if e { '▶' } else { '⏸' }
                } else {
                    ' '
                }
            }
            Self::Downloaded => ' ',
            Self::Downloading(progress) => return format!("⭳ [{:02}%]", progress),
            Self::DownloadFailed => '⚠',
        }
        .into()
    }
}


================================================
FILE: crates/database/Cargo.toml
================================================
[package]
name = "database"
version = "0.1.0"
edition = "2024"

[dependencies]
varuint = "0.7.1"
ytpapi2.workspace = true
log = "*"
serde_json = "1.0.114"

================================================
FILE: crates/database/src/lib.rs
================================================
use std::{fs::OpenOptions, path::PathBuf, sync::RwLock};

mod reader;
mod writer;

pub use writer::write_video;
use ytpapi2::YoutubeMusicVideoRef;

pub struct YTLocalDatabase {
    cache_dir: PathBuf,
    references: RwLock<Vec<YoutubeMusicVideoRef>>,
}

impl YTLocalDatabase {
    pub fn new(cache_dir: PathBuf) -> Self {
        Self {
            cache_dir,
            references: RwLock::new(Vec::new()),
        }
    }

    pub fn clone_from(&self, videos: &Vec<YoutubeMusicVideoRef>) {
        self.references.write().unwrap().clone_from(videos);
    }

    pub fn remove_video(&self, video: &YoutubeMusicVideoRef) {
        let mut database = self.references.write().unwrap();
        database.retain(|v| v.video_id != video.video_id);
        drop(database);
        self.write();
    }

    pub fn append(&self, video: YoutubeMusicVideoRef) {
        let mut file = OpenOptions::new()
            .append(true)
            .create(true)
            .open(self.cache_dir.join("db.bin"))
            .unwrap();
        write_video(&mut file, &video);
        self.references.write().unwrap().push(video);
    }
}


================================================
FILE: crates/database/src/reader.rs
================================================
use std::io::{Cursor, Read};

use varuint::ReadVarint;
use ytpapi2::YoutubeMusicVideoRef;

use crate::YTLocalDatabase;

impl YTLocalDatabase {
    pub fn read(&self) -> Option<Vec<YoutubeMusicVideoRef>> {
        let mut buffer = Cursor::new(std::fs::read(self.cache_dir.join("db.bin")).ok()?);
        let mut videos = Vec::new();
        while buffer.get_mut().len() > buffer.position() as usize {
            videos.push(read_video(&mut buffer)?);
        }
        Some(videos)
    }
}

/// Reads a video from the cursor
fn read_video(buffer: &mut Cursor<Vec<u8>>) -> Option<YoutubeMusicVideoRef> {
    Some(YoutubeMusicVideoRef {
        title: read_str(buffer)?,
        author: read_str(buffer)?,
        album: read_str(buffer)?,
        video_id: read_str(buffer)?,
        duration: read_str(buffer)?,
    })
}

/// Reads a string from the cursor
fn read_str(cursor: &mut Cursor<Vec<u8>>) -> Option<String> {
    let mut buf = vec![0u8; read_u32(cursor)? as usize];
    cursor.read_exact(&mut buf).ok()?;
    String::from_utf8(buf).ok()
}

/// Reads a u32 from the cursor
fn read_u32(cursor: &mut Cursor<Vec<u8>>) -> Option<u32> {
    ReadVarint::<u32>::read_varint(cursor).ok()
}


================================================
FILE: crates/database/src/writer.rs
================================================
use std::{fs::OpenOptions, io::Write};

use varuint::WriteVarint;
use ytpapi2::YoutubeMusicVideoRef;

use crate::YTLocalDatabase;

impl YTLocalDatabase {
    pub fn write(&self) {
        let db = self.references.read().unwrap();
        let mut file = OpenOptions::new()
            .write(true)
            .append(false)
            .create(true)
            .truncate(true)
            .open(self.cache_dir.join("db.bin"))
            .unwrap();
        for video in db.iter() {
            write_video(&mut file, video)
        }
    }
}
impl YTLocalDatabase {
    pub fn fix_db(&self) {
        let mut db = self.references.write().unwrap();
        db.clear();
        let cache_folder = self.cache_dir.join("downloads");
        if !cache_folder.is_dir() {
            println!(
                "[WARN] The download folder in the cache wasn't found ({:?})",
                cache_folder
            );
            return;
        }
        for entry in std::fs::read_dir(&cache_folder).unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();
            // Check if the file is a json file (+ sloppy check if there is any files or directory)
            if path.extension().unwrap_or_default() != "json" {
                continue;
            }
            // Read the file if not readable do not add it to the database
            let content = match std::fs::read_to_string(&path) {
                Ok(content) => content,
                Err(e) => {
                    match std::fs::remove_file(&path) {
                        Ok(_) => println!(
                            "[INFO] Removing file {:?} because the file is not readable: {e:?}",
                            path.file_name()
                        ),
                        Err(ef) => println!(
                            "[ERROR] file {:?} is not readable: {e:?}, but could not be deleted: {ef:?}",
                            path.file_name()
                        ),
                    }
                    continue;
                }
            };
            // Check if the file is a valid json file
            let video = match serde_json::from_str::<YoutubeMusicVideoRef>(&content) {
                Ok(parsed) => parsed,
                Err(e) => {
                    match std::fs::remove_file(&path) {
                        Ok(_) => println!(
                            "[INFO] Removing file {:?} because the file is not a valid json file: {e:?}",
                            path.file_name()
                        ),
                        Err(ef) => println!(
                            "[ERROR] file {:?} is not a valid json file: {e:?}, but could not be deleted: {ef:?}",
                            path.file_name()
                        ),
                    }
                    continue;
                }
            };
            // Check if the video file exists
            let video_file = cache_folder.join(format!("{}.mp4", video.video_id));
            if !video_file.exists() {
                match std::fs::remove_file(&path) {
                    Ok(_) => println!(
                        "[INFO] Removing file {:?} because the video file does not exist",
                        path.file_name()
                    ),
                    Err(ef) => println!(
                        "[ERROR] video assocated to file {:?} does not exist, but the file could not be deleted: {ef:?}",
                        path.file_name()
                    ),
                }
                continue;
            }
            // Read the video file
            let video_file = match std::fs::read(&video_file) {
                Ok(video_file) => video_file,
                Err(e) => {
                    match std::fs::remove_file(&path) {
                        Ok(_) => println!(
                            "[INFO] Removing file {:?} because the video file is not readable: {e:?}",
                            path.file_name()
                        ),
                        Err(ef) => println!(
                            "[ERROR] video associated to file {:?} is not readable: {e:?}, but the file could not be deleted: {ef:?}",
                            path.file_name()
                        ),
                    }
                    continue;
                }
            };
            // Check if the video file contains the header
            if !video_file.starts_with(&[
                0, 0, 0, 24, 102, 116, 121, 112, 100, 97, 115, 104, 0, 0, 0, 0,
            ]) {
                match std::fs::remove_file(&path) {
                    Ok(_) => println!(
                        "[INFO] Removing file {:?} because the video file does not contain the header",
                        path.file_name()
                    ),
                    Err(ef) => println!(
                        "[ERROR] video associated to file {:?} does not contain the header, but the file could not be deleted: {ef:?}",
                        path.file_name()
                    ),
                }
                continue;
            }
            db.push(video);
        }
    }
}

/// Writes a video to a file
pub fn write_video(buffer: &mut impl Write, video: &YoutubeMusicVideoRef) {
    write_str(buffer, &video.title);
    write_str(buffer, &video.author);
    write_str(buffer, &video.album);
    write_str(buffer, &video.video_id);
    write_str(buffer, &video.duration);
}

/// Writes a string from the cursor
fn write_str(cursor: &mut impl Write, value: &str) {
    write_u32(cursor, value.len() as u32);
    cursor.write_all(value.as_bytes()).unwrap();
}

/// Writes a u32 from the cursor
fn write_u32(cursor: &mut impl Write, value: u32) {
    cursor.write_varint(value).unwrap();
}


================================================
FILE: crates/download-manager/Cargo.toml
================================================
[package]
name = "download-manager"
version = "0.1.0"
edition = "2024"

[dependencies]
ytpapi2.workspace = true
flume = "0.11.0"
once_cell = "1.19.0"
tokio = { version = "1.36.0", features = ["rt-multi-thread"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
database.workspace = true
common-structs.workspace = true

#  --- YT Download ---
rusty_ytdl = { git = "https://github.com/Mithronn/rusty_ytdl/", branch = "main", features = ["rustls", "search", "live"], default-features = false}

log = "0.4.20"

================================================
FILE: crates/download-manager/src/lib.rs
================================================
mod task;

use std::{
    collections::{HashSet, VecDeque},
    path::PathBuf,
    sync::{Arc, Mutex},
    time::Duration,
};

use database::YTLocalDatabase;
use tokio::{select, task::JoinHandle, time::sleep};
use ytpapi2::YoutubeMusicVideoRef;

use common_structs::MusicDownloadStatus;

pub type MessageHandler = Arc<dyn Fn(DownloadManagerMessage) + Send + Sync + 'static>;

pub enum DownloadManagerMessage {
    VideoStatusUpdate(String, MusicDownloadStatus),
}

pub struct DownloadManager {
    database: &'static YTLocalDatabase,
    cache_dir: PathBuf,
    parallel_downloads: u16,
    handles: Mutex<Vec<JoinHandle<()>>>,
    download_list: Mutex<VecDeque<YoutubeMusicVideoRef>>,
    in_download: Mutex<HashSet<String>>,
}

impl DownloadManager {
    pub fn new(
        cache_dir: PathBuf,
        database: &'static YTLocalDatabase,
        parallel_downloads: u16,
    ) -> Self {
        Self {
            database,
            cache_dir,
            parallel_downloads,
            handles: Mutex::new(Vec::new()),
            download_list: Mutex::new(VecDeque::new()),
            in_download: Mutex::new(HashSet::new()),
        }
    }

    pub fn remove_from_in_downloads(&self, video: &String) {
        self.in_download.lock().unwrap().remove(video);
    }

    fn take(&self) -> Option<YoutubeMusicVideoRef> {
        self.download_list.lock().unwrap().pop_front()
    }

    /// This has to be called as a service stream
    /// HANDLES.lock().unwrap().push(run_service(async move {
    ///     run_service_stream(sender);
    /// }));
    pub fn run_service_stream(
        &'static self,
        cancelation: impl Future<Output = ()> + Clone + Send + 'static,
        sender: MessageHandler,
    ) {
        let fut = async move {
            loop {
                if let Some(id) = self.take() {
                    self.start_download(id, sender.clone()).await;
                } else {
                    sleep(Duration::from_millis(200)).await;
                }
            }
        };
        let service = tokio::task::spawn(async move {
            select! {
                _ = fut => {},
                _ = cancelation => {},
            }
        });
        self.handles.lock().unwrap().push(service);
    }

    pub fn spawn_system(
        &'static self,
        cancelation: impl Future<Output = ()> + Clone + Send + 'static,
        sender: MessageHandler,
    ) {
        for _ in 0..self.parallel_downloads {
            self.run_service_stream(cancelation.clone(), sender.clone());
        }
    }

    pub fn clean(
        &'static self,
        cancelation: impl Future<Output = ()> + Clone + Send + 'static,
        sender: MessageHandler,
    ) {
        self.download_list.lock().unwrap().clear();
        self.in_download.lock().unwrap().clear();
        {
            let mut handle = self.handles.lock().unwrap();
            for i in handle.iter() {
                i.abort()
            }
            handle.clear();
        }
        self.spawn_system(cancelation, sender);
    }

    pub fn set_download_list(&self, to_add: impl IntoIterator<Item = YoutubeMusicVideoRef>) {
        let mut list = self.download_list.lock().unwrap();
        list.clear();
        list.extend(to_add);
    }

    pub fn add_to_download_list(&self, to_add: impl IntoIterator<Item = YoutubeMusicVideoRef>) {
        let mut list = self.download_list.lock().unwrap();
        list.extend(to_add);
    }
}


================================================
FILE: crates/download-manager/src/task.rs
================================================
use std::sync::Arc;

use log::error;
use rusty_ytdl::{
    DownloadOptions, Video, VideoError, VideoOptions, VideoQuality, VideoSearchOptions,
};
use tokio::select;
use ytpapi2::YoutubeMusicVideoRef;

use crate::{DownloadManager, DownloadManagerMessage, MessageHandler, MusicDownloadStatus};

fn new_video_with_id(id: &str) -> Result<Video<'_>, VideoError> {
    let search_options = VideoSearchOptions::Custom(Arc::new(|format| {
        format.has_audio && !format.has_video && format.mime_type.container == "mp4"
    }));
    let video_options = VideoOptions {
        quality: VideoQuality::Custom(
            search_options.clone(),
            Arc::new(|x, y| x.audio_bitrate.cmp(&y.audio_bitrate)),
        ),
        filter: search_options,
        download_options: DownloadOptions {
            dl_chunk_size: Some(1024 * 100_u64),
        },
        ..Default::default()
    };

    Video::new_with_options(id, video_options)
}

pub async fn download<P: AsRef<std::path::Path>>(
    video: &Video<'_>,
    path: P,
    sender: MessageHandler,
) -> Result<(), VideoError> {
    use std::io::Write;
    let stream = video.stream().await?;

    let length = stream.content_length();

    let mut file =
        std::fs::File::create(&path).map_err(|e| VideoError::DownloadError(e.to_string()))?;

    let mut total = 0;
    while let Some(chunk) = stream.chunk().await? {
        total += chunk.len();

        sender(DownloadManagerMessage::VideoStatusUpdate(
            video.get_video_id(),
            MusicDownloadStatus::Downloading((total as f64 / length as f64 * 100.0) as usize),
        ));

        file.write_all(&chunk)
            .map_err(|e| VideoError::DownloadError(e.to_string()))?;
    }

    file.flush()
        .map_err(|e| VideoError::DownloadError(e.to_string()))?;

    if total != length || length == 0 {
        std::fs::remove_file(path).map_err(|e| VideoError::DownloadError(e.to_string()))?;
        return Err(VideoError::DownloadError(format!(
            "Downloaded file is not the same size as the content length ({}/{})",
            total, length
        )));
    }

    Ok(())
}

impl DownloadManager {
    async fn handle_download(&self, id: &str, sender: MessageHandler) -> Result<(), VideoError> {
        let idc = id.to_string();

        let video = new_video_with_id(id)?;

        sender(DownloadManagerMessage::VideoStatusUpdate(
            idc.clone(),
            MusicDownloadStatus::Downloading(0),
        ));
        let file = self.cache_dir.join("downloads").join(format!("{id}.mp4"));
        download(&video, file, sender.clone()).await?;
        sender(DownloadManagerMessage::VideoStatusUpdate(
            idc.clone(),
            MusicDownloadStatus::Downloading(100),
        ));
        Ok(())
    }
    pub async fn start_download(&self, song: YoutubeMusicVideoRef, s: MessageHandler) -> bool {
        {
            let mut downloads = self.in_download.lock().unwrap();
            if downloads.contains(&song.video_id) {
                return false;
            }
            downloads.insert(song.video_id.clone());
        }
        s(DownloadManagerMessage::VideoStatusUpdate(
            song.video_id.clone(),
            MusicDownloadStatus::Downloading(1),
        ));
        let download_path_mp4 = self
            .cache_dir
            .join(format!("downloads/{}.mp4", &song.video_id));
        let download_path_json = self
            .cache_dir
            .join(format!("downloads/{}.json", &song.video_id));
        if download_path_json.exists() {
            s(DownloadManagerMessage::VideoStatusUpdate(
                song.video_id.clone(),
                MusicDownloadStatus::Downloaded,
            ));
            return true;
        }
        if download_path_mp4.exists() {
            std::fs::remove_file(&download_path_mp4).unwrap();
        }
        match self.handle_download(&song.video_id, s.clone()).await {
            Ok(_) => {
                std::fs::write(download_path_json, serde_json::to_string(&song).unwrap()).unwrap();
                self.database.append(song.clone());
                s(DownloadManagerMessage::VideoStatusUpdate(
                    song.video_id.clone(),
                    MusicDownloadStatus::Downloaded,
                ));
                self.in_download.lock().unwrap().remove(&song.video_id);
                true
            }
            Err(e) => {
                if download_path_mp4.exists() {
                    std::fs::remove_file(download_path_mp4).unwrap();
                }
                s(DownloadManagerMessage::VideoStatusUpdate(
                    song.video_id.clone(),
                    MusicDownloadStatus::DownloadFailed,
                ));
                error!("Error downloading {}: {e}", song.video_id);
                false
            }
        }
    }

    pub fn start_task_unary(
        &'static self,
        s: MessageHandler,
        song: YoutubeMusicVideoRef,
        cancelation: impl Future<Output = ()> + Send + 'static,
    ) {
        let fut = async move {
            self.start_download(song, s).await;
        };
        let service = tokio::task::spawn(async move {
            select! {
                _ = fut => {},
                _ = cancelation => {},
            }
        });
        self.handles.lock().unwrap().push(service);
    }
}

#[tokio::test]
async fn video_download_test() {
    let ids = vec!["iFbNzVFgjCk", "ni-xbEK271I"]; //second not working, need checking
    for id in ids {
        let video = Video::new(id).unwrap();
        let stream = video.stream().await.unwrap();
        let content_length = stream.content_length();
        let mut total = 0;
        while let Some(chunk) = stream.chunk().await.unwrap() {
            total += chunk.len();
        }
        assert_eq!(total, content_length);
    }
}


================================================
FILE: crates/player/Cargo.toml
================================================
[package]
name = "player"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
flume = "0.11.0"
rodio = { version = "0.21.1", default-features = false, features = ["playback", "symphonia-aac", "symphonia-isomp4"] }
log = "0.4.29"

================================================
FILE: crates/player/src/error.rs
================================================
// Custom Error Enum to handle different failures
#[derive(Debug)]
pub enum PlayError {
    Io(std::io::Error),
    DecoderError(rodio::decoder::DecoderError),
    StreamError(rodio::StreamError),
    PlayError(rodio::PlayError),
    SeekError(rodio::source::SeekError),
}

impl From<rodio::PlayError> for PlayError {
    fn from(err: rodio::PlayError) -> Self {
        PlayError::PlayError(err)
    }
}


================================================
FILE: crates/player/src/lib.rs
================================================
mod error;

use std::time::Duration;

pub use error::PlayError;

mod player;
pub use player::Player;

mod player_options;
pub use player_options::PlayerOptions;

mod player_data;
pub(crate) use player_data::PlayerData;

pub(crate) static VOLUME_STEP: u8 = 5;
pub(crate) static SEEK_STEP: Duration = Duration::from_secs(5);


================================================
FILE: crates/player/src/player.rs
================================================
use flume::Sender;
use rodio::cpal::traits::HostTrait;
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};

use std::fs::File;
use std::path::Path;
use std::time::Duration;

use crate::{PlayError, PlayerData, PlayerOptions, SEEK_STEP};

pub struct Player {
    sink: Sink,
    stream: OutputStream,
    data: PlayerData,
    error_sender: Sender<PlayError>,
    options: PlayerOptions,
}

impl Player {
    fn try_from_device(device: rodio::cpal::Device) -> Result<OutputStream, PlayError> {
        // In rodio 0.21, try_from_device is available on OutputStream
        OutputStreamBuilder::default()
            .with_device(device)
            .open_stream()
            .map_err(PlayError::StreamError)
    }

    /// Try to create a stream from the default device, falling back to others
    fn try_default() -> Result<OutputStream, PlayError> {
        // Use rodio's internal cpal re-export
        let host = rodio::cpal::default_host();

        let default_device = host
            .default_output_device()
            .ok_or(PlayError::StreamError(rodio::StreamError::NoDevice))?;

        Self::try_from_device(default_device).or_else(|original_err| {
            let devices = host.output_devices().map_err(|_| original_err)?;

            for d in devices {
                if let Ok(res) = Self::try_from_device(d) {
                    return Ok(res);
                }
            }
            Err(PlayError::StreamError(rodio::StreamError::NoDevice))
        })
    }

    pub fn new(error_sender: Sender<PlayError>, options: PlayerOptions) -> Result<Self, PlayError> {
        let stream = Self::try_default()?;

        // sink::try_new requires a reference to the handle
        let sink = Sink::connect_new(stream.mixer());

        sink.set_volume(options.initial_volume_f32());

        Ok(Self {
            sink,
            stream,
            error_sender,
            data: PlayerData::new(options.initial_volume()),
            options,
        })
    }

    pub fn update(&self) -> Result<Self, PlayError> {
        let stream = Self::try_default()?;
        let sink = Sink::connect_new(stream.mixer());

        sink.set_volume(self.data.volume_f32());

        Ok(Self {
            sink,
            stream,
            error_sender: self.error_sender.clone(),
            data: self.data.clone(),
            options: self.options.clone(),
        })
    }
    pub fn change_volume(&mut self, positive: bool) {
        self.data.change_volume(positive);
        self.sink.set_volume(self.data.volume_f32());
    }

    pub fn is_finished(&self) -> bool {
        self.sink.empty()
    }

    pub fn play_at(&mut self, path: &Path, time: Duration) -> Result<(), PlayError> {
        log::info!("Playing file: {:?} at time: {:?}", path, time);
        self.play(path)?;
        if let Err(e) = self.sink.try_seek(time) {
            log::error!("Seek error: {}", e);
            let _ = self.error_sender.send(PlayError::SeekError(e));
        }

        Ok(())
    }

    pub fn play(&mut self, path: &Path) -> Result<(), PlayError> {
        log::info!("Playing file: {:?}", path);
        self.data.set_current_file(Some(path.to_path_buf()));

        self.stop();

        let file = File::open(path).map_err(PlayError::Io)?;

        if file.metadata().map(|m| m.len()).unwrap_or(0) == 0 {
            return Err(PlayError::Io(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "File is empty",
            )));
        }

        let decoder = Decoder::new(file).map_err(PlayError::DecoderError)?;

        self.data.set_total_duration(decoder.total_duration());

        // Check if sink is detached or empty and recreate if necessary
        if self.sink.empty() {
            // Using try_new with the stored handle
            self.sink = Sink::connect_new(self.stream.mixer());
        }

        self.sink.set_volume(self.data.volume_f32());
        self.sink.append(decoder);

        Ok(())
    }

    pub fn stop(&mut self) {
        // rodio 0.21: To stop, you can clear the sink.
        if !self.sink.empty() {
            self.sink.clear();
        }
    }

    pub fn elapsed(&self) -> Duration {
        self.sink.get_pos()
    }

    pub fn duration(&self) -> Option<f64> {
        self.data
            .total_duration()
            .map(|duration| duration.as_secs_f64())
    }

    pub fn toggle_playback(&mut self) {
        if self.sink.is_paused() {
            self.sink.play();
        } else {
            self.sink.pause();
        }
    }

    pub fn seek_fw(&mut self) {
        let current_elapsed = self.elapsed();
        let new_pos = current_elapsed + SEEK_STEP;

        self.seek_to(new_pos);
    }

    pub fn seek_bw(&mut self) {
        let current_elapsed = self.elapsed();
        let new_pos = current_elapsed.saturating_sub(SEEK_STEP);
        self.seek_to(new_pos);
    }

    pub fn seek_to(&mut self, time: Duration) {
        log::info!("Seek to: {:?}", time);
        if self.is_finished() {
            return;
        }
        let file = self.data.current_file().expect("Current file not set");

        if let Err(e) = self.sink.try_seek(time) {
            log::error!("Seek error: {}", e);
            let _ = self.error_sender.send(PlayError::SeekError(e));
        } else {
            // If the sink is finished, we need to reset the music
            // This happens when the user seeks to the start of the song before the buffer.
            if self.is_finished() {
                log::info!("Sink is finished while seeking, resetting the music");
                if let Err(e) = self.play_at(&file, time) {
                    log::error!("Error playing file: {:?}", e);
                    let _ = self.error_sender.send(e);
                }
            }
        }
    }

    pub fn percentage(&self) -> f64 {
        self.duration().map_or(0.0, |duration| {
            let elapsed = self.elapsed().as_secs_f64();
            elapsed / duration
        })
    }

    pub fn volume_percent(&self) -> u8 {
        self.data.volume()
    }

    pub fn volume(&self) -> i32 {
        self.data.volume().into()
    }

    pub fn volume_up(&mut self) {
        let volume = self.volume() + 5;
        self.set_volume(volume);
    }

    pub fn volume_down(&mut self) {
        let volume = self.volume() - 5;
        self.set_volume(volume);
    }

    pub fn set_volume(&mut self, mut volume: i32) {
        volume = volume.clamp(0, 100);
        self.data.set_volume(volume as u8);
        self.sink.set_volume((volume as f32) / 100.0);
    }

    pub fn pause(&mut self) {
        if !self.sink.is_paused() {
            self.toggle_playback();
        }
    }

    pub fn resume(&mut self) {
        if self.sink.is_paused() {
            self.toggle_playback();
        }
    }

    pub fn is_paused(&self) -> bool {
        self.sink.is_paused()
    }

    pub fn seek(&mut self, secs: i64) {
        if secs.is_positive() {
            self.seek_fw();
        } else {
            self.seek_bw();
        }
    }

    pub fn get_progress(&self) -> (f64, u32, u32) {
        let position = self.elapsed();
        let duration = self.duration().unwrap_or(99.0) as u32;
        let percent = self.percentage() * 100.0;
        (percent.min(100.0), position.as_secs() as u32, duration)
    }
}


================================================
FILE: crates/player/src/player_data.rs
================================================
use std::{path::PathBuf, time::Duration};

use crate::VOLUME_STEP;

#[derive(Clone)]
pub struct PlayerData {
    total_duration: Option<Duration>,
    current_file: Option<PathBuf>,
    volume: u8,
}

impl PlayerData {
    pub fn new(volume: u8) -> Self {
        Self {
            total_duration: None,
            current_file: None,
            volume,
        }
    }

    /// Changes the volume by the volume step. If positive is true, the volume is increased, otherwise it is decreased.
    pub fn change_volume(&mut self, positive: bool) {
        if positive {
            self.set_volume(self.volume().saturating_add(VOLUME_STEP).min(100));
        } else {
            self.set_volume(self.volume().saturating_sub(VOLUME_STEP));
        }
    }

    /// Returns the volume as a f32 between 0.0 and 1.0
    pub fn volume_f32(&self) -> f32 {
        f32::from(self.volume()) / 100.0
    }

    /// Returns the volume as a u8 between 0 and 100
    pub fn volume(&self) -> u8 {
        self.volume
    }

    /// Sets the volume to the given value
    pub fn set_volume(&mut self, volume: u8) {
        self.volume = volume;
    }

    /// Returns the total duration of the current file
    pub fn total_duration(&self) -> Option<Duration> {
        self.total_duration
    }

    /// Sets the total duration of the current file
    pub fn set_total_duration(&mut self, total_duration: Option<Duration>) {
        self.total_duration = total_duration;
    }

    /// Returns the current file
    pub fn current_file(&self) -> Option<PathBuf> {
        self.current_file.clone()
    }

    /// Sets the current file
    pub fn set_current_file(&mut self, current_file: Option<PathBuf>) {
        self.current_file = current_file;
    }
}


================================================
FILE: crates/player/src/player_options.rs
================================================
#[derive(Debug, Clone)]
pub struct PlayerOptions {
    initial_volume: u8,
}

impl PlayerOptions {
    /// Creates a new PlayerOptions with the given initial volume
    pub fn new(initial_volume: u8) -> Self {
        Self {
            initial_volume: initial_volume.min(100),
        }
    }

    /// Returns the initial volume as a u8 between 0 and 100
    pub fn initial_volume(&self) -> u8 {
        self.initial_volume
    }

    /// Returns the initial volume as a f32 between 0.0 and 1.0
    pub fn initial_volume_f32(&self) -> f32 {
        f32::from(self.initial_volume()) / 100.0
    }
}


================================================
FILE: crates/ytermusic/Cargo.toml
================================================
[package]
edition = "2021"
name = "ytermusic"
version = "0.1.0"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

ytpapi2.workspace = true

#  --- Threading & Sync ---
flume = "0.11.0"
once_cell = "1.19.0"
tokio = { version = "1.36.0", features = ["rt-multi-thread"] }

#  --- Encoding ---
bincode = { version = "1.3.3" }
directories = "5.0.1"
rand = "0.8.5"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
urlencoding = "2.1.3"
varuint = "0.7.1"

#  --- UI ---
crossterm = "0.27.0"
ratatui = { version = "0.26.1", features = ["serde"] }
unicode-bidi = "0.3"

#  --- Player ---
player.workspace = true

#  --- Media Control ---
souvlaki = "0.7.3"

#  --- Alloc ---
mimalloc = { version = "0.1.39", default-features = false }

#  --- Config ---
toml = "0.8.11"

#  --- Logging ---
log = "0.4.21"

#  --- Database ---
database.workspace = true
download-manager.workspace = true
common-structs.workspace = true

# -- Cookies auto retreival --
rookie = "0.5.2"

ctrlc = "3.5.0"

[target."cfg(target_os = \"windows\")".dependencies]
raw-window-handle = "0.4.3"
winit = "0.26.1"
[target."cfg(target_os = \"macos\")".dependencies]
winit = "0.26.1"

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


================================================
FILE: crates/ytermusic/src/config.rs
================================================
use log::info;
use ratatui::style::{Color, Modifier, Style};
use serde::{Deserialize, Serialize};

use crate::utils::get_project_dirs;

#[derive(Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct GlobalConfig {
    /// Maximum number of parallel downloads.
    /// If your downloads are failing, try lowering
    /// this.
    /// Default value is 4.
    #[serde(default = "parallel_downloads")]
    pub parallel_downloads: u16,
}

#[derive(Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct MusicPlayerConfig {
    /// Initial volume of the player, in percent.
    /// Default value is 50, clamped at 100.
    #[serde(default = "default_volume")]
    pub initial_volume: u8,
    #[serde(default = "default_true")]
    pub dbus: bool,
    #[serde(default = "default_true")]
    pub hide_channels_on_homepage: bool,
    #[serde(default = "default_false")]
    pub hide_albums_on_homepage: bool,
    #[serde(default = "enable_volume_slider")]
    pub volume_slider: bool,
    /// Whether to shuffle playlists before playing
    #[serde(default)]
    pub shuffle: bool,
    #[serde(default = "default_paused_style", with = "StyleDef")]
    pub gauge_paused_style: Style,
    #[serde(default = "default_playing_style", with = "StyleDef")]
    pub gauge_playing_style: Style,
    #[serde(default = "default_nomusic_style", with = "StyleDef")]
    pub gauge_nomusic_style: Style,
    #[serde(default = "default_paused_style", with = "StyleDef")]
    pub text_paused_style: Style,
    #[serde(default = "default_playing_style", with = "StyleDef")]
    pub text_playing_style: Style,
    #[serde(default = "default_nomusic_style", with = "StyleDef")]
    pub text_next_style: Style,
    #[serde(default = "default_nomusic_style", with = "StyleDef")]
    pub text_waiting_style: Style,
    #[serde(default = "default_downloading_style", with = "StyleDef")]
    pub text_downloading_style: Style,
    #[serde(default = "default_error_style", with = "StyleDef")]
    pub text_error_style: Style,
    #[serde(default = "default_searching_style", with = "StyleDef")]
    pub text_searching_style: Style,
}

#[derive(Debug, Deserialize, Serialize)]
#[serde(remote = "Style")]
struct StyleDef {
    #[serde(default)]
    fg: Option<Color>,
    #[serde(default)]
    bg: Option<Color>,
    #[serde(default = "Modifier::empty")]
    add_modifier: Modifier,
    #[serde(default = "Modifier::empty")]
    sub_modifier: Modifier,
    #[serde(default)]
    underline_color: Option<Color>,
}

impl Default for MusicPlayerConfig {
    fn default() -> Self {
        Self {
            hide_albums_on_homepage: default_false(),
            hide_channels_on_homepage: default_true(),
            dbus: default_true(),
            initial_volume: default_volume(),
            shuffle: Default::default(),
            gauge_paused_style: default_paused_style(),
            gauge_playing_style: default_playing_style(),
            gauge_nomusic_style: default_nomusic_style(),
            text_paused_style: default_paused_style(),
            text_playing_style: default_playing_style(),
            text_next_style: default_nomusic_style(),
            text_waiting_style: default_nomusic_style(),
            text_error_style: default_error_style(),
            text_searching_style: default_searching_style(),
            text_downloading_style: default_downloading_style(),
            volume_slider: enable_volume_slider(),
        }
    }
}

fn default_searching_style() -> Style {
    Style::default().fg(Color::LightCyan)
}

fn default_error_style() -> Style {
    Style::default().fg(Color::Red)
}

fn parallel_downloads() -> u16 {
    4
}

fn default_false() -> bool {
    false
}

fn default_true() -> bool {
    true
}

fn enable_volume_slider() -> bool {
    true
}

fn default_paused_style() -> Style {
    Style::default().fg(Color::Yellow)
}

fn default_playing_style() -> Style {
    Style::default().fg(Color::Green)
}

fn default_nomusic_style() -> Style {
    Style::default().fg(Color::White)
}

fn default_downloading_style() -> Style {
    Style::default().fg(Color::Blue)
}

fn default_volume() -> u8 {
    50
}

#[derive(Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct PlaylistConfig {}

#[derive(Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct SearchConfig {}

#[allow(unused)]
#[derive(Debug, Default, Deserialize, Serialize)]
#[non_exhaustive]
pub struct Config {
    #[serde(default)]
    pub global: GlobalConfig,
    #[serde(default)]
    pub player: MusicPlayerConfig,
    #[serde(default)]
    pub playlist: PlaylistConfig,
    #[serde(default)]
    pub search: SearchConfig,
}

impl Config {
    pub fn new() -> Self {
        // TODO handle errors
        let opt = || {
            let project_dirs = get_project_dirs()?;
            let config_path = project_dirs.config_dir().join("config.toml");
            config_path
                .parent()
                .map(|p| std::fs::create_dir_all(p).ok());
            info!("Loading config from {:?}", config_path);
            if !config_path.exists() {
                let default_config = Self::default();
                std::fs::write(
                    project_dirs.config_dir().join("config.toml"),
                    toml::to_string_pretty(&default_config).ok()?,
                )
                .ok()?;
                return Some(default_config);
            }
            let config_string = std::fs::read_to_string(config_path).ok()?;
            let config = toml::from_str::<Self>(&config_string).ok()?;
            std::fs::write(
                project_dirs.config_dir().join("config.applied.toml"),
                toml::to_string_pretty(&config).ok()?,
            )
            .ok()?;
            Some(config)
        };
        opt().unwrap_or_default()
    }
}


================================================
FILE: crates/ytermusic/src/consts.rs
================================================
use std::path::PathBuf;

use log::warn;
use once_cell::sync::Lazy;

use crate::{config, utils::get_project_dirs};

pub const HEADER_TUTORIAL: &str = r#"To configure the YTerMusic:
1. Open the YouTube Music website in your browser
2. Open the developer tools (F12)
3. Go to the Network tab
4. Go to https://music.youtube.com
5. Copy the `cookie` header from the associated request
6. Paste it in the `headers.txt` file in format `Cookie: <cookie>`
7. On a newline of `headers.txt` add a user-agent in format `User-Agent: <Mozilla/5.0 (Example)>
8. Restart YterMusic"#;

pub static CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| {
    let pdir = get_project_dirs();
    if let Some(dir) = pdir {
        return dir.cache_dir().to_path_buf();
    };
    warn!("Failed to get cache dir! Defaulting to './data'");
    PathBuf::from("./data")
});

pub static CONFIG: Lazy<config::Config> = Lazy::new(config::Config::new);

pub const INTRODUCTION: &str = r#"Usage: ytermusic [options]

YTerMusic is a TUI based Youtube Music Player that aims to be as fast and simple as possible.
In order to get your music, create a file "headers.txt" in the config folder, and copy the Cookie and User-Agent from request header of the music.youtube.com html document "/" page.
More info at: https://github.com/ccgauche/ytermusic

Options:
        -h or --help        Show this menu
        --files             Show the location of the ytermusic files
        --fix-db            Fix the database in cache
        --clear-cache       Erase all the files in cache

Shortcuts:
        Use your mouse to click in lists if your terminal has mouse support
        Space                     play/pause
        Enter                     select a playlist or a music
        f                         search
        s                         shuffle
        r                         remove a music from the main playlist
        Arrow Right or >          skip 5 seconds
        Arrow Left or <           go back 5 seconds
        CTRL + Arrow Right (>)    go to the next song
        CTRL + Arrow Left  (<)    go to the previous song
        +                         volume up
        -                         volume down
        Arrow down                scroll down
        Arrow up                  scroll up
        ESC                       exit the current menu
        CTRL + C or CTRL + D      quit
"#;


================================================
FILE: crates/ytermusic/src/database.rs
================================================
use database::YTLocalDatabase;
use once_cell::sync::Lazy;

use crate::consts::CACHE_DIR;

pub static DATABASE: Lazy<YTLocalDatabase> = Lazy::new(|| YTLocalDatabase::new(CACHE_DIR.clone()));


================================================
FILE: crates/ytermusic/src/errors.rs
================================================
use flume::Sender;

use crate::term::{ManagerMessage, Screens};

/// Utils to handle errors
pub fn handle_error_option<T, E>(
    updater: &Sender<ManagerMessage>,
    error_type: &'static str,
    a: Result<E, T>,
) -> Option<E>
where
    T: std::fmt::Debug,
{
    match a {
        Ok(e) => Some(e),
        Err(a) => {
            updater
                .send(ManagerMessage::PassTo(
                    Screens::DeviceLost,
                    Box::new(ManagerMessage::Error(
                        format!("{error_type} {a:?}"),
                        Box::new(None),
                    )),
                ))
                .unwrap();
            None
        }
    }
}

/// Utils to handle errors
pub fn handle_error<T>(updater: &Sender<ManagerMessage>, error_type: &'static str, a: Result<(), T>)
where
    T: std::fmt::Debug,
{
    let _ = handle_error_option(updater, error_type, a);
}


================================================
FILE: crates/ytermusic/src/main.rs
================================================
use consts::{CACHE_DIR, INTRODUCTION};
use flume::{Receiver, Sender};
use log::{error, info};
use once_cell::sync::Lazy;
use structures::performance::STARTUP_TIME;
use term::{Manager, ManagerMessage};
use tokio::select;

use std::{
    future::Future,
    panic,
    path::{Path, PathBuf},
    str::FromStr,
    sync::RwLock,
};
use systems::{logger::init, player::player_system};

use crate::{
    consts::HEADER_TUTORIAL,
    structures::{media::run_window_handler, sound_action::download_manager_handler},
    systems::{logger::get_log_file_path, DOWNLOAD_MANAGER},
    utils::get_project_dirs,
};

mod config;
mod consts;
mod database;
mod errors;
mod shutdown;
mod structures;
mod systems;
mod term;
mod utils;
pub use shutdown::{is_shutdown_sent, shutdown, ShutdownSignal};
mod tasks;

pub use database::DATABASE;

use mimalloc::MiMalloc;

// Changes the allocator to improve performance especially on Windows
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;

fn run_service<T>(future: T) -> tokio::task::JoinHandle<()>
where
    T: Future + Send + 'static,
{
    tokio::task::spawn(async move {
        select! {
            _ = future => {},
            _ = ShutdownSignal => {},
        }
    })
}

static COOKIES: Lazy<RwLock<Option<String>>> = Lazy::new(|| RwLock::new(None));

pub fn try_get_cookies() -> Option<String> {
    let cookies = COOKIES.read().unwrap();
    cookies.clone()
}

fn main() {
    // Check if the first param is --files
    if let Some(arg) = std::env::args().nth(1) {
        match arg.as_str() {
            "-h" | "--help" => {
                println!("{}", INTRODUCTION);
                return;
            }
            "--files" => {
                println!("# Location of ytermusic files");
                println!(" - Logs: {}", get_log_file_path().display());
                println!(" - Headers: {}", get_header_file().unwrap().1.display());
                println!(" - Cache: {}", CACHE_DIR.display());
                return;
            }
            "--fix-db" => {
                DATABASE.fix_db();
                DATABASE.write();
                println!("[INFO] Database fixed");
                return;
            }
            "--clear-cache" => {
                match std::fs::remove_dir_all(&*CACHE_DIR) {
                    Ok(_) => {
                        println!("[INFO] Cache cleared");
                    }
                    Err(e) => {
                        println!("[ERROR] Can't clear cache: {e}");
                    }
                }
                return;
            }
            "--with-auto-cookies" => {
                std::fs::write(get_log_file_path(), "# YTerMusic log file\n\n").unwrap();
                init().expect("Failed to initialize logger");
                let param = std::env::args().nth(2);
                if let Some(cookies) = cookies(param) {
                    let mut cookies_guard = COOKIES.write().unwrap();
                    info!("Cookies: {cookies}");
                    *cookies_guard = Some(cookies);
                    info!("Cookies loaded");
                } else {
                    error!("Can't load cookies");
                    error!("Maybe rookie didn't find any cookies or any browser");
                    error!("Please make sure you have cookies in your browser");
                    return;
                }
            }
            e => {
                println!("Unknown argument `{e}`");
                println!("Here are the available arguments:");
                println!(" - --files: Show the location of the ytermusic files");
                println!(" - --clear-cache: Erase all the files in cache");
                println!(" - --fix-db: Fix the database");
                return;
            }
        }
    } else {
        std::fs::write(get_log_file_path(), "# YTerMusic log file\n\n").unwrap();
        init().expect("Failed to initialize logger");
    }
    panic::set_hook(Box::new(|e| {
        println!("{e}");
        error!("{e}");
        shutdown();
    }));
    app_start();
}

fn cookies(specific_browser: Option<String>) -> Option<String> {
    let loaded = match specific_browser {
        Some(browser) => match browser.as_str() {
            "all" => rookie::load,
            "firefox" => rookie::firefox,
            "chrome" => rookie::chrome,
            "edge" => rookie::edge,
            "opera" => rookie::opera,
            "brave" => rookie::brave,
            "vivaldi" => rookie::vivaldi,
            "chromium" => rookie::chromium,
            #[cfg(target_os = "macos")]
            "safari" => rookie::safari,
            "arc" => rookie::arc,
            "librewolf" => rookie::librewolf,
            "opera-gx" | "opera_gx" => rookie::opera_gx,
            #[cfg(target_os = "windows")]
            "internet_explorer" | "internet-explorer" | "ie" => rookie::internet_explorer,
            #[cfg(target_os = "windows")]
            "octo_browser" | "octo-browser" => rookie::octo_browser,
            _ => {
                println!("Unknown browser `{browser}`");
                error!("Unknown browser `{browser}`");
                return None;
            }
        },
        None => rookie::load,
    }(Some(vec!["youtube.com".to_string()]))
    .unwrap();
    let mut cookies = Vec::new();
    let current_timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();
    for cookie in loaded {
        if cookie.domain != ".youtube.com" && cookie.domain != "music.youtube.com" {
            continue;
        }
        if cookie
            .expires
            .map(|e| e < current_timestamp)
            .unwrap_or(false)
        {
            continue;
        }
        if cookies.iter().any(|(name, _)| name == &cookie.name) {
            continue;
        }
        cookies.push((cookie.name, cookie.value));
    }
    let cookies = cookies
        .iter()
        .map(|(name, value)| format!("{name}={value}"))
        .collect::<Vec<_>>();
    let cookies = cookies.join("; ");
    Some(cookies)
}

fn get_header_file() -> Result<(String, PathBuf), (std::io::Error, PathBuf)> {
    let fp = PathBuf::from_str("headers.txt").unwrap();
    if let Ok(e) = std::fs::read_to_string(&fp) {
        return Ok((e, fp));
    }
    let fp = get_project_dirs()
        .ok_or_else(|| {
            (
                std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "Can't find project dir. This is a `directories` crate issue",
                ),
                Path::new("./").to_owned(),
            )
        })?
        .config_dir()
        .to_owned();
    if let Err(e) = std::fs::create_dir_all(&fp) {
        println!("Can't create app directory {e} in `{}`", fp.display());
    }
    let fp = fp.join("headers.txt");
    std::fs::read_to_string(&fp).map_or_else(|e| Err((e, fp.clone())), |e| Ok((e, fp.clone())))
}

async fn app_start_main(updater_r: Receiver<ManagerMessage>, updater_s: Sender<ManagerMessage>) {
    STARTUP_TIME.log("Init");

    std::fs::create_dir_all(CACHE_DIR.join("downloads")).unwrap();

    if try_get_cookies().is_none() {
        if let Err((error, filepath)) = get_header_file() {
            println!("Can't read or find `{}`", filepath.display());
            println!("Error: {error}");
            println!("{HEADER_TUTORIAL}");
            // prevent console window closing on windows, does nothing on linux
            std::io::stdin().read_line(&mut String::new()).unwrap();
            return;
        }
    }

    STARTUP_TIME.log("Startup");

    // Spawn the clean task
    tasks::clean::spawn_clean_task();

    STARTUP_TIME.log("Spawned clean task");
    // Spawn the player task
    let (sa, player) = player_system(updater_s.clone());
    // Spawn the downloader system
    DOWNLOAD_MANAGER.spawn_system(ShutdownSignal, download_manager_handler(sa.clone()));
    STARTUP_TIME.log("Spawned system task");
    tasks::last_playlist::spawn_last_playlist_task(updater_s.clone());
    STARTUP_TIME.log("Spawned last playlist task");
    // Spawn the API task
    tasks::api::spawn_api_task(updater_s.clone());
    STARTUP_TIME.log("Spawned api task");
    // Spawn the database getter task
    tasks::local_musics::spawn_local_musics_task(updater_s);

    STARTUP_TIME.log("Running manager");
    let mut manager = Manager::new(sa, player).await;
    manager.run(&updater_r).unwrap();
}

fn app_start() {
    let (updater_s, updater_r) = flume::unbounded::<ManagerMessage>();
    let updater_s_c = updater_s.clone();
    ctrlc::set_handler(move || {
        info!("CTRL-C received");
        shutdown()
    })
    .expect("Error setting Ctrl-C handler");
    std::thread::spawn(move || {
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .expect("Failed to build runtime")
            .block_on(async move {
                select! {
                    _ = app_start_main(updater_r, updater_s) => {},
                    _ = ShutdownSignal => {},
                };
            });
        info!("Runtime closed");
    });
    run_window_handler(&updater_s_c);
}


================================================
FILE: crates/ytermusic/src/shutdown.rs
================================================
use std::{
    future::Future,
    pin::Pin,
    sync::atomic::{AtomicBool, Ordering},
    task::{Context, Poll},
};

use log::info;

use std::sync::{Condvar, Mutex};

pub struct SharedEvent {
    lock: Mutex<bool>,
    cvar: Condvar,
}

impl SharedEvent {
    // const fn allows this to be called in a static context
    pub const fn new() -> Self {
        Self {
            lock: Mutex::new(false),
            cvar: Condvar::new(),
        }
    }

    /// Blocks the current thread until notify() is called.
    pub fn wait(&self) {
        let mut ready = self.lock.lock().unwrap();
        while !*ready {
            ready = self.cvar.wait(ready).unwrap();
        }
    }

    /// Wakes up ALL waiting threads.
    pub fn notify(&self) {
        let mut ready = self.lock.lock().unwrap();
        *ready = true;
        self.cvar.notify_all();
    }
}

// Global static initialization
static SHUTDOWN_WAKER: SharedEvent = SharedEvent::new();

#[allow(dead_code)]
pub fn block_until_shutdown() {
    SHUTDOWN_WAKER.wait();
}

static SHUTDOWN_SENT: AtomicBool = AtomicBool::new(false);

pub fn is_shutdown_sent() -> bool {
    SHUTDOWN_SENT.load(Ordering::Relaxed)
}

#[derive(Clone)]
pub struct ShutdownSignal;

impl Future for ShutdownSignal {
    type Output = ();

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if SHUTDOWN_SENT.load(Ordering::Relaxed) {
            Poll::Ready(())
        } else {
            Poll::Pending
        }
    }
}

pub fn shutdown() {
    SHUTDOWN_SENT.store(true, Ordering::Relaxed);
    SHUTDOWN_WAKER.notify();
    info!("Shutdown signal sent, waiting for shutdown");
}


================================================
FILE: crates/ytermusic/src/structures/app_status.rs
================================================
use common_structs::{AppStatus, MusicDownloadStatus};
use ratatui::style::{Modifier, Style};

pub trait MusicDownloadStatusExt {
    fn style(&self, playing: Option<bool>) -> Style;
}

pub trait AppStatusExt {
    fn style(&self) -> Style;
}

use crate::consts::CONFIG;

impl MusicDownloadStatusExt for MusicDownloadStatus {
    fn style(&self, playing: Option<bool>) -> Style {
        let k = match self {
            Self::NotDownloaded => CONFIG.player.text_waiting_style,
            Self::Downloaded => {
                if let Some(e) = playing {
                    if e {
                        CONFIG.player.text_playing_style
                    } else {
                        CONFIG.player.text_paused_style
                    }
                } else {
                    CONFIG.player.text_next_style
                }
            }
            Self::Downloading(_) => CONFIG.player.text_downloading_style,
            Self::DownloadFailed => CONFIG.player.text_error_style,
        };
        if playing.is_some() {
            k.add_modifier(Modifier::BOLD)
        } else {
            k
        }
    }
}

impl AppStatusExt for AppStatus {
    fn style(&self) -> Style {
        match self {
            Self::Paused => CONFIG.player.gauge_paused_style,
            Self::Playing => CONFIG.player.gauge_playing_style,
            Self::NoMusic => CONFIG.player.gauge_nomusic_style,
        }
    }
}


================================================
FILE: crates/ytermusic/src/structures/media.rs
================================================
use std::time::Duration;

use flume::Sender;
use log::{error, info};
use player::Player;
use souvlaki::{
    Error, MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, MediaPosition,
    SeekDirection,
};
use ytpapi2::YoutubeMusicVideoRef;

use crate::{consts::CONFIG, shutdown, term::ManagerMessage};

use super::sound_action::SoundAction;

pub struct Media {
    controls: Option<MediaControls>,

    current_meta: Option<(String, String, String, Option<Duration>)>,
    current_playback: Option<MediaPlayback>,
}

impl Media {
    pub fn new(updater: Sender<ManagerMessage>, soundaction_sender: Sender<SoundAction>) -> Self {
        if !CONFIG.player.dbus {
            info!("Media controls disabled by config");
            return Self {
                controls: None,
                current_meta: None,
                current_playback: None,
            };
        }
        let mut handle = get_handle(&updater);
        if let Some(e) = handle.as_mut() {
            if let Err(e) = connect(e, soundaction_sender) {
                error!("Media actions are not supported on this platform: {e:?}",);
            }
        } else {
            error!("Media controls are not supported on this platform");
        }
        Self {
            controls: handle,
            current_meta: None,
            current_playback: None,
        }
    }

    pub fn update(
        &mut self,
        current: Option<YoutubeMusicVideoRef>,
        sink: &Player,
    ) -> Result<(), souvlaki::Error> {
        if let Some(e) = &mut self.controls {
            let media_meta = MediaMetadata {
                title: current.as_ref().map(|video| video.title.as_str()),
                album: current.as_ref().map(|video| video.album.as_str()),
                artist: current.as_ref().map(|video| video.author.as_str()),
                cover_url: None,
                duration: sink
                    .duration()
                    .map(|duration| Duration::from_secs(duration as u64)),
            };
            if self.current_meta
                != Some((
                    media_meta.title.unwrap_or("").to_string(),
                    media_meta.album.unwrap_or("").to_string(),
                    media_meta.artist.unwrap_or("").to_string(),
                    sink.duration()
                        .map(|duration| Duration::from_secs(duration as u64)),
                ))
            {
                self.current_meta = Some((
                    media_meta.title.unwrap_or("").to_string(),
                    media_meta.album.unwrap_or("").to_string(),
                    media_meta.artist.unwrap_or("").to_string(),
                    sink.duration()
                        .map(|duration| Duration::from_secs(duration as u64)),
                ));
                e.set_metadata(media_meta)?;
            }
            let playback = if sink.is_finished() {
                MediaPlayback::Stopped
            } else if sink.is_paused() {
                MediaPlayback::Paused {
                    progress: Some(MediaPosition(sink.elapsed())),
                }
            } else {
                MediaPlayback::Playing {
                    progress: Some(MediaPosition(sink.elapsed())),
                }
            };
            if self.current_playback != Some(playback.clone()) {
                self.current_playback = Some(playback.clone());
                e.set_playback(playback)?;
            }
        }
        Ok(())
    }
}

fn connect(mpris: &mut MediaControls, sender: Sender<SoundAction>) -> Result<(), Error> {
    mpris.attach(move |e| match e {
        MediaControlEvent::Toggle | MediaControlEvent::Play | MediaControlEvent::Pause => {
            sender.send(SoundAction::PlayPause).unwrap();
        }
        MediaControlEvent::Next => {
            sender.send(SoundAction::Next(1)).unwrap();
        }
        MediaControlEvent::Previous => {
            sender.send(SoundAction::Previous(1)).unwrap();
        }
        MediaControlEvent::Stop => {
            sender.send(SoundAction::Cleanup).unwrap();
        }
        MediaControlEvent::Seek(a) => match a {
            souvlaki::SeekDirection::Forward => {
                sender.send(SoundAction::Forward).unwrap();
            }
            souvlaki::SeekDirection::Backward => {
                sender.send(SoundAction::Backward).unwrap();
            }
        },
        // TODO(functionnality): implement seek amount
        MediaControlEvent::SeekBy(a, _b) => {
            if a == SeekDirection::Forward {
                sender.send(SoundAction::Forward).unwrap();
            } else {
                sender.send(SoundAction::Backward).unwrap();
            }
        }

        MediaControlEvent::SetPosition(a) => {
            sender.send(SoundAction::SeekTo(a.0)).unwrap();
        }
        MediaControlEvent::OpenUri(a) => {
            todo!("Implement URI opening {a:?}")
        }
        MediaControlEvent::Raise => {
            todo!("Implement raise")
        }
        MediaControlEvent::Quit => {
            shutdown();
        }
        MediaControlEvent::SetVolume(e) => {
            sender.send(SoundAction::SetVolume(e as f32)).unwrap();
        }
    })
}

#[cfg(not(target_os = "windows"))]
fn get_handle(updater: &Sender<ManagerMessage>) -> Option<MediaControls> {
    use crate::errors::handle_error_option;
    use souvlaki::PlatformConfig;
    handle_error_option(
        updater,
        "Can't create media controls",
        MediaControls::new(PlatformConfig {
            dbus_name: "ytermusic",
            display_name: "YTerMusic",
            hwnd: None,
        })
        .map_err(|e| format!("{e:?}")),
    )
}
#[cfg(not(target_os = "macos"))]
pub fn run_window_handler(_updater: &Sender<ManagerMessage>) -> Option<()> {
    use crate::is_shutdown_sent;

    loop {
        if !is_shutdown_sent() {
            crate::shutdown::block_until_shutdown()
        } else {
            use std::process::exit;

            info!("event loop closed");
            exit(0);
        }
    }
}

#[cfg(target_os = "macos")]
pub fn run_window_handler(updater: &Sender<ManagerMessage>) -> Option<()> {
    use std::process::exit;

    use winit::event_loop::EventLoop;
    use winit::platform::macos::{ActivationPolicy, EventLoopExtMacOS};
    use winit::window::WindowBuilder;

    use crate::errors::handle_error_option;
    let thread = std::thread::current();
    info!("Current Thread Name: {:?}", thread.name());
    info!("Current Thread ID:   {:?}", thread.id());

    // On macOS, winit requires the EventLoop to be created on the main thread.
    // Unlike Windows, we cannot use `new_any_thread`.
    // We create a hidden window to ensure NSApplication is active and capable of receiving events.
    let mut event_loop = EventLoop::new();
    event_loop.set_activation_policy(ActivationPolicy::Regular);

    // Create a hidden window. While souvlaki doesn't need the handle in the config,
    // the existence of the window helps keep the event loop and application state valid.
    let _window = handle_error_option(
        updater,
        "OS Error while creating media hook window",
        WindowBuilder::new().with_visible(false).build(&event_loop),
    )?;
    event_loop.run(move |_event, _window_target, ctrl_flow| {
        use crate::is_shutdown_sent;

        if is_shutdown_sent() {
            info!("event loop closed");
            *ctrl_flow = winit::event_loop::ControlFlow::Exit;
            exit(0);
        }
    });
}

#[cfg(target_os = "windows")]
fn get_handle(updater: &Sender<ManagerMessage>) -> Option<MediaControls> {
    use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
    use souvlaki::PlatformConfig;
    use winit::event_loop::EventLoop;
    use winit::{platform::windows::EventLoopExtWindows, window::WindowBuilder};

    use crate::errors::handle_error_option;
    use crate::term::Screens;

    let config = PlatformConfig {
        dbus_name: "ytermusic",
        display_name: "YTerMusic",
        hwnd: if let RawWindowHandle::Win32(h) = handle_error_option(
            updater,
            "OS Error while creating media hook window",
            WindowBuilder::new()
                .with_visible(false)
                .build(&EventLoop::<()>::new_any_thread()),
        )?
        .raw_window_handle()
        {
            Some(h.hwnd)
        } else {
            updater
                .send(ManagerMessage::PassTo(
                    Screens::DeviceLost,
                    Box::new(ManagerMessage::Error(
                        "No window handle found".to_string(),
                        Box::new(None),
                    )),
                ))
                .unwrap();
            return None;
        },
    };

    handle_error_option(
        updater,
        "Can't create media controls",
        MediaControls::new(config).map_err(|x| format!("{:?}", x)),
    )
}


================================================
FILE: crates/ytermusic/src/structures/mod.rs
================================================
pub mod app_status;
pub mod media;
pub mod performance;
pub mod sound_action;


================================================
FILE: crates/ytermusic/src/structures/performance.rs
================================================
use std::time::Instant;

use log::info;
use once_cell::sync::Lazy;

pub struct Performance {
    pub initial: Instant,
}

impl Performance {
    pub fn new() -> Self {
        Self {
            initial: Instant::now(),
        }
    }

    pub fn get_ms(&self) -> u128 {
        self.initial.elapsed().as_millis()
    }

    pub fn log(&self, message: &str) {
        info!(target: "performance", "{}: {}ms", message, self.get_ms());
    }
}

pub fn guard<'a>(name: &'a str) -> PerformanceGuard<'a> {
    PerformanceGuard::new(name)
}

pub struct PerformanceGuard<'a> {
    name: &'a str,
    start: Performance,
}

impl<'a> PerformanceGuard<'a> {
    pub fn new(name: &'a str) -> Self {
        Self {
            name,
            start: Performance::new(),
        }
    }
}

impl<'a> Drop for PerformanceGuard<'a> {
    fn drop(&mut self) {
        self.start.log(self.name);
    }
}

#[allow(dead_code)]
pub fn mesure<T>(name: &str, f: impl FnOnce() -> T) -> T {
    let start = Instant::now();
    let t = f();
    let end = Instant::now();
    info!(target: "performance",
        "{}: {}ms",
        name,
        end.duration_since(start).as_millis()
    );
    t
}

pub static STARTUP_TIME: Lazy<Performance> = Lazy::new(Performance::new);


================================================
FILE: crates/ytermusic/src/structures/sound_action.rs
================================================
use common_structs::MusicDownloadStatus;
use download_manager::{DownloadManagerMessage, MessageHandler};
use flume::Sender;
use log::{error, trace};
use std::{fs, sync::Arc, time::Duration};
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::CACHE_DIR,
    errors::handle_error_option,
    systems::{player::PlayerState, DOWNLOAD_MANAGER},
    ShutdownSignal, DATABASE,
};

/// Actions that can be sent to the player from other services
#[derive(Debug, Clone)]
pub enum SoundAction {
    /// Set the volume of the player to the given value
    SetVolume(f32),
    Cleanup,
    PlayPause,
    RestartPlayer,
    Plus,
    Minus,
    /// Seek to a specific time in the current song in seconds
    SeekTo(Duration),
    Previous(usize),
    Forward,
    Backward,
    Next(usize),
    AddVideosToQueue(Vec<YoutubeMusicVideoRef>),
    AddVideoUnary(YoutubeMusicVideoRef),
    DeleteVideoUnary,
    ReplaceQueue(Vec<YoutubeMusicVideoRef>),
    VideoStatusUpdate(String, MusicDownloadStatus),
}

impl SoundAction {
    fn insert(player: &mut PlayerState, video: String, status: MusicDownloadStatus) {
        if matches!(
            player.music_status.get(&video),
            Some(&MusicDownloadStatus::DownloadFailed)
        ) {
            DOWNLOAD_MANAGER.remove_from_in_downloads(&video);
        }
        if matches!(
            player.music_status.get(&video),
            Some(&MusicDownloadStatus::Downloading(_) | &MusicDownloadStatus::Downloaded)
        ) && status == MusicDownloadStatus::NotDownloaded
        {
            return;
        }
        player.music_status.insert(video, status);
    }

    pub fn apply_sound_action(self, player: &mut PlayerState) {
        match self {
            Self::SetVolume(volume) => player.sink.set_volume((volume * 100.) as i32),
            Self::SeekTo(time) => player.sink.seek_to(time),
            Self::Backward => player.sink.seek_bw(),
            Self::Forward => player.sink.seek_fw(),
            Self::PlayPause => player.sink.toggle_playback(),
            Self::Cleanup => {
                player.list.clear();
                player.current = 0;
                player.music_status.clear();
                player.sink.stop();
            }
            Self::Plus => player.sink.volume_up(),
            Self::Minus => player.sink.volume_down(),
            Self::Next(a) => {
                player.sink.stop();

                player.set_relative_current(a as _);
            }
            Self::VideoStatusUpdate(video, status) => {
                player.music_status.insert(video, status);
            }
            Self::AddVideosToQueue(video) => {
                let db = DATABASE.read().unwrap();
                for v in video {
                    Self::insert(
                        player,
                        v.video_id.clone(),
                        if db.iter().any(|e| e.video_id == v.video_id) {
                            MusicDownloadStatus::Downloaded
                        } else {
                            MusicDownloadStatus::NotDownloaded
                        },
                    );
                    player.list.push(v)
                }
            }
            Self::Previous(a) => {
                player.set_relative_current(-(a as isize));
                player.sink.stop();
            }
            Self::RestartPlayer => {
                player.sink =
                    handle_error_option(&player.updater, "update player", player.sink.update())
                        .unwrap();
                if let Some(e) = player.current().cloned() {
                    Self::AddVideoUnary(e).apply_sound_action(player);
                }
            }
            Self::AddVideoUnary(video) => {
                Self::insert(
                    player,
                    video.video_id.clone(),
                    if DATABASE
                        .read()
                        .unwrap()
                        .iter()
                        .any(|e| e.video_id == video.video_id)
                    {
                        MusicDownloadStatus::Downloaded
                    } else {
                        MusicDownloadStatus::NotDownloaded
                    },
                );
                if player.list.is_empty() {
                    player.list.push(video);
                } else {
                    player.list.insert(player.current + 1, video);
                }
            }
            Self::DeleteVideoUnary => {
                let index_list = player.list_selector.get_relative_position();
                let video = player.relative_current(index_list).cloned().unwrap();
                if matches!(
                    player.music_status.get(&video.video_id), // not sure abt conditions, needs testing
                    Some(
                        &MusicDownloadStatus::DownloadFailed
                            | &MusicDownloadStatus::Downloading(_)
                            | &MusicDownloadStatus::NotDownloaded
                    )
                ) {
                    DOWNLOAD_MANAGER.remove_from_in_downloads(&video.video_id);
                }
                player.music_status.remove(&video.video_id); // maybe not necessary to do it

                //manage deleting in the list
                player.list.retain(|vid| *vid != video);
                player.list_selector.list_size -= 1;
                if index_list < 0 {
                    player.set_relative_current(-1);
                }
                if index_list == 0 {
                    Self::Next(0).apply_sound_action(player);
                }

                // manage deleting physically
                DATABASE.remove_video(&video);

                let cache_folder = CACHE_DIR.join("downloads");
                let json_path = cache_folder.join(format!("{}.json", &video.video_id));
                match fs::remove_file(json_path) {
                    Ok(_) => trace!("Deleted JSON file"),
                    Err(e) => error!("Error deleting JSON video file: {}", e),
                }

                let mp4_path = cache_folder.join(format!("{}.mp4", &video.video_id));
                match fs::remove_file(mp4_path) {
                    Ok(_) => trace!("Deleted MP4 file"),
                    Err(e) => error!("Error deleting MP4 video file: {}", e),
                }
            }
            Self::ReplaceQueue(videos) => {
                player.list.truncate(player.current + 1);
                DOWNLOAD_MANAGER.clean(
                    ShutdownSignal,
                    download_manager_handler(player.soundaction_sender.clone()),
                );
                Self::AddVideosToQueue(videos).apply_sound_action(player);
                Self::Next(1).apply_sound_action(player);
            }
        }
    }
}

pub fn download_manager_handler(sender: Sender<SoundAction>) -> MessageHandler {
    Arc::new(move |message| match message {
        DownloadManagerMessage::VideoStatusUpdate(video, status) => {
            sender
                .send(SoundAction::VideoStatusUpdate(video, status))
                .unwrap();
        }
    })
}


================================================
FILE: crates/ytermusic/src/systems/logger.rs
================================================
use std::{io::Write, path::PathBuf};

use flume::Sender;
use once_cell::sync::Lazy;

static LOG: Lazy<Sender<String>> = Lazy::new(|| {
    let (tx, rx) = flume::unbounded::<String>();
    std::thread::spawn(move || {
        let mut buffer = String::new();
        let filepath = get_log_file_path();
        let mut file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(filepath)
            .unwrap();
        while let Ok(e) = rx.recv() {
            buffer.clear();
            buffer.push_str(&(e + "\n"));
            while let Ok(e) = rx.try_recv() {
                buffer.push_str(&(e + "\n"));
            }
            file.write_all(buffer.as_bytes()).unwrap();
        }
    });
    tx
});

pub fn get_log_file_path() -> PathBuf {
    if let Some(val) = get_project_dirs() {
        if let Err(e) = std::fs::create_dir_all(val.cache_dir()) {
            panic!("Failed to create cache dir: {}", e);
        }
        val.cache_dir().join("log.txt")
    } else {
        PathBuf::from("log.txt")
    }
}

static LOGGER: SimpleLogger = SimpleLogger;
static LEVEL: Lazy<(LevelFilter, Level)> = Lazy::new(|| {
    let logger_env = std::env::var("YTERMUSIC_LOG");
    if let Ok(logger_env) = logger_env {
        if logger_env == "true" {
            (LevelFilter::Trace, Level::Trace)
        } else {
            (LevelFilter::Info, Level::Info)
        }
    } else {
        (LevelFilter::Info, Level::Info)
    }
});

pub fn init() -> Result<(), SetLoggerError> {
    log::set_logger(&LOGGER).map(|()| log::set_max_level(LEVEL.0))?;
    info!("Logger mode {}", LEVEL.1);
    Ok(())
}

use log::{info, Level, LevelFilter, Metadata, Record, SetLoggerError};

use crate::utils::get_project_dirs;

static FILTER: &[&str] = &["rustls", "tokio-util", "want-", "mio-"];

struct SimpleLogger;

impl log::Log for SimpleLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        metadata.level() <= LEVEL.1
    }

    fn log(&self, record: &Record) {
        if self.enabled(record.metadata()) {
            if FILTER.iter().any(|x| record.file().unwrap().contains(x)) {
                return;
            }
            LOG.send(format!(
                "{} - {} [{}]",
                record.level(),
                record.args(),
                record.file().unwrap_or_default()
            ))
            .unwrap();
        }
    }

    fn flush(&self) {}
}


================================================
FILE: crates/ytermusic/src/systems/mod.rs
================================================
use download_manager::DownloadManager;
use once_cell::sync::Lazy;

use crate::{
    consts::{CACHE_DIR, CONFIG},
    DATABASE,
};

pub mod logger;
pub mod player;

pub static DOWNLOAD_MANAGER: Lazy<DownloadManager> = Lazy::new(|| {
    DownloadManager::new(
        CACHE_DIR.to_path_buf(),
        &DATABASE,
        CONFIG.global.parallel_downloads,
    )
});


================================================
FILE: crates/ytermusic/src/systems/player.rs
================================================
use std::{
    collections::{HashMap, VecDeque},
    sync::atomic::Ordering,
};

use common_structs::MusicDownloadStatus;
use flume::{unbounded, Receiver, Sender};
use log::error;
use player::{PlayError, Player, PlayerOptions};

use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::{CACHE_DIR, CONFIG},
    errors::{handle_error, handle_error_option},
    structures::{media::Media, sound_action::SoundAction},
    systems::DOWNLOAD_MANAGER,
    term::{list_selector::ListSelector, playlist::PLAYER_RUNNING, ManagerMessage, Screens},
    DATABASE,
};

pub struct PlayerState {
    pub goto: Screens,
    pub list: Vec<YoutubeMusicVideoRef>,
    pub current: usize,
    pub rtcurrent: Option<YoutubeMusicVideoRef>,
    pub music_status: HashMap<String, MusicDownloadStatus>,
    pub list_selector: ListSelector,
    pub controls: Media,
    pub sink: Player,
    pub updater: Sender<ManagerMessage>,
    pub soundaction_sender: Sender<SoundAction>,
    pub soundaction_receiver: Receiver<SoundAction>,
    pub stream_error_receiver: Receiver<PlayError>,
}

impl PlayerState {
    fn new(
        soundaction_sender: Sender<SoundAction>,
        soundaction_receiver: Receiver<SoundAction>,
        updater: Sender<ManagerMessage>,
    ) -> Self {
        let (stream_error_sender, stream_error_receiver) = unbounded::<PlayError>();
        let sink = handle_error_option(
            &updater,
            "player creation error",
            Player::new(
                stream_error_sender,
                PlayerOptions::new(CONFIG.player.initial_volume),
            ),
        )
        .unwrap();
        Self {
            controls: Media::new(updater.clone(), soundaction_sender.clone()),
            soundaction_receiver,
            list_selector: ListSelector::default(),
            music_status: HashMap::new(),
            updater,
            stream_error_receiver,
            soundaction_sender,
            sink,
            goto: Screens::Playlist,
            list: Vec::new(),
            current: 0,
            rtcurrent: None,
        }
    }

    pub fn current(&self) -> Option<&YoutubeMusicVideoRef> {
        self.relative_current(0)
    }

    pub fn relative_current(&self, n: isize) -> Option<&YoutubeMusicVideoRef> {
        self.list.get(self.current.saturating_add_signed(n))
    }

    pub fn set_relative_current(&mut self, n: isize) {
        self.current = self.current.saturating_add_signed(n);
    }

    pub fn is_current_download_failed(&self) -> bool {
        self.current()
            .as_ref()
            .map(|x| {
                self.music_status.get(&x.video_id) == Some(&MusicDownloadStatus::DownloadFailed)
            })
            .unwrap_or(false)
    }

    pub fn is_current_downloaded(&self) -> bool {
        self.current()
            .as_ref()
            .map(|x| self.music_status.get(&x.video_id) == Some(&MusicDownloadStatus::Downloaded))
            .unwrap_or(false)
    }

    pub fn update(&mut self) {
        PLAYER_RUNNING.store(self.current().is_some(), Ordering::SeqCst);
        self.update_controls();
        self.handle_stream_errors();
        if self.current > self.list.len() {
            self.current = self.list.len();
        }
        while let Ok(e) = self.soundaction_receiver.try_recv() {
            e.apply_sound_action(self);
        }
        if self.is_current_download_failed() {
            SoundAction::Next(1).apply_sound_action(self);
        }
        if self.sink.is_finished() {
            if self.is_current_downloaded() && self.rtcurrent.as_ref() == self.current() {
                self.set_relative_current(1);
            }
            self.handle_stream_errors();
            self.update_controls();
            // If the current song is finished, we play the next one but if the next one has failed to download, we skip it
            // TODO(optimize this)
            while self
                .current()
                .map(|x| {
                    self.music_status.get(&x.video_id) == Some(&MusicDownloadStatus::DownloadFailed)
                })
                .unwrap_or(false)
            {
                self.set_relative_current(1);
            }

            if self.is_current_downloaded() {
                if let Some(video) = self.current().cloned() {
                    let k = CACHE_DIR.join(format!("downloads/{}.mp4", &video.video_id));
                    if let Err(e) = self.sink.play(k.as_path()) {
                        if matches!(e, PlayError::DecoderError(_)) {
                            // Cleaning the file

                            DATABASE.remove_video(&video);
                            handle_error(
                                &self.updater,
                                "invalid cleaning MP4",
                                std::fs::remove_file(k),
                            );
                            handle_error(
                                &self.updater,
                                "invalid cleaning JSON",
                                std::fs::remove_file(
                                    CACHE_DIR.join(format!("downloads/{}.json", &video.video_id)),
                                ),
                            );
                            self.current = 0;
                            DATABASE.write();
                        } else {
                            self.updater
                                .send(ManagerMessage::PassTo(
                                    Screens::DeviceLost,
                                    Box::new(ManagerMessage::Error(
                                        format!("{e:?}"),
                                        Box::new(None),
                                    )),
                                ))
                                .unwrap();
                        }
                    }
                }
            }
        } else {
            self.rtcurrent = self.current().cloned();
        }
        let to_download = self
            .list
            .iter()
            .skip(self.current)
            .chain(self.list.iter().take(self.current).rev())
            .filter(|x| {
                self.music_status.get(&x.video_id) == Some(&MusicDownloadStatus::NotDownloaded)
            })
            .take(12)
            .cloned()
            .collect::<VecDeque<_>>();
        DOWNLOAD_MANAGER.set_download_list(to_download);
    }

    fn handle_stream_errors(&self) {
        while let Ok(e) = self.stream_error_receiver.try_recv() {
            error!("Stream error: {:?}", e);
            handle_error(&self.updater, "audio device stream error", Err(e));
        }
    }
    fn update_controls(&mut self) {
        let current = self.current().cloned();
        let result = self
            .controls
            .update(current, &self.sink)
            .map_err(|x| format!("{x:?}"));
        handle_error::<String>(&self.updater, "Can't update finished media control", result);
    }
}

pub fn player_system(updater: Sender<ManagerMessage>) -> (Sender<SoundAction>, PlayerState) {
    let (tx, rx) = flume::unbounded::<SoundAction>();
    (tx.clone(), PlayerState::new(tx, rx, updater))
}


================================================
FILE: crates/ytermusic/src/tasks/api.rs
================================================
use std::sync::{Arc, Mutex};

use flume::Sender;
use log::{error, info};
use once_cell::sync::Lazy;
use tokio::task::JoinSet;
use ytpapi2::{Endpoint, YoutubeMusicInstance, YoutubeMusicPlaylistRef};

use crate::{
    consts::CONFIG,
    get_header_file, run_service,
    structures::performance,
    term::{ManagerMessage, Screens},
};

pub fn get_text_cookies_expired_or_invalid() -> String {
    let (Ok((_, path)) | Err((_, path))) = get_header_file();
    format!(
        "The `{}` file is not configured correctly. \nThe cookies are expired or invalid.",
        path.display()
    )
}

pub fn spawn_api_task(updater_s: Sender<ManagerMessage>) {
    run_service(async move {
        info!("API task on");
        let guard = performance::guard("API task");
        let client =
            YoutubeMusicInstance::from_header_file(get_header_file().unwrap().1.as_path()).await;
        match client {
            Ok(api) => {
                let api = Arc::new(api);
                let mut set = JoinSet::new();
                let api_ = api.clone();
                let updater_s_ = updater_s.clone();
                set.spawn(async move {
                    let search_results = api_.get_home(2).await;
                    match search_results {
                        Ok(e) => {
                            for playlist in e.playlists {
                                spawn_browse_playlist_task(
                                    playlist.clone(),
                                    api_.clone(),
                                    updater_s_.clone(),
                                )
                            }
                        }
                        Err(e) => {
                            error!("get_home {e:?}")
                        }
                    }
                });
                let api_ = api.clone();
                let updater_s_ = updater_s.clone();
                set.spawn(async move {
                    let search_results = api_.get_library(&Endpoint::MusicLikedPlaylists, 2).await;
                    match search_results {
                        Ok(e) => {
                            for playlist in e {
                                spawn_browse_playlist_task(
                                    playlist.clone(),
                                    api_.clone(),
                                    updater_s_.clone(),
                                )
                            }
                        }
                        Err(e) => {
                            error!("MusicLikedPlaylists -> {e:?}");
                        }
                    }
                });
                let api_ = api.clone();
                let updater_s_ = updater_s.clone();
                set.spawn(async move {
                    let search_results = api_.get_library(&Endpoint::MusicLibraryLanding, 2).await;
                    match search_results {
                        Ok(e) => {
                            for playlist in e {
                                spawn_browse_playlist_task(
                                    playlist.clone(),
                                    api_.clone(),
                                    updater_s_.clone(),
                                )
                            }
                        }
                        Err(e) => {
                            error!("MusicLibraryLanding -> {e:?}");
                        }
                    }
                });
                while let Some(e) = set.join_next().await {
                    e.unwrap();
                }
            }
            Err(e) => match &e {
                ytpapi2::YoutubeMusicError::NoCookieAttribute
                | ytpapi2::YoutubeMusicError::NoSapsidInCookie
                | ytpapi2::YoutubeMusicError::InvalidCookie(_)
                | ytpapi2::YoutubeMusicError::NeedToLogin
                | ytpapi2::YoutubeMusicError::CantFindInnerTubeApiKey(_)
                | ytpapi2::YoutubeMusicError::CantFindInnerTubeClientVersion(_)
                | ytpapi2::YoutubeMusicError::CantFindVisitorData(_)
                | ytpapi2::YoutubeMusicError::IoError(_) => {
                    error!("{}", get_text_cookies_expired_or_invalid());
                    error!("{e:?}");
                    updater_s
                        .send(
                            ManagerMessage::Error(
                                get_text_cookies_expired_or_invalid(),
                                Box::new(Some(ManagerMessage::Quit)),
                            )
                            .pass_to(Screens::DeviceLost),
                        )
                        .unwrap();
                }
                e => {
                    error!("{e:?}");
                }
            },
        }
        drop(guard);
    });
}

static BROWSED_PLAYLISTS: Lazy<Mutex<Vec<(String, String)>>> = Lazy::new(|| Mutex::new(vec![]));

fn spawn_browse_playlist_task(
    playlist: YoutubeMusicPlaylistRef,
    api: Arc<YoutubeMusicInstance>,
    updater_s: Sender<ManagerMessage>,
) {
    if playlist.browse_id.starts_with("UC") && CONFIG.player.hide_channels_on_homepage {
        log::info!(
            "Skipping channel (CONFIG) {} {}",
            playlist.name,
            playlist.browse_id
        );
        return;
    }
    if playlist.browse_id.starts_with("MPREb_") && CONFIG.player.hide_albums_on_homepage {
        log::info!(
            "Skipping album (CONFIG) {} {}",
            playlist.name,
            playlist.browse_id
        );
        return;
    }
    {
        let mut k = BROWSED_PLAYLISTS.lock().unwrap();
        if k.iter()
            .any(|(name, id)| name == &playlist.name && id == &playlist.browse_id)
        {
            return;
        }
        k.push((playlist.name.clone(), playlist.browse_id.clone()));
    }

    run_service(async move {
        let guard = format!("Browse playlist {} {}", playlist.name, playlist.browse_id);
        let guard = performance::guard(&guard);
        match api.get_playlist(&playlist, 5).await {
            Ok(videos) => {
                if videos.len() < 2 {
                    info!("Playlist {} is too small so skipped", playlist.name);
                    return;
                }
                let _ = updater_s.send(
                    ManagerMessage::AddElementToChooser((
                        format!("{} ({})", playlist.name, playlist.subtitle),
                        videos,
                    ))
                    .pass_to(Screens::Playlist),
                );
            }
            Err(e) => {
                error!("{e:?}");
            }
        }

        drop(guard);
    });
}


================================================
FILE: crates/ytermusic/src/tasks/clean.rs
================================================
use crate::{consts::CACHE_DIR, run_service, structures::performance};

/// This function is called on start to clean the database and the files
/// that are incompletely downloaded due to a crash.
pub fn spawn_clean_task() {
    run_service(async move {
        let guard = performance::guard("Clean task");
        for i in std::fs::read_dir(CACHE_DIR.join("downloads")).unwrap() {
            let path = i.unwrap().path();
            if path.extension().unwrap_or_default() == "mp4" {
                let mut path1 = path.clone();
                path1.set_extension("json");
                if !path1.exists() {
                    std::fs::remove_file(&path).unwrap();
                }
            }
        }
        drop(guard);
    });
}


================================================
FILE: crates/ytermusic/src/tasks/last_playlist.rs
================================================
use flume::Sender;
use log::info;
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::CACHE_DIR,
    run_service,
    structures::performance,
    term::{ManagerMessage, Screens},
};

pub fn spawn_last_playlist_task(updater_s: Sender<ManagerMessage>) {
    run_service(async move {
        let guard = performance::guard("Last playlist");
        info!("Last playlist task on");
        let playlist = std::fs::read_to_string(CACHE_DIR.join("last-playlist.json")).ok()?;
        let mut playlist: (String, Vec<YoutubeMusicVideoRef>) =
            serde_json::from_str(&playlist).ok()?;
        if !playlist.0.starts_with("Last playlist: ") {
            playlist.0 = format!("Last playlist: {}", playlist.0);
        }
        updater_s
            .send(ManagerMessage::AddElementToChooser(playlist).pass_to(Screens::Playlist))
            .unwrap();
        drop(guard);
        Some(())
    });
}


================================================
FILE: crates/ytermusic/src/tasks/local_musics.rs
================================================
use flume::Sender;
use log::info;
use rand::seq::SliceRandom;
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::{CACHE_DIR, CONFIG},
    run_service,
    structures::performance,
    term::{ManagerMessage, Screens},
    DATABASE,
};

pub fn spawn_local_musics_task(updater_s: Sender<ManagerMessage>) {
    run_service(async move {
        info!("Database getter task on");
        let guard = performance::guard("Local musics");
        if let Some(videos) = DATABASE.read() {
            shuffle_and_send(videos, &updater_s);
        } else {
            let mut videos = Vec::new();
            for files in std::fs::read_dir(CACHE_DIR.join("downloads")).unwrap() {
                let path = files.unwrap().path();
                if path.as_os_str().to_string_lossy().ends_with(".json") {
                    let video =
                        serde_json::from_str(std::fs::read_to_string(path).unwrap().as_str())
                            .unwrap();
                    videos.push(video);
                }
            }
            shuffle_and_send(videos, &updater_s);

            DATABASE.write();
        }
        drop(guard);
    });
}

fn shuffle_and_send(mut videos: Vec<YoutubeMusicVideoRef>, updater_s: &Sender<ManagerMessage>) {
    DATABASE.clone_from(&videos);

    if CONFIG.player.shuffle {
        videos.shuffle(&mut rand::thread_rng());
    }

    updater_s
        .send(
            ManagerMessage::AddElementToChooser(("Local musics".to_owned(), videos))
                .pass_to(Screens::Playlist),
        )
        .unwrap();
}


================================================
FILE: crates/ytermusic/src/tasks/mod.rs
================================================
pub mod api;
pub mod clean;
pub mod last_playlist;
pub mod local_musics;


================================================
FILE: crates/ytermusic/src/term/device_lost.rs
================================================
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::{
    layout::{Alignment, Rect},
    widgets::{Block, BorderType, Borders, Paragraph},
    Frame,
};

use crate::consts::CONFIG;

use super::{EventResponse, ManagerMessage, Screen, Screens};

// Audio device not connected!
pub struct DeviceLost(pub Vec<String>, pub Option<ManagerMessage>);

impl Screen for DeviceLost {
    fn on_mouse_press(&mut self, _: crossterm::event::MouseEvent, _: &Rect) -> EventResponse {
        EventResponse::None
    }

    fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
        match key.code {
            KeyCode::Enter | KeyCode::Char(' ') => {
                if let Some(m) = self.1.take() {
                    EventResponse::Message(vec![m])
                } else {
                    ManagerMessage::RestartPlayer
                        .pass_to(Screens::MusicPlayer)
                        .event()
                }
            }
            KeyCode::Esc => ManagerMessage::Quit.event(),
            _ => EventResponse::None,
        }
    }

    fn render(&mut self, frame: &mut Frame) {
        frame.render_widget(
            Paragraph::new(format!(
                "{}\nPress [Enter] or [Space] to retry.\nOr [Esc] to exit",
                self.0.join("\n")
            ))
            .style(CONFIG.player.text_error_style)
            .alignment(Alignment::Center)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .style(CONFIG.player.text_next_style)
                    .title(" Error ")
                    .border_type(BorderType::Plain),
            ),
            frame.size(),
        );
    }

    fn handle_global_message(&mut self, m: ManagerMessage) -> EventResponse {
        match m {
            ManagerMessage::Error(a, m) => {
                self.1 = *m;
                self.0.push(a);
                EventResponse::Message(vec![ManagerMessage::ChangeState(Screens::DeviceLost)])
            }
            _ => EventResponse::None,
        }
    }

    fn close(&mut self, _: Screens) -> EventResponse {
        self.0.clear();
        EventResponse::None
    }

    fn open(&mut self) -> EventResponse {
        EventResponse::None
    }
}


================================================
FILE: crates/ytermusic/src/term/item_list.rs
================================================
use crossterm::event::{KeyCode, KeyEvent, MouseEventKind};
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::Style,
    text::Text,
    widgets::{Block, Borders, List, ListState, StatefulWidget, Widget},
};

use super::{rect_contains, relative_pos};

pub trait ListItemAction {
    fn render_style(&self, string: &str, selected: bool) -> Style;
}

pub struct ListItem<Action> {
    list: Vec<(String, Action)>,
    current_position: usize,
    title: String,
}

impl<Action> Default for ListItem<Action> {
    fn default() -> Self {
        Self {
            list: Default::default(),
            current_position: Default::default(),
            title: Default::default(),
        }
    }
}

impl<Action: Clone> ListItem<Action> {
    pub fn new(title: String) -> Self {
        Self {
            list: Default::default(),
            current_position: Default::default(),
            title,
        }
    }

    pub fn on_mouse_press(
        &mut self,
        mouse_event: crossterm::event::MouseEvent,
        frame_data: &Rect,
    ) -> Option<Action> {
        if let MouseEventKind::Down(_) = mouse_event.kind {
            let x = mouse_event.column;
            let y = mouse_event.row;
            if rect_contains(frame_data, x, y, 1) {
                let (_, y) = relative_pos(frame_data, x, y, 1);
                if let Some((i, b)) = self
                    .get_item_frame(frame_data.height as usize)
                    .get(y as usize)
                    .map(|(a, (_, c))| (*a, c.clone()))
                {
                    self.current_position = i;
                    return Some(b);
                }
            }
        } else if let MouseEventKind::ScrollDown = &mouse_event.kind {
            self.select_down();
        } else if let MouseEventKind::ScrollUp = &mouse_event.kind {
            self.select_up();
        }
        None
    }

    pub fn on_key_press(&mut self, key: KeyEvent) -> Option<&Action> {
        match key.code {
            KeyCode::Enter => {
                if let Some(a) = self.select() {
                    return Some(a);
                }
            }
            KeyCode::Char('+') | KeyCode::Up | KeyCode::Char('k') => self.select_up(),
            KeyCode::Char('-') | KeyCode::Down | KeyCode::Char('j') => self.select_down(),
            _ => {}
        }
        None
    }

    pub fn get_item_frame(&self, height: usize) -> Vec<(usize, &(String, Action))> {
        let height = height.saturating_sub(2); // Remove the borders
                                               // Add a little offset when the list is full
        let start = self.current_position.saturating_sub(3);
        let length = self.list.len();
        let length_after_start = length.saturating_sub(start);
        // Tries to take all the space left if length_after_start is smaller than height
        let start = start.saturating_sub(height.saturating_sub(length_after_start));
        self.list
            .iter()
            .enumerate()
            .skip(start)
            .take(height)
            .collect::<Vec<_>>()
    }

    pub fn click_on(&mut self, y_position: usize, height: usize) -> Option<(usize, &Action)> {
        self.get_item_frame(height)
            .iter()
            .enumerate()
            .find(|(i, _)| *i == y_position)
            .map(|(_, w)| (w.0, &w.1 .1))
    }

    pub fn select(&self) -> Option<&Action> {
        self.list
            .get(self.current_position)
            .map(|(_, action)| action)
    }

    pub fn select_down(&mut self) {
        if self.current_position == self.list.len() - 1 {
            self.select_to(0);
        } else {
            self.select_to(self.current_position.saturating_add(1));
        }
    }

    pub fn select_up(&mut self) {
        if self.current_position == 0 {
            self.select_to(self.list.len() - 1);
        } else {
            self.select_to(self.current_position.saturating_sub(1));
        }
    }

    pub fn select_to(&mut self, position: usize) {
        self.current_position = position.min(self.list.len().saturating_sub(1));
    }

    pub fn update(&mut self, list: Vec<(String, Action)>, current: usize) {
        self.list = list;
        self.current_position = current.min(self.list.len().saturating_sub(1));
    }

    pub fn update_contents(&mut self, list: Vec<(String, Action)>) {
        self.list = list;
        self.current_position = self.current_position.min(self.list.len().saturating_sub(1));
    }
    pub fn clear(&mut self) {
        self.list.clear();
        self.current_position = 0;
    }

    pub fn add_element(&mut self, element: (String, Action)) {
        self.list.push(element);
    }

    pub fn set_title(&mut self, a: String) {
        self.title = a;
    }

    pub fn current_position(&self) -> usize {
        self.current_position
    }
}

impl<Action: ListItemAction + Clone> Widget for &ListItem<Action> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        StatefulWidget::render(
            List::new(
                self.get_item_frame(area.height as usize)
                    .iter()
                    .map(|(i, (string, action))| {
                        let style = action.render_style(string, self.current_position == *i);
                        ratatui::widgets::ListItem::new(Text::from(string.as_str())).style(style)
                    })
                    .collect::<Vec<_>>(),
            )
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .title(self.title.as_str()),
            ),
            area,
            buf,
            &mut ListState::default(),
        );
    }
}


================================================
FILE: crates/ytermusic/src/term/list_selector.rs
================================================
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::Style,
    text::Text,
    widgets::{Block, Borders, List, ListItem, ListState, StatefulWidget},
};

#[derive(Default)]
pub struct ListSelector {
    pub list_size: usize,
    current_position: usize,
    scroll_position: usize,
}

impl ListSelector {
    pub fn get_item_frame(&self, height: usize) -> (usize, usize) {
        let height = height.saturating_sub(2); // Remove the borders
                                               // Add a little offset when the list is full
        let start = self.scroll_position.saturating_sub(3);
        let length = self.list_size;
        let length_after_start = length.saturating_sub(start);
        // Tries to take all the space left if length_after_start is smaller than height
        let start = start.saturating_sub(height.saturating_sub(length_after_start));
        (
            start.min(self.list_size),
            (start + height).min(self.list_size),
        )
    }

    pub fn get_relative_position(&self) -> isize {
        //Supposing you don't have more than 2^32-1 songs on a 64bit computer. Even if so, panics after.
        match self.scroll_position.cmp(&self.current_position) {
            std::cmp::Ordering::Less => {
                let pos = (self.current_position - self.scroll_position) as isize;
                -pos
            }
            std::cmp::Ordering::Equal => 0,
            std::cmp::Ordering::Greater => (self.scroll_position - self.current_position) as isize,
        }
    }

    pub fn is_scrolling(&self) -> bool {
        self.scroll_position != self.current_position
    }

    pub fn click_on(&mut self, y_position: usize, height: usize) -> Option<usize> {
        let (a, b) = self.get_item_frame(height);
        (a..b)
            .enumerate()
            .find(|(i, _)| *i == y_position)
            .map(|(_, w)| w)
    }

    pub fn play(&mut self) -> Option<usize> {
        self.current_position = self.scroll_position;
        self.select()
    }

    pub fn scroll_down(&mut self) {
        self.scroll_to(self.scroll_position.saturating_add(1));
    }

    pub fn scroll_up(&mut self) {
        self.scroll_to(self.scroll_position.saturating_sub(1));
    }

    pub fn scroll_to(&mut self, position: usize) {
        self.scroll_position = position.min(self.list_size.saturating_sub(1));
    }

    pub fn select(&self) -> Option<usize> {
        if self.current_position < self.list_size {
            Some(self.current_position)
        } else {
            None
        }
    }

    pub fn update(&mut self, list_size: usize, current: usize) {
        if !self.is_scrolling() {
            self.scroll_position = current;
        }
        self.current_position = current;
        self.list_size = list_size;
        self.current_position = self.current_position.min(self.list_size.saturating_sub(1));
        self.scroll_position = self.scroll_position.min(self.list_size.saturating_sub(1));
    }

    pub fn render(
        &self,
        area: Rect,
        buf: &mut Buffer,
        style_fn: impl Fn(usize, bool, bool) -> (Style, String),
        render_title: &str,
    ) {
        let (a, b) = self.get_item_frame(area.height as usize);
        StatefulWidget::render(
            List::new(
                (a..b)
                    .map(|i| {
                        let (style, text) =
                            style_fn(i, self.current_position == i, self.scroll_position == i);
                        ListItem::new(Text::from(text)).style(style)
                    })
                    .collect::<Vec<_>>(),
            )
            .block(Block::default().borders(Borders::ALL).title(render_title)),
            area,
            buf,
            &mut ListState::default(),
        );
    }
}


================================================
FILE: crates/ytermusic/src/term/mod.rs
================================================
pub mod device_lost;
pub mod item_list;
pub mod list_selector;
pub mod music_player;
pub mod playlist;
pub mod playlist_view;
pub mod search;
pub mod vertical_gauge;

use std::{
    io::{self},
    time::{Duration, Instant},
};

use crossterm::{
    event::{
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyEvent, KeyEventKind, KeyModifiers,
        MouseEvent,
    },
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use flume::{Receiver, Sender};
use ratatui::{backend::CrosstermBackend, layout::Rect, Frame, Terminal};
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    is_shutdown_sent, shutdown, structures::sound_action::SoundAction, systems::player::PlayerState,
};

use self::{device_lost::DeviceLost, item_list::ListItem, playlist::Chooser, search::Search};

use crate::term::playlist_view::PlaylistView;

// A trait to handle the different screens
pub trait Screen {
    fn on_mouse_press(&mut self, mouse_event: MouseEvent, frame_data: &Rect) -> EventResponse;
    fn on_key_press(&mut self, mouse_event: KeyEvent, frame_data: &Rect) -> EventResponse;
    fn render(&mut self, frame: &mut Frame);
    fn handle_global_message(&mut self, message: ManagerMessage) -> EventResponse;
    fn close(&mut self, new_screen: Screens) -> EventResponse;
    fn open(&mut self) -> EventResponse;
}

#[derive(Debug, Clone)]
pub enum EventResponse {
    Message(Vec<ManagerMessage>),
    None,
}

// A message that can be sent to the manager
#[derive(Debug, Clone)]
pub enum ManagerMessage {
    Error(String, Box<Option<ManagerMessage>>),
    PassTo(Screens, Box<ManagerMessage>),
    Inspect(String, Screens, Vec<YoutubeMusicVideoRef>),
    ChangeState(Screens),
    SearchFrom(Screens),
    PlayerFrom(Screens),
    #[allow(dead_code)]
    PlaylistFrom(Screens),
    RestartPlayer,
    Quit,
    AddElementToChooser((String, Vec<YoutubeMusicVideoRef>)),
}

impl ManagerMessage {
    pub fn pass_to(self, screen: Screens) -> Self {
        Self::PassTo(screen, Box::new(self))
    }
    pub fn event(self) -> EventResponse {
        EventResponse::Message(vec![self])
    }
}

// The different screens
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Screens {
    MusicPlayer = 0x0,
    Playlist = 0x1,
    Search = 0x2,
    DeviceLost = 0x3,
    PlaylistViewer = 0x4,
}

// The screen manager that handles the different screens
pub struct Manager {
    music_player: PlayerState,
    chooser: Chooser,
    search: Search,
    device_lost: DeviceLost,
    current_screen: Screens,
    playlist_viewer: PlaylistView,
}

impl Manager {
    pub async fn new(action_sender: Sender<SoundAction>, music_player: PlayerState) -> Self {
        Self {
            music_player,
            chooser: Chooser {
                action_sender: action_sender.clone(),
                goto: Screens::MusicPlayer,
                item_list: ListItem::new(" Choose a playlist ".to_owned()),
            },
            playlist_viewer: PlaylistView {
                sender: action_sender.clone(),
                items: ListItem::new(" Playlist ".to_owned()),
                goto: Screens::Playlist,
                videos: Vec::new(),
            },
            search: Search::new(action_sender).await,
            current_screen: Screens::Playlist,
            device_lost: DeviceLost(Vec::new(), None),
        }
    }
    pub fn current_screen(&mut self) -> &mut dyn Screen {
        self.get_screen(self.current_screen)
    }
    pub fn get_screen(&mut self, screen: Screens) -> &mut dyn Screen {
        match screen {
            Screens::MusicPlayer => &mut self.music_player,
            Screens::Playlist => &mut self.chooser,
            Screens::Search => &mut self.search,
            Screens::DeviceLost => &mut self.device_lost,
            Screens::PlaylistViewer => &mut self.playlist_viewer,
        }
    }
    pub fn set_current_screen(&mut self, screen: Screens) {
        self.current_screen = screen;
        let k = self.current_screen().open();
        self.handle_event(k);
    }
    pub fn handle_event(&mut self, event: EventResponse) -> bool {
        match event {
            EventResponse::Message(messages) => {
                for message in messages {
                    if self.handle_manager_message(message) {
                        return true;
                    }
                }
            }
            EventResponse::None => {}
        }
        false
    }
    pub fn handle_manager_message(&mut self, e: ManagerMessage) -> bool {
        match e {
            ManagerMessage::PassTo(e, a) => {
                let rs = self.get_screen(e).handle_global_message(*a);
                self.handle_event(rs);
            }
            ManagerMessage::Quit => {
                let c = self.current_screen;
                self.current_screen().close(c);
                return true;
            }
            ManagerMessage::ChangeState(e) => {
                self.current_screen().close(e);
                self.set_current_screen(e);
            }
            ManagerMessage::SearchFrom(e) => {
                self.current_screen().close(Screens::Search);
                self.search.goto = e;
                self.set_current_screen(Screens::Search);
            }
            ManagerMessage::PlayerFrom(e) => {
                self.current_screen().close(Screens::MusicPlayer);
                self.music_player.goto = e;
                self.set_current_screen(Screens::MusicPlayer);
            }
            ManagerMessage::PlaylistFrom(e) => {
                self.current_screen().close(Screens::Playlist);
                self.chooser.goto = e;
                self.set_current_screen(Screens::Playlist);
            }
            e => {
                return self.handle_manager_message(ManagerMessage::PassTo(
                    Screens::DeviceLost,
                    Box::new(ManagerMessage::Error(
                        format!(
                        "Invalid manager message (Forward the message to a screen maybe):\n{e:?}"
                    ),
                        Box::new(None),
                    )),
                ));
            }
        }
        false
    }

    /// The main loop of the manager
    pub fn run(&mut self, updater: &Receiver<ManagerMessage>) -> Result<(), io::Error> {
        // setup terminal
        enable_raw_mode()?;
        let mut stdout = io::stdout();
        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
        let backend = CrosstermBackend::new(stdout);
        let mut terminal = Terminal::new(backend)?;

        // create app and run it
        let tick_rate = Duration::from_millis(250);

        let mut last_tick = Instant::now();
        'a: loop {
            if is_shutdown_sent() {
                break;
            }
            while let Ok(e) = updater.try_recv() {
                if self.handle_manager_message(e) {
                    break 'a;
                }
            }
            let rectsize = terminal.size()?;
            terminal.draw(|f| {
                self.music_player.update();
                self.current_screen().render(f);
            })?;

            let timeout = tick_rate
                .checked_sub(last_tick.elapsed())
                .unwrap_or_else(|| Duration::from_secs(0));
            if crossterm::event::poll(timeout)? {
                match event::read()? {
                    Event::Key(key) if key.kind != KeyEventKind::Release => {
                        if (key.code == event::KeyCode::Char('c')
                            || key.code == event::KeyCode::Char('d'))
                            && key.modifiers == KeyModifiers::CONTROL
                        {
                            break;
                        }
                        let k = self.current_screen().on_key_press(key, &rectsize);
                        if self.handle_event(k) {
                            break;
                        }
                    }
                    Event::Mouse(mouse) => {
                        let k = self.current_screen().on_mouse_press(mouse, &rectsize);
                        if self.handle_event(k) {
                            break;
                        }
                    }
                    _ => (),
                }
            }
            if last_tick.elapsed() >= tick_rate {
                last_tick = Instant::now();
            }
        }

        // restore terminal
        disable_raw_mode()?;
        execute!(
            terminal.backend_mut(),
            LeaveAlternateScreen,
            DisableMouseCapture
        )?;
        terminal.show_cursor()?;

        shutdown();

        Ok(())
    }
}

// UTILS SECTION TO SPLIT THE TERMINAL INTO DIFFERENT PARTS

pub fn split_y_start(f: Rect, start_size: u16) -> [Rect; 2] {
    let mut rectlistvol = f;
    rectlistvol.height = start_size;
    let mut rectprogress = f;
    rectprogress.y += start_size;
    rectprogress.height = rectprogress.height.saturating_sub(start_size);
    [rectlistvol, rectprogress]
}
pub fn split_y(f: Rect, end_size: u16) -> [Rect; 2] {
    let mut rectlistvol = f;
    rectlistvol.height = rectlistvol.height.saturating_sub(end_size);
    let mut rectprogress = f;
    rectprogress.y += rectprogress.height.saturating_sub(end_size);
    rectprogress.height = end_size;
    [rectlistvol, rectprogress]
}
pub fn split_x(f: Rect, end_size: u16) -> [Rect; 2] {
    let mut rectlistvol = f;
    rectlistvol.width = rectlistvol.width.saturating_sub(end_size);
    let mut rectprogress = f;
    rectprogress.x += rectprogress.width.saturating_sub(end_size);
    rectprogress.width = end_size;
    [rectlistvol, rectprogress]
}

pub fn rect_contains(rect: &Rect, x: u16, y: u16, margin: u16) -> bool {
    rect.x + margin <= x
        && x <= rect.x + rect.width.saturating_sub(margin)
        && rect.y + margin <= y
        && y <= rect.y + rect.height.saturating_sub(margin)
}

pub fn relative_pos(rect: &Rect, x: u16, y: u16, margin: u16) -> (u16, u16) {
    (
        x.saturating_sub(rect.x + margin),
        y.saturating_sub(rect.y + margin),
    )
}


================================================
FILE: crates/ytermusic/src/term/music_player.rs
================================================
use common_structs::{AppStatus, MusicDownloadStatus};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEventKind};

use rand::seq::SliceRandom;
use ratatui::widgets::{Block, Borders, Gauge};

use crate::{
    consts::CONFIG,
    structures::{
        app_status::{AppStatusExt, MusicDownloadStatusExt},
        sound_action::SoundAction,
    },
    systems::{player::PlayerState, DOWNLOAD_MANAGER},
    utils::{invert, to_bidi_string},
};

use super::{
    rect_contains, relative_pos, split_x, split_y, vertical_gauge::VerticalGauge, EventResponse,
    ManagerMessage, Screen, Screens,
};

impl PlayerState {
    pub fn activate(&mut self, index: usize) {
        match index.cmp(&self.current) {
            std::cmp::Ordering::Less => {
                SoundAction::Previous(self.current - index).apply_sound_action(self);
            }
            std::cmp::Ordering::Equal => {
                SoundAction::PlayPause.apply_sound_action(self);
            }
            std::cmp::Ordering::Greater => {
                SoundAction::Next(index - self.current).apply_sound_action(self)
            }
        }
    }
}
impl Screen for PlayerState {
    fn on_mouse_press(
        &mut self,
        mouse_event: crossterm::event::MouseEvent,
        frame_data: &ratatui::layout::Rect,
    ) -> EventResponse {
        let x = mouse_event.column;
        let y = mouse_event.row;
        let [top_rect, bottom] = split_y(*frame_data, 3);
        let [list_rect, volume_rect] = split_x(top_rect, 10);
        if let MouseEventKind::Down(_) = &mouse_event.kind {
            if rect_contains(&list_rect, x, y, 1) {
                let (_, y) = relative_pos(&list_rect, x, y, 1);
                if let Some(e) = self
                    .list_selector
                    .click_on(y as usize, list_rect.height as usize)
                {
                    self.activate(e);
                }
            }
            if rect_contains(&bottom, x, y, 1) {
                let (x, _) = relative_pos(&bottom, x, y, 1);
                let size = bottom.width as usize - 2;
                let percent = x as f64 / size as f64;
                if let Some(duration) = self.sink.duration() {
                    let new_position = (duration * 1000. * percent) as u64;
                    self.sink
                        .seek_to(std::time::Duration::from_millis(new_position));
                }
            }
            if rect_contains(&volume_rect, x, y, 1) {
                let (_, y) = relative_pos(&volume_rect, x, y, 1);
                let size = volume_rect.height as usize - 2;
                let percent = 100. - y as f64 / size as f64 * 100.;
                self.sink.set_volume(percent as i32)
            }
        } else if let MouseEventKind::ScrollUp = &mouse_event.kind {
            if rect_contains(&volume_rect, x, y, 1) {
                SoundAction::Plus.apply_sound_action(self);
            } else if rect_contains(&bottom, x, y, 1) {
                SoundAction::Forward.apply_sound_action(self);
            } else {
                self.list_selector.scroll_up();
            }
        } else if let MouseEventKind::ScrollDown = &mouse_event.kind {
            if rect_contains(&volume_rect, x, y, 1) {
                SoundAction::Minus.apply_sound_action(self);
            } else if rect_contains(&bottom, x, y, 1) {
                SoundAction::Backward.apply_sound_action(self);
            } else {
                self.list_selector.scroll_down();
            }
        }
        EventResponse::None
    }

    fn on_key_press(&mut self, key: KeyEvent, _: &ratatui::layout::Rect) -> EventResponse {
        match key.code {
            KeyCode::Esc => ManagerMessage::ChangeState(self.goto).event(),
            KeyCode::F(5) => {
                // Get all musics that have failled to download
                let mut musics = Vec::new();
                self.music_status
                    .iter_mut()
                    .for_each(|(key, music_status)| {
                        if MusicDownloadStatus::DownloadFailed != *music_status {
                            return;
                        }
                        if let Some(e) = self.list.iter().find(|x| &x.video_id == key) {
                            musics.push(e.clone());
                            *music_status = MusicDownloadStatus::NotDownloaded;
                        }
                    });
                // Download them
                DOWNLOAD_MANAGER.add_to_download_list(musics);
                EventResponse::None
            }
            KeyCode::Char('f') => ManagerMessage::SearchFrom(Screens::MusicPlayer).event(),
            KeyCode::Char('s') => {
                self.list.shuffle(&mut rand::thread_rng());
                self.current = 0;
                self.sink.stop();
                EventResponse::None
            }
            KeyCode::Char('C') => {
                SoundAction::Cleanup.apply_sound_action(self);
                EventResponse::None
            }
            KeyCode::Char(' ') => {
                SoundAction::PlayPause.apply_sound_action(self);
                EventResponse::None
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.list_selector.scroll_up();
                EventResponse::None
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.list_selector.scroll_down();
                EventResponse::None
            }
            KeyCode::Enter => {
                if let Some(e) = self.list_selector.play() {
                    self.activate(e);
                }
                EventResponse::None
            }
            KeyCode::Char('+') | KeyCode::Char('=') => {
                SoundAction::Plus.apply_sound_action(self);
                EventResponse::None
            }
            KeyCode::Char('-') => {
                SoundAction::Minus.apply_sound_action(self);
                EventResponse::None
            }
            KeyCode::Char('<') | KeyCode::Left | KeyCode::Char('h') => {
                if key.modifiers.contains(KeyModifiers::CONTROL) {
                    SoundAction::Previous(1).apply_sound_action(self);
                } else {
                    SoundAction::Backward.apply_sound_action(self);
                }
                EventResponse::None
            }
            KeyCode::Char('>') | KeyCode::Right | KeyCode::Char('l') => {
                if key.modifiers.contains(KeyModifiers::CONTROL) {
                    SoundAction::Next(1).apply_sound_action(self);
                } else {
                    SoundAction::Forward.apply_sound_action(self);
                }
                EventResponse::None
            }
            KeyCode::Char('r') => {
                SoundAction::DeleteVideoUnary.apply_sound_action(self);
                EventResponse::None
            }
            _ => EventResponse::None,
        }
    }

    fn render(&mut self, f: &mut ratatui::Frame) {
        let render_volume_slider = CONFIG.player.volume_slider;
        let [top_rect, progress_rect] = split_y(f.size(), 3);
        let [list_rect, volume_rect] = split_x(top_rect, if render_volume_slider { 10 } else { 0 });
        let colors = if self.sink.is_paused() {
            AppStatus::Paused
        } else if self.sink.is_finished() {
            AppStatus::NoMusic
        } else {
            AppStatus::Playing
        }
        .style();
        if render_volume_slider {
            f.render_widget(
                VerticalGauge::default()
                    .block(Block::default().title(" Volume ").borders(Borders::ALL))
                    .gauge_style(colors)
                    .ratio((self.sink.volume() as f64 / 100.).clamp(0.0, 1.0)),
                volume_rect,
            );
        }
        let current_time = self.sink.elapsed().as_secs();
        let total_time = self.sink.duration().map(|x| x as u32).unwrap_or(0);
        f.render_widget(
            Gauge::default()
                .block(
                    Block::default()
                        .title(
                            self.current()
                                .map(|x| format!(" {} ", to_bidi_string(&x.to_string())))
                                .unwrap_or_else(|| " No music playing ".to_owned()),
                        )
                        .borders(Borders::ALL),
                )
                .gauge_style(colors)
                .ratio(
                    if self.sink.is_finished() {
                        0.5
                    } else {
                        self.sink.percentage().min(100.)
                    }
                    .clamp(0.0, 1.0),
                )
                .label(format!(
                    "{}:{:02} / {}:{:02}",
                    current_time / 60,
                    current_time % 60,
                    total_time / 60,
                    total_time % 60
                )),
            progress_rect,
        );
        // Create a List from all list items and highlight the currently selected one
        self.list_selector.update(self.list.len(), self.current);
        self.list_selector.render(
            list_rect,
            f.buffer_mut(),
            |index, select, scroll| {
                let music_state = self
                    .list
                    .get(index)
                    .and_then(|x| self.music_status.get(&x.video_id))
                    .copied()
                    .unwrap_or(MusicDownloadStatus::Downloaded);
                let music_state_c = music_state.character(Some(!self.sink.is_paused()));
                (
                    if select {
                        music_state.style(Some(!self.sink.is_paused()))
                    } else if scroll {
                        invert(music_state.style(None))
                    } else {
                        music_state.style(None)
                    },
                    if let Some(e) = self.list.get(index) {
                        format!(
                            " {music_state_c} {} | {}",
                            to_bidi_string(&e.author),
                            to_bidi_string(&e.title)
                        )
                    } else {
                        String::new()
                    },
                )
            },
            " Playlist ",
        )
    }

    fn handle_global_message(&mut self, message: ManagerMessage) -> EventResponse {
        match message {
            ManagerMessage::RestartPlayer => {
                SoundAction::RestartPlayer.apply_sound_action(self);
                ManagerMessage::ChangeState(Screens::MusicPlayer).event()
            }
            _ => EventResponse::None,
        }
    }

    fn close(&mut self, _: Screens) -> EventResponse {
        EventResponse::None
    }

    fn open(&mut self) -> EventResponse {
        EventResponse::None
    }
}


================================================
FILE: crates/ytermusic/src/term/playlist.rs
================================================
use std::sync::atomic::AtomicBool;

use crossterm::event::{KeyCode, KeyEvent};
use flume::Sender;
use ratatui::{layout::Rect, style::Style, Frame};
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::{CACHE_DIR, CONFIG},
    structures::sound_action::{download_manager_handler, SoundAction},
    systems::DOWNLOAD_MANAGER,
    utils::{invert, to_bidi_string},
    ShutdownSignal, DATABASE,
};

use super::{
    item_list::{ListItem, ListItemAction},
    EventResponse, ManagerMessage, Screen, Screens,
};

#[derive(Clone)]
pub enum ChooserAction {
    Play(PlayListEntry),
}

impl ListItemAction for ChooserAction {
    fn render_style(&self, _: &str, selected: bool) -> Style {
        if selected {
            invert(CONFIG.player.text_next_style)
        } else {
            CONFIG.player.text_next_style
        }
    }
}

pub struct Chooser {
    pub item_list: ListItem<ChooserAction>,
    pub goto: Screens,
    pub action_sender: Sender<SoundAction>,
}

#[derive(Clone)]
pub struct PlayListEntry {
    pub name: String,
    pub videos: Vec<YoutubeMusicVideoRef>,
    pub text_to_show: String,
}

impl PlayListEntry {
    pub fn new(name: String, videos: Vec<YoutubeMusicVideoRef>) -> Self {
        Self {
            text_to_show: format_playlist(&name, &videos),
            name,
            videos,
        }
    }

    pub fn tupplelize(&self) -> (&String, &Vec<YoutubeMusicVideoRef>) {
        (&self.name, &self.videos)
    }
}
pub fn format_playlist(name: &str, videos: &[YoutubeMusicVideoRef]) -> String {
    let db = DATABASE.read().unwrap();
    let local_videos = videos
        .iter()
        .filter(|x| db.iter().any(|y| x.video_id == y.video_id))
        .count();
    format!(
        "{}     ({}/{} {}%)",
        to_bidi_string(name),
        local_videos,
        videos.len(),
        (local_videos as f32 / videos.len() as f32 * 100.0) as u8
    )
}
impl Screen for Chooser {
    fn on_mouse_press(
        &mut self,
        mouse_event: crossterm::event::MouseEvent,
        frame_data: &Rect,
    ) -> EventResponse {
        if let Some(ChooserAction::Play(a)) = self.item_list.on_mouse_press(mouse_event, frame_data)
        {
            if PLAYER_RUNNING.load(std::sync::atomic::Ordering::SeqCst) {
                return EventResponse::Message(vec![ManagerMessage::Inspect(
                    a.name,
                    Screens::Playlist,
                    a.videos,
                )
                .pass_to(Screens::PlaylistViewer)]);
            }
            self.play(&a);
            EventResponse::Message(vec![ManagerMessage::PlayerFrom(Screens::Playlist)])
        } else {
            EventResponse::None
        }
    }

    fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
        if let Some(ChooserAction::Play(a)) = self.item_list.on_key_press(key).cloned() {
            if PLAYER_RUNNING.load(std::sync::atomic::Ordering::SeqCst) {
                return EventResponse::Message(vec![ManagerMessage::Inspect(
                    a.name,
                    Screens::Playlist,
                    a.videos,
                )
                .pass_to(Screens::PlaylistViewer)]);
            }
            self.play(&a);
            return EventResponse::Message(vec![ManagerMessage::ChangeState(Screens::MusicPlayer)]);
        }
        match key.code {
            KeyCode::Esc => return ManagerMessage::ChangeState(Screens::MusicPlayer).event(),
            KeyCode::Char('f') => return ManagerMessage::SearchFrom(Screens::Playlist).event(),
            _ => {}
        }
        EventResponse::None
    }

    fn render(&mut self, frame: &mut Frame) {
        frame.render_widget(&self.item_list, frame.size());
    }

    fn handle_global_message(&mut self, message: super::ManagerMessage) -> EventResponse {
        if let ManagerMessage::AddElementToChooser(a) = message {
            self.add_element(a);
        }
        EventResponse::None
    }

    fn close(&mut self, _: Screens) -> EventResponse {
        EventResponse::None
    }

    fn open(&mut self) -> EventResponse {
        EventResponse::None
    }
}
pub static PLAYER_RUNNING: AtomicBool = AtomicBool::new(false);

impl Chooser {
    fn play(&mut self, a: &PlayListEntry) {
        if a.name != "Local musics" {
            std::fs::write(
                CACHE_DIR.join("last-playlist.json"),
                serde_json::to_string(&a.tupplelize()).unwrap(),
            )
            .unwrap();
        }
        self.action_sender.send(SoundAction::Cleanup).unwrap();
        DOWNLOAD_MANAGER.clean(
            ShutdownSignal,
            download_manager_handler(self.action_sender.clone()),
        );
        self.action_sender
            .send(SoundAction::AddVideosToQueue(a.videos.clone()))
            .unwrap();
    }
    fn add_element(&mut self, element: (String, Vec<YoutubeMusicVideoRef>)) {
        let entry = PlayListEntry::new(element.0, element.1);
        self.item_list
            .add_element((entry.text_to_show.clone(), ChooserAction::Play(entry)));
    }
}


================================================
FILE: crates/ytermusic/src/term/playlist_view.rs
================================================
use crossterm::event::{KeyCode, KeyEvent};
use flume::Sender;
use ratatui::{layout::Rect, style::Style, Frame};
use ytpapi2::YoutubeMusicVideoRef;

use crate::{
    consts::CONFIG,
    structures::sound_action::SoundAction,
    utils::{invert, to_bidi_string},
    DATABASE,
};

use super::{
    item_list::{ListItem, ListItemAction},
    EventResponse, ManagerMessage, Screen, Screens,
};

#[derive(Clone)]
pub struct PlayListAction(usize, bool);

impl ListItemAction for PlayListAction {
    fn render_style(&self, _: &str, selected: bool) -> Style {
        if selected {
            if self.1 {
                invert(CONFIG.player.text_downloading_style)
            } else {
                invert(CONFIG.player.text_next_style)
            }
        } else if self.1 {
            CONFIG.player.text_downloading_style
        } else {
            CONFIG.player.text_next_style
        }
    }
}

// Audio device not connected!
pub struct PlaylistView {
    pub items: ListItem<PlayListAction>,
    pub videos: Vec<YoutubeMusicVideoRef>,
    pub goto: Screens,
    pub sender: Sender<SoundAction>,
}

impl Screen for PlaylistView {
    fn on_mouse_press(&mut self, e: crossterm::event::MouseEvent, r: &Rect) -> EventResponse {
        if let Some(PlayListAction(v, _)) = self.items.on_mouse_press(e, r) {
            self.sender
                .send(SoundAction::ReplaceQueue(
                    self.videos.iter().skip(v).cloned().collect(),
                ))
                .unwrap();
            EventResponse::Message(vec![ManagerMessage::PlayerFrom(Screens::Playlist)])
        } else {
            EventResponse::None
        }
    }

    fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
        if let Some(PlayListAction(v, _)) = self.items.on_key_press(key) {
            self.sender
                .send(SoundAction::ReplaceQueue(
                    self.videos.iter().skip(*v).cloned().collect(),
                ))
                .unwrap();
            return EventResponse::Message(vec![ManagerMessage::PlayerFrom(Screens::Playlist)]);
        }
        match key.code {
            KeyCode::Esc => ManagerMessage::ChangeState(self.goto).event(),
            KeyCode::Char('f') => ManagerMessage::SearchFrom(Screens::PlaylistViewer).event(),
            _ => EventResponse::None,
        }
    }

    fn render(&mut self, frame: &mut Frame) {
        frame.render_widget(&self.items, frame.size());
    }

    fn handle_global_message(&mut self, m: ManagerMessage) -> EventResponse {
        match m {
            ManagerMessage::Inspect(a, screen, m) => {
                self.items
                    .set_title(format!(" Inspecting {} ", to_bidi_string(&a)));
                self.goto = screen;
                let db = DATABASE.read().unwrap();
                self.items.update(
                    m.iter()
                        .enumerate()
                        .map(|(i, m)| {
                            (
                                format!("  {}", to_bidi_string(&m.to_string())),
                                PlayListAction(i, !db.iter().any(|x| x.video_id == m.video_id)),
                            )
                        })
                        .collect(),
                    0,
                );
                self.videos = m;

                EventResponse::Message(vec![ManagerMessage::ChangeState(Screens::PlaylistViewer)])
            }
            _ => EventResponse::None,
        }
    }

    fn close(&mut self, _: Screens) -> EventResponse {
        EventResponse::None
    }

    fn open(&mut self) -> EventResponse {
        EventResponse::None
    }
}


================================================
FILE: crates/ytermusic/src/term/search.rs
================================================
use std::sync::{Arc, RwLock};

use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use flume::Sender;
use log::error;
use ratatui::{
    layout::{Alignment, Rect},
    style::Style,
    widgets::{Block, BorderType, Borders, Paragraph},
    Frame,
};
use tokio::task::JoinHandle;
use ytpapi2::{
    HeaderMap, HeaderValue, SearchResults, YoutubeMusicInstance, YoutubeMusicPlaylistRef,
    YoutubeMusicVideoRef,
};

use crate::{
    consts::CONFIG,
    get_header_file, run_service,
    structures::sound_action::{download_manager_handler, SoundAction},
    systems::DOWNLOAD_MANAGER,
    try_get_cookies,
    utils::{invert, to_bidi_string},
    ShutdownSignal, DATABASE,
};

use super::{
    item_list::{ListItem, ListItemAction},
    playlist::format_playlist,
    split_y_start, EventResponse, ManagerMessage, Screen, Screens,
};

pub struct Search {
    pub text: String,
    pub goto: Screens,
    pub list: Arc<RwLock<ListItem<Status>>>,
    pub search_handle: Option<JoinHandle<()>>,
    pub api: Option<Arc<YoutubeMusicInstance>>,
    pub action_sender: Sender<SoundAction>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Status {
    Local(YoutubeMusicVideoRef),
    Unknown(YoutubeMusicVideoRef),
    PlayList(YoutubeMusicPlaylistRef, Vec<YoutubeMusicVideoRef>),
}
impl ListItemAction for Status {
    fn render_style(&self, _: &str, selected: bool) -> Style {
        let k = match self {
            Self::Local(_) => CONFIG.player.text_next_style,
            Self::Unknown(_) => CONFIG.player.text_downloading_style,
            Self::PlayList(_, _) => CONFIG.player.text_next_style,
        };
        if selected {
            invert(k)
        } else {
            k
        }
    }
}

impl Screen for Search {
    fn on_mouse_press(
        &mut self,
        mouse_event: crossterm::event::MouseEvent,
        frame_data: &Rect,
    ) -> EventResponse {
        let splitted = split_y_start(*frame_data, 3);
        if let Some(e) = self
            .list
            .write()
            .unwrap()
            .on_mouse_press(mouse_event, &splitted[1])
        {
            self.execute_status(e, mouse_event.modifiers)
        } else {
            EventResponse::None
        }
    }

    fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
        if KeyCode::Esc == key.code {
            return ManagerMessage::ChangeState(self.goto).event();
        }
        if let Some(e) = self.list.write().unwrap().on_key_press(key) {
            return self.execute_status(e.clone(), key.modifiers);
        }
        let textbefore = self.text.trim().to_owned();
        match key.code {
            KeyCode::Delete | KeyCode::Backspace => {
                self.text.pop();
            }
            KeyCode::Char(a) => {
                self.text.push(a);
            }
            _ => {}
        }
        if textbefore == self.text.trim() {
            return EventResponse::None;
        }

        if let Some(handle) = self.search_handle.take() {
            handle.abort();
        }

        let text = self.text.to_lowercase();

        let local = DATABASE
            .read()
            .unwrap()
            .iter()
            .filter(|x| {
                x.title.to_lowercase().contains(&text) || x.author.to_lowercase().contains(&text)
            })
            .cloned()
            .map(|video| {
                (
                    format!(" {} ", to_bidi_string(&video.to_string())),
                    Status::Local(video),
                )
            })
            .take(100)
            .collect::<Vec<_>>();
        self.list.write().unwrap().update_contents(local.clone());

        if let Some(api) = self.api.clone() {
            let text = self.text.clone();
            let items = self.list.clone();
            self.search_handle = Some(run_service(async move {
                // Sleep to prevent spamming the api
                tokio::time::sleep(std::time::Duration::from_millis(300)).await;
                let mut item = Vec::new();
                match api
                    .search(&text.replace('\\', "\\\\").replace('\"', "\\\""), 0)
                    .await
                {
                    Ok(SearchResults {
                        videos: e,
                        playlists: p,
                    }) => {
                        for video in e.into_iter() {
                            let id = video.video_id.clone();
                            item.push((
                                format!(" {} ", to_bidi_string(&video.to_string())),
                                if DATABASE.read().unwrap().iter().any(|x| x.video_id == id) {
                                    Status::Local(video)
                                } else {
                                    Status::Unknown(video)
                                },
                            ));
                        }
                        for playlist in p.into_iter() {
                            let api = api.clone();
                            let items = items.clone();
                            run_service(async move {
                                match api.get_playlist(&playlist, 0).await {
                                    Ok(e) => {
                                        if e.is_empty() {
                                            return;
                                        }
                                        items.write().unwrap().add_element((
                                            format_playlist(
                                                &format!(
                                                    " [P] {} ({})",
                                                    to_bidi_string(&playlist.name),
                                                    to_bidi_string(&playlist.subtitle)
                                                ),
                                                &e,
                                            ),
                                            Status::PlayList(playlist, e),
                                        ));
                                    }
                                    Err(e) => {
                                        error!("{e:?}");
                                    }
                                };
                            });
                        }
                    }
                    Err(e) => {
                        error!("{e:?}");
                    }
                }
                let mut local = local;
                local.append(&mut item);
                items.write().unwrap().update_contents(local);
            }));
        }

        EventResponse::None
    }

    fn render(&mut self, frame: &mut Frame) {
        let splitted = split_y_start(frame.size(), 3);
        frame.render_widget(
            Paragraph::new(self.text.clone())
                .style(CONFIG.player.text_searching_style)
                .alignment(Alignment::Center)
                .block(
                    Block::default()
                        .borders(Borders::ALL)
                        .style(CONFIG.player.text_next_style)
                        .title(" Search ")
                        .border_type(BorderType::Plain),
                ),
            splitted[0],
        );
        //  Select the playlist to play
        let items = self.list.read().unwrap();
        frame.render_widget(&*items, splitted[1]);
    }

    fn handle_global_message(&mut self, _: super::ManagerMessage) -> EventResponse {
        EventResponse::None
    }

    fn close(&mut self, _: Screens) -> EventResponse {
        EventResponse::None
    }

    fn open(&mut self) -> EventResponse {
        EventResponse::None
    }
}
impl Search {
    pub async fn new(action_sender: Sender<SoundAction>) -> Self {
        Self {
            text: String::new(),
            list: Arc::new(RwLock::new(ListItem::new(
                "Select a song to play".to_string(),
            ))),
            goto: Screens::MusicPlayer,
            search_handle: None,
            api: if let Some(cookies) = try_get_cookies() {
                let mut headermap = HeaderMap::new();
                headermap.insert(
                    "cookie",
                    HeaderValue::from_str(&cookies).unwrap(),
                );
                headermap.insert(
                    "user-agent",
                    HeaderValue::from_static("Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"),
                );
                YoutubeMusicInstance::new(headermap, None).await    //don't think we need a brand account for search
            } else {
                YoutubeMusicInstance::from_header_file(get_header_file().unwrap().1.as_path()).await
            }
                .ok()
                .map(Arc::new),
            action_sender,
        }
    }

    pub fn execute_status(&self, e: Status, modifiers: KeyModifiers) -> EventResponse {
        match e {
            Status::Local(e) | Status::Unknown(e) => {
                self.action_sender
                    .send(SoundAction::AddVideoUnary(e.clone()))
                    .unwrap();
                DOWNLOAD_MANAGER.start_task_unary(
                    download_manager_handler(self.action_sender.clone()),
                    e,
                    ShutdownSignal,
                );
                if modifiers.contains(KeyModifiers::CONTROL) {
                    EventResponse::None
                } else {
                    ManagerMessage::PlayerFrom(Screens::Playlist).event()
                }
            }
            Status::PlayList(e, v) => ManagerMessage::Inspect(e.name, Screens::Search, v)
                .pass_to(Screens::PlaylistViewer)
                .event(),
        }
    }
}


================================================
FILE: crates/ytermusic/src/term/vertical_gauge.rs
================================================
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::{Color, Style},
    widgets::{Block, Widget},
};

pub struct VerticalGauge<'a> {
    block: Option<Block<'a>>,
    ratio: f64,
    style: Style,
    gauge_style: Style,
}

impl<'a> Widget for VerticalGauge<'a> {
    fn render(mut self, area: Rect, buf: &mut Buffer) {
        buf.set_style(area, self.style);
        let gauge_area = match self.block.take() {
            Some(b) => {
                let inner_area = b.inner(area);
                b.render(area, buf);
                inner_area
            }
            None => area,
        };
        buf.set_style(gauge_area, self.gauge_style);
        if gauge_area.height < 1 {
            return;
        }

        // compute label value and its position
        // label is put at the center of the gauge_area
        let label = {
            let pct = f64::round(self.ratio * 100.0);
            format!("{pct}%")
        };
        let clamped_label_width = gauge_area.width.min(label.len() as u16);
        let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
        let label_row = gauge_area.top() + gauge_area.height / 2;

        // the gauge will be filled proportionally to the ratio
        let filled_height = f64::from(gauge_area.height) * self.ratio;
        let end = gauge_area.bottom() - filled_height.round() as u16;
        for y in gauge_area.top()..end {
            // render the filled area (left to end)
            for x in gauge_area.left()..gauge_area.right() {
                buf.get_mut(x, y)
                    .set_symbol(" ")
                    .set_bg(self.gauge_style.bg.unwrap_or(Color::Reset))
                    .set_fg(self.gauge_style.fg.unwrap_or(Color::Reset));
            }
        }
        for y in end..gauge_area.bottom() {
            // render the empty area (end to right)
            for x in gauge_area.left()..gauge_area.right() {
                buf.get_mut(x, y)
                    .set_symbol(" ")
                    .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset))
                    .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset));
            }
        }
        for x in label_col..label_col + clamped_label_width {
            if gauge_area.height / 2 > end.saturating_sub(2) {
                buf.get_mut(x, label_row)
                    .set_symbol(&label[(x - label_col) as usize..(x - label_col + 1) as usize])
                    .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset))
                    .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset));
            } else {
                buf.get_mut(x, label_row)
                    .set_symbol(&label[(x - label_col) as usize..(x - label_col + 1) as usize])
                    .set_bg(self.gauge_style.bg.unwrap_or(Color::Reset))
                    .set_fg(self.gauge_style.fg.unwrap_or(Color::Reset));
            }
        }
    }
}

impl<'a> Default for VerticalGauge<'a> {
    fn default() -> VerticalGauge<'a> {
        VerticalGauge {
            block: None,
            ratio: 0.0,
            style: Style::default(),
            gauge_style: Style::default(),
        }
    }
}

impl<'a> VerticalGauge<'a> {
    pub fn block(mut self, block: Block<'a>) -> VerticalGauge<'a> {
        self.block = Some(block);
        self
    }

    /// Sets ratio ([0.0, 1.0]) directly.
    pub fn ratio(mut self, ratio: f64) -> VerticalGauge<'a> {
        assert!(
            (0.0..=1.0).contains(&ratio),
            "Ratio should be between 0 and 1 inclusively."
        );
        self.ratio = ratio;
        self
    }

    pub fn gauge_style(mut self, style: Style) -> VerticalGauge<'a> {
        self.gauge_style = style;
        self
    }
}


================================================
FILE: crates/ytermusic/src/utils.rs
================================================
use directories::ProjectDirs;
use ratatui::style::{Color, Style};
use unicode_bidi::{BidiInfo, Level};

/// Get directories for the project for config, cache, etc.
pub fn get_project_dirs() -> Option<ProjectDirs> {
    ProjectDirs::from("com", "ccgauche", "ytermusic")
}
/// Invert a style
pub fn invert(style: Style) -> Style {
    if style.bg.is_none() {
        return Style {
            fg: Some(color_contrast(style.fg.unwrap_or(Color::Reset))),
            bg: style.fg,
            add_modifier: style.add_modifier,
            sub_modifier: style.sub_modifier,
            underline_color: style.underline_color,
        };
    }
    Style {
        fg: style.bg,
        bg: style.fg,
        add_modifier: style.add_modifier,
        sub_modifier: style.sub_modifier,
        underline_color: style.underline_color,
    }
}

/// Returns a color with a high contrast to the input color (white or black)
pub fn color_contrast(color: Color) -> Color {
    match color {
        Color::Black => Color::White,
        Color::White => Color::Black,
        Color::Red => Color::White,
        Color::Green => Color::Black,
        Color::Yellow => Color::Black,
        Color::Blue => Color::White,
        Color::Magenta => Color::White,
        Color::Cyan => Color::Black,
        Color::Gray => Color::White,
        Color::DarkGray => Color::Black,
        Color::LightRed => Color::White,
        Color::LightGreen => Color::Black,
        Color::LightYellow => Color::Black,
        Color::LightBlue => Color::White,
        Color::LightMagenta => Color::White,
        Color::LightCyan => Color::Black,
        Color::Indexed(v) => {
            if v < 8 {
                Color::White
            } else {
                Color::Black
            }
        }
        Color::Rgb(r, g, b) => {
            if r as u32 + g as u32 + b as u32 > 382 {
                Color::Black
            } else {
                Color::White
            }
        }
        Color::Reset => Color::Black,
    }
}

/// Reorder a string using the Unicode Bidirectional Algorithm.
/// This ensures RTL text (e.g. Hebrew, Arabic) displays correctly in the terminal.
pub fn to_bidi_string(s: &str) -> String {
    let bidi_info = BidiInfo::new(s, None);
    if let Some(para) = bidi_info.paragraphs.first() {
        if para.level != Level::ltr() {
            return bidi_info.reorder_line(para, para.range.clone()).to_string();
        }
        // Check if any character has RTL level even in an LTR paragraph
        let start = para.range.start;
        let end = para.range.end;
        if bidi_info.levels[start..end]
            .iter()
            .any(|l| *l != Level::ltr())
        {
            return bidi_info.reorder_line(para, para.range.clone()).to_string();
        }
    }
    s.to_string()
}


================================================
FILE: crates/ytpapi2/Cargo.toml
================================================
[package]
name = "ytpapi2"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
reqwest = { version = "0.11.24", features = [
    "gzip",
    "deflate",
    "cookies",
    "rustls-tls",
], default-features = false }
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
tokio = { version = "1.36.0", features = ["full"] }
sha1 = "0.10.6"
log = "0.4.20"

================================================
FILE: crates/ytpapi2/src/json_extractor.rs
================================================
use std::fmt::Display;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::YoutubeMusicPlaylistRef;

/// Applies recursively the `transformer` function to the given json value
/// and returns the transformed values.
pub(crate) fn from_json<T: PartialEq>(
    json: &Value,
    transformer: impl Fn(&Value) -> Option<T>,
) -> crate::Result<Vec<T>> {
    /// Execute a function on each element of a json value recursively.
    /// When the function returns something, the value is added to the result.
    pub(crate) fn inner_crawl<T: PartialEq>(
        value: &Value,
        playlists: &mut Vec<T>,
        transformer: &impl Fn(&Value) -> Option<T>,
    ) {
        if let Some(e) = transformer(value) {
            // Maybe an hashset would be better
            if !playlists.contains(&e) {
                playlists.push(e);
            }
            return;
        }
        match value {
            Value::Array(a) => a
                .iter()
                .for_each(|x| inner_crawl(x, playlists, transformer)),
            Value::Object(a) => a
                .values()
                .for_each(|x| inner_crawl(x, playlists, transformer)),
            _ => (),
        }
    }
    let mut playlists = Vec::new();
    inner_crawl(json, &mut playlists, &transformer);
    Ok(playlists)
}

#[derive(Debug, Clone, PartialOrd, Eq, Ord, PartialEq, Hash, Serialize, Deserialize)]
pub struct YoutubeMusicVideoRef {
    pub title: String,
    pub author: String,
    pub album: String,
    pub video_id: String,
    pub duration: String,
}

impl Display for YoutubeMusicVideoRef {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} | {}", self.author, self.title)
    }
}

/// Tries to extract a playlist from a json value.
/// Quite flexible to reduce odds of API change breaking this.
pub(crate) fn get_playlist(value: &Value) -> Option<YoutubeMusicPlaylistRef> {
    let object = value.as_object()?;
    let title_text = get_text(object.get("title")?, true, false)?;
    let subtitle = object
        .get("subtitle")
        .and_then(|x| get_text(x, false, false));
    let browse_id = &object
        .get("navigationEndpoint")
        .and_then(|x| x.get("browseEndpoint"))
        .and_then(|x| x.get("browseId"))
        .and_then(Value::as_str)?;
    Some(YoutubeMusicPlaylistRef {
        name: title_text,
        subtitle: subtitle.unwrap_or_default(),
        browse_id: browse_id.to_string(),
    })
}

#[derive(Debug, Clone, PartialOrd, Eq, Ord, PartialEq, Hash, Serialize, Deserialize)]
pub struct Continuation {
    pub(crate) continuation: String,
    pub(crate) click_tracking_params: String,
}

pub fn get_continuation(value: &Value) -> Option<Continuation> {
    let continuation = value
        .get("nextContinuationData")
        .and_then(|x| x.get("continuation"))
        .and_then(Value::as_str)?;
    let click_tracking_params = value
        .get("nextContinuationData")
        .and_then(|x| x.get("clickTrackingParams"))
        .and_then(Value::as_str)?;
    Some(Continuation {
        continuation: continuation.to_string(),
        click_tracking_params: click_tracking_params.to_string(),
    })
}

pub fn get_playlist_search(value: &Value) -> Option<YoutubeMusicPlaylistRef> {
    let browse_id = &value
        .get("navigationEndpoint")
        .and_then(|x| x.get("browseEndpoint"))
        .and_then(|x| x.get("browseId"))
        .and_then(Value::as_str)?;
    let titles: Vec<String> = value
        .get("flexColumns")?
        .as_array()?
        .iter()
        .flat_map(|x| {
            x.get("musicResponsiveListItemFlexColumnRenderer")
                .and_then(|x| x.get("text"))
                .and_then(|x| get_text(x, false, false))
        })
        .collect();
    Some(YoutubeMusicPlaylistRef {
        name: titles.first()?.clone(),
        subtitle: titles.get(1)?.clone(),
        browse_id: browse_id.to_string(),
    })
}

pub fn extract_playlist_info(value: &Value) -> Option<(String, String)> {
    let header = value.get("header")?.get("musicDetailHeaderRenderer")?;
    let title = get_text(header.get("title")?, false, false)?;
    let subtitles = header
        .get("subtitle")?
        .get("runs")?
        .as_array()?
        .iter()
        .flat_map(|x| get_text(x, false, false))
        .filter(|x| x != " • ")
        .collect::<Vec<String>>();
    Some((title, subtitles.get(1)?.clone()))
}

pub fn get_video_from_album(value: &Value) -> Option<YoutubeMusicVideoRef> {
    let video_id = value
        .get("playlistItemData")
        .and_then(|x| x.get("videoId"))
        .and_then(Value::as_str)?;
    let title: Vec<String> = value
        .get("flexColumns")?
        .as_array()?
        .iter()
        .flat_map(|x| {
            x.get("musicResponsiveListItemFlexColumnRenderer")
                .and_then(|x| x.get("text"))
                .and_then(|x| get_text(x, false, false))
        })
        .collect();
    Some(YoutubeMusicVideoRef {
        title: title.first()?.clone(),
        author: String::new(),
        album: String::new(),
        video_id: video_id.to_string(),
        duration: String::new(),
    })
}

/// Tries to extract the text from a json value.
/// text_clean: Weather to include singleton text.
/// dot: Weather to use the dotted text instead of the space
fn get_text(value: &Value, text_clean: bool, dot: bool) -> Option<String> {
    if let Some(e) = value.as_str() {
        Some(e.to_string())
    } else {
        let obj = value.as_object()?;
        if let Some(e) = obj.get("text") {
            if text_clean && obj.values().count() == 1 {
                return None;
            }
            get_text(e, text_clean, dot)
        } else if let Some(e) = obj.get("runs") {
            let k = e
                .as_array()?
                .iter()
                .flat_map(|x| get_text(x, text_clean, dot))
                .collect::<Vec<_>>();
            if k.is_empty() {
                None
            } else {
                Some(join_clean(&k, dot))
            }
        } else {
            None
        }
    }
}

fn join_clean(strings: &[String], dot: bool) -> String {
    strings
        .iter()
        .map(|x| x.trim())
        .filter(|x| !x.is_empty())
        .collect::<Vec<_>>()
        .join(if dot { " • " } else { " " })
}

/// Tries to find a video id in the json
pub fn get_videoid(value: &Value) -> Option<String> {
    match value {
        Value::Array(e) => e.iter().find_map(get_videoid),
        Value::Object(e) => e
            .get("videoId")
            .and_then(Value::as_str)
            .map(|x| x.to_string())
            .or_else(|| e.values().find_map(get_videoid)),
        _ => None,
    }
}

/// Tries to extract a video from a json value.
/// Quite flexible to reduce odds of API change breaking this.
pub(crate) fn get_video(value: &Value) -> Option<YoutubeMusicVideoRef> {
    // Extract the text part (title, author, album) from a json value.
    let mut texts = value
        .as_object()?
        .get("flexColumns")?
        .as_array()?
        .iter()
        .flat_map(|x| {
            x.as_object()
                .and_then(|x| x.values().next())
                .and_then(|x| get_text(x, true, true))
        });

    Some(YoutubeMusicVideoRef {
        video_id: get_videoid(value)?,
        title: texts.next()?,
        author: texts.next()?,
        album: texts.next().unwrap_or_default(),
        duration: String::new(),
    })
}


================================================
FILE: crates/ytpapi2/src/lib.rs
================================================
use std::{
    path::Path,
    string::FromUtf8Error,
    time::{SystemTime, UNIX_EPOCH},
};

use json_extractor::{
    extract_playlist_info, from_json, get_continuation, get_playlist, get_playlist_search,
    get_video, get_video_from_album, Continuation,
};
use log::{debug, error, trace};
pub use reqwest::header::HeaderMap;
pub use reqwest::header::*;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha1::{Digest, Sha1};
use string_utils::StringUtils;

mod json_extractor;
mod string_utils;

pub use json_extractor::YoutubeMusicVideoRef;

pub type Result<T> = std::result::Result<T, YoutubeMusicError>;

const YTM_DOMAIN: &str = "https://music.youtube.com";

#[cfg(test)]
fn get_headers() -> HeaderMap {
    let mut headers = HeaderMap::new();
    let file = std::fs::read_to_string("../headers.txt").unwrap();
    for header in file.lines() {
        if header.trim().is_empty() {
            continue;
        }
        let (key, value) = header.split_once(": ").unwrap();
        headers.insert(
            match key {
                "Cookie" => reqwest::header::COOKIE,
                "User-Agent" => reqwest::header::USER_AGENT,
                _ => {
                    println!("Unknown header key: {}", key);
                    continue;
                }
            },
            value.parse().unwrap(),
        );
    }
    headers
}

#[cfg(test)]
fn get_account_id() -> Option<String> {
    let file = std::fs::read_to_string("../account_id.txt").unwrap();
    let account_id = match std::fs::read_to_string(file) {
        Ok(id) => Some(id),
        Err(_) => None,
    };
    return account_id;
}

#[test]
fn advanced_like() {
    use tokio::runtime::Runtime;
    Runtime::new().unwrap().block_on(async {
        let ytm = YoutubeMusicInstance::new(get_headers(), get_account_id())
            .await
            .unwrap();
        println!("{}", ytm.compute_sapi_hash());
        let search = ytm
            .get_library(&Endpoint::MusicLibraryLanding, 0)
            .await
            .unwrap();
        assert_eq!(search.is_empty(), false);
        println!("{:?}", search[1]);
        println!("{:?}", ytm.get_playlist(&search[1], 0).await.unwrap());
    });
}

#[test]
fn advanced_test() {
    use tokio::runtime::Runtime;
    Runtime::new().unwrap().block_on(async {
        let ytm = YoutubeMusicInstance::new(get_headers(), get_account_id())
            .await
            .unwrap();
        let search = ytm.search("j'ai la danse qui va avec", 0).await.unwrap();
        assert_eq!(search.videos.is_empty(), false);
        assert_eq!(search.playlists.is_empty(), false);
        let playlist_contents = ytm.get_playlist(&search.playlists[1], 0).await.unwrap();
        println!("{:?}", playlist_contents);
    });
}

#[test]
fn home_test() {
    use tokio::runtime::Runtime;
    Runtime::new().unwrap().block_on(async {
        let ytm = YoutubeMusicInstance::new(get_headers(), get_account_id())
            .await
            .unwrap();
        let search = ytm.get_home(0).await.unwrap();
        println!("{:?}", search.playlists);
        assert_eq!(search.playlists.is_empty(), false);
        let playlist_contents = ytm.get_playlist(&search.playlists[0], 0).await.unwrap();
        println!("{:?}", playlist_contents);
    });
}

#[derive(Debug, Clone, PartialOrd, Eq, Ord, PartialEq, Hash, Serialize, Deserialize)]
pub struct YoutubeMusicPlaylistRef {
    pub name: String,
    pub subtitle: String,
    pub browse_id: String,
}

pub struct YoutubeMusicInstance {
    sapisid: String,
    innertube_api_key: String,
    client_version: String,
    cookies: String,
    account_id: Option<String>,
}

impl YoutubeMusicInstance {
    pub async fn from_header_file(path: &Path) -> Result<Self> {
        let mut headers = HeaderMap::new();
        for header in tokio::fs::read_to_string(path)
            .await
            .map_err(YoutubeMusicError::IoError)?
            .lines()
        {
            if let Some((key, value)) = header.split_once(": ") {
                headers.insert(
                    match key.to_lowercase().as_str() {
                        "cookie" => reqwest::header::COOKIE,
                        "user-agent" => reqwest::header::USER_AGENT,
                        _ => {
                            #[cfg(test)]
                            println!("Unknown header key: {key}");
                            continue;
                        }
                    },
                    value.parse().unwrap(),
                );
            }
        }
        if !headers.contains_key(reqwest::header::COOKIE) {
            return Err(YoutubeMusicError::InvalidHeaders);
        }
        if !headers.contains_key(reqwest::header::USER_AGENT) {
            headers.insert(
                reqwest::header::USER_AGENT,
                "Mozilla/5.0 (X11; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0"
                    .parse()
                    .unwrap(),
            );
        }
        let account_path = path
            .parent()
            .unwrap_or(Path::new("../"))
            .join("account_id.txt");
        let account_id = match tokio::fs::read_to_string(account_path).await {
            Ok(mut id) => {
                if id.ends_with("\n") {
                    id.pop();
                    if id.ends_with("\r") {
                        id.pop();
                    }
                }
                Some(id)
            }
            Err(_) => None, //don't care if there is no files or nothing in the file
        };
        Self::new(headers, account_id).await
    }

    pub async fn new(headers: HeaderMap, account_id: Option<String>) -> Result<Self> {
        trace!("Creating new YoutubeMusicInstance");
        let rest_client = reqwest::ClientBuilder::default()
            .default_headers(headers.clone())
            .build()
            .map_err(YoutubeMusicError::RequestError)?;
        trace!("Fetching YoutubeMusic homepage");
        let response: String = rest_client
            .get(YTM_DOMAIN)
            .headers(headers.clone())
            .send()
            .await
            .map_err(YoutubeMusicError::RequestError)?
            .text()
            .await
            .map_err(YoutubeMusicError::RequestError)?;
        trace!("Fetched");

        if response.contains("<base href=\"https://accounts.google.com/v3/signin/\">")
            || response.contains("<base href=\"https://consent.youtube.com/\">")
        {
            error!("Need to login");
            return Err(YoutubeMusicError::NeedToLogin);
        }
        trace!("Parsing cookies");
        let cookies = headers
            .get("Cookie")
            .ok_or(YoutubeMusicError::NoCookieAttribute)?;
        let cookies_bytes = cookies.as_bytes();
        let cookies = String::from_utf8(cookies_bytes.to_vec())
            .map_err(YoutubeMusicError::InvalidCookie)?
            .to_string();
        let sapisid = cookies
            .between("SAPISID=", ";")
            .ok_or_else(|| YoutubeMusicError::NoSapsidInCookie)?;
        trace!("Cookies parsed! SAPISID: {}", sapisid);
        let innertube_api_key = response
            .between("INNERTUBE_API_KEY\":\"", "\"")
            .ok_or_else(|| YoutubeMusicError::CantFindInnerTubeApiKey(response.to_string()))?;
        trace!("Innertube API key: {}", innertube_api_key);
        let client_version = response
            .between("INNERTUBE_CLIENT_VERSION\":\"", "\"")
            .ok_or_else(|| {
                YoutubeMusicError::CantFindInnerTubeClientVersion(response.to_string())
            })?;
        trace!("Innertube client version: {}", client_version);
        // New file for brand accounts, maybe put it in config or headers.txt is better but more complex.
        trace!("account id {:?}", account_id);
        Ok(Self {
            sapisid: sapisid.to_string(),
            innertube_api_key: innertube_api_key.to_string(),
            client_version: client_version.to_string(),
            cookies,
            account_id,
        })
    }
    fn compute_sapi_hash(&self) -> String {
        let start = SystemTime::now();
        let since_the_epoch = start
            .duration_since(UNIX_EPOCH)
            .expect("Time went backwards");
        let timestamp = since_the_epoch.as_secs();
        let mut hasher = Sha1::new();
        hasher.update(format!("{timestamp} {} {YTM_DOMAIN}", self.sapisid));
        let result = hasher.finalize();
        let mut hex = String::with_capacity(40);
        for byte in result {
            hex.push_str(&format!("{byte:02x}"));
        }
        trace!("Computed SAPI Hash{timestamp}_{hex}");
        format!("{timestamp}_{hex}")
    }
    async fn browse_continuation(
        &self,
        continuation: &Continuation,
        continuations: bool,
    ) -> Result<(Value, Vec<Continuation>)> {
        let playlist_json: Value =
            serde_json::from_str(&self.browse_continuation_raw(continuation).await?)
                .map_err(YoutubeMusicError::SerdeJson)?;
        debug!("Browse continuation response: {playlist_json}");
        if playlist_json.get("error").is_some() {
            error!("Error in browse_continuation");
            error!("{:?}", playlist_json);
            return Err(YoutubeMusicError::YoutubeMusicError(playlist_json));
        }
        let continuation = if continuations {
            from_json(&playlist_json, get_continuation)?
        } else {
            Vec::new()
        };
        Ok((playlist_json, continuation))
    }
    async fn browse_continuation_raw(
        &self,
        Continuation {
            continuation,
            click_tracking_params,
        }: &Continuation,
    ) -> Result<String> {
        trace!("Browse continuation {continuation}");
        let url = format!(
            "https://music.youtub
Download .txt
gitextract_sweneqjg/

├── .github/
│   └── workflows/
│       ├── check.yml
│       ├── ci.yml
│       ├── clippy.yml
│       ├── fmt.yml
│       ├── release.yml
│       ├── test-linux.yml
│       ├── test-macos.yml
│       └── test-windows.yml
├── .gitignore
├── Cargo.toml
├── LICENCE
├── README.md
└── crates/
    ├── common-structs/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── app_status.rs
    │       ├── lib.rs
    │       └── music_download_status.rs
    ├── database/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       ├── reader.rs
    │       └── writer.rs
    ├── download-manager/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── lib.rs
    │       └── task.rs
    ├── player/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── error.rs
    │       ├── lib.rs
    │       ├── player.rs
    │       ├── player_data.rs
    │       └── player_options.rs
    ├── ytermusic/
    │   ├── Cargo.toml
    │   └── src/
    │       ├── config.rs
    │       ├── consts.rs
    │       ├── database.rs
    │       ├── errors.rs
    │       ├── main.rs
    │       ├── shutdown.rs
    │       ├── structures/
    │       │   ├── app_status.rs
    │       │   ├── media.rs
    │       │   ├── mod.rs
    │       │   ├── performance.rs
    │       │   └── sound_action.rs
    │       ├── systems/
    │       │   ├── logger.rs
    │       │   ├── mod.rs
    │       │   └── player.rs
    │       ├── tasks/
    │       │   ├── api.rs
    │       │   ├── clean.rs
    │       │   ├── last_playlist.rs
    │       │   ├── local_musics.rs
    │       │   └── mod.rs
    │       ├── term/
    │       │   ├── device_lost.rs
    │       │   ├── item_list.rs
    │       │   ├── list_selector.rs
    │       │   ├── mod.rs
    │       │   ├── music_player.rs
    │       │   ├── playlist.rs
    │       │   ├── playlist_view.rs
    │       │   ├── search.rs
    │       │   └── vertical_gauge.rs
    │       └── utils.rs
    └── ytpapi2/
        ├── Cargo.toml
        └── src/
            ├── json_extractor.rs
            ├── lib.rs
            └── string_utils.rs
Download .txt
SYMBOL INDEX (350 symbols across 39 files)

FILE: crates/common-structs/src/app_status.rs
  type AppStatus (line 2) | pub enum AppStatus {

FILE: crates/common-structs/src/music_download_status.rs
  type MusicDownloadStatus (line 2) | pub enum MusicDownloadStatus {
    method character (line 10) | pub fn character(&self, playing: Option<bool>) -> String {

FILE: crates/database/src/lib.rs
  type YTLocalDatabase (line 9) | pub struct YTLocalDatabase {
    method new (line 15) | pub fn new(cache_dir: PathBuf) -> Self {
    method clone_from (line 22) | pub fn clone_from(&self, videos: &Vec<YoutubeMusicVideoRef>) {
    method remove_video (line 26) | pub fn remove_video(&self, video: &YoutubeMusicVideoRef) {
    method append (line 33) | pub fn append(&self, video: YoutubeMusicVideoRef) {

FILE: crates/database/src/reader.rs
  method read (line 9) | pub fn read(&self) -> Option<Vec<YoutubeMusicVideoRef>> {
  function read_video (line 20) | fn read_video(buffer: &mut Cursor<Vec<u8>>) -> Option<YoutubeMusicVideoR...
  function read_str (line 31) | fn read_str(cursor: &mut Cursor<Vec<u8>>) -> Option<String> {
  function read_u32 (line 38) | fn read_u32(cursor: &mut Cursor<Vec<u8>>) -> Option<u32> {

FILE: crates/database/src/writer.rs
  method write (line 9) | pub fn write(&self) {
  method fix_db (line 24) | pub fn fix_db(&self) {
  function write_video (line 130) | pub fn write_video(buffer: &mut impl Write, video: &YoutubeMusicVideoRef) {
  function write_str (line 139) | fn write_str(cursor: &mut impl Write, value: &str) {
  function write_u32 (line 145) | fn write_u32(cursor: &mut impl Write, value: u32) {

FILE: crates/download-manager/src/lib.rs
  type MessageHandler (line 16) | pub type MessageHandler = Arc<dyn Fn(DownloadManagerMessage) + Send + Sy...
  type DownloadManagerMessage (line 18) | pub enum DownloadManagerMessage {
  type DownloadManager (line 22) | pub struct DownloadManager {
    method new (line 32) | pub fn new(
    method remove_from_in_downloads (line 47) | pub fn remove_from_in_downloads(&self, video: &String) {
    method take (line 51) | fn take(&self) -> Option<YoutubeMusicVideoRef> {
    method run_service_stream (line 59) | pub fn run_service_stream(
    method spawn_system (line 82) | pub fn spawn_system(
    method clean (line 92) | pub fn clean(
    method set_download_list (line 109) | pub fn set_download_list(&self, to_add: impl IntoIterator<Item = Youtu...
    method add_to_download_list (line 115) | pub fn add_to_download_list(&self, to_add: impl IntoIterator<Item = Yo...

FILE: crates/download-manager/src/task.rs
  function new_video_with_id (line 12) | fn new_video_with_id(id: &str) -> Result<Video<'_>, VideoError> {
  function download (line 31) | pub async fn download<P: AsRef<std::path::Path>>(
  method handle_download (line 72) | async fn handle_download(&self, id: &str, sender: MessageHandler) -> Res...
  method start_download (line 89) | pub async fn start_download(&self, song: YoutubeMusicVideoRef, s: Messag...
  method start_task_unary (line 142) | pub fn start_task_unary(
  function video_download_test (line 162) | async fn video_download_test() {

FILE: crates/player/src/error.rs
  type PlayError (line 3) | pub enum PlayError {
    method from (line 12) | fn from(err: rodio::PlayError) -> Self {

FILE: crates/player/src/player.rs
  type Player (line 11) | pub struct Player {
    method try_from_device (line 20) | fn try_from_device(device: rodio::cpal::Device) -> Result<OutputStream...
    method try_default (line 29) | fn try_default() -> Result<OutputStream, PlayError> {
    method new (line 49) | pub fn new(error_sender: Sender<PlayError>, options: PlayerOptions) ->...
    method update (line 66) | pub fn update(&self) -> Result<Self, PlayError> {
    method change_volume (line 80) | pub fn change_volume(&mut self, positive: bool) {
    method is_finished (line 85) | pub fn is_finished(&self) -> bool {
    method play_at (line 89) | pub fn play_at(&mut self, path: &Path, time: Duration) -> Result<(), P...
    method play (line 100) | pub fn play(&mut self, path: &Path) -> Result<(), PlayError> {
    method stop (line 131) | pub fn stop(&mut self) {
    method elapsed (line 138) | pub fn elapsed(&self) -> Duration {
    method duration (line 142) | pub fn duration(&self) -> Option<f64> {
    method toggle_playback (line 148) | pub fn toggle_playback(&mut self) {
    method seek_fw (line 156) | pub fn seek_fw(&mut self) {
    method seek_bw (line 163) | pub fn seek_bw(&mut self) {
    method seek_to (line 169) | pub fn seek_to(&mut self, time: Duration) {
    method percentage (line 192) | pub fn percentage(&self) -> f64 {
    method volume_percent (line 199) | pub fn volume_percent(&self) -> u8 {
    method volume (line 203) | pub fn volume(&self) -> i32 {
    method volume_up (line 207) | pub fn volume_up(&mut self) {
    method volume_down (line 212) | pub fn volume_down(&mut self) {
    method set_volume (line 217) | pub fn set_volume(&mut self, mut volume: i32) {
    method pause (line 223) | pub fn pause(&mut self) {
    method resume (line 229) | pub fn resume(&mut self) {
    method is_paused (line 235) | pub fn is_paused(&self) -> bool {
    method seek (line 239) | pub fn seek(&mut self, secs: i64) {
    method get_progress (line 247) | pub fn get_progress(&self) -> (f64, u32, u32) {

FILE: crates/player/src/player_data.rs
  type PlayerData (line 6) | pub struct PlayerData {
    method new (line 13) | pub fn new(volume: u8) -> Self {
    method change_volume (line 22) | pub fn change_volume(&mut self, positive: bool) {
    method volume_f32 (line 31) | pub fn volume_f32(&self) -> f32 {
    method volume (line 36) | pub fn volume(&self) -> u8 {
    method set_volume (line 41) | pub fn set_volume(&mut self, volume: u8) {
    method total_duration (line 46) | pub fn total_duration(&self) -> Option<Duration> {
    method set_total_duration (line 51) | pub fn set_total_duration(&mut self, total_duration: Option<Duration>) {
    method current_file (line 56) | pub fn current_file(&self) -> Option<PathBuf> {
    method set_current_file (line 61) | pub fn set_current_file(&mut self, current_file: Option<PathBuf>) {

FILE: crates/player/src/player_options.rs
  type PlayerOptions (line 2) | pub struct PlayerOptions {
    method new (line 8) | pub fn new(initial_volume: u8) -> Self {
    method initial_volume (line 15) | pub fn initial_volume(&self) -> u8 {
    method initial_volume_f32 (line 20) | pub fn initial_volume_f32(&self) -> f32 {

FILE: crates/ytermusic/src/config.rs
  type GlobalConfig (line 9) | pub struct GlobalConfig {
  type MusicPlayerConfig (line 20) | pub struct MusicPlayerConfig {
  type StyleDef (line 60) | struct StyleDef {
  method default (line 74) | fn default() -> Self {
  function default_searching_style (line 96) | fn default_searching_style() -> Style {
  function default_error_style (line 100) | fn default_error_style() -> Style {
  function parallel_downloads (line 104) | fn parallel_downloads() -> u16 {
  function default_false (line 108) | fn default_false() -> bool {
  function default_true (line 112) | fn default_true() -> bool {
  function enable_volume_slider (line 116) | fn enable_volume_slider() -> bool {
  function default_paused_style (line 120) | fn default_paused_style() -> Style {
  function default_playing_style (line 124) | fn default_playing_style() -> Style {
  function default_nomusic_style (line 128) | fn default_nomusic_style() -> Style {
  function default_downloading_style (line 132) | fn default_downloading_style() -> Style {
  function default_volume (line 136) | fn default_volume() -> u8 {
  type PlaylistConfig (line 142) | pub struct PlaylistConfig {}
  type SearchConfig (line 146) | pub struct SearchConfig {}
  type Config (line 151) | pub struct Config {
    method new (line 163) | pub fn new() -> Self {

FILE: crates/ytermusic/src/consts.rs
  constant HEADER_TUTORIAL (line 8) | pub const HEADER_TUTORIAL: &str = r#"To configure the YTerMusic:
  constant INTRODUCTION (line 29) | pub const INTRODUCTION: &str = r#"Usage: ytermusic [options]

FILE: crates/ytermusic/src/errors.rs
  function handle_error_option (line 6) | pub fn handle_error_option<T, E>(
  function handle_error (line 32) | pub fn handle_error<T>(updater: &Sender<ManagerMessage>, error_type: &'s...

FILE: crates/ytermusic/src/main.rs
  function run_service (line 45) | fn run_service<T>(future: T) -> tokio::task::JoinHandle<()>
  function try_get_cookies (line 59) | pub fn try_get_cookies() -> Option<String> {
  function main (line 64) | fn main() {
  function cookies (line 133) | fn cookies(specific_browser: Option<String>) -> Option<String> {
  function get_header_file (line 191) | fn get_header_file() -> Result<(String, PathBuf), (std::io::Error, PathB...
  function app_start_main (line 215) | async fn app_start_main(updater_r: Receiver<ManagerMessage>, updater_s: ...
  function app_start (line 255) | fn app_start() {

FILE: crates/ytermusic/src/shutdown.rs
  type SharedEvent (line 12) | pub struct SharedEvent {
    method new (line 19) | pub const fn new() -> Self {
    method wait (line 27) | pub fn wait(&self) {
    method notify (line 35) | pub fn notify(&self) {
  function block_until_shutdown (line 46) | pub fn block_until_shutdown() {
  function is_shutdown_sent (line 52) | pub fn is_shutdown_sent() -> bool {
  type ShutdownSignal (line 57) | pub struct ShutdownSignal;
  type Output (line 60) | type Output = ();
  method poll (line 62) | fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Outpu...
  function shutdown (line 71) | pub fn shutdown() {

FILE: crates/ytermusic/src/structures/app_status.rs
  type MusicDownloadStatusExt (line 4) | pub trait MusicDownloadStatusExt {
    method style (line 5) | fn style(&self, playing: Option<bool>) -> Style;
    method style (line 15) | fn style(&self, playing: Option<bool>) -> Style {
  type AppStatusExt (line 8) | pub trait AppStatusExt {
    method style (line 9) | fn style(&self) -> Style;
    method style (line 41) | fn style(&self) -> Style {

FILE: crates/ytermusic/src/structures/media.rs
  type Media (line 16) | pub struct Media {
    method new (line 24) | pub fn new(updater: Sender<ManagerMessage>, soundaction_sender: Sender...
    method update (line 48) | pub fn update(
  function connect (line 101) | fn connect(mpris: &mut MediaControls, sender: Sender<SoundAction>) -> Re...
  function get_handle (line 151) | fn get_handle(updater: &Sender<ManagerMessage>) -> Option<MediaControls> {
  function run_window_handler (line 166) | pub fn run_window_handler(_updater: &Sender<ManagerMessage>) -> Option<(...
  function run_window_handler (line 182) | pub fn run_window_handler(updater: &Sender<ManagerMessage>) -> Option<()> {
  function get_handle (line 219) | fn get_handle(updater: &Sender<ManagerMessage>) -> Option<MediaControls> {

FILE: crates/ytermusic/src/structures/performance.rs
  type Performance (line 6) | pub struct Performance {
    method new (line 11) | pub fn new() -> Self {
    method get_ms (line 17) | pub fn get_ms(&self) -> u128 {
    method log (line 21) | pub fn log(&self, message: &str) {
  function guard (line 26) | pub fn guard<'a>(name: &'a str) -> PerformanceGuard<'a> {
  type PerformanceGuard (line 30) | pub struct PerformanceGuard<'a> {
  function new (line 36) | pub fn new(name: &'a str) -> Self {
  method drop (line 45) | fn drop(&mut self) {
  function mesure (line 51) | pub fn mesure<T>(name: &str, f: impl FnOnce() -> T) -> T {

FILE: crates/ytermusic/src/structures/sound_action.rs
  type SoundAction (line 17) | pub enum SoundAction {
    method insert (line 39) | fn insert(player: &mut PlayerState, video: String, status: MusicDownlo...
    method apply_sound_action (line 56) | pub fn apply_sound_action(self, player: &mut PlayerState) {
  function download_manager_handler (line 181) | pub fn download_manager_handler(sender: Sender<SoundAction>) -> MessageH...

FILE: crates/ytermusic/src/systems/logger.rs
  function get_log_file_path (line 28) | pub fn get_log_file_path() -> PathBuf {
  function init (line 53) | pub fn init() -> Result<(), SetLoggerError> {
  type SimpleLogger (line 65) | struct SimpleLogger;
    method enabled (line 68) | fn enabled(&self, metadata: &Metadata) -> bool {
    method log (line 72) | fn log(&self, record: &Record) {
    method flush (line 87) | fn flush(&self) {}

FILE: crates/ytermusic/src/systems/player.rs
  type PlayerState (line 22) | pub struct PlayerState {
    method new (line 38) | fn new(
    method current (line 69) | pub fn current(&self) -> Option<&YoutubeMusicVideoRef> {
    method relative_current (line 73) | pub fn relative_current(&self, n: isize) -> Option<&YoutubeMusicVideoR...
    method set_relative_current (line 77) | pub fn set_relative_current(&mut self, n: isize) {
    method is_current_download_failed (line 81) | pub fn is_current_download_failed(&self) -> bool {
    method is_current_downloaded (line 90) | pub fn is_current_downloaded(&self) -> bool {
    method update (line 97) | pub fn update(&mut self) {
    method handle_stream_errors (line 181) | fn handle_stream_errors(&self) {
    method update_controls (line 187) | fn update_controls(&mut self) {
  function player_system (line 197) | pub fn player_system(updater: Sender<ManagerMessage>) -> (Sender<SoundAc...

FILE: crates/ytermusic/src/tasks/api.rs
  function get_text_cookies_expired_or_invalid (line 16) | pub fn get_text_cookies_expired_or_invalid() -> String {
  function spawn_api_task (line 24) | pub fn spawn_api_task(updater_s: Sender<ManagerMessage>) {
  function spawn_browse_playlist_task (line 127) | fn spawn_browse_playlist_task(

FILE: crates/ytermusic/src/tasks/clean.rs
  function spawn_clean_task (line 5) | pub fn spawn_clean_task() {

FILE: crates/ytermusic/src/tasks/last_playlist.rs
  function spawn_last_playlist_task (line 12) | pub fn spawn_last_playlist_task(updater_s: Sender<ManagerMessage>) {

FILE: crates/ytermusic/src/tasks/local_musics.rs
  function spawn_local_musics_task (line 14) | pub fn spawn_local_musics_task(updater_s: Sender<ManagerMessage>) {
  function shuffle_and_send (line 39) | fn shuffle_and_send(mut videos: Vec<YoutubeMusicVideoRef>, updater_s: &S...

FILE: crates/ytermusic/src/term/device_lost.rs
  type DeviceLost (line 13) | pub struct DeviceLost(pub Vec<String>, pub Option<ManagerMessage>);
  method on_mouse_press (line 16) | fn on_mouse_press(&mut self, _: crossterm::event::MouseEvent, _: &Rect) ...
  method on_key_press (line 20) | fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
  method render (line 36) | fn render(&mut self, frame: &mut Frame) {
  method handle_global_message (line 55) | fn handle_global_message(&mut self, m: ManagerMessage) -> EventResponse {
  method close (line 66) | fn close(&mut self, _: Screens) -> EventResponse {
  method open (line 71) | fn open(&mut self) -> EventResponse {

FILE: crates/ytermusic/src/term/item_list.rs
  type ListItemAction (line 12) | pub trait ListItemAction {
    method render_style (line 13) | fn render_style(&self, string: &str, selected: bool) -> Style;
  type ListItem (line 16) | pub struct ListItem<Action> {
  method default (line 23) | fn default() -> Self {
  function new (line 33) | pub fn new(title: String) -> Self {
  function on_mouse_press (line 41) | pub fn on_mouse_press(
  function on_key_press (line 68) | pub fn on_key_press(&mut self, key: KeyEvent) -> Option<&Action> {
  function get_item_frame (line 82) | pub fn get_item_frame(&self, height: usize) -> Vec<(usize, &(String, Act...
  function click_on (line 98) | pub fn click_on(&mut self, y_position: usize, height: usize) -> Option<(...
  function select (line 106) | pub fn select(&self) -> Option<&Action> {
  function select_down (line 112) | pub fn select_down(&mut self) {
  function select_up (line 120) | pub fn select_up(&mut self) {
  function select_to (line 128) | pub fn select_to(&mut self, position: usize) {
  function update (line 132) | pub fn update(&mut self, list: Vec<(String, Action)>, current: usize) {
  function update_contents (line 137) | pub fn update_contents(&mut self, list: Vec<(String, Action)>) {
  function clear (line 141) | pub fn clear(&mut self) {
  function add_element (line 146) | pub fn add_element(&mut self, element: (String, Action)) {
  function set_title (line 150) | pub fn set_title(&mut self, a: String) {
  function current_position (line 154) | pub fn current_position(&self) -> usize {
  method render (line 160) | fn render(self, area: Rect, buf: &mut Buffer) {

FILE: crates/ytermusic/src/term/list_selector.rs
  type ListSelector (line 10) | pub struct ListSelector {
    method get_item_frame (line 17) | pub fn get_item_frame(&self, height: usize) -> (usize, usize) {
    method get_relative_position (line 31) | pub fn get_relative_position(&self) -> isize {
    method is_scrolling (line 43) | pub fn is_scrolling(&self) -> bool {
    method click_on (line 47) | pub fn click_on(&mut self, y_position: usize, height: usize) -> Option...
    method play (line 55) | pub fn play(&mut self) -> Option<usize> {
    method scroll_down (line 60) | pub fn scroll_down(&mut self) {
    method scroll_up (line 64) | pub fn scroll_up(&mut self) {
    method scroll_to (line 68) | pub fn scroll_to(&mut self, position: usize) {
    method select (line 72) | pub fn select(&self) -> Option<usize> {
    method update (line 80) | pub fn update(&mut self, list_size: usize, current: usize) {
    method render (line 90) | pub fn render(

FILE: crates/ytermusic/src/term/mod.rs
  type Screen (line 36) | pub trait Screen {
    method on_mouse_press (line 37) | fn on_mouse_press(&mut self, mouse_event: MouseEvent, frame_data: &Rec...
    method on_key_press (line 38) | fn on_key_press(&mut self, mouse_event: KeyEvent, frame_data: &Rect) -...
    method render (line 39) | fn render(&mut self, frame: &mut Frame);
    method handle_global_message (line 40) | fn handle_global_message(&mut self, message: ManagerMessage) -> EventR...
    method close (line 41) | fn close(&mut self, new_screen: Screens) -> EventResponse;
    method open (line 42) | fn open(&mut self) -> EventResponse;
  type EventResponse (line 46) | pub enum EventResponse {
  type ManagerMessage (line 53) | pub enum ManagerMessage {
    method pass_to (line 68) | pub fn pass_to(self, screen: Screens) -> Self {
    method event (line 71) | pub fn event(self) -> EventResponse {
  type Screens (line 79) | pub enum Screens {
  type Manager (line 88) | pub struct Manager {
    method new (line 98) | pub async fn new(action_sender: Sender<SoundAction>, music_player: Pla...
    method current_screen (line 117) | pub fn current_screen(&mut self) -> &mut dyn Screen {
    method get_screen (line 120) | pub fn get_screen(&mut self, screen: Screens) -> &mut dyn Screen {
    method set_current_screen (line 129) | pub fn set_current_screen(&mut self, screen: Screens) {
    method handle_event (line 134) | pub fn handle_event(&mut self, event: EventResponse) -> bool {
    method handle_manager_message (line 147) | pub fn handle_manager_message(&mut self, e: ManagerMessage) -> bool {
    method run (line 193) | pub fn run(&mut self, updater: &Receiver<ManagerMessage>) -> Result<()...
  function split_y_start (line 268) | pub fn split_y_start(f: Rect, start_size: u16) -> [Rect; 2] {
  function split_y (line 276) | pub fn split_y(f: Rect, end_size: u16) -> [Rect; 2] {
  function split_x (line 284) | pub fn split_x(f: Rect, end_size: u16) -> [Rect; 2] {
  function rect_contains (line 293) | pub fn rect_contains(rect: &Rect, x: u16, y: u16, margin: u16) -> bool {
  function relative_pos (line 300) | pub fn relative_pos(rect: &Rect, x: u16, y: u16, margin: u16) -> (u16, u...

FILE: crates/ytermusic/src/term/music_player.rs
  method activate (line 23) | pub fn activate(&mut self, index: usize) {
  method on_mouse_press (line 38) | fn on_mouse_press(
  method on_key_press (line 93) | fn on_key_press(&mut self, key: KeyEvent, _: &ratatui::layout::Rect) -> ...
  method render (line 175) | fn render(&mut self, f: &mut ratatui::Frame) {
  method handle_global_message (line 263) | fn handle_global_message(&mut self, message: ManagerMessage) -> EventRes...
  method close (line 273) | fn close(&mut self, _: Screens) -> EventResponse {
  method open (line 277) | fn open(&mut self) -> EventResponse {

FILE: crates/ytermusic/src/term/playlist.rs
  type ChooserAction (line 22) | pub enum ChooserAction {
  method render_style (line 27) | fn render_style(&self, _: &str, selected: bool) -> Style {
  type Chooser (line 36) | pub struct Chooser {
    method play (line 142) | fn play(&mut self, a: &PlayListEntry) {
    method add_element (line 159) | fn add_element(&mut self, element: (String, Vec<YoutubeMusicVideoRef>)) {
  type PlayListEntry (line 43) | pub struct PlayListEntry {
    method new (line 50) | pub fn new(name: String, videos: Vec<YoutubeMusicVideoRef>) -> Self {
    method tupplelize (line 58) | pub fn tupplelize(&self) -> (&String, &Vec<YoutubeMusicVideoRef>) {
  function format_playlist (line 62) | pub fn format_playlist(name: &str, videos: &[YoutubeMusicVideoRef]) -> S...
  method on_mouse_press (line 77) | fn on_mouse_press(
  method on_key_press (line 99) | fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
  method render (line 120) | fn render(&mut self, frame: &mut Frame) {
  method handle_global_message (line 124) | fn handle_global_message(&mut self, message: super::ManagerMessage) -> E...
  method close (line 131) | fn close(&mut self, _: Screens) -> EventResponse {
  method open (line 135) | fn open(&mut self) -> EventResponse {

FILE: crates/ytermusic/src/term/playlist_view.rs
  type PlayListAction (line 19) | pub struct PlayListAction(usize, bool);
  method render_style (line 22) | fn render_style(&self, _: &str, selected: bool) -> Style {
  type PlaylistView (line 38) | pub struct PlaylistView {
  method on_mouse_press (line 46) | fn on_mouse_press(&mut self, e: crossterm::event::MouseEvent, r: &Rect) ...
  method on_key_press (line 59) | fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
  method render (line 75) | fn render(&mut self, frame: &mut Frame) {
  method handle_global_message (line 79) | fn handle_global_message(&mut self, m: ManagerMessage) -> EventResponse {
  method close (line 106) | fn close(&mut self, _: Screens) -> EventResponse {
  method open (line 110) | fn open(&mut self) -> EventResponse {

FILE: crates/ytermusic/src/term/search.rs
  type Search (line 34) | pub struct Search {
    method new (line 227) | pub async fn new(action_sender: Sender<SoundAction>) -> Self {
    method execute_status (line 255) | pub fn execute_status(&self, e: Status, modifiers: KeyModifiers) -> Ev...
  type Status (line 43) | pub enum Status {
  method render_style (line 49) | fn render_style(&self, _: &str, selected: bool) -> Style {
  method on_mouse_press (line 64) | fn on_mouse_press(
  method on_key_press (line 82) | fn on_key_press(&mut self, key: KeyEvent, _: &Rect) -> EventResponse {
  method render (line 194) | fn render(&mut self, frame: &mut Frame) {
  method handle_global_message (line 214) | fn handle_global_message(&mut self, _: super::ManagerMessage) -> EventRe...
  method close (line 218) | fn close(&mut self, _: Screens) -> EventResponse {
  method open (line 222) | fn open(&mut self) -> EventResponse {

FILE: crates/ytermusic/src/term/vertical_gauge.rs
  type VerticalGauge (line 8) | pub struct VerticalGauge<'a> {
  method render (line 16) | fn render(mut self, area: Rect, buf: &mut Buffer) {
  method default (line 79) | fn default() -> VerticalGauge<'a> {
  function block (line 90) | pub fn block(mut self, block: Block<'a>) -> VerticalGauge<'a> {
  function ratio (line 96) | pub fn ratio(mut self, ratio: f64) -> VerticalGauge<'a> {
  function gauge_style (line 105) | pub fn gauge_style(mut self, style: Style) -> VerticalGauge<'a> {

FILE: crates/ytermusic/src/utils.rs
  function get_project_dirs (line 6) | pub fn get_project_dirs() -> Option<ProjectDirs> {
  function invert (line 10) | pub fn invert(style: Style) -> Style {
  function color_contrast (line 30) | pub fn color_contrast(color: Color) -> Color {
  function to_bidi_string (line 68) | pub fn to_bidi_string(s: &str) -> String {

FILE: crates/ytpapi2/src/json_extractor.rs
  function from_json (line 10) | pub(crate) fn from_json<T: PartialEq>(
  type YoutubeMusicVideoRef (line 44) | pub struct YoutubeMusicVideoRef {
  method fmt (line 53) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  function get_playlist (line 60) | pub(crate) fn get_playlist(value: &Value) -> Option<YoutubeMusicPlaylist...
  type Continuation (line 79) | pub struct Continuation {
  function get_continuation (line 84) | pub fn get_continuation(value: &Value) -> Option<Continuation> {
  function get_playlist_search (line 99) | pub fn get_playlist_search(value: &Value) -> Option<YoutubeMusicPlaylist...
  function extract_playlist_info (line 122) | pub fn extract_playlist_info(value: &Value) -> Option<(String, String)> {
  function get_video_from_album (line 136) | pub fn get_video_from_album(value: &Value) -> Option<YoutubeMusicVideoRe...
  function get_text (line 163) | fn get_text(value: &Value, text_clean: bool, dot: bool) -> Option<String> {
  function join_clean (line 190) | fn join_clean(strings: &[String], dot: bool) -> String {
  function get_videoid (line 200) | pub fn get_videoid(value: &Value) -> Option<String> {
  function get_video (line 214) | pub(crate) fn get_video(value: &Value) -> Option<YoutubeMusicVideoRef> {

FILE: crates/ytpapi2/src/lib.rs
  type Result (line 24) | pub type Result<T> = std::result::Result<T, YoutubeMusicError>;
  constant YTM_DOMAIN (line 26) | const YTM_DOMAIN: &str = "https://music.youtube.com";
  function get_headers (line 29) | fn get_headers() -> HeaderMap {
  function get_account_id (line 53) | fn get_account_id() -> Option<String> {
  function advanced_like (line 63) | fn advanced_like() {
  function advanced_test (line 81) | fn advanced_test() {
  function home_test (line 96) | fn home_test() {
  type YoutubeMusicPlaylistRef (line 111) | pub struct YoutubeMusicPlaylistRef {
  type YoutubeMusicInstance (line 117) | pub struct YoutubeMusicInstance {
    method from_header_file (line 126) | pub async fn from_header_file(path: &Path) -> Result<Self> {
    method new (line 178) | pub async fn new(headers: HeaderMap, account_id: Option<String>) -> Re...
    method compute_sapi_hash (line 234) | fn compute_sapi_hash(&self) -> String {
    method browse_continuation (line 250) | async fn browse_continuation(
    method browse_continuation_raw (line 271) | async fn browse_continuation_raw(
    method browse_raw (line 314) | async fn browse_raw(
    method browse (line 352) | async fn browse(
    method get_library (line 380) | pub async fn get_library(
    method get_playlist (line 410) | pub async fn get_playlist(
    method get_playlist_raw (line 418) | pub async fn get_playlist_raw(
    method search (line 455) | pub async fn search(
    method get_home (line 490) | pub async fn get_home(&self, mut n_continuations: usize) -> Result<Sea...
  function parse_playlist (line 522) | fn parse_playlist(playlist_json: &Value) -> Result<Vec<YoutubeMusicVideo...
  type SearchResults (line 543) | pub struct SearchResults {
  type Endpoint (line 549) | pub enum Endpoint {
    method get_key (line 558) | fn get_key(&self) -> String {
    method get_param (line 567) | fn get_param(&self) -> String {
    method get_route (line 576) | fn get_route(&self) -> String {
  type YoutubeMusicError (line 588) | pub enum YoutubeMusicError {

FILE: crates/ytpapi2/src/string_utils.rs
  type StringUtils (line 4) | pub trait StringUtils {
    method after (line 5) | fn after(&self, needle: &str) -> Option<&str>;
    method before (line 6) | fn before(&self, needle: &str) -> Option<&str>;
    method between (line 7) | fn between(&self, start: &str, end: &str) -> Option<&str>;
    method to_owned_ (line 9) | fn to_owned_(&self) -> Option<String>;
    method parse_ (line 10) | fn parse_<T: FromStr>(&self) -> Option<T>;
    method trim_ (line 11) | fn trim_(&self) -> Option<&str>;
    method after (line 15) | fn after(&self, needle: &str) -> Option<&str> {
    method before (line 19) | fn before(&self, needle: &str) -> Option<&str> {
    method between (line 23) | fn between(&self, start: &str, end: &str) -> Option<&str> {
    method to_owned_ (line 28) | fn to_owned_(&self) -> Option<String> {
    method parse_ (line 32) | fn parse_<T: FromStr>(&self) -> Option<T> {
    method trim_ (line 36) | fn trim_(&self) -> Option<&str> {
    method after (line 42) | fn after(&self, needle: &str) -> Option<&str> {
    method before (line 46) | fn before(&self, needle: &str) -> Option<&str> {
    method between (line 50) | fn between(&self, start: &str, end: &str) -> Option<&str> {
    method to_owned_ (line 55) | fn to_owned_(&self) -> Option<String> {
    method parse_ (line 59) | fn parse_<T: FromStr>(&self) -> Option<T> {
    method trim_ (line 63) | fn trim_(&self) -> Option<&str> {
    method after (line 69) | fn after(&self, needle: &str) -> Option<&str> {
    method before (line 73) | fn before(&self, needle: &str) -> Option<&str> {
    method between (line 77) | fn between(&self, start: &str, end: &str) -> Option<&str> {
    method to_owned_ (line 81) | fn to_owned_(&self) -> Option<String> {
    method parse_ (line 85) | fn parse_<E: FromStr>(&self) -> Option<E> {
    method trim_ (line 89) | fn trim_(&self) -> Option<&str> {
Condensed preview — 63 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (217K chars).
[
  {
    "path": ".github/workflows/check.yml",
    "chars": 508,
    "preview": "name: Check\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  check:\n    name: Check\n    runs-on: ubun"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 773,
    "preview": "name: CI\n\non:\n  push:\n    branches: [ main, master ]\n  pull_request:\n    branches: [ main, master ]\n\nenv:\n  CARGO_TERM_C"
  },
  {
    "path": ".github/workflows/clippy.yml",
    "chars": 571,
    "preview": "name: Clippy\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  clippy:\n    name: Clippy\n    runs-on: u"
  },
  {
    "path": ".github/workflows/fmt.yml",
    "chars": 409,
    "preview": "name: Format\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  fmt:\n    name: Format Check\n    runs-on"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3232,
    "preview": "name: Release\n\non:\n  release:\n    types: [published]\n\njobs:\n  release:\n    name: Build and Release (${{ matrix.target }}"
  },
  {
    "path": ".github/workflows/test-linux.yml",
    "chars": 698,
    "preview": "name: Test Linux\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  test-linux:\n    name: Test Linux ($"
  },
  {
    "path": ".github/workflows/test-macos.yml",
    "chars": 377,
    "preview": "name: Test macOS\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  test-macos:\n    name: Test macOS (s"
  },
  {
    "path": ".github/workflows/test-windows.yml",
    "chars": 385,
    "preview": "name: Test Windows\n\non:\n  workflow_dispatch:\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  test-windows:\n    name: Test Wind"
  },
  {
    "path": ".gitignore",
    "chars": 175,
    "preview": "/target\n/data \nytermusic.exe\nheaders.txt\naccount_id.txt\n/player/target\n/ytpapi/target\n/ytpapi2/target\n/rustube/target\n/l"
  },
  {
    "path": "Cargo.toml",
    "chars": 409,
    "preview": "[workspace]\nresolver = \"3\"\nmembers = [\"crates/common-structs\",\"crates/database\", \"crates/download-manager\",\"crates/playe"
  },
  {
    "path": "LICENCE",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 6714,
    "preview": "# YTerMusic\n\n![YTeRMUSiC](./assets/banner/YTeRMUSiC.png \"YTeRMUSiC\")\n\nYTerMusic is a TUI based Youtube Music Player that"
  },
  {
    "path": "crates/common-structs/Cargo.toml",
    "chars": 85,
    "preview": "[package]\nname = \"common-structs\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n"
  },
  {
    "path": "crates/common-structs/src/app_status.rs",
    "chars": 96,
    "preview": "#[derive(PartialEq, Debug, Clone)]\npub enum AppStatus {\n    Paused,\n    Playing,\n    NoMusic,\n}\n"
  },
  {
    "path": "crates/common-structs/src/lib.rs",
    "chars": 127,
    "preview": "mod app_status;\nmod music_download_status;\n\npub use app_status::AppStatus;\npub use music_download_status::MusicDownloadS"
  },
  {
    "path": "crates/common-structs/src/music_download_status.rs",
    "chars": 667,
    "preview": "#[derive(PartialEq, Debug, Clone, Copy)]\npub enum MusicDownloadStatus {\n    NotDownloaded,\n    Downloaded,\n    Downloadi"
  },
  {
    "path": "crates/database/Cargo.toml",
    "chars": 154,
    "preview": "[package]\nname = \"database\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nvaruint = \"0.7.1\"\nytpapi2.workspace = tru"
  },
  {
    "path": "crates/database/src/lib.rs",
    "chars": 1122,
    "preview": "use std::{fs::OpenOptions, path::PathBuf, sync::RwLock};\n\nmod reader;\nmod writer;\n\npub use writer::write_video;\nuse ytpa"
  },
  {
    "path": "crates/database/src/reader.rs",
    "chars": 1191,
    "preview": "use std::io::{Cursor, Read};\n\nuse varuint::ReadVarint;\nuse ytpapi2::YoutubeMusicVideoRef;\n\nuse crate::YTLocalDatabase;\n\n"
  },
  {
    "path": "crates/database/src/writer.rs",
    "chars": 5768,
    "preview": "use std::{fs::OpenOptions, io::Write};\n\nuse varuint::WriteVarint;\nuse ytpapi2::YoutubeMusicVideoRef;\n\nuse crate::YTLocal"
  },
  {
    "path": "crates/download-manager/Cargo.toml",
    "chars": 528,
    "preview": "[package]\nname = \"download-manager\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nytpapi2.workspace = true\nflume = "
  },
  {
    "path": "crates/download-manager/src/lib.rs",
    "chars": 3443,
    "preview": "mod task;\n\nuse std::{\n    collections::{HashSet, VecDeque},\n    path::PathBuf,\n    sync::{Arc, Mutex},\n    time::Duratio"
  },
  {
    "path": "crates/download-manager/src/task.rs",
    "chars": 5848,
    "preview": "use std::sync::Arc;\n\nuse log::error;\nuse rusty_ytdl::{\n    DownloadOptions, Video, VideoError, VideoOptions, VideoQualit"
  },
  {
    "path": "crates/player/Cargo.toml",
    "chars": 325,
    "preview": "[package]\nname = \"player\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust-"
  },
  {
    "path": "crates/player/src/error.rs",
    "chars": 405,
    "preview": "// Custom Error Enum to handle different failures\n#[derive(Debug)]\npub enum PlayError {\n    Io(std::io::Error),\n    Deco"
  },
  {
    "path": "crates/player/src/lib.rs",
    "chars": 323,
    "preview": "mod error;\n\nuse std::time::Duration;\n\npub use error::PlayError;\n\nmod player;\npub use player::Player;\n\nmod player_options"
  },
  {
    "path": "crates/player/src/player.rs",
    "chars": 7352,
    "preview": "use flume::Sender;\nuse rodio::cpal::traits::HostTrait;\nuse rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Sou"
  },
  {
    "path": "crates/player/src/player_data.rs",
    "chars": 1744,
    "preview": "use std::{path::PathBuf, time::Duration};\n\nuse crate::VOLUME_STEP;\n\n#[derive(Clone)]\npub struct PlayerData {\n    total_d"
  },
  {
    "path": "crates/player/src/player_options.rs",
    "chars": 599,
    "preview": "#[derive(Debug, Clone)]\npub struct PlayerOptions {\n    initial_volume: u8,\n}\n\nimpl PlayerOptions {\n    /// Creates a new"
  },
  {
    "path": "crates/ytermusic/Cargo.toml",
    "chars": 1311,
    "preview": "[package]\nedition = \"2021\"\nname = \"ytermusic\"\nversion = \"0.1.0\"\n\n# See more keys and their definitions at https://doc.ru"
  },
  {
    "path": "crates/ytermusic/src/config.rs",
    "chars": 5826,
    "preview": "use log::info;\nuse ratatui::style::{Color, Modifier, Style};\nuse serde::{Deserialize, Serialize};\n\nuse crate::utils::get"
  },
  {
    "path": "crates/ytermusic/src/consts.rs",
    "chars": 2376,
    "preview": "use std::path::PathBuf;\n\nuse log::warn;\nuse once_cell::sync::Lazy;\n\nuse crate::{config, utils::get_project_dirs};\n\npub c"
  },
  {
    "path": "crates/ytermusic/src/database.rs",
    "chars": 190,
    "preview": "use database::YTLocalDatabase;\nuse once_cell::sync::Lazy;\n\nuse crate::consts::CACHE_DIR;\n\npub static DATABASE: Lazy<YTLo"
  },
  {
    "path": "crates/ytermusic/src/errors.rs",
    "chars": 901,
    "preview": "use flume::Sender;\n\nuse crate::term::{ManagerMessage, Screens};\n\n/// Utils to handle errors\npub fn handle_error_option<T"
  },
  {
    "path": "crates/ytermusic/src/main.rs",
    "chars": 9201,
    "preview": "use consts::{CACHE_DIR, INTRODUCTION};\nuse flume::{Receiver, Sender};\nuse log::{error, info};\nuse once_cell::sync::Lazy;"
  },
  {
    "path": "crates/ytermusic/src/shutdown.rs",
    "chars": 1657,
    "preview": "use std::{\n    future::Future,\n    pin::Pin,\n    sync::atomic::{AtomicBool, Ordering},\n    task::{Context, Poll},\n};\n\nus"
  },
  {
    "path": "crates/ytermusic/src/structures/app_status.rs",
    "chars": 1423,
    "preview": "use common_structs::{AppStatus, MusicDownloadStatus};\nuse ratatui::style::{Modifier, Style};\n\npub trait MusicDownloadSta"
  },
  {
    "path": "crates/ytermusic/src/structures/media.rs",
    "chars": 8924,
    "preview": "use std::time::Duration;\n\nuse flume::Sender;\nuse log::{error, info};\nuse player::Player;\nuse souvlaki::{\n    Error, Medi"
  },
  {
    "path": "crates/ytermusic/src/structures/mod.rs",
    "chars": 78,
    "preview": "pub mod app_status;\npub mod media;\npub mod performance;\npub mod sound_action;\n"
  },
  {
    "path": "crates/ytermusic/src/structures/performance.rs",
    "chars": 1251,
    "preview": "use std::time::Instant;\n\nuse log::info;\nuse once_cell::sync::Lazy;\n\npub struct Performance {\n    pub initial: Instant,\n}"
  },
  {
    "path": "crates/ytermusic/src/structures/sound_action.rs",
    "chars": 7149,
    "preview": "use common_structs::MusicDownloadStatus;\nuse download_manager::{DownloadManagerMessage, MessageHandler};\nuse flume::Send"
  },
  {
    "path": "crates/ytermusic/src/systems/logger.rs",
    "chars": 2429,
    "preview": "use std::{io::Write, path::PathBuf};\n\nuse flume::Sender;\nuse once_cell::sync::Lazy;\n\nstatic LOG: Lazy<Sender<String>> = "
  },
  {
    "path": "crates/ytermusic/src/systems/mod.rs",
    "chars": 362,
    "preview": "use download_manager::DownloadManager;\nuse once_cell::sync::Lazy;\n\nuse crate::{\n    consts::{CACHE_DIR, CONFIG},\n    DAT"
  },
  {
    "path": "crates/ytermusic/src/systems/player.rs",
    "chars": 7181,
    "preview": "use std::{\n    collections::{HashMap, VecDeque},\n    sync::atomic::Ordering,\n};\n\nuse common_structs::MusicDownloadStatus"
  },
  {
    "path": "crates/ytermusic/src/tasks/api.rs",
    "chars": 6705,
    "preview": "use std::sync::{Arc, Mutex};\n\nuse flume::Sender;\nuse log::{error, info};\nuse once_cell::sync::Lazy;\nuse tokio::task::Joi"
  },
  {
    "path": "crates/ytermusic/src/tasks/clean.rs",
    "chars": 747,
    "preview": "use crate::{consts::CACHE_DIR, run_service, structures::performance};\n\n/// This function is called on start to clean the"
  },
  {
    "path": "crates/ytermusic/src/tasks/last_playlist.rs",
    "chars": 909,
    "preview": "use flume::Sender;\nuse log::info;\nuse ytpapi2::YoutubeMusicVideoRef;\n\nuse crate::{\n    consts::CACHE_DIR,\n    run_servic"
  },
  {
    "path": "crates/ytermusic/src/tasks/local_musics.rs",
    "chars": 1573,
    "preview": "use flume::Sender;\nuse log::info;\nuse rand::seq::SliceRandom;\nuse ytpapi2::YoutubeMusicVideoRef;\n\nuse crate::{\n    const"
  },
  {
    "path": "crates/ytermusic/src/tasks/mod.rs",
    "chars": 73,
    "preview": "pub mod api;\npub mod clean;\npub mod last_playlist;\npub mod local_musics;\n"
  },
  {
    "path": "crates/ytermusic/src/term/device_lost.rs",
    "chars": 2260,
    "preview": "use crossterm::event::{KeyCode, KeyEvent};\nuse ratatui::{\n    layout::{Alignment, Rect},\n    widgets::{Block, BorderType"
  },
  {
    "path": "crates/ytermusic/src/term/item_list.rs",
    "chars": 5699,
    "preview": "use crossterm::event::{KeyCode, KeyEvent, MouseEventKind};\nuse ratatui::{\n    buffer::Buffer,\n    layout::Rect,\n    styl"
  },
  {
    "path": "crates/ytermusic/src/term/list_selector.rs",
    "chars": 3795,
    "preview": "use ratatui::{\n    buffer::Buffer,\n    layout::Rect,\n    style::Style,\n    text::Text,\n    widgets::{Block, Borders, Lis"
  },
  {
    "path": "crates/ytermusic/src/term/mod.rs",
    "chars": 10163,
    "preview": "pub mod device_lost;\npub mod item_list;\npub mod list_selector;\npub mod music_player;\npub mod playlist;\npub mod playlist_"
  },
  {
    "path": "crates/ytermusic/src/term/music_player.rs",
    "chars": 10921,
    "preview": "use common_structs::{AppStatus, MusicDownloadStatus};\nuse crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent"
  },
  {
    "path": "crates/ytermusic/src/term/playlist.rs",
    "chars": 5053,
    "preview": "use std::sync::atomic::AtomicBool;\n\nuse crossterm::event::{KeyCode, KeyEvent};\nuse flume::Sender;\nuse ratatui::{layout::"
  },
  {
    "path": "crates/ytermusic/src/term/playlist_view.rs",
    "chars": 3653,
    "preview": "use crossterm::event::{KeyCode, KeyEvent};\nuse flume::Sender;\nuse ratatui::{layout::Rect, style::Style, Frame};\nuse ytpa"
  },
  {
    "path": "crates/ytermusic/src/term/search.rs",
    "chars": 9810,
    "preview": "use std::sync::{Arc, RwLock};\n\nuse crossterm::event::{KeyCode, KeyEvent, KeyModifiers};\nuse flume::Sender;\nuse log::erro"
  },
  {
    "path": "crates/ytermusic/src/term/vertical_gauge.rs",
    "chars": 3744,
    "preview": "use ratatui::{\n    buffer::Buffer,\n    layout::Rect,\n    style::{Color, Style},\n    widgets::{Block, Widget},\n};\n\npub st"
  },
  {
    "path": "crates/ytermusic/src/utils.rs",
    "chars": 2804,
    "preview": "use directories::ProjectDirs;\nuse ratatui::style::{Color, Style};\nuse unicode_bidi::{BidiInfo, Level};\n\n/// Get director"
  },
  {
    "path": "crates/ytpapi2/Cargo.toml",
    "chars": 472,
    "preview": "[package]\nname = \"ytpapi2\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n# See more keys and their definitions at https://doc.rust"
  },
  {
    "path": "crates/ytpapi2/src/json_extractor.rs",
    "chars": 7509,
    "preview": "use std::fmt::Display;\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\nuse crate::YoutubeMusicPlaylistRef;"
  },
  {
    "path": "crates/ytpapi2/src/lib.rs",
    "chars": 21988,
    "preview": "use std::{\n    path::Path,\n    string::FromUtf8Error,\n    time::{SystemTime, UNIX_EPOCH},\n};\n\nuse json_extractor::{\n    "
  },
  {
    "path": "crates/ytpapi2/src/string_utils.rs",
    "chars": 2477,
    "preview": "use std::str::FromStr;\n\n#[allow(dead_code)]\npub trait StringUtils {\n    fn after(&self, needle: &str) -> Option<&str>;\n "
  }
]

About this extraction

This page contains the full source code of the ccgauche/ytermusic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 63 files (201.2 KB), approximately 47.8k tokens, and a symbol index with 350 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!