Full Code of 13unk0wn/Feather for AI

main 1f1d7be2b146 cached
19 files
69.2 KB
15.7k tokens
76 symbols
1 requests
Download .txt
Repository: 13unk0wn/Feather
Branch: main
Commit: 1f1d7be2b146
Files: 19
Total size: 69.2 KB

Directory structure:
gitextract__by_cgd2/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── feather/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── src/
│   │   ├── database.rs
│   │   ├── lib.rs
│   │   ├── player.rs
│   │   └── yt.rs
│   └── structure.txt
└── feather_frontend/
    ├── .gitignore
    ├── Cargo.toml
    └── src/
        ├── backend.rs
        ├── history.rs
        ├── lib.rs
        ├── main.rs
        ├── player.rs
        └── search.rs

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

================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to FEATHER

Thank you for considering contributing to FEATHER! 🚀  
We welcome all contributions, whether it's fixing bugs, improving documentation, or adding new features.  

## 📌 Contribution Guidelines  

### ❗ Important Notice  
Feather v0.1 (`main` branch) **is only accepting bug fix pull requests**.  
- ✅ **Bug Fixes** → v0.1 (`main`)  
- ❌ **New Features, Enhancements, or Keybinding Changes** → v0.2 (`0.2` branch)  


### 1. Use the `dev` Branch for Pull Requests Made to the `main` Branch  
All contributions (except critical bug fixes for `main`) should be made to the `dev` branch.  
Before starting, make sure your local `dev` branch is up to date:  

### 2. You Can Directly Commit to the `v0.2` Branch for Additional Features  

```bash
git checkout dev
git pull origin dev
```

Always create a new feature branch for your changes:  

```bash
git checkout -b feature-branch
```

After making changes, commit and push:  

```bash
git add .
git commit -m "Describe your changes"
git push origin feature-branch
```

### 2. Submitting a Pull Request (PR)  
1. Go to the [GitHub repository](https://github.com/13unk0wn/Feather).  
2. Click **"New Pull Request"**.  
3. Ensure you are merging **your branch into the correct branch**:  
   - Bug fixes → `dev` (v0.1)  
   - New features, enhancements →  `0.2` (v0.2)  
4. Provide a clear PR description explaining the changes.  
5. Request a review from maintainers.  

Once approved, your PR will be merged into `dev`  and later into `main` for a stable release.  

### 3. Code Style & Best Practices  
- ✅ Follow the existing code style and formatting.  
- ✅ Write meaningful commit messages.  
- ✅ Keep PRs small and focused on a single feature/fix.  
- ✅ Test your code before submitting.  

### 4. Issues & Discussions  
- **Bug Reports:** If you find a bug, check if an issue already exists. Otherwise, create a new issue.  
- **Feature Requests:** If you have an idea, discuss it in an issue before implementing it.  

### 5. Need Help?  
If you're unsure about anything, feel free to open a discussion or ask in an issue. We're happy to help!  

🙌 **Happy Coding!** 🚀  


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 13unk0wn

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

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

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



================================================
FILE: README.md
================================================
# Feather 🎵

Feather is a lightweight, efficient, and locally hosted YouTube Music TUI built with Rust. It is designed to provide a minimalistic yet powerful music streaming experience directly from YouTube, using `yt-dlp` and `mpv`.

## 🎯 Aim

A lightweight, ad-less player with only essential features.

## ✨ Features

- 🎶 **Stream YouTube Music** without downloading files.
- ⚡ **Minimal Memory Usage**, targeting **60MB - 80MB RAM**.
- 🚀 **Fast Playback**, with loading times around 3 seconds.
- 🖥️ **Terminal User Interface (TUI)** built using Ratatui.
- 🔄 **Self-Update Feature** (planned).

## 🛠️ Installation

### 📌 Prerequisites

Ensure you have the following installed:

- 🦀 **Rust** (latest stable version)
- 📥 **yt-dlp** (for fetching YouTube data)
- 🎵 **mpv** (for playback)

### 🔧 Build from Source

```sh
git clone https://github.com/13unk0wn/Feather.git
cd Feather/feather_frontend
cargo build --release
```

### ▶️ Run Feather

```sh
./target/release/feather_frontend
```

## 🎮 Usage

Navigate through the TUI to search and play music. Additional controls and keyboard shortcuts will be documented soon.

### 🛠️ Handling YouTube Restrictions

If a song fails to play due to YouTube restrictions, you can bypass them by adding your cookies to the environment:

```sh
export FEATHER_COOKIES="paste your cookies here"
```

- This is **optional** and should only be used if playback errors occur.
- Feather can play songs without cookies, but adding them may help `mpv` bypass certain restrictions.

## 🌄 Screenshot

![Feather TUI Screenshot](screenshots/preview.png)

## 🛠️ Compatibility

Feather has been tested on **Linux Mint (Debian Edition)**, but all libraries used are compatible with other Linux distributions.
Windows and Macos are not officially supported.

## 🛣️ Roadmap

### 🚀 Current Version: v0.1.0
- 🎶 Implement player
- 🔍 Implement search
- �햐 Implement history

### 🔥 Upcoming: v0.2.0
- ⚡ Improve performance
- 🎨 Improve UI
- 🌜 Add support for playing playlists
- 🎼 Add support for creating user playlists
- ⚙️ Add user configuration support

## 🤝 Contributing

Check out [CONTRIBUTION.md](https://github.com/13unk0wn/Feather/blob/main/CONTRIBUTING.md)

If you have any doubts regarding contribution, feel free to reach out via:
- GitHub Issues
- @x: [13unk0wn](https://x.com/13unk0wn)
- Email: [13unk0wn.proton.me](mailto:13unk0wn@proton.me)

## 🌟 Special Thanks

A big thank you to the maintainers and contributors of:
- [RustyPipe](https://codeberg.org/ThetaDev/rustypipe) — for providing essential tools for YouTube playback.
- [mpv](https://github.com/mpv-player/mpv) — for making a great media player that powers Feather's playback.
- [Ratatui](https://github.com/tui-rs-revival/ratatui) — for enabling the terminal-based UI experience.
- [Sled](https://github.com/spacejam/sled) - database

## 🌟 License

Feather is licensed under the MIT License.

---

### 📝 Notes

This project is still in early development. Expect rapid iterations and improvements. Suggestions and feedback are always appreciated!




================================================
FILE: feather/.gitignore
================================================
/target
rustypipe_cache.json
Cargo.lock


================================================
FILE: feather/Cargo.toml
================================================
[package]
name = "feather"
version = "0.1.0"
edition = "2024"
build = "build.rs"

[dependencies]
rustypipe = "0.9.0"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
bincode = "1.3.3"
sled = { version = "0.34.7",features = ["compression"] }
thiserror = "1.0"
tempfile = "3.16.0"
libmpv2 = "4.1.0"
dirs = "6.0.0"

[build-dependencies]
pkg-config = "0.3"

[lib]
name = "feather"
path = "src/lib.rs"




================================================
FILE: feather/build.rs
================================================
fn main() {
    if cfg!(target_os = "macos") && pkg_config::probe_library("mpv").is_err() {
        println!("cargo:warning=Could not find mpv via pkg-config. Make sure it is installed");
    }
}


================================================
FILE: feather/src/database.rs
================================================
// This file manages the history database and contains all necessary functions related to history management
use crate::{ArtistName, SongId, SongName};
use serde::{Deserialize, Serialize};
use sled::Db;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;

/// Represents a history entry for a song that has been played.
#[derive(Serialize, Deserialize, Debug)]
pub struct HistoryEntry {
    pub song_name: SongName,          // Name of the song
    pub song_id: SongId,              // Unique identifier for the song
    pub artist_name: Vec<ArtistName>, // List of artists associated with the song
    time_stamp: u64,                  // Timestamp when the song was played
}

impl HistoryEntry {
    /// Creates a new history entry with the current timestamp.
    pub fn new(
        song_name: SongName,
        song_id: SongId,
        artist_name: Vec<ArtistName>,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let time_stamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
        Ok(Self {
            song_name,
            song_id,
            artist_name,
            time_stamp,
        })
    }
}

/// Database handler for managing song history.
pub struct HistoryDB {
    db: Db, // Sled database instance
}

/// Represents possible errors that can occur in history operations.
#[derive(Error, Debug)]
pub enum HistoryError {
    #[error("Database error: {0}")]
    DbError(#[from] sled::Error), // Errors related to the sled database
    #[error("Serialization error: {0}")]
    SerializationError(#[from] bincode::Error), // Errors during serialization/deserialization
    #[error("Basic error: {0}")]
    Error(Box<dyn std::error::Error>), // Generic error wrapper
}

impl HistoryDB {
    pub fn new() -> Result<Self, sled::Error> {
        let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
        path.push("Feather/history_db");

        let db = sled::Config::new()
            .path(path)
            .cache_capacity(256 * 1024)
            .use_compression(true)
            .open()?;

        Ok(HistoryDB { db })
    }

    /// Adds a new entry to the history database.
    /// Limits the total stored entries to 50.
    pub fn add_entry(&self, entry: &HistoryEntry) -> Result<(), HistoryError> {
        let key = entry.song_id.as_bytes();
        let value = bincode::serialize(entry)?;
        self.db.insert(key, value)?;
        self.limit_history_size(50)?;
        Ok(())
    }

    /// Ensures the history database does not exceed `max_size` entries.
    /// Removes the oldest entries if necessary.
    pub fn limit_history_size(&self, max_size: usize) -> Result<(), HistoryError> {
        while self.db.len() > max_size {
            if let Some((key, _)) = self.db.first()? {
                self.db.remove(key)?;
            }
        }
        Ok(())
    }

    /// Retrieves up to 50 history entries, sorted by most recent first.
    pub fn get_history(&self) -> Result<Vec<HistoryEntry>, HistoryError> {
        let mut history = Vec::with_capacity(self.db.len().min(50)); // Pre-allocate vector
        for item in self.db.iter().take(50) {
            let (_, value) = item?;
            if let Ok(entry) = bincode::deserialize::<HistoryEntry>(&value) {
                history.push(entry);
            }
        }
        history.sort_unstable_by(|e1, e2| e2.time_stamp.cmp(&e1.time_stamp)); // Sort by timestamp descending
        Ok(history)
    }

    /// Deletes a specific history entry by song ID.
    pub fn delete_entry(&self, song_id: &str) -> Result<(), HistoryError> {
        self.db.remove(song_id.as_bytes())?; // Convert song ID to bytes
        Ok(())
    }

    /// Clears all history entries from the database.
    pub fn clear_history(&self) -> Result<(), HistoryError> {
        self.db.clear()?;
        Ok(())
    }

    /// Retrieves the most recently played song's ID, if available.
    pub fn get_last_played_song(&self) -> Result<Option<SongId>, HistoryError> {
        if let Some((_, last_entry)) = self.db.last()? {
            let entry: HistoryEntry = bincode::deserialize(&last_entry)?;
            Ok(Some(entry.song_id))
        } else {
            Ok(None)
        }
    }
}

// Unchanged UserPlaylist and PlaylistManager sections...
// #[derive(Serialize, Deserialize, Debug, Clone)]
// struct UserPlaylist {
//     playlist_name: PlaylistName,
//     songs: Vec<Song>,
// }

// #[derive(Error, Debug)]
// pub enum PlaylistManagerError {
//     #[error("Database error: {0}")]
//     DbError(#[from] sled::Error),
//     #[error("Serialization error: {0}")]
//     SerializationError(#[from] bincode::Error),
//     #[error("Playlist '{0}' not found")]
//     PlaylistNotFound(String),
//     #[error("Song '{0}' not found in playlist '{1}'")]
//     SongNotFound(String, String),
//     #[error("Duplicate playlist name: '{0}'")]
//     DuplicatePlaylist(String),
//     #[error("Failed to add song '{0}' to playlist '{1}'")]
//     AddSongError(String, String),
//     #[error("Failed to remove song '{0}' from playlist '{1}'")]
//     RemoveSongError(String, String),
//     #[error("Unknown error: {0}")]
//     Other(String),
// }

// #[derive(Serialize, Deserialize, Debug, Clone)]
// struct Song {
//     song_name: SongName,
//     song_id: SongId,
//     artist: Vec<ArtistName>,
// }

// struct PlaylistManager {
//     db: sled::Db,
// }

// impl PlaylistManager {
//     pub fn new(path: &str) -> Result<Self, PlaylistManagerError> {
//         let db = sled::open(path)?;
//         Ok(Self { db })
//     }
//     fn create_playlist(&self, name: &str) -> Result<(), PlaylistManagerError> {
//         if self.db.get(name)?.is_some() {
//             return Err(PlaylistManagerError::DuplicatePlaylist(name.to_string()));
//         }
//         let playlist = UserPlaylist {
//             playlist_name: name.to_string(),
//             songs: Vec::new(),
//         };
//         let value = bincode::serialize(&playlist)?;
//         self.db.insert(name, value)?;
//         self.db.flush()?;
//         Ok(())
//     }
//     fn add_song_to_playlist(
//         &self,
//         playlist_name: &str,
//         song: Song,
//     ) -> Result<(), PlaylistManagerError> {
//         let raw_data = self
//             .db
//             .get(playlist_name)?
//             .ok_or_else(|| PlaylistManagerError::Other("Error: In Opening Playlist".to_string()))?
//             .to_vec();

//         let mut playlist: UserPlaylist = bincode::deserialize(&raw_data)?;

//         playlist.songs.retain(|s| s.song_id != song.song_id);
//         playlist.songs.push(song);

//         let serialized_data = bincode::serialize(&playlist)?;
//         self.db.insert(playlist_name, serialized_data)?;
//         self.db.flush()?;

//         Ok(())
//     }
//     fn remove_song_from_playlist(
//         &self,
//         playlist_name: &str,
//         song_id: &str,
//     ) -> Result<(), PlaylistManagerError> {
//         let raw_data = self
//             .db
//             .get(playlist_name)?
//             .ok_or_else(|| PlaylistManagerError::Other("Error: In Opening Playlist".to_string()))?
//             .to_vec();

//         let mut playlist: UserPlaylist = bincode::deserialize(&raw_data)?;

//         playlist.songs.retain(|s| s.song_id != song_id);
//         let serialized_data = bincode::serialize(&playlist)?;
//         self.db.insert(playlist_name, serialized_data)?;
//         self.db.flush()?;

//         Ok(())
//     }

//     fn get_playlist(&self, playlist_name: &str) -> Result<UserPlaylist, PlaylistManagerError> {
//         let data = self
//             .db
//             .get(playlist_name)?
//             .ok_or_else(|| PlaylistManagerError::PlaylistNotFound(playlist_name.to_string()))?
//             .to_vec();
//         let playlist: UserPlaylist = bincode::deserialize(&data)?;
//         Ok(playlist)
//     }
//     fn delete_playlist(&self, playlist_name: &str) -> Result<(), PlaylistManagerError> {
//         self.db
//             .remove(&playlist_name)?
//             .ok_or_else(|| PlaylistManagerError::PlaylistNotFound(playlist_name.to_string()));
//         self.db.flush()?;
//         Ok(())
//     }
// }

// // Tests unchanged...
// #[cfg(test)]
// mod tests {
//     use super::*;
//     use tempfile::tempdir;

//     fn sample_song(name: &str, id: &str) -> Song {
//         Song {
//             song_name: name.to_string(),
//             song_id: id.to_string(),
//             artist: vec!["Artist One".to_string(), "Artist Two".to_string()],
//         }
//     }

//     #[test]
//     fn test_playlist_manager() {
//         let temp_dir = tempdir().unwrap();
//         let db_path = temp_dir.path().to_str().unwrap();
//         let manager = PlaylistManager::new(db_path).unwrap();

//         let playlist_name = "MyPlaylist";

//         assert!(manager.create_playlist(playlist_name).is_ok());

//         let song1 = sample_song("Song A", "123");
//         let song2 = sample_song("Song B", "456");

//         assert!(manager
//             .add_song_to_playlist(playlist_name, song1.clone())
//             .is_ok());
//         assert!(manager
//             .add_song_to_playlist(playlist_name, song2.clone())
//             .is_ok());

//         let playlist = manager.get_playlist(playlist_name).unwrap();
//         assert_eq!(playlist.songs.len(), 2);
//         assert!(playlist.songs.iter().any(|s| s.song_id == "123"));
//         assert!(playlist.songs.iter().any(|s| s.song_id == "456"));

//         assert!(manager
//             .remove_song_from_playlist(playlist_name, "123")
//             .is_ok());
//         let playlist = manager.get_playlist(playlist_name).unwrap();
//         assert_eq!(playlist.songs.len(), 1);
//         assert!(playlist.songs.iter().all(|s| s.song_id != "123"));

//         assert!(manager.delete_playlist(playlist_name).is_ok());
//         let result = manager.get_playlist(playlist_name);
//         assert!(matches!(
//             result,
//             Err(PlaylistManagerError::PlaylistNotFound(_))
//         ));
//     }
// }


================================================
FILE: feather/src/lib.rs
================================================
pub mod database;
pub mod player;
pub mod yt;

/// Input/Return Types
pub type ArtistName = String;
pub type SongName = String;
pub type SongId = String;
pub type SongUrl = String;
pub type PlaylistName = String;
pub type PlaylistId = String;
pub type ChannelName = String;


================================================
FILE: feather/src/player.rs
================================================
use libmpv2::Mpv; // We are not using libmpv library because it was requiring user to install an old version which was not available in many distros so we decided to opt for libmpv2 which is a fork of it
use std::sync::Arc;

/// The `Player` struct represents a media player using the MPV library.
/// It provides functionalities to control playback, retrieve metadata,
/// and manage audio optimizations.
pub struct Player {
    /// An instance of the MPV player wrapped in an `Arc` for thread safety.
    pub player: Arc<Mpv>,
}

/// Enum representing possible errors when interacting with the MPV player.
#[derive(Debug, thiserror::Error)]
pub enum MpvError {
    #[error("Mpv error: {0}")]
    Mpv(#[from] libmpv2::Error),
    #[error("Failed to initialize MPV")]
    InitializationError,
    #[error("Command execution failed: {0}")]
    CommandError(String),
    #[error("Failed to load file: {0}")]
    LoadFileError(String),
    #[error("Property retrieval failed: {0}")]
    PropertyError(String),
    #[error("Unknown error: {0}")]
    Other(String),
}

impl Player {
    /// Creates a new `Player` instance and configures MPV settings for optimized audio playback.
    pub fn new(cookies: Option<String>) -> Result<Self, MpvError> {
        let mpv = Mpv::new()?;
        if cookies.is_some() {
            // setting cookies  if given by user
            mpv.set_property("cookies-file", cookies.unwrap())?;
        }

        // Disable video to save memory
        mpv.set_property("video", "no")?;

        // Optimize caching for lower memory usage
        //mpv.set_property("cache-secs", 2)?; // Reduced to 2 seconds
        // mpv.set_property("demuxer-readahead-secs", 1)?; // Reduced to 1 second
        //mpv.set_property("demuxer-max-bytes", 512 * 1024)?; // 512 KB max buffer

        // Configure network request headers for YouTube playback
        mpv.set_property("ytdl-raw-options", "no-check-certificate=")?;
        mpv.set_property("loop", "inf")?; // Looping enabled (to be removed with autoplay)
        mpv.set_property(
            "http-header-fields",
            "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        )?;

        // Audio optimization
        mpv.set_property("audio-buffer", 0.1)?; // 100ms audio buffer
        mpv.set_property("audio-channels", "stereo")?; // Force stereo audio

        let mpv = Arc::new(mpv);
        Ok(Self { player: mpv })
    }

    /// Loads and plays a media file from a given URL.
    pub fn play(&self, url: &str) -> Result<(), MpvError> {
         if let Ok(true) = self.player.get_property("pause") {
            self.unpause()?;
        } // Quick fix will improve 
        self.player.command("loadfile", &[url])?; // Replace the current playback
        Ok(())
    }

    /// Pauses playback.
    pub fn pause(&self) -> Result<(), MpvError> {
        self.player.command("set", &["pause", "yes"])?;
        Ok(())
    }

    /// Resumes playback.
    pub fn unpause(&self) -> Result<(), MpvError> {
        self.player.command("set", &["pause", "no"])?;
        Ok(())
    }

    /// Toggles between play and pause states.
    pub fn play_pause(&self) -> Result<(), MpvError> {
        match self.player.get_property::<bool>("pause") {
            Ok(true) => self.unpause()?,
            Ok(false) => self.pause()?,
            Err(_) => todo!(),
        }
        Ok(())
    }

    /// Seeks forward by 5 seconds in the current track.
    pub fn seek_forward(&self) -> Result<(), MpvError> {
        self.player.command("seek", &["5", "relative"])?;
        Ok(())
    }

    /// Seeks backward by 5 seconds in the current track.
    pub fn seek_backword(&self) -> Result<(), MpvError> {
        self.player.command("seek", &["-5", "relative"])?;
        Ok(())
    }

    /// Retrieves the current playback time as a string.
    pub fn get_current_time(&self) -> String {
        self.player
            .get_property("time-pos")
            .unwrap_or(0.0)
            .to_string()
    }

    /// Retrieves the duration of the currently playing media.
    pub fn duration(&self) -> String {
        self.player
            .get_property("duration")
            .unwrap_or(0.0)
            .to_string()
    }

    /// Returns whether a media file is currently playing.
    pub fn is_playing(&self) -> Result<bool, MpvError> {
        let pause: bool = self.player.get_property("pause")?;
        Ok(!pause)
    }
}


================================================
FILE: feather/src/yt.rs
================================================
use crate::{ArtistName, ChannelName, PlaylistId, PlaylistName, SongId, SongName, SongUrl};
use std::path::PathBuf;
use rustypipe::{
    client::{RustyPipe, RustyPipeQuery},
    model::MusicItem,
    param::StreamFilter,
};
use std::collections::HashMap;

/// A client for interacting with YouTube music using RustyPipe.
pub struct YoutubeClient {
    client: RustyPipeQuery,
}

impl YoutubeClient {
    /// Creates a new instance of `YoutubeClient`.
    pub fn new() -> Self {
        let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
        path.push("Feather");
        let rp = RustyPipe::builder().storage_dir(path).build().unwrap();
        let client = rp.query();
        YoutubeClient { client }
    }

    /// Searches for music based on the given query.
    /// Returns a vector of tuples where each entry contains a song name and ID,
    /// along with a list of associated artist names.
    pub async fn search(
        &self,
        query: &str,
    ) -> Result<Vec<((SongName, SongId), Vec<ArtistName>)>, String> {
        match self.client.music_search_main(query).await {
            Ok(results) => {
                let mut search_result = vec![];

                for item in results.items.items {
                    if let MusicItem::Track(data) = item {
                        let song_id_pair = (data.name, data.id);
                        let artist_names: Vec<String> =
                            data.artists.into_iter().map(|id| id.name).collect();
                        search_result.push((song_id_pair, artist_names));
                    }
                }

                Ok(search_result)
            }
            Err(_) => Err("Error in Search Result".to_string()),
        }
    }

    /// Fetches the audio stream URL for a given song ID.
    pub async fn fetch_song_url(&self, id: &SongId) -> Result<SongUrl, String> {
        match self.client.player(&id).await {
            Ok(player) => match player.select_audio_stream(&StreamFilter::default()) {
                Some(stream) => return Ok(stream.url.clone()),
                None => return Err("Audio Stream not Found".to_string()),
            },
            Err(_) => return Err("Link cannot be Found".to_string()),
        }
    }

    /// Searches for playlists based on a given query.
    /// Returns a hashmap where the key is the playlist name and the value is a tuple
    /// containing the playlist ID and a list of associated channel names.
    pub async fn fetch_playlist(
        &self,
        search_query: &str,
    ) -> Result<HashMap<PlaylistName, (PlaylistId, Vec<ChannelName>)>, String> {
        match self.client.music_search_playlists(search_query, true).await {
            Ok(playlists) => {
                let mut result = HashMap::new();

                for playlist in playlists.items.items {
                    let playlist_id = playlist.id;
                    let channel_names: Vec<String> = playlist
                        .channel
                        .into_iter()
                        .map(|channel| channel.name)
                        .collect();

                    result.insert(playlist.name, (playlist_id, channel_names));
                }

                Ok(result)
            }
            Err(e) => Err(format!("Error in fetching playlists: {}", e)),
        }
    }

    /// Fetches songs from a given playlist ID.
    /// Returns a hashmap where each key is a tuple of (song name, song ID), and
    /// the value is a list of associated artist names.
    pub async fn fetch_playlist_songs(
        &self,
        playlist_id: PlaylistId,
    ) -> Result<HashMap<(SongName, SongId), Vec<ArtistName>>, String> {
        match self.client.playlist(playlist_id).await {
            Ok(playlist_data) => {
                let mut song_map = HashMap::new();

                for video in playlist_data.videos.items {
                    let song_key = (video.name, video.id);
                    let artist_names: Vec<String> = video
                        .channel
                        .into_iter()
                        .map(|channel| channel.name)
                        .collect();

                    song_map.insert(song_key, artist_names);
                }

                Ok(song_map)
            }
            Err(e) => Err(format!("Error fetching playlist songs: {}", e)),
        }
    }

    /// Fetches related songs for a given song ID.
    /// Returns a hashmap where each key is a tuple of (song name, song ID), and
    /// the value is a list of associated artist names.
    pub async fn fetch_related_song(
        &self,
        song_id: SongId,
    ) -> Result<HashMap<(SongName, SongId), Vec<ArtistName>>, String> {
        match self.client.music_related(song_id).await {
            Ok(music_list) => {
                let tracks = music_list.tracks;
                let mut results = HashMap::new();
                for track in tracks {
                    let song_id_name = (track.name, track.id);
                    let artist_names = track
                        .artists
                        .into_iter()
                        .map(|artist| artist.name)
                        .collect::<Vec<ArtistName>>();
                    results.insert(song_id_name, artist_names);
                }
                Ok(results)
            }
            Err(_) => Err("Error finding related songs".to_string()),
        }
    }
}
// #[tokio::test]
// async fn test_search() {
//     let client = YoutubeClient::new();

//     match client.search("Beanie").await {
//         Ok(results) => {
//             for ((song, id), artists) in results {
//                 println!("Song: {}", song);
//                 println!("Id : {}", id);
//                 println!("{}", client.fetch_song_url(id.clone()).await.unwrap());

//                 for artist in artists {
//                     println!("  - Artist: {}", artist);
//                 }
//                 test_fetch_related_song(id).await;
//                 break;
//             }
//         }
//         Err(e) => println!("Search failed: {}", e),
//     }
// }
// #[tokio::test]
// async fn test_fetch_playlist() {
//     let client = YoutubeClient::new();
//     let query = "lofi beats";

//     match client.fetch_playlist(query).await {
//         Ok(playlists) => {
//             for (playlist_name, (playlist_id, channel_names)) in playlists {
//                 println!("Playlist: {} (ID: {})", playlist_name, playlist_id);
//                 test_fetch_playlist_songs(playlist_id).await;

//                 // for channel in channel_names {
//                 //     println!("  - Channel: {}", channel);
//                 // }
//                 break;
//             }
//         }
//         Err(e) => eprintln!("Test failed: {}", e),
//     }
// }
// async fn test_fetch_playlist_songs(playlist_id :  String) {
//     let client = YoutubeClient::new();

//     match client.fetch_playlist_songs(playlist_id).await {
//         Ok(songs) => {
//             for ((song_name, song_id), artist_names) in songs {
//                 println!("Song: {} (ID: {})", song_name, song_id);
//                 let url  =  client.fetch_song_url(&song_id).await.unwrap();
//                 println!("{url:?}");
//                 for artist in artist_names {
//                     println!("  - Artist: {}", artist);
//                 }
//             }
//         }
//         Err(e) => eprintln!("Test failed: {}", e),
//     }
// }

// async fn test_fetch_related_song(song_id  :  String) {
//     let client = YoutubeClient::new();

//     match client.fetch_related_song(song_id).await {
//         Ok(related_songs) => {
//             for ((song_name, song_id), artist_names) in related_songs {
//                 println!("Related Song: {} (ID: {})", song_name, song_id);
//                 for artist in artist_names {
//                     println!("  - Artist: {}", artist);
//                 }
//             }
//         }
//         Err(e) => eprintln!("Test failed: {}", e),
//     }
// }


================================================
FILE: feather/structure.txt
================================================
Feather
│── backend
│   │── yt.rs                
│   │   │── search(query: &str) -> Vec<String>       # Search YouTube and return video URLs. [done]
│   │   │── fetch_url(video_id: &str) -> String      # Get the direct link for mpv to play. [done]
│   │   │── fetch_playlist(playlist_id: &str) -> Vec<String>  # Fetch all songs in a playlist. [done]
│   │   │── fetch_related(video_id: &str) -> Vec<String>      # Get related songs (for autoplay).[unresolved]
│   │
│   │── mpv.rs               
│   │   │── play(url: &str)                           # Play a song using mpv.
│   │   │── pause()                                   # Pause or resume playback.
│   │   │── stop()                                    # Stop the current playback.
│   │   │── skip(seconds: i64)                        # Skip forward/backward by seconds.
│   │   │── volume(level: u8)                         # Adjust the volume level.
│   │   │── next()                                    # Play the next song in the queue.
│   │   │── on_song_end()                             # Triggered when a song ends (handles autoplay).
│   │
│   │── database.rs        
│   │   │── add_to_history(song: &str)               # Save song to history. [done]
│   │   │── get_history() -> Vec<String>             # Retrieve history list. [done]
│   │   │── delete_from_history(index: usize)        # Remove a song from history. [done]
│   │   │── save_playlist(name: &str, songs: Vec<String>)   # Create or update a playlist.[done]
│   │   │── load_playlist(name: &str) -> Vec<String>        # Load songs from a playlist.[done]
│   │   │── delete_playlist(name: &str)              # Delete a playlist.[done]
│   │   │── get_last_played() -> Option<String>      # Get the last played song.[done]
│   │   │── add_to_queue(song: &str)                 # Add song to queue.
│   │   │── remove_from_queue(index: usize)          # Remove song from queue.
│   │   │── get_next_song() -> Option<String>        # Get next song from queue.
│   │   │── clear_queue()                            # Clear the queue.
│   │
│   │── config.rs        
│   │   │── load_config() -> Config                  # Load settings from `config.lua`.
│   │   │── save_config(config: &Config)             # Save updated settings.
│   │   │── watch_config()                           # Optional: Reload config if changed.
│   │
│── config.lua       # User settings (autoplay, volume, history limit, storage path).


================================================
FILE: feather_frontend/.gitignore
================================================
target/
rustypipe_cache.json


================================================
FILE: feather_frontend/Cargo.toml
================================================
[package]
name = "feather_frontend"
version = "0.1.0"
edition = "2024"
authors = ["13unk0wn 13unk0wn@proton.me"]
description = "A lightweight YouTube Music TUI in Rust."
license = "MIT"
categories = ["command-line-utilities", "multimedia"]
keywords = ["music", "youtube", "tui", "rust"]



[dependencies]
color-eyre = "0.6.3"
crossterm = "0.28.1"
ratatui = "0.29.0"
tui-textarea = "0.7.0"
feather = {path  = "../feather"}
tokio = "1.43.0"
tui-scrollview = "0.3"
thiserror ="1.0"
wee_alloc = "0.4"

[profile.release]
opt-level = 3  # Maximum optimization
lto = true     # Link Time Optimization
codegen-units = 1  # Optimize for binary size
strip = true   # Remove debug symbols
panic = 'abort'  # Reduce unwinding overhead

# [replace-with]
# global_allocator = "wee_alloc"


================================================
FILE: feather_frontend/src/backend.rs
================================================
use feather::{
    ArtistName, SongId, SongName,
    database::{HistoryDB, HistoryEntry},
    player::{MpvError, Player},
    yt::YoutubeClient,
};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;

use thiserror::Error;

/// The `Backend` struct manages the YouTube client, music player, and history database.
/// It also tracks the currently playing song.
pub struct Backend {
    pub yt: YoutubeClient,         // YouTube client for fetching song URLs
    pub player: Player,            // Music player instance
    pub history: Arc<HistoryDB>,   // Shared history database
    pub song: Mutex<Option<Song>>, // Mutex-protected optional current song
}

/// Represents a song with its name, ID, and artist(s).
#[derive(Clone)]
pub struct Song {
    pub song_name: SongName,      // Name of the song
    pub song_id: SongId,          // Unique identifier for the song
    artist_name: Vec<ArtistName>, // List of artists performing the song
}

/// Implements conversion from `Song` to `HistoryEntry`, ensuring valid history records.
impl From<Song> for HistoryEntry {
    fn from(value: Song) -> Self {
        HistoryEntry::new(value.song_name, value.song_id, value.artist_name)
            .expect("Cannot Form History Entry")
    }
}

impl Song {
    /// Creates a new `Song` instance.
    pub fn new(song_name: SongName, song_id: SongId, artist_name: Vec<ArtistName>) -> Self {
        Self {
            song_name,
            song_id,
            artist_name,
        }
    }
}

/// Defines possible errors that can occur in the `Backend`.
#[derive(Error, Debug)]
pub enum BackendError {
    #[error("Player error: {0}")]
    Mpv(#[from] MpvError), // Error related to the music player

    #[error("Failed to fetch YouTube URL")]
    YoutubeFetch(String), // Error when fetching a song URL from YouTube

    #[error("Mutex poisoned: {0}")]
    MutexPoisoned(String), // Error when accessing a poisoned mutex

    #[error("History database error: {0}")]
    HistoryError(String), // Error related to history database operations

    #[error("Playback error: {0}")]
    PlaybackError(String), // Error related to playback issues
}

impl Backend {
    /// Creates a new `Backend` instance.
    ///
    /// # Arguments
    /// * `history` - Shared reference to the history database.
    /// * `cookies` - Optional cookie string for authentication.
    ///
    /// # Returns
    /// * `Result<Self, BackendError>` - Returns `Backend` on success or an error on failure.
    pub fn new(history: Arc<HistoryDB>, cookies: Option<String>) -> Result<Self, BackendError> {
        Ok(Self {
            yt: YoutubeClient::new(),
            player: Player::new(cookies).map_err(BackendError::Mpv)?,
            history,
            song: Mutex::new(None),
        })
    }

    /// Plays a song by fetching its URL from YouTube and passing it to the player.
    ///
    /// # Arguments
    /// * `song` - The song to be played.
    ///
    /// # Returns
    /// * `Result<(), BackendError>` - Returns `Ok(())` on success or an error on failure.
    pub async fn play_music(&self, song: Song) -> Result<(), BackendError> {
        const MAX_RETRIES: i32 = 8;
        let id = song.song_id.to_string();

        // Fetch song URL with retry mechanism
        let url = {
            let mut attempts = 0;
            loop {
                match self.yt.fetch_song_url(&id).await {
                    Ok(url) => break url,
                    Err(_) if attempts < MAX_RETRIES => {
                        attempts += 1;
                        tokio::time::sleep(Duration::from_millis(100)).await;
                        continue;
                    }
                    Err(e) => {
                        return Err(BackendError::YoutubeFetch(format!(
                            "Failed to fetch URL after {} attempts: {:?}",
                            MAX_RETRIES, e
                        )));
                    }
                }
            }
        };

        // Update the currently playing song in a mutex-protected section
        {
            let mut current_song = self
                .song
                .lock()
                .map_err(|e| BackendError::MutexPoisoned(e.to_string()))?;
            *current_song = Some(song.clone());
        }

        // Play the song
        self.player.play(&url).map_err(BackendError::Mpv)?;

        // Add the song to history
        self.history
            .add_entry(&HistoryEntry::from(song))
            .map_err(|e| BackendError::HistoryError(e.to_string()))?;

        Ok(())
    }
}


================================================
FILE: feather_frontend/src/history.rs
================================================
use crate::backend::{Backend, Song};
use crossterm::event::{KeyCode, KeyEvent};
use feather::database::HistoryDB;
use ratatui::prelude::{Buffer, Color, Constraint, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::Span;
use ratatui::widgets::{
    Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarState,
    StatefulWidget, Widget,
};
use std::sync::Arc;
use tokio::sync::mpsc;

// Defines a struct to manage playback history UI
pub struct History {
    history: Arc<HistoryDB>,               // Database connection for history
    selected: usize,                       // Index of currently selected item
    vertical_scroll_state: ScrollbarState, // State for vertical scrollbar
    max_len: usize,                        // Total number of history items
    selected_song: Option<Song>,           // Currently selected song details
    backend: Arc<Backend>,                 // Audio backend for playback
    tx_player: mpsc::Sender<bool>,         // Channel to communicate with player
}

impl History {
    // Constructor initializing the History struct
    pub fn new(
        history: Arc<HistoryDB>,
        backend: Arc<Backend>,
        tx_player: mpsc::Sender<bool>,
    ) -> Self {
        Self {
            history,
            selected: 0,
            vertical_scroll_state: ScrollbarState::default(),
            max_len: 0,
            selected_song: None,
            backend,
            tx_player,
        }
    }

    // Handles keyboard input for navigation and actions
    pub fn handle_keystrokes(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Char('j') | KeyCode::Down => {
                // Move selection down
                self.select_next();
            }
            KeyCode::Char('k') | KeyCode::Up => {
                // Move selection up
                self.select_previous();
            }
            KeyCode::Char('d') => {
                // Delete selected entry
                if let Some(song) = &self.selected_song {
                    let _ = self.history.delete_entry(&song.song_id);
                }
            }
            KeyCode::Enter => {
                // Play selected song
                if let Some(song) = self.selected_song.clone() {
                    let backend = Arc::clone(&self.backend);
                    let tx_player = self.tx_player.clone();
                    tokio::spawn(async move {
                        // Spawn async task for playback
                        if backend.play_music(song).await.is_ok() {
                            let _ = tx_player.send(true).await;
                        }
                    });
                }
            }
            _ => (), // Ignore other keys
        }
    }

    // Moves selection to next item, respecting bounds
    fn select_next(&mut self) {
        if self.max_len > 0 {
            self.selected = (self.selected + 1).min(self.max_len - 1);
            self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);
        }
    }

    // Moves selection to previous item, preventing underflow
    fn select_previous(&mut self) {
        self.selected = self.selected.saturating_sub(1);
        self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);
    }

    // Renders the history UI component
    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
        let chunks = Layout::default()
            .direction(ratatui::layout::Direction::Vertical)
            .constraints([Constraint::Length(3), Constraint::Min(0)]) // Split layout
            .split(area);

        // Render title bar
        Paragraph::new("History")
            .style(Style::default().fg(Color::White))
            .block(Block::default().borders(Borders::ALL))
            .render(chunks[0], buf);

        // Setup history list area with scrollbar
        let history_area = chunks[1];
        let scrollbar = Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight)
            .begin_symbol(Some("↑"))
            .end_symbol(Some("↓"));
        scrollbar.render(history_area, buf, &mut self.vertical_scroll_state);

        // Fetch and render history items
        if let Ok(items) = self.history.get_history() {
            self.max_len = items.len();
            self.vertical_scroll_state = self.vertical_scroll_state.content_length(self.max_len);

            let view_items: Vec<ListItem> = items
                .into_iter()
                .enumerate()
                .map(|(i, item)| {
                    // Format each item for display
                    let is_selected = i == self.selected;
                    if is_selected {
                        self.selected_song = Some(Song::new(
                            item.song_name.clone(),
                            item.song_id.clone(),
                            item.artist_name.clone(),
                        ));
                    }
                    let style = if is_selected {
                        // Highlight selected item
                        Style::default().fg(Color::Yellow).bg(Color::Blue)
                    } else {
                        Style::default()
                    };
                    let text = format!("{} - {}", item.song_name, item.artist_name.join(", "));
                    ListItem::new(Span::styled(text, style))
                })
                .collect();

            let mut list_state = ListState::default();
            list_state.select(Some(self.selected));
            StatefulWidget::render(
                // Render the list
                List::new(view_items)
                    .block(Block::default().borders(Borders::ALL))
                    .highlight_symbol("▶"),
                history_area,
                buf,
                &mut list_state,
            );
        } else {
            // Handle history loading failure
            self.max_len = 0;
            self.selected = 0;
            Paragraph::new("Failed to load history").render(history_area, buf);
        }
    }
}


================================================
FILE: feather_frontend/src/lib.rs
================================================
pub mod backend;
pub mod history;
pub mod player;
pub mod search;


================================================
FILE: feather_frontend/src/main.rs
================================================
use color_eyre::eyre::Result;
use crossterm::event::{Event, KeyCode, KeyEvent, poll, read};
use feather::database::HistoryDB;
use feather_frontend::{backend::Backend, history::History, player::SongPlayer, search::Search};
use ratatui::{
    DefaultTerminal,
    buffer::Buffer,
    layout::{Constraint, Layout, Rect},
    widgets::{Block, Borders, Cell, Paragraph, Row, Table, Widget},
};
use std::{env, sync::Arc};
use tokio::{
    sync::mpsc,
    time::{Duration, interval},
};

/// Entry point for the async runtime.
#[tokio::main]
async fn main() -> Result<()> {
    color_eyre::install().unwrap();
    let terminal = ratatui::init();
    let _app = App::new().render(terminal).await;
    ratatui::restore();
    Ok(())
}

/// Enum representing different states of the application.
#[derive(Debug)]
enum State {
    HelpMode,
    Global,
    Search,
    History,
    // UserPlaylist,
    // CurrentPlayingPlaylist,
    SongPlayer,
}

/// Main application struct managing the state and UI components.
struct App<'a> {
    state: State,
    search: Search<'a>,
    history: History,
    // user_playlist: UserPlaylist,
    // current_playling_playlist: CurrentPlayingPlaylist,
    top_bar: TopBar,
    player: SongPlayer,
    // backend: Arc<Backend>,
    help_mode: bool,
    exit: bool,
}

impl App<'_> {
    /// Creates a new instance of the application.
    fn new() -> Self {
        let history = Arc::new(HistoryDB::new().unwrap());
        let get_cookies = env::var("FEATHER_COOKIES").ok(); // Fetch cookies from environment variables if available.
        let backend = Arc::new(Backend::new(history.clone(), get_cookies).unwrap());
        let (tx, rx) = mpsc::channel(32);

        App {
            state: State::Global,
            search: Search::new(backend.clone(), tx.clone()),
            history: History::new(history, backend.clone(), tx.clone()),
            // user_playlist: UserPlaylist {},
            // current_playling_playlist: CurrentPlayingPlaylist {},
            top_bar: TopBar::new(),
            player: SongPlayer::new(backend.clone(), rx),
            // backend,
            help_mode: false,
            exit: false,
        }
    }

    /// Handles global keystrokes and state transitions.
    fn handle_global_keystrokes(&mut self, key: KeyEvent) {
        match self.state {
            State::Global => match key.code {
                KeyCode::Char('s') => self.state = State::Search,
                KeyCode::Char('h') => self.state = State::History,
                KeyCode::Char('p') => self.state = State::SongPlayer,
                KeyCode::Char('?') => {
                    self.help_mode = true;
                    self.state = State::HelpMode;
                }
                KeyCode::Esc => {
                    self.exit = true;
                }
                _ => (),
            },
            State::Search => match key.code {
                KeyCode::Esc => self.state = State::Global,
                _ => self.search.handle_keystrokes(key),
            },
            State::HelpMode => match key.code {
                KeyCode::Esc => {
                    self.state = State::Global;
                    self.help_mode = false;
                }
                _ => (),
            },
            State::History => match key.code {
                KeyCode::Esc => self.state = State::Global,
                _ => self.history.handle_keystrokes(key),
            },
            State::SongPlayer => match key.code {
                KeyCode::Esc => self.state = State::Global,
                _ => self.player.handle_keystrokes(key),
            },
        }
    }

    /// Main render loop for updating the UI.
    async fn render(mut self, mut terminal: DefaultTerminal) {
        let mut redraw_interval = interval(Duration::from_millis(250)); // Redraw every 250ms

        while !self.exit {
            terminal
                .draw(|frame| {
                    let area = frame.area();
                    let layout = Layout::default()
                        .direction(ratatui::layout::Direction::Vertical)
                        .constraints([
                            Constraint::Percentage(10),
                            Constraint::Percentage(75),
                            Constraint::Percentage(15),
                        ])
                        .split(area);

                    let middle_layout = Layout::default()
                        .direction(ratatui::layout::Direction::Horizontal)
                        .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
                        .split(layout[1]);

                    if !self.help_mode {
                        self.top_bar
                            .render(layout[0], frame.buffer_mut(), &self.state);
                        self.search.render(middle_layout[0], frame.buffer_mut());
                        self.history.render(middle_layout[1], frame.buffer_mut());
                        self.player.render(layout[2], frame.buffer_mut());
                    } else {
                        let rows = vec![
                            Row::new(vec![Cell::from("s"), Cell::from("Search")]),
                            Row::new(vec![Cell::from("h"), Cell::from("History")]),
                            Row::new(vec![Cell::from("p"), Cell::from("Player")]),
                            Row::new(vec![Cell::from("?"), Cell::from("Toggle Help Mode")]),
                            Row::new(vec![
                                Cell::from("TAB (Search)"),
                                Cell::from("Toggle between search input and results"),
                            ]),
                            Row::new(vec![
                                Cell::from("Esc (Global)"),
                                Cell::from("Quit application"),
                            ]),
                            Row::new(vec![
                                Cell::from("Esc (Non-Global)"),
                                Cell::from("Switch to Global Mode"),
                            ]),
                            Row::new(vec![
                                Cell::from("↑ / k(History/Search)"),
                                Cell::from("Navigate up in list"),
                            ]),
                            Row::new(vec![
                                Cell::from("↓ / j(History/Search)"),
                                Cell::from("Navigate down in list"),
                            ]),
                            Row::new(vec![
                                Cell::from("Space / ; (Player)"),
                                Cell::from("Pause current song"),
                            ]),
                            Row::new(vec![
                                Cell::from("→ (Player)"),
                                Cell::from("Skip forward 5 seconds"),
                            ]),
                            Row::new(vec![
                                Cell::from("← (Player)"),
                                Cell::from("Rewind 5 seconds"),
                            ]),
                        ];

                        let help_table = Table::new(
                            rows,
                            [Constraint::Percentage(20), Constraint::Percentage(80)],
                        )
                        .block(Block::default().borders(Borders::ALL).title("Help"))
                        .header(Row::new(vec![Cell::from("Key"), Cell::from("Action")]));

                        help_table.render(area, frame.buffer_mut());
                    }
                })
                .unwrap();

            tokio::select! {
                _ = redraw_interval.tick() => {}
                _ = async {
                    if poll(Duration::from_millis(100)).unwrap() {
                        if let Event::Key(key) = read().unwrap() {
                            self.handle_global_keystrokes(key);
                        }
                    }
                } => {}
            }
        }
    }
}

/// Represents the top bar UI component.
struct TopBar;

impl TopBar {
    fn new() -> Self {
        Self
    }
    fn render(&mut self, area: Rect, buf: &mut Buffer, state: &State) {
        let s = format!("Feather | Current Mode : {:?}", state);
        Paragraph::new(s)
            .block(Block::default().borders(Borders::ALL))
            .render(area, buf);
    }
}

#[allow(unused)]
/// Placeholder struct for user playlists.
struct UserPlaylist {}
#[allow(unused)]
/// Placeholder struct for currently playing playlist.
struct CurrentPlayingPlaylist {}


================================================
FILE: feather_frontend/src/player.rs
================================================
use crate::backend::{Backend, Song};
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::prelude::{Alignment, Buffer, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Widget};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::task;

#[derive(PartialEq, PartialOrd, Debug)]
enum SongState {
    Idle,              // No song is playing
    Playing,           // A song is currently playing
    Loading,           // Song is loading
    ErrorPlayingoSong, // An error occurred while playing the song
}

#[derive(Clone)]
pub struct SongDetails {
    song: Song,             // Information about the song
    current_time: String,   // Current playback time (formatted as MM:SS)
    total_duration: String, // Total duration of the song
}

pub struct SongPlayer {
    backend: Arc<Backend>,            // Backend reference for controlling playback
    songstate: Arc<Mutex<SongState>>, // Current state of the player (Idle, Playing, etc.)
    song_playing: Arc<Mutex<Option<SongDetails>>>, // Details of the currently playing song
    rx: mpsc::Receiver<bool>,         // Receiver to listen for playback events
}

impl SongPlayer {
    pub fn new(backend: Arc<Backend>, rx: mpsc::Receiver<bool>) -> Self {
        let player = Self {
            backend,
            songstate: Arc::new(Mutex::new(SongState::Idle)),
            song_playing: Arc::new(Mutex::new(None)),
            rx,
        };
        player.observe_time(); // Start observing playback time
        player
    }

    // Function to continuously update the current playback time
    fn observe_time(&self) {
        let backend = Arc::clone(&self.backend);
        let song_playing = Arc::clone(&self.song_playing);

        tokio::task::spawn(async move {
            loop {
                // Try to get the current playback position from MPV
                match backend.player.player.get_property::<f64>("time-pos") {
                    Ok(time) => {
                        // Lock the song_playing mutex and update the current playback time
                        if let Ok(mut song_lock) = song_playing.lock() {
                            if let Some(song) = song_lock.as_mut() {
                                song.current_time = format!("{:.0}", time);
                            }
                        }
                    }
                    Err(_) => (), // Ignore errors (e.g., if MPV is not running)
                }

                tokio::time::sleep(Duration::from_millis(500)).await; // Update every 500ms
            }
        });
    }

    // Handle key presses for playback control
    pub fn handle_keystrokes(&mut self, key: KeyEvent) {
        if let Ok(state) = self.songstate.lock() {
            if *state == SongState::Playing {
                match key.code {
                    KeyCode::Char(' ') | KeyCode::Char(';') => {
                        // Toggle play/pause
                        if let Ok(_) = self.backend.player.play_pause() {};
                    }
                    KeyCode::Right | KeyCode::Char('l') => {
                        // Seek forward
                        self.backend.player.seek_forward().ok();
                    }
                    KeyCode::Left | KeyCode::Char('j') => {
                        // Seek backward
                        self.backend.player.seek_backword().ok();
                    }
                    _ => (),
                };
            }
        }
    }

    // Function to check whether a song is playing
    fn check_playing(&mut self) {
        let songstate = Arc::clone(&self.songstate);
        let backend = Arc::clone(&self.backend);
        let song_playing = Arc::clone(&self.song_playing);

        task::spawn(async move {
            const MAX_IDLE_COUNT: i32 = 5; // Max checks before considering it an error
            let mut idle_count = 0;

            // Initial delay before checking playback status
            tokio::time::sleep(Duration::from_secs(1)).await;

            loop {
                match backend.player.is_playing() {
                    Ok(true) => {
                        if let Ok(mut state) = songstate.lock() {
                            if let Ok(mut song_lock) = song_playing.lock() {
                                if let Ok(song) = backend.song.lock() {
                                    if let Some(value) = song.as_ref() {
                                        let total_duration = backend
                                            .player
                                            .duration()
                                            .parse::<f64>()
                                            .map(|d| {
                                                let total = d as i64;
                                                format!("{:02}:{:02}", total / 60, total % 60)
                                            })
                                            .unwrap_or_default();
                                        *song_lock = Some(SongDetails {
                                            song: value.clone(),
                                            current_time: backend.player.get_current_time(),
                                            total_duration,
                                        });
                                        *state = SongState::Playing;
                                        return; // Exit once playing is confirmed
                                    }
                                }
                            }
                        }
                        idle_count = 0; // Reset idle count since the song is playing
                    }
                    Ok(false) => {
                        // Song is not playing, set state to Idle
                        if let Ok(mut state) = songstate.lock() {
                            *state = SongState::Idle;
                        }
                        idle_count += 1;
                    }
                    Err(_) => idle_count += 1, // Increase idle count if an error occurs
                }

                // If too many idle checks, assume an error occurred
                if idle_count >= MAX_IDLE_COUNT {
                    if let Ok(mut state) = songstate.lock() {
                        if *state == SongState::Loading {
                            *state = SongState::ErrorPlayingoSong;
                        }
                    }
                }
                tokio::time::sleep(Duration::from_secs(2)).await; // Check every 2 seconds
            }
        });
    }

    // Render the player UI
    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
        // Check for playback event signals
        if self.rx.try_recv().is_ok() {
            if let Ok(mut state) = self.songstate.lock() {
                *state = SongState::Loading;
            }
            self.check_playing(); // Start checking for playback status
        }

        let block = Block::default().borders(Borders::ALL);
        let inner = block.inner(area);
        block.render(area, buf);

        if let Ok(state) = self.songstate.lock() {
            let text = match *state {
                SongState::Idle => vec![Line::from("No song is playing")],
                SongState::Playing => {
                    if let Ok(song_playing) = self.song_playing.lock() {
                        song_playing.as_ref().map_or_else(
                            || vec![Line::from("Loading...")],
                            |song| {
                                let current_time = song
                                    .current_time
                                    .parse::<i64>()
                                    .map(|t| format!("{:02}:{:02}", t / 60, t % 60))
                                    .unwrap_or_default();
                                vec![
                                    Line::from(Span::styled(
                                        song.song.song_name.clone(),
                                        Style::default().add_modifier(Modifier::BOLD),
                                    )),
                                    Line::from(format!("{}/{}", current_time, song.total_duration)),
                                ]
                            },
                        )
                    } else {
                        vec![Line::from("Error accessing song details")]
                    }
                }
                SongState::Loading => {
                    vec![Line::from("Loading Song")]
                }
                SongState::ErrorPlayingoSong => {
                    vec![Line::from("Error Playing Song")]
                }
            };
            Paragraph::new(text)
                .alignment(Alignment::Center)
                .render(inner, buf);
        }
    }
}


================================================
FILE: feather_frontend/src/search.rs
================================================
use crate::backend::{Backend, Song};
use crossterm::event::{KeyCode, KeyEvent};
use feather::{ArtistName, SongId, SongName};
use ratatui::{
    buffer::Buffer,
    layout::{Constraint, Layout, Rect},
    style::{Color, Style},
    text::Span,
    widgets::{
        Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarState,
        StatefulWidget, Widget,
    },
};
use std::sync::Arc;
use tokio::{
    sync::mpsc,
    time::{Duration, sleep},
};
use tui_textarea::TextArea;

// Defines possible states for the search interface
enum SearchState {
    SearchBar,     // When focused on input field
    SearchResults, // When browsing search results
}

pub struct Search<'a> {
    textarea: TextArea<'a>, // Text input widget for search queries
    state: SearchState,     // Current UI state
    query: String,          // Current search query text
    tx: mpsc::Sender<Result<Vec<((String, String), Vec<String>)>, String>>, // Sender for search results
    rx: mpsc::Receiver<Result<Vec<((String, String), Vec<String>)>, String>>, // Receiver for search results
    tx_player: mpsc::Sender<bool>, // Channel to communicate with player
    backend: Arc<Backend>,         // Audio backend for search and playback
    vertical_scroll_state: ScrollbarState, // Vertical scrollbar state
    display_content: bool,         // Flag to show search results
    results: Result<Option<Vec<((SongName, SongId), Vec<ArtistName>)>>, String>, // Search results or error
    selected: usize,             // Index of selected result
    selected_song: Option<Song>, // Currently selected song details
    max_len: Option<usize>,      // Total number of search results
}

impl Search<'_> {
    // Constructor initializing the Search struct
    pub fn new(backend: Arc<Backend>, tx_player: mpsc::Sender<bool>) -> Self {
        let (tx, rx) = mpsc::channel(32); // Create channel for async search results
        Self {
            query: String::new(),
            state: SearchState::SearchBar,
            textarea: TextArea::default(),
            tx,
            rx,
            tx_player,
            backend,
            vertical_scroll_state: ScrollbarState::default(),
            display_content: false,
            results: Ok(None),
            selected: 0,
            selected_song: None,
            max_len: None,
        }
    }

    // Handles keyboard input based on current state
    pub fn handle_keystrokes(&mut self, key: KeyEvent) {
        if let SearchState::SearchBar = self.state {
            match key.code {
                KeyCode::Tab => {
                    // Switch to results state
                    self.change_state();
                }
                KeyCode::Enter => {
                    // Execute search
                    self.display_content = false;
                    self.selected = 0;
                    let text = self.textarea.lines();
                    if !text.is_empty() {
                        self.query = text[0].trim().to_string();
                        let tx = self.tx.clone();
                        let query = self.query.clone();
                        let backend = self.backend.clone();
                        tokio::spawn(async move {
                            // Async task for search
                            sleep(Duration::from_millis(500)).await; // Debounce
                            match backend.yt.search(&query).await {
                                Ok(songs) => {
                                    let _ = tx.send(Ok(songs)).await;
                                }
                                Err(e) => {
                                    let _ = tx.send(Err(e)).await;
                                }
                            }
                        });
                    }
                }
                _ => {
                    self.textarea.input(key);
                } // Handle text input
            }
        } else {
            // SearchResults state
            match key.code {
                KeyCode::Tab => {
                    self.change_state();
                } // Switch to search bar
                KeyCode::Char('j') | KeyCode::Down => {
                    // Move selection down
                    self.selected = self.selected.saturating_add(1);
                    if let Some(len) = self.max_len {
                        self.selected = self.selected.min(len - 1);
                    }
                    self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);
                }
                KeyCode::Char('k') | KeyCode::Up => {
                    // Move selection up
                    self.selected = self.selected.saturating_sub(1);
                    self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);
                }
                KeyCode::Enter => {
                    // Play selected song
                    if let Some(song) = self.selected_song.clone() {
                        let backend = self.backend.clone();
                        let tx_player = self.tx_player.clone();
                        tokio::spawn(async move {
                            let _ = backend.play_music(song).await.is_ok();
                            let _ = tx_player.send(true).await;
                        });
                    }
                }
                _ => {}
            }
        }
    }

    // Toggles between search bar and results view
    pub fn change_state(&mut self) {
        match self.state {
            SearchState::SearchResults => self.state = SearchState::SearchBar,
            _ => self.state = SearchState::SearchResults,
        }
    }

    // Renders the search UI
    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
        let chunks = Layout::default()
            .direction(ratatui::layout::Direction::Vertical)
            .constraints([
                Constraint::Length(3), // Search bar height
                Constraint::Min(0),    // Results area
                Constraint::Length(3), // Bottom bar
            ])
            .split(area);
        let searchbar_area = chunks[0];
        let results_area = chunks[1];
        let bottom_area = chunks[2];

        // Check for new search results
        if let Ok(response) = self.rx.try_recv() {
            if let Ok(result) = response {
                self.results = Ok(Some(result));
            } else if let Err(e) = response {
                self.results = Err(e);
            }
            self.display_content = true;
        }

        // Render search bar
        let search_block = Block::default().title("Search Music").borders(Borders::ALL);
        self.textarea.set_cursor_line_style(Style::default());
        self.textarea
            .set_placeholder_text("Search Song or Playlist");
        self.textarea.set_style(Style::default().fg(Color::White));
        self.textarea.set_block(search_block);
        self.textarea.render(searchbar_area, buf);

        // Render vertical scrollbar
        let vertical_scrollbar =
            Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight)
                .begin_symbol(Some("↑"))
                .end_symbol(Some("↓"));
        vertical_scrollbar.render(results_area, buf, &mut self.vertical_scroll_state);

        // Render search results if available
        if self.display_content {
            if let Ok(result) = self.results.clone() {
                if let Some(r) = result {
                    self.max_len = Some(r.len());
                    let items: Vec<ListItem> = r
                        .into_iter()
                        .enumerate()
                        .map(|(i, ((song, songid), artists))| {
                            // Format results
                            let style = if i == self.selected {
                                self.selected_song =
                                    Some(Song::new(song.clone(), songid.clone(), artists.clone()));
                                Style::default().fg(Color::Yellow).bg(Color::Blue)
                            } else {
                                Style::default()
                            };
                            let text = format!("{} - {}", song, artists.join(", "));
                            ListItem::new(Span::styled(text, style))
                        })
                        .collect();

                    let mut list_state = ListState::default();
                    list_state.select(Some(self.selected));
                    StatefulWidget::render(
                        // Render results list
                        List::new(items)
                            .block(Block::default().title("Results").borders(Borders::ALL))
                            .highlight_symbol("▶"),
                        results_area,
                        buf,
                        &mut list_state,
                    );
                }
            }
        }

        // Render bottom help bar
        let bottom_bar = Paragraph::new("Press '?' for Help in Global Mode")
            .style(Style::default().fg(Color::White))
            .block(Block::default().borders(Borders::ALL));
        bottom_bar.render(bottom_area, buf); // Note: custom_area undefined, likely should be bottom_area

        // Render outer border
        let outer_block = Block::default().borders(Borders::ALL);
        outer_block.render(area, buf);
    }
}
Download .txt
gitextract__by_cgd2/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── feather/
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── build.rs
│   ├── src/
│   │   ├── database.rs
│   │   ├── lib.rs
│   │   ├── player.rs
│   │   └── yt.rs
│   └── structure.txt
└── feather_frontend/
    ├── .gitignore
    ├── Cargo.toml
    └── src/
        ├── backend.rs
        ├── history.rs
        ├── lib.rs
        ├── main.rs
        ├── player.rs
        └── search.rs
Download .txt
SYMBOL INDEX (76 symbols across 10 files)

FILE: feather/build.rs
  function main (line 1) | fn main() {

FILE: feather/src/database.rs
  type HistoryEntry (line 11) | pub struct HistoryEntry {
    method new (line 20) | pub fn new(
  type HistoryDB (line 36) | pub struct HistoryDB {
    method new (line 52) | pub fn new() -> Result<Self, sled::Error> {
    method add_entry (line 67) | pub fn add_entry(&self, entry: &HistoryEntry) -> Result<(), HistoryErr...
    method limit_history_size (line 77) | pub fn limit_history_size(&self, max_size: usize) -> Result<(), Histor...
    method get_history (line 87) | pub fn get_history(&self) -> Result<Vec<HistoryEntry>, HistoryError> {
    method delete_entry (line 100) | pub fn delete_entry(&self, song_id: &str) -> Result<(), HistoryError> {
    method clear_history (line 106) | pub fn clear_history(&self) -> Result<(), HistoryError> {
    method get_last_played_song (line 112) | pub fn get_last_played_song(&self) -> Result<Option<SongId>, HistoryEr...
  type HistoryError (line 42) | pub enum HistoryError {

FILE: feather/src/lib.rs
  type ArtistName (line 6) | pub type ArtistName = String;
  type SongName (line 7) | pub type SongName = String;
  type SongId (line 8) | pub type SongId = String;
  type SongUrl (line 9) | pub type SongUrl = String;
  type PlaylistName (line 10) | pub type PlaylistName = String;
  type PlaylistId (line 11) | pub type PlaylistId = String;
  type ChannelName (line 12) | pub type ChannelName = String;

FILE: feather/src/player.rs
  type Player (line 7) | pub struct Player {
    method new (line 31) | pub fn new(cookies: Option<String>) -> Result<Self, MpvError> {
    method play (line 63) | pub fn play(&self, url: &str) -> Result<(), MpvError> {
    method pause (line 72) | pub fn pause(&self) -> Result<(), MpvError> {
    method unpause (line 78) | pub fn unpause(&self) -> Result<(), MpvError> {
    method play_pause (line 84) | pub fn play_pause(&self) -> Result<(), MpvError> {
    method seek_forward (line 94) | pub fn seek_forward(&self) -> Result<(), MpvError> {
    method seek_backword (line 100) | pub fn seek_backword(&self) -> Result<(), MpvError> {
    method get_current_time (line 106) | pub fn get_current_time(&self) -> String {
    method duration (line 114) | pub fn duration(&self) -> String {
    method is_playing (line 122) | pub fn is_playing(&self) -> Result<bool, MpvError> {
  type MpvError (line 14) | pub enum MpvError {

FILE: feather/src/yt.rs
  type YoutubeClient (line 11) | pub struct YoutubeClient {
    method new (line 17) | pub fn new() -> Self {
    method search (line 28) | pub async fn search(
    method fetch_song_url (line 52) | pub async fn fetch_song_url(&self, id: &SongId) -> Result<SongUrl, Str...
    method fetch_playlist (line 65) | pub async fn fetch_playlist(
    method fetch_playlist_songs (line 93) | pub async fn fetch_playlist_songs(
    method fetch_related_song (line 121) | pub async fn fetch_related_song(

FILE: feather_frontend/src/backend.rs
  type Backend (line 15) | pub struct Backend {
    method new (line 77) | pub fn new(history: Arc<HistoryDB>, cookies: Option<String>) -> Result...
    method play_music (line 93) | pub async fn play_music(&self, song: Song) -> Result<(), BackendError> {
  type Song (line 24) | pub struct Song {
    method new (line 40) | pub fn new(song_name: SongName, song_id: SongId, artist_name: Vec<Arti...
  method from (line 32) | fn from(value: Song) -> Self {
  type BackendError (line 51) | pub enum BackendError {

FILE: feather_frontend/src/history.rs
  type History (line 15) | pub struct History {
    method new (line 27) | pub fn new(
    method handle_keystrokes (line 44) | pub fn handle_keystrokes(&mut self, key: KeyEvent) {
    method select_next (line 78) | fn select_next(&mut self) {
    method select_previous (line 86) | fn select_previous(&mut self) {
    method render (line 92) | pub fn render(&mut self, area: Rect, buf: &mut Buffer) {

FILE: feather_frontend/src/main.rs
  function main (line 19) | async fn main() -> Result<()> {
  type State (line 29) | enum State {
  type App (line 40) | struct App<'a> {
  function new (line 55) | fn new() -> Self {
  function handle_global_keystrokes (line 76) | fn handle_global_keystrokes(&mut self, key: KeyEvent) {
  function render (line 114) | async fn render(mut self, mut terminal: DefaultTerminal) {
  type TopBar (line 208) | struct TopBar;
    method new (line 211) | fn new() -> Self {
    method render (line 214) | fn render(&mut self, area: Rect, buf: &mut Buffer, state: &State) {
  type UserPlaylist (line 224) | struct UserPlaylist {}
  type CurrentPlayingPlaylist (line 227) | struct CurrentPlayingPlaylist {}

FILE: feather_frontend/src/player.rs
  type SongState (line 13) | enum SongState {
  type SongDetails (line 21) | pub struct SongDetails {
  type SongPlayer (line 27) | pub struct SongPlayer {
    method new (line 35) | pub fn new(backend: Arc<Backend>, rx: mpsc::Receiver<bool>) -> Self {
    method observe_time (line 47) | fn observe_time(&self) {
    method handle_keystrokes (line 72) | pub fn handle_keystrokes(&mut self, key: KeyEvent) {
    method check_playing (line 95) | fn check_playing(&mut self) {
    method render (line 160) | pub fn render(&mut self, area: Rect, buf: &mut Buffer) {

FILE: feather_frontend/src/search.rs
  type SearchState (line 22) | enum SearchState {
  type Search (line 27) | pub struct Search<'a> {
  function new (line 45) | pub fn new(backend: Arc<Backend>, tx_player: mpsc::Sender<bool>) -> Self {
  function handle_keystrokes (line 65) | pub fn handle_keystrokes(&mut self, key: KeyEvent) {
  function change_state (line 136) | pub fn change_state(&mut self) {
  function render (line 144) | pub fn render(&mut self, area: Rect, buf: &mut Buffer) {
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (75K chars).
[
  {
    "path": "CONTRIBUTING.md",
    "chars": 2152,
    "preview": "# Contributing to FEATHER\n\nThank you for considering contributing to FEATHER! 🚀  \nWe welcome all contributions, whether "
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2025 13unk0wn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 3042,
    "preview": "# Feather 🎵\n\nFeather is a lightweight, efficient, and locally hosted YouTube Music TUI built with Rust. It is designed t"
  },
  {
    "path": "feather/.gitignore",
    "chars": 40,
    "preview": "/target\nrustypipe_cache.json\nCargo.lock\n"
  },
  {
    "path": "feather/Cargo.toml",
    "chars": 448,
    "preview": "[package]\nname = \"feather\"\nversion = \"0.1.0\"\nedition = \"2024\"\nbuild = \"build.rs\"\n\n[dependencies]\nrustypipe = \"0.9.0\"\ntok"
  },
  {
    "path": "feather/build.rs",
    "chars": 196,
    "preview": "fn main() {\n    if cfg!(target_os = \"macos\") && pkg_config::probe_library(\"mpv\").is_err() {\n        println!(\"cargo:warn"
  },
  {
    "path": "feather/src/database.rs",
    "chars": 10147,
    "preview": "// This file manages the history database and contains all necessary functions related to history management\nuse crate::"
  },
  {
    "path": "feather/src/lib.rs",
    "chars": 274,
    "preview": "pub mod database;\npub mod player;\npub mod yt;\n\n/// Input/Return Types\npub type ArtistName = String;\npub type SongName = "
  },
  {
    "path": "feather/src/player.rs",
    "chars": 4421,
    "preview": "use libmpv2::Mpv; // We are not using libmpv library because it was requiring user to install an old version which was n"
  },
  {
    "path": "feather/src/yt.rs",
    "chars": 8092,
    "preview": "use crate::{ArtistName, ChannelName, PlaylistId, PlaylistName, SongId, SongName, SongUrl};\nuse std::path::PathBuf;\nuse r"
  },
  {
    "path": "feather/structure.txt",
    "chars": 2444,
    "preview": "Feather\n│── backend\n│   │── yt.rs                \n│   │   │── search(query: &str) -> Vec<String>       # Search YouTube "
  },
  {
    "path": "feather_frontend/.gitignore",
    "chars": 29,
    "preview": "target/\nrustypipe_cache.json\n"
  },
  {
    "path": "feather_frontend/Cargo.toml",
    "chars": 774,
    "preview": "[package]\nname = \"feather_frontend\"\nversion = \"0.1.0\"\nedition = \"2024\"\nauthors = [\"13unk0wn 13unk0wn@proton.me\"]\ndescrip"
  },
  {
    "path": "feather_frontend/src/backend.rs",
    "chars": 4572,
    "preview": "use feather::{\n    ArtistName, SongId, SongName,\n    database::{HistoryDB, HistoryEntry},\n    player::{MpvError, Player}"
  },
  {
    "path": "feather_frontend/src/history.rs",
    "chars": 6086,
    "preview": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse feather::database::HistoryDB;\nuse ra"
  },
  {
    "path": "feather_frontend/src/lib.rs",
    "chars": 66,
    "preview": "pub mod backend;\npub mod history;\npub mod player;\npub mod search;\n"
  },
  {
    "path": "feather_frontend/src/main.rs",
    "chars": 8628,
    "preview": "use color_eyre::eyre::Result;\nuse crossterm::event::{Event, KeyCode, KeyEvent, poll, read};\nuse feather::database::Histo"
  },
  {
    "path": "feather_frontend/src/player.rs",
    "chars": 8943,
    "preview": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse ratatui::prelude::{Alignment, Buffer"
  },
  {
    "path": "feather_frontend/src/search.rs",
    "chars": 9480,
    "preview": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse feather::{ArtistName, SongId, SongNa"
  }
]

About this extraction

This page contains the full source code of the 13unk0wn/Feather GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (69.2 KB), approximately 15.7k tokens, and a symbol index with 76 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!