[
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to FEATHER\n\nThank you for considering contributing to FEATHER! 🚀  \nWe welcome all contributions, whether it's fixing bugs, improving documentation, or adding new features.  \n\n## 📌 Contribution Guidelines  \n\n### ❗ Important Notice  \nFeather v0.1 (`main` branch) **is only accepting bug fix pull requests**.  \n- ✅ **Bug Fixes** → v0.1 (`main`)  \n- ❌ **New Features, Enhancements, or Keybinding Changes** → v0.2 (`0.2` branch)  \n\n\n### 1. Use the `dev` Branch for Pull Requests Made to the `main` Branch  \nAll contributions (except critical bug fixes for `main`) should be made to the `dev` branch.  \nBefore starting, make sure your local `dev` branch is up to date:  \n\n### 2. You Can Directly Commit to the `v0.2` Branch for Additional Features  \n\n```bash\ngit checkout dev\ngit pull origin dev\n```\n\nAlways create a new feature branch for your changes:  \n\n```bash\ngit checkout -b feature-branch\n```\n\nAfter making changes, commit and push:  \n\n```bash\ngit add .\ngit commit -m \"Describe your changes\"\ngit push origin feature-branch\n```\n\n### 2. Submitting a Pull Request (PR)  \n1. Go to the [GitHub repository](https://github.com/13unk0wn/Feather).  \n2. Click **\"New Pull Request\"**.  \n3. Ensure you are merging **your branch into the correct branch**:  \n   - Bug fixes → `dev` (v0.1)  \n   - New features, enhancements →  `0.2` (v0.2)  \n4. Provide a clear PR description explaining the changes.  \n5. Request a review from maintainers.  \n\nOnce approved, your PR will be merged into `dev`  and later into `main` for a stable release.  \n\n### 3. Code Style & Best Practices  \n- ✅ Follow the existing code style and formatting.  \n- ✅ Write meaningful commit messages.  \n- ✅ Keep PRs small and focused on a single feature/fix.  \n- ✅ Test your code before submitting.  \n\n### 4. Issues & Discussions  \n- **Bug Reports:** If you find a bug, check if an issue already exists. Otherwise, create a new issue.  \n- **Feature Requests:** If you have an idea, discuss it in an issue before implementing it.  \n\n### 5. Need Help?  \nIf you're unsure about anything, feel free to open a discussion or ask in an issue. We're happy to help!  \n\n🙌 **Happy Coding!** 🚀  \n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 13unk0wn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "# Feather 🎵\n\nFeather 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`.\n\n## 🎯 Aim\n\nA lightweight, ad-less player with only essential features.\n\n## ✨ Features\n\n- 🎶 **Stream YouTube Music** without downloading files.\n- ⚡ **Minimal Memory Usage**, targeting **60MB - 80MB RAM**.\n- 🚀 **Fast Playback**, with loading times around 3 seconds.\n- 🖥️ **Terminal User Interface (TUI)** built using Ratatui.\n- 🔄 **Self-Update Feature** (planned).\n\n## 🛠️ Installation\n\n### 📌 Prerequisites\n\nEnsure you have the following installed:\n\n- 🦀 **Rust** (latest stable version)\n- 📥 **yt-dlp** (for fetching YouTube data)\n- 🎵 **mpv** (for playback)\n\n### 🔧 Build from Source\n\n```sh\ngit clone https://github.com/13unk0wn/Feather.git\ncd Feather/feather_frontend\ncargo build --release\n```\n\n### ▶️ Run Feather\n\n```sh\n./target/release/feather_frontend\n```\n\n## 🎮 Usage\n\nNavigate through the TUI to search and play music. Additional controls and keyboard shortcuts will be documented soon.\n\n### 🛠️ Handling YouTube Restrictions\n\nIf a song fails to play due to YouTube restrictions, you can bypass them by adding your cookies to the environment:\n\n```sh\nexport FEATHER_COOKIES=\"paste your cookies here\"\n```\n\n- This is **optional** and should only be used if playback errors occur.\n- Feather can play songs without cookies, but adding them may help `mpv` bypass certain restrictions.\n\n## 🌄 Screenshot\n\n![Feather TUI Screenshot](screenshots/preview.png)\n\n## 🛠️ Compatibility\n\nFeather has been tested on **Linux Mint (Debian Edition)**, but all libraries used are compatible with other Linux distributions.\nWindows and Macos are not officially supported.\n\n## 🛣️ Roadmap\n\n### 🚀 Current Version: v0.1.0\n- 🎶 Implement player\n- 🔍 Implement search\n- �햐 Implement history\n\n### 🔥 Upcoming: v0.2.0\n- ⚡ Improve performance\n- 🎨 Improve UI\n- 🌜 Add support for playing playlists\n- 🎼 Add support for creating user playlists\n- ⚙️ Add user configuration support\n\n## 🤝 Contributing\n\nCheck out [CONTRIBUTION.md](https://github.com/13unk0wn/Feather/blob/main/CONTRIBUTING.md)\n\nIf you have any doubts regarding contribution, feel free to reach out via:\n- GitHub Issues\n- @x: [13unk0wn](https://x.com/13unk0wn)\n- Email: [13unk0wn.proton.me](mailto:13unk0wn@proton.me)\n\n## 🌟 Special Thanks\n\nA big thank you to the maintainers and contributors of:\n- [RustyPipe](https://codeberg.org/ThetaDev/rustypipe) — for providing essential tools for YouTube playback.\n- [mpv](https://github.com/mpv-player/mpv) — for making a great media player that powers Feather's playback.\n- [Ratatui](https://github.com/tui-rs-revival/ratatui) — for enabling the terminal-based UI experience.\n- [Sled](https://github.com/spacejam/sled) - database\n\n## 🌟 License\n\nFeather is licensed under the MIT License.\n\n---\n\n### 📝 Notes\n\nThis project is still in early development. Expect rapid iterations and improvements. Suggestions and feedback are always appreciated!\n\n\n"
  },
  {
    "path": "feather/.gitignore",
    "content": "/target\nrustypipe_cache.json\nCargo.lock\n"
  },
  {
    "path": "feather/Cargo.toml",
    "content": "[package]\nname = \"feather\"\nversion = \"0.1.0\"\nedition = \"2024\"\nbuild = \"build.rs\"\n\n[dependencies]\nrustypipe = \"0.9.0\"\ntokio = { version = \"1\", features = [\"full\"] }\nserde = { version = \"1.0\", features = [\"derive\"] }\nbincode = \"1.3.3\"\nsled = { version = \"0.34.7\",features = [\"compression\"] }\nthiserror = \"1.0\"\ntempfile = \"3.16.0\"\nlibmpv2 = \"4.1.0\"\ndirs = \"6.0.0\"\n\n[build-dependencies]\npkg-config = \"0.3\"\n\n[lib]\nname = \"feather\"\npath = \"src/lib.rs\"\n\n\n"
  },
  {
    "path": "feather/build.rs",
    "content": "fn main() {\n    if cfg!(target_os = \"macos\") && pkg_config::probe_library(\"mpv\").is_err() {\n        println!(\"cargo:warning=Could not find mpv via pkg-config. Make sure it is installed\");\n    }\n}\n"
  },
  {
    "path": "feather/src/database.rs",
    "content": "// This file manages the history database and contains all necessary functions related to history management\nuse crate::{ArtistName, SongId, SongName};\nuse serde::{Deserialize, Serialize};\nuse sled::Db;\nuse std::path::PathBuf;\nuse std::time::{SystemTime, UNIX_EPOCH};\nuse thiserror::Error;\n\n/// Represents a history entry for a song that has been played.\n#[derive(Serialize, Deserialize, Debug)]\npub struct HistoryEntry {\n    pub song_name: SongName,          // Name of the song\n    pub song_id: SongId,              // Unique identifier for the song\n    pub artist_name: Vec<ArtistName>, // List of artists associated with the song\n    time_stamp: u64,                  // Timestamp when the song was played\n}\n\nimpl HistoryEntry {\n    /// Creates a new history entry with the current timestamp.\n    pub fn new(\n        song_name: SongName,\n        song_id: SongId,\n        artist_name: Vec<ArtistName>,\n    ) -> Result<Self, Box<dyn std::error::Error>> {\n        let time_stamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();\n        Ok(Self {\n            song_name,\n            song_id,\n            artist_name,\n            time_stamp,\n        })\n    }\n}\n\n/// Database handler for managing song history.\npub struct HistoryDB {\n    db: Db, // Sled database instance\n}\n\n/// Represents possible errors that can occur in history operations.\n#[derive(Error, Debug)]\npub enum HistoryError {\n    #[error(\"Database error: {0}\")]\n    DbError(#[from] sled::Error), // Errors related to the sled database\n    #[error(\"Serialization error: {0}\")]\n    SerializationError(#[from] bincode::Error), // Errors during serialization/deserialization\n    #[error(\"Basic error: {0}\")]\n    Error(Box<dyn std::error::Error>), // Generic error wrapper\n}\n\nimpl HistoryDB {\n    pub fn new() -> Result<Self, sled::Error> {\n        let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(\"/tmp\"));\n        path.push(\"Feather/history_db\");\n\n        let db = sled::Config::new()\n            .path(path)\n            .cache_capacity(256 * 1024)\n            .use_compression(true)\n            .open()?;\n\n        Ok(HistoryDB { db })\n    }\n\n    /// Adds a new entry to the history database.\n    /// Limits the total stored entries to 50.\n    pub fn add_entry(&self, entry: &HistoryEntry) -> Result<(), HistoryError> {\n        let key = entry.song_id.as_bytes();\n        let value = bincode::serialize(entry)?;\n        self.db.insert(key, value)?;\n        self.limit_history_size(50)?;\n        Ok(())\n    }\n\n    /// Ensures the history database does not exceed `max_size` entries.\n    /// Removes the oldest entries if necessary.\n    pub fn limit_history_size(&self, max_size: usize) -> Result<(), HistoryError> {\n        while self.db.len() > max_size {\n            if let Some((key, _)) = self.db.first()? {\n                self.db.remove(key)?;\n            }\n        }\n        Ok(())\n    }\n\n    /// Retrieves up to 50 history entries, sorted by most recent first.\n    pub fn get_history(&self) -> Result<Vec<HistoryEntry>, HistoryError> {\n        let mut history = Vec::with_capacity(self.db.len().min(50)); // Pre-allocate vector\n        for item in self.db.iter().take(50) {\n            let (_, value) = item?;\n            if let Ok(entry) = bincode::deserialize::<HistoryEntry>(&value) {\n                history.push(entry);\n            }\n        }\n        history.sort_unstable_by(|e1, e2| e2.time_stamp.cmp(&e1.time_stamp)); // Sort by timestamp descending\n        Ok(history)\n    }\n\n    /// Deletes a specific history entry by song ID.\n    pub fn delete_entry(&self, song_id: &str) -> Result<(), HistoryError> {\n        self.db.remove(song_id.as_bytes())?; // Convert song ID to bytes\n        Ok(())\n    }\n\n    /// Clears all history entries from the database.\n    pub fn clear_history(&self) -> Result<(), HistoryError> {\n        self.db.clear()?;\n        Ok(())\n    }\n\n    /// Retrieves the most recently played song's ID, if available.\n    pub fn get_last_played_song(&self) -> Result<Option<SongId>, HistoryError> {\n        if let Some((_, last_entry)) = self.db.last()? {\n            let entry: HistoryEntry = bincode::deserialize(&last_entry)?;\n            Ok(Some(entry.song_id))\n        } else {\n            Ok(None)\n        }\n    }\n}\n\n// Unchanged UserPlaylist and PlaylistManager sections...\n// #[derive(Serialize, Deserialize, Debug, Clone)]\n// struct UserPlaylist {\n//     playlist_name: PlaylistName,\n//     songs: Vec<Song>,\n// }\n\n// #[derive(Error, Debug)]\n// pub enum PlaylistManagerError {\n//     #[error(\"Database error: {0}\")]\n//     DbError(#[from] sled::Error),\n//     #[error(\"Serialization error: {0}\")]\n//     SerializationError(#[from] bincode::Error),\n//     #[error(\"Playlist '{0}' not found\")]\n//     PlaylistNotFound(String),\n//     #[error(\"Song '{0}' not found in playlist '{1}'\")]\n//     SongNotFound(String, String),\n//     #[error(\"Duplicate playlist name: '{0}'\")]\n//     DuplicatePlaylist(String),\n//     #[error(\"Failed to add song '{0}' to playlist '{1}'\")]\n//     AddSongError(String, String),\n//     #[error(\"Failed to remove song '{0}' from playlist '{1}'\")]\n//     RemoveSongError(String, String),\n//     #[error(\"Unknown error: {0}\")]\n//     Other(String),\n// }\n\n// #[derive(Serialize, Deserialize, Debug, Clone)]\n// struct Song {\n//     song_name: SongName,\n//     song_id: SongId,\n//     artist: Vec<ArtistName>,\n// }\n\n// struct PlaylistManager {\n//     db: sled::Db,\n// }\n\n// impl PlaylistManager {\n//     pub fn new(path: &str) -> Result<Self, PlaylistManagerError> {\n//         let db = sled::open(path)?;\n//         Ok(Self { db })\n//     }\n//     fn create_playlist(&self, name: &str) -> Result<(), PlaylistManagerError> {\n//         if self.db.get(name)?.is_some() {\n//             return Err(PlaylistManagerError::DuplicatePlaylist(name.to_string()));\n//         }\n//         let playlist = UserPlaylist {\n//             playlist_name: name.to_string(),\n//             songs: Vec::new(),\n//         };\n//         let value = bincode::serialize(&playlist)?;\n//         self.db.insert(name, value)?;\n//         self.db.flush()?;\n//         Ok(())\n//     }\n//     fn add_song_to_playlist(\n//         &self,\n//         playlist_name: &str,\n//         song: Song,\n//     ) -> Result<(), PlaylistManagerError> {\n//         let raw_data = self\n//             .db\n//             .get(playlist_name)?\n//             .ok_or_else(|| PlaylistManagerError::Other(\"Error: In Opening Playlist\".to_string()))?\n//             .to_vec();\n\n//         let mut playlist: UserPlaylist = bincode::deserialize(&raw_data)?;\n\n//         playlist.songs.retain(|s| s.song_id != song.song_id);\n//         playlist.songs.push(song);\n\n//         let serialized_data = bincode::serialize(&playlist)?;\n//         self.db.insert(playlist_name, serialized_data)?;\n//         self.db.flush()?;\n\n//         Ok(())\n//     }\n//     fn remove_song_from_playlist(\n//         &self,\n//         playlist_name: &str,\n//         song_id: &str,\n//     ) -> Result<(), PlaylistManagerError> {\n//         let raw_data = self\n//             .db\n//             .get(playlist_name)?\n//             .ok_or_else(|| PlaylistManagerError::Other(\"Error: In Opening Playlist\".to_string()))?\n//             .to_vec();\n\n//         let mut playlist: UserPlaylist = bincode::deserialize(&raw_data)?;\n\n//         playlist.songs.retain(|s| s.song_id != song_id);\n//         let serialized_data = bincode::serialize(&playlist)?;\n//         self.db.insert(playlist_name, serialized_data)?;\n//         self.db.flush()?;\n\n//         Ok(())\n//     }\n\n//     fn get_playlist(&self, playlist_name: &str) -> Result<UserPlaylist, PlaylistManagerError> {\n//         let data = self\n//             .db\n//             .get(playlist_name)?\n//             .ok_or_else(|| PlaylistManagerError::PlaylistNotFound(playlist_name.to_string()))?\n//             .to_vec();\n//         let playlist: UserPlaylist = bincode::deserialize(&data)?;\n//         Ok(playlist)\n//     }\n//     fn delete_playlist(&self, playlist_name: &str) -> Result<(), PlaylistManagerError> {\n//         self.db\n//             .remove(&playlist_name)?\n//             .ok_or_else(|| PlaylistManagerError::PlaylistNotFound(playlist_name.to_string()));\n//         self.db.flush()?;\n//         Ok(())\n//     }\n// }\n\n// // Tests unchanged...\n// #[cfg(test)]\n// mod tests {\n//     use super::*;\n//     use tempfile::tempdir;\n\n//     fn sample_song(name: &str, id: &str) -> Song {\n//         Song {\n//             song_name: name.to_string(),\n//             song_id: id.to_string(),\n//             artist: vec![\"Artist One\".to_string(), \"Artist Two\".to_string()],\n//         }\n//     }\n\n//     #[test]\n//     fn test_playlist_manager() {\n//         let temp_dir = tempdir().unwrap();\n//         let db_path = temp_dir.path().to_str().unwrap();\n//         let manager = PlaylistManager::new(db_path).unwrap();\n\n//         let playlist_name = \"MyPlaylist\";\n\n//         assert!(manager.create_playlist(playlist_name).is_ok());\n\n//         let song1 = sample_song(\"Song A\", \"123\");\n//         let song2 = sample_song(\"Song B\", \"456\");\n\n//         assert!(manager\n//             .add_song_to_playlist(playlist_name, song1.clone())\n//             .is_ok());\n//         assert!(manager\n//             .add_song_to_playlist(playlist_name, song2.clone())\n//             .is_ok());\n\n//         let playlist = manager.get_playlist(playlist_name).unwrap();\n//         assert_eq!(playlist.songs.len(), 2);\n//         assert!(playlist.songs.iter().any(|s| s.song_id == \"123\"));\n//         assert!(playlist.songs.iter().any(|s| s.song_id == \"456\"));\n\n//         assert!(manager\n//             .remove_song_from_playlist(playlist_name, \"123\")\n//             .is_ok());\n//         let playlist = manager.get_playlist(playlist_name).unwrap();\n//         assert_eq!(playlist.songs.len(), 1);\n//         assert!(playlist.songs.iter().all(|s| s.song_id != \"123\"));\n\n//         assert!(manager.delete_playlist(playlist_name).is_ok());\n//         let result = manager.get_playlist(playlist_name);\n//         assert!(matches!(\n//             result,\n//             Err(PlaylistManagerError::PlaylistNotFound(_))\n//         ));\n//     }\n// }\n"
  },
  {
    "path": "feather/src/lib.rs",
    "content": "pub mod database;\npub mod player;\npub mod yt;\n\n/// Input/Return Types\npub type ArtistName = String;\npub type SongName = String;\npub type SongId = String;\npub type SongUrl = String;\npub type PlaylistName = String;\npub type PlaylistId = String;\npub type ChannelName = String;\n"
  },
  {
    "path": "feather/src/player.rs",
    "content": "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\nuse std::sync::Arc;\n\n/// The `Player` struct represents a media player using the MPV library.\n/// It provides functionalities to control playback, retrieve metadata,\n/// and manage audio optimizations.\npub struct Player {\n    /// An instance of the MPV player wrapped in an `Arc` for thread safety.\n    pub player: Arc<Mpv>,\n}\n\n/// Enum representing possible errors when interacting with the MPV player.\n#[derive(Debug, thiserror::Error)]\npub enum MpvError {\n    #[error(\"Mpv error: {0}\")]\n    Mpv(#[from] libmpv2::Error),\n    #[error(\"Failed to initialize MPV\")]\n    InitializationError,\n    #[error(\"Command execution failed: {0}\")]\n    CommandError(String),\n    #[error(\"Failed to load file: {0}\")]\n    LoadFileError(String),\n    #[error(\"Property retrieval failed: {0}\")]\n    PropertyError(String),\n    #[error(\"Unknown error: {0}\")]\n    Other(String),\n}\n\nimpl Player {\n    /// Creates a new `Player` instance and configures MPV settings for optimized audio playback.\n    pub fn new(cookies: Option<String>) -> Result<Self, MpvError> {\n        let mpv = Mpv::new()?;\n        if cookies.is_some() {\n            // setting cookies  if given by user\n            mpv.set_property(\"cookies-file\", cookies.unwrap())?;\n        }\n\n        // Disable video to save memory\n        mpv.set_property(\"video\", \"no\")?;\n\n        // Optimize caching for lower memory usage\n        //mpv.set_property(\"cache-secs\", 2)?; // Reduced to 2 seconds\n        // mpv.set_property(\"demuxer-readahead-secs\", 1)?; // Reduced to 1 second\n        //mpv.set_property(\"demuxer-max-bytes\", 512 * 1024)?; // 512 KB max buffer\n\n        // Configure network request headers for YouTube playback\n        mpv.set_property(\"ytdl-raw-options\", \"no-check-certificate=\")?;\n        mpv.set_property(\"loop\", \"inf\")?; // Looping enabled (to be removed with autoplay)\n        mpv.set_property(\n            \"http-header-fields\",\n            \"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\",\n        )?;\n\n        // Audio optimization\n        mpv.set_property(\"audio-buffer\", 0.1)?; // 100ms audio buffer\n        mpv.set_property(\"audio-channels\", \"stereo\")?; // Force stereo audio\n\n        let mpv = Arc::new(mpv);\n        Ok(Self { player: mpv })\n    }\n\n    /// Loads and plays a media file from a given URL.\n    pub fn play(&self, url: &str) -> Result<(), MpvError> {\n         if let Ok(true) = self.player.get_property(\"pause\") {\n            self.unpause()?;\n        } // Quick fix will improve \n        self.player.command(\"loadfile\", &[url])?; // Replace the current playback\n        Ok(())\n    }\n\n    /// Pauses playback.\n    pub fn pause(&self) -> Result<(), MpvError> {\n        self.player.command(\"set\", &[\"pause\", \"yes\"])?;\n        Ok(())\n    }\n\n    /// Resumes playback.\n    pub fn unpause(&self) -> Result<(), MpvError> {\n        self.player.command(\"set\", &[\"pause\", \"no\"])?;\n        Ok(())\n    }\n\n    /// Toggles between play and pause states.\n    pub fn play_pause(&self) -> Result<(), MpvError> {\n        match self.player.get_property::<bool>(\"pause\") {\n            Ok(true) => self.unpause()?,\n            Ok(false) => self.pause()?,\n            Err(_) => todo!(),\n        }\n        Ok(())\n    }\n\n    /// Seeks forward by 5 seconds in the current track.\n    pub fn seek_forward(&self) -> Result<(), MpvError> {\n        self.player.command(\"seek\", &[\"5\", \"relative\"])?;\n        Ok(())\n    }\n\n    /// Seeks backward by 5 seconds in the current track.\n    pub fn seek_backword(&self) -> Result<(), MpvError> {\n        self.player.command(\"seek\", &[\"-5\", \"relative\"])?;\n        Ok(())\n    }\n\n    /// Retrieves the current playback time as a string.\n    pub fn get_current_time(&self) -> String {\n        self.player\n            .get_property(\"time-pos\")\n            .unwrap_or(0.0)\n            .to_string()\n    }\n\n    /// Retrieves the duration of the currently playing media.\n    pub fn duration(&self) -> String {\n        self.player\n            .get_property(\"duration\")\n            .unwrap_or(0.0)\n            .to_string()\n    }\n\n    /// Returns whether a media file is currently playing.\n    pub fn is_playing(&self) -> Result<bool, MpvError> {\n        let pause: bool = self.player.get_property(\"pause\")?;\n        Ok(!pause)\n    }\n}\n"
  },
  {
    "path": "feather/src/yt.rs",
    "content": "use crate::{ArtistName, ChannelName, PlaylistId, PlaylistName, SongId, SongName, SongUrl};\nuse std::path::PathBuf;\nuse rustypipe::{\n    client::{RustyPipe, RustyPipeQuery},\n    model::MusicItem,\n    param::StreamFilter,\n};\nuse std::collections::HashMap;\n\n/// A client for interacting with YouTube music using RustyPipe.\npub struct YoutubeClient {\n    client: RustyPipeQuery,\n}\n\nimpl YoutubeClient {\n    /// Creates a new instance of `YoutubeClient`.\n    pub fn new() -> Self {\n        let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from(\"/tmp\"));\n        path.push(\"Feather\");\n        let rp = RustyPipe::builder().storage_dir(path).build().unwrap();\n        let client = rp.query();\n        YoutubeClient { client }\n    }\n\n    /// Searches for music based on the given query.\n    /// Returns a vector of tuples where each entry contains a song name and ID,\n    /// along with a list of associated artist names.\n    pub async fn search(\n        &self,\n        query: &str,\n    ) -> Result<Vec<((SongName, SongId), Vec<ArtistName>)>, String> {\n        match self.client.music_search_main(query).await {\n            Ok(results) => {\n                let mut search_result = vec![];\n\n                for item in results.items.items {\n                    if let MusicItem::Track(data) = item {\n                        let song_id_pair = (data.name, data.id);\n                        let artist_names: Vec<String> =\n                            data.artists.into_iter().map(|id| id.name).collect();\n                        search_result.push((song_id_pair, artist_names));\n                    }\n                }\n\n                Ok(search_result)\n            }\n            Err(_) => Err(\"Error in Search Result\".to_string()),\n        }\n    }\n\n    /// Fetches the audio stream URL for a given song ID.\n    pub async fn fetch_song_url(&self, id: &SongId) -> Result<SongUrl, String> {\n        match self.client.player(&id).await {\n            Ok(player) => match player.select_audio_stream(&StreamFilter::default()) {\n                Some(stream) => return Ok(stream.url.clone()),\n                None => return Err(\"Audio Stream not Found\".to_string()),\n            },\n            Err(_) => return Err(\"Link cannot be Found\".to_string()),\n        }\n    }\n\n    /// Searches for playlists based on a given query.\n    /// Returns a hashmap where the key is the playlist name and the value is a tuple\n    /// containing the playlist ID and a list of associated channel names.\n    pub async fn fetch_playlist(\n        &self,\n        search_query: &str,\n    ) -> Result<HashMap<PlaylistName, (PlaylistId, Vec<ChannelName>)>, String> {\n        match self.client.music_search_playlists(search_query, true).await {\n            Ok(playlists) => {\n                let mut result = HashMap::new();\n\n                for playlist in playlists.items.items {\n                    let playlist_id = playlist.id;\n                    let channel_names: Vec<String> = playlist\n                        .channel\n                        .into_iter()\n                        .map(|channel| channel.name)\n                        .collect();\n\n                    result.insert(playlist.name, (playlist_id, channel_names));\n                }\n\n                Ok(result)\n            }\n            Err(e) => Err(format!(\"Error in fetching playlists: {}\", e)),\n        }\n    }\n\n    /// Fetches songs from a given playlist ID.\n    /// Returns a hashmap where each key is a tuple of (song name, song ID), and\n    /// the value is a list of associated artist names.\n    pub async fn fetch_playlist_songs(\n        &self,\n        playlist_id: PlaylistId,\n    ) -> Result<HashMap<(SongName, SongId), Vec<ArtistName>>, String> {\n        match self.client.playlist(playlist_id).await {\n            Ok(playlist_data) => {\n                let mut song_map = HashMap::new();\n\n                for video in playlist_data.videos.items {\n                    let song_key = (video.name, video.id);\n                    let artist_names: Vec<String> = video\n                        .channel\n                        .into_iter()\n                        .map(|channel| channel.name)\n                        .collect();\n\n                    song_map.insert(song_key, artist_names);\n                }\n\n                Ok(song_map)\n            }\n            Err(e) => Err(format!(\"Error fetching playlist songs: {}\", e)),\n        }\n    }\n\n    /// Fetches related songs for a given song ID.\n    /// Returns a hashmap where each key is a tuple of (song name, song ID), and\n    /// the value is a list of associated artist names.\n    pub async fn fetch_related_song(\n        &self,\n        song_id: SongId,\n    ) -> Result<HashMap<(SongName, SongId), Vec<ArtistName>>, String> {\n        match self.client.music_related(song_id).await {\n            Ok(music_list) => {\n                let tracks = music_list.tracks;\n                let mut results = HashMap::new();\n                for track in tracks {\n                    let song_id_name = (track.name, track.id);\n                    let artist_names = track\n                        .artists\n                        .into_iter()\n                        .map(|artist| artist.name)\n                        .collect::<Vec<ArtistName>>();\n                    results.insert(song_id_name, artist_names);\n                }\n                Ok(results)\n            }\n            Err(_) => Err(\"Error finding related songs\".to_string()),\n        }\n    }\n}\n// #[tokio::test]\n// async fn test_search() {\n//     let client = YoutubeClient::new();\n\n//     match client.search(\"Beanie\").await {\n//         Ok(results) => {\n//             for ((song, id), artists) in results {\n//                 println!(\"Song: {}\", song);\n//                 println!(\"Id : {}\", id);\n//                 println!(\"{}\", client.fetch_song_url(id.clone()).await.unwrap());\n\n//                 for artist in artists {\n//                     println!(\"  - Artist: {}\", artist);\n//                 }\n//                 test_fetch_related_song(id).await;\n//                 break;\n//             }\n//         }\n//         Err(e) => println!(\"Search failed: {}\", e),\n//     }\n// }\n// #[tokio::test]\n// async fn test_fetch_playlist() {\n//     let client = YoutubeClient::new();\n//     let query = \"lofi beats\";\n\n//     match client.fetch_playlist(query).await {\n//         Ok(playlists) => {\n//             for (playlist_name, (playlist_id, channel_names)) in playlists {\n//                 println!(\"Playlist: {} (ID: {})\", playlist_name, playlist_id);\n//                 test_fetch_playlist_songs(playlist_id).await;\n\n//                 // for channel in channel_names {\n//                 //     println!(\"  - Channel: {}\", channel);\n//                 // }\n//                 break;\n//             }\n//         }\n//         Err(e) => eprintln!(\"Test failed: {}\", e),\n//     }\n// }\n// async fn test_fetch_playlist_songs(playlist_id :  String) {\n//     let client = YoutubeClient::new();\n\n//     match client.fetch_playlist_songs(playlist_id).await {\n//         Ok(songs) => {\n//             for ((song_name, song_id), artist_names) in songs {\n//                 println!(\"Song: {} (ID: {})\", song_name, song_id);\n//                 let url  =  client.fetch_song_url(&song_id).await.unwrap();\n//                 println!(\"{url:?}\");\n//                 for artist in artist_names {\n//                     println!(\"  - Artist: {}\", artist);\n//                 }\n//             }\n//         }\n//         Err(e) => eprintln!(\"Test failed: {}\", e),\n//     }\n// }\n\n// async fn test_fetch_related_song(song_id  :  String) {\n//     let client = YoutubeClient::new();\n\n//     match client.fetch_related_song(song_id).await {\n//         Ok(related_songs) => {\n//             for ((song_name, song_id), artist_names) in related_songs {\n//                 println!(\"Related Song: {} (ID: {})\", song_name, song_id);\n//                 for artist in artist_names {\n//                     println!(\"  - Artist: {}\", artist);\n//                 }\n//             }\n//         }\n//         Err(e) => eprintln!(\"Test failed: {}\", e),\n//     }\n// }\n"
  },
  {
    "path": "feather/structure.txt",
    "content": "Feather\n│── backend\n│   │── yt.rs                \n│   │   │── search(query: &str) -> Vec<String>       # Search YouTube and return video URLs. [done]\n│   │   │── fetch_url(video_id: &str) -> String      # Get the direct link for mpv to play. [done]\n│   │   │── fetch_playlist(playlist_id: &str) -> Vec<String>  # Fetch all songs in a playlist. [done]\n│   │   │── fetch_related(video_id: &str) -> Vec<String>      # Get related songs (for autoplay).[unresolved]\n│   │\n│   │── mpv.rs               \n│   │   │── play(url: &str)                           # Play a song using mpv.\n│   │   │── pause()                                   # Pause or resume playback.\n│   │   │── stop()                                    # Stop the current playback.\n│   │   │── skip(seconds: i64)                        # Skip forward/backward by seconds.\n│   │   │── volume(level: u8)                         # Adjust the volume level.\n│   │   │── next()                                    # Play the next song in the queue.\n│   │   │── on_song_end()                             # Triggered when a song ends (handles autoplay).\n│   │\n│   │── database.rs        \n│   │   │── add_to_history(song: &str)               # Save song to history. [done]\n│   │   │── get_history() -> Vec<String>             # Retrieve history list. [done]\n│   │   │── delete_from_history(index: usize)        # Remove a song from history. [done]\n│   │   │── save_playlist(name: &str, songs: Vec<String>)   # Create or update a playlist.[done]\n│   │   │── load_playlist(name: &str) -> Vec<String>        # Load songs from a playlist.[done]\n│   │   │── delete_playlist(name: &str)              # Delete a playlist.[done]\n│   │   │── get_last_played() -> Option<String>      # Get the last played song.[done]\n│   │   │── add_to_queue(song: &str)                 # Add song to queue.\n│   │   │── remove_from_queue(index: usize)          # Remove song from queue.\n│   │   │── get_next_song() -> Option<String>        # Get next song from queue.\n│   │   │── clear_queue()                            # Clear the queue.\n│   │\n│   │── config.rs        \n│   │   │── load_config() -> Config                  # Load settings from `config.lua`.\n│   │   │── save_config(config: &Config)             # Save updated settings.\n│   │   │── watch_config()                           # Optional: Reload config if changed.\n│   │\n│── config.lua       # User settings (autoplay, volume, history limit, storage path).\n"
  },
  {
    "path": "feather_frontend/.gitignore",
    "content": "target/\nrustypipe_cache.json\n"
  },
  {
    "path": "feather_frontend/Cargo.toml",
    "content": "[package]\nname = \"feather_frontend\"\nversion = \"0.1.0\"\nedition = \"2024\"\nauthors = [\"13unk0wn 13unk0wn@proton.me\"]\ndescription = \"A lightweight YouTube Music TUI in Rust.\"\nlicense = \"MIT\"\ncategories = [\"command-line-utilities\", \"multimedia\"]\nkeywords = [\"music\", \"youtube\", \"tui\", \"rust\"]\n\n\n\n[dependencies]\ncolor-eyre = \"0.6.3\"\ncrossterm = \"0.28.1\"\nratatui = \"0.29.0\"\ntui-textarea = \"0.7.0\"\nfeather = {path  = \"../feather\"}\ntokio = \"1.43.0\"\ntui-scrollview = \"0.3\"\nthiserror =\"1.0\"\nwee_alloc = \"0.4\"\n\n[profile.release]\nopt-level = 3  # Maximum optimization\nlto = true     # Link Time Optimization\ncodegen-units = 1  # Optimize for binary size\nstrip = true   # Remove debug symbols\npanic = 'abort'  # Reduce unwinding overhead\n\n# [replace-with]\n# global_allocator = \"wee_alloc\"\n"
  },
  {
    "path": "feather_frontend/src/backend.rs",
    "content": "use feather::{\n    ArtistName, SongId, SongName,\n    database::{HistoryDB, HistoryEntry},\n    player::{MpvError, Player},\n    yt::YoutubeClient,\n};\nuse std::sync::Arc;\nuse std::sync::Mutex;\nuse std::time::Duration;\n\nuse thiserror::Error;\n\n/// The `Backend` struct manages the YouTube client, music player, and history database.\n/// It also tracks the currently playing song.\npub struct Backend {\n    pub yt: YoutubeClient,         // YouTube client for fetching song URLs\n    pub player: Player,            // Music player instance\n    pub history: Arc<HistoryDB>,   // Shared history database\n    pub song: Mutex<Option<Song>>, // Mutex-protected optional current song\n}\n\n/// Represents a song with its name, ID, and artist(s).\n#[derive(Clone)]\npub struct Song {\n    pub song_name: SongName,      // Name of the song\n    pub song_id: SongId,          // Unique identifier for the song\n    artist_name: Vec<ArtistName>, // List of artists performing the song\n}\n\n/// Implements conversion from `Song` to `HistoryEntry`, ensuring valid history records.\nimpl From<Song> for HistoryEntry {\n    fn from(value: Song) -> Self {\n        HistoryEntry::new(value.song_name, value.song_id, value.artist_name)\n            .expect(\"Cannot Form History Entry\")\n    }\n}\n\nimpl Song {\n    /// Creates a new `Song` instance.\n    pub fn new(song_name: SongName, song_id: SongId, artist_name: Vec<ArtistName>) -> Self {\n        Self {\n            song_name,\n            song_id,\n            artist_name,\n        }\n    }\n}\n\n/// Defines possible errors that can occur in the `Backend`.\n#[derive(Error, Debug)]\npub enum BackendError {\n    #[error(\"Player error: {0}\")]\n    Mpv(#[from] MpvError), // Error related to the music player\n\n    #[error(\"Failed to fetch YouTube URL\")]\n    YoutubeFetch(String), // Error when fetching a song URL from YouTube\n\n    #[error(\"Mutex poisoned: {0}\")]\n    MutexPoisoned(String), // Error when accessing a poisoned mutex\n\n    #[error(\"History database error: {0}\")]\n    HistoryError(String), // Error related to history database operations\n\n    #[error(\"Playback error: {0}\")]\n    PlaybackError(String), // Error related to playback issues\n}\n\nimpl Backend {\n    /// Creates a new `Backend` instance.\n    ///\n    /// # Arguments\n    /// * `history` - Shared reference to the history database.\n    /// * `cookies` - Optional cookie string for authentication.\n    ///\n    /// # Returns\n    /// * `Result<Self, BackendError>` - Returns `Backend` on success or an error on failure.\n    pub fn new(history: Arc<HistoryDB>, cookies: Option<String>) -> Result<Self, BackendError> {\n        Ok(Self {\n            yt: YoutubeClient::new(),\n            player: Player::new(cookies).map_err(BackendError::Mpv)?,\n            history,\n            song: Mutex::new(None),\n        })\n    }\n\n    /// Plays a song by fetching its URL from YouTube and passing it to the player.\n    ///\n    /// # Arguments\n    /// * `song` - The song to be played.\n    ///\n    /// # Returns\n    /// * `Result<(), BackendError>` - Returns `Ok(())` on success or an error on failure.\n    pub async fn play_music(&self, song: Song) -> Result<(), BackendError> {\n        const MAX_RETRIES: i32 = 8;\n        let id = song.song_id.to_string();\n\n        // Fetch song URL with retry mechanism\n        let url = {\n            let mut attempts = 0;\n            loop {\n                match self.yt.fetch_song_url(&id).await {\n                    Ok(url) => break url,\n                    Err(_) if attempts < MAX_RETRIES => {\n                        attempts += 1;\n                        tokio::time::sleep(Duration::from_millis(100)).await;\n                        continue;\n                    }\n                    Err(e) => {\n                        return Err(BackendError::YoutubeFetch(format!(\n                            \"Failed to fetch URL after {} attempts: {:?}\",\n                            MAX_RETRIES, e\n                        )));\n                    }\n                }\n            }\n        };\n\n        // Update the currently playing song in a mutex-protected section\n        {\n            let mut current_song = self\n                .song\n                .lock()\n                .map_err(|e| BackendError::MutexPoisoned(e.to_string()))?;\n            *current_song = Some(song.clone());\n        }\n\n        // Play the song\n        self.player.play(&url).map_err(BackendError::Mpv)?;\n\n        // Add the song to history\n        self.history\n            .add_entry(&HistoryEntry::from(song))\n            .map_err(|e| BackendError::HistoryError(e.to_string()))?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "feather_frontend/src/history.rs",
    "content": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse feather::database::HistoryDB;\nuse ratatui::prelude::{Buffer, Color, Constraint, Layout, Rect};\nuse ratatui::style::Style;\nuse ratatui::text::Span;\nuse ratatui::widgets::{\n    Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarState,\n    StatefulWidget, Widget,\n};\nuse std::sync::Arc;\nuse tokio::sync::mpsc;\n\n// Defines a struct to manage playback history UI\npub struct History {\n    history: Arc<HistoryDB>,               // Database connection for history\n    selected: usize,                       // Index of currently selected item\n    vertical_scroll_state: ScrollbarState, // State for vertical scrollbar\n    max_len: usize,                        // Total number of history items\n    selected_song: Option<Song>,           // Currently selected song details\n    backend: Arc<Backend>,                 // Audio backend for playback\n    tx_player: mpsc::Sender<bool>,         // Channel to communicate with player\n}\n\nimpl History {\n    // Constructor initializing the History struct\n    pub fn new(\n        history: Arc<HistoryDB>,\n        backend: Arc<Backend>,\n        tx_player: mpsc::Sender<bool>,\n    ) -> Self {\n        Self {\n            history,\n            selected: 0,\n            vertical_scroll_state: ScrollbarState::default(),\n            max_len: 0,\n            selected_song: None,\n            backend,\n            tx_player,\n        }\n    }\n\n    // Handles keyboard input for navigation and actions\n    pub fn handle_keystrokes(&mut self, key: KeyEvent) {\n        match key.code {\n            KeyCode::Char('j') | KeyCode::Down => {\n                // Move selection down\n                self.select_next();\n            }\n            KeyCode::Char('k') | KeyCode::Up => {\n                // Move selection up\n                self.select_previous();\n            }\n            KeyCode::Char('d') => {\n                // Delete selected entry\n                if let Some(song) = &self.selected_song {\n                    let _ = self.history.delete_entry(&song.song_id);\n                }\n            }\n            KeyCode::Enter => {\n                // Play selected song\n                if let Some(song) = self.selected_song.clone() {\n                    let backend = Arc::clone(&self.backend);\n                    let tx_player = self.tx_player.clone();\n                    tokio::spawn(async move {\n                        // Spawn async task for playback\n                        if backend.play_music(song).await.is_ok() {\n                            let _ = tx_player.send(true).await;\n                        }\n                    });\n                }\n            }\n            _ => (), // Ignore other keys\n        }\n    }\n\n    // Moves selection to next item, respecting bounds\n    fn select_next(&mut self) {\n        if self.max_len > 0 {\n            self.selected = (self.selected + 1).min(self.max_len - 1);\n            self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);\n        }\n    }\n\n    // Moves selection to previous item, preventing underflow\n    fn select_previous(&mut self) {\n        self.selected = self.selected.saturating_sub(1);\n        self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);\n    }\n\n    // Renders the history UI component\n    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {\n        let chunks = Layout::default()\n            .direction(ratatui::layout::Direction::Vertical)\n            .constraints([Constraint::Length(3), Constraint::Min(0)]) // Split layout\n            .split(area);\n\n        // Render title bar\n        Paragraph::new(\"History\")\n            .style(Style::default().fg(Color::White))\n            .block(Block::default().borders(Borders::ALL))\n            .render(chunks[0], buf);\n\n        // Setup history list area with scrollbar\n        let history_area = chunks[1];\n        let scrollbar = Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight)\n            .begin_symbol(Some(\"↑\"))\n            .end_symbol(Some(\"↓\"));\n        scrollbar.render(history_area, buf, &mut self.vertical_scroll_state);\n\n        // Fetch and render history items\n        if let Ok(items) = self.history.get_history() {\n            self.max_len = items.len();\n            self.vertical_scroll_state = self.vertical_scroll_state.content_length(self.max_len);\n\n            let view_items: Vec<ListItem> = items\n                .into_iter()\n                .enumerate()\n                .map(|(i, item)| {\n                    // Format each item for display\n                    let is_selected = i == self.selected;\n                    if is_selected {\n                        self.selected_song = Some(Song::new(\n                            item.song_name.clone(),\n                            item.song_id.clone(),\n                            item.artist_name.clone(),\n                        ));\n                    }\n                    let style = if is_selected {\n                        // Highlight selected item\n                        Style::default().fg(Color::Yellow).bg(Color::Blue)\n                    } else {\n                        Style::default()\n                    };\n                    let text = format!(\"{} - {}\", item.song_name, item.artist_name.join(\", \"));\n                    ListItem::new(Span::styled(text, style))\n                })\n                .collect();\n\n            let mut list_state = ListState::default();\n            list_state.select(Some(self.selected));\n            StatefulWidget::render(\n                // Render the list\n                List::new(view_items)\n                    .block(Block::default().borders(Borders::ALL))\n                    .highlight_symbol(\"▶\"),\n                history_area,\n                buf,\n                &mut list_state,\n            );\n        } else {\n            // Handle history loading failure\n            self.max_len = 0;\n            self.selected = 0;\n            Paragraph::new(\"Failed to load history\").render(history_area, buf);\n        }\n    }\n}\n"
  },
  {
    "path": "feather_frontend/src/lib.rs",
    "content": "pub mod backend;\npub mod history;\npub mod player;\npub mod search;\n"
  },
  {
    "path": "feather_frontend/src/main.rs",
    "content": "use color_eyre::eyre::Result;\nuse crossterm::event::{Event, KeyCode, KeyEvent, poll, read};\nuse feather::database::HistoryDB;\nuse feather_frontend::{backend::Backend, history::History, player::SongPlayer, search::Search};\nuse ratatui::{\n    DefaultTerminal,\n    buffer::Buffer,\n    layout::{Constraint, Layout, Rect},\n    widgets::{Block, Borders, Cell, Paragraph, Row, Table, Widget},\n};\nuse std::{env, sync::Arc};\nuse tokio::{\n    sync::mpsc,\n    time::{Duration, interval},\n};\n\n/// Entry point for the async runtime.\n#[tokio::main]\nasync fn main() -> Result<()> {\n    color_eyre::install().unwrap();\n    let terminal = ratatui::init();\n    let _app = App::new().render(terminal).await;\n    ratatui::restore();\n    Ok(())\n}\n\n/// Enum representing different states of the application.\n#[derive(Debug)]\nenum State {\n    HelpMode,\n    Global,\n    Search,\n    History,\n    // UserPlaylist,\n    // CurrentPlayingPlaylist,\n    SongPlayer,\n}\n\n/// Main application struct managing the state and UI components.\nstruct App<'a> {\n    state: State,\n    search: Search<'a>,\n    history: History,\n    // user_playlist: UserPlaylist,\n    // current_playling_playlist: CurrentPlayingPlaylist,\n    top_bar: TopBar,\n    player: SongPlayer,\n    // backend: Arc<Backend>,\n    help_mode: bool,\n    exit: bool,\n}\n\nimpl App<'_> {\n    /// Creates a new instance of the application.\n    fn new() -> Self {\n        let history = Arc::new(HistoryDB::new().unwrap());\n        let get_cookies = env::var(\"FEATHER_COOKIES\").ok(); // Fetch cookies from environment variables if available.\n        let backend = Arc::new(Backend::new(history.clone(), get_cookies).unwrap());\n        let (tx, rx) = mpsc::channel(32);\n\n        App {\n            state: State::Global,\n            search: Search::new(backend.clone(), tx.clone()),\n            history: History::new(history, backend.clone(), tx.clone()),\n            // user_playlist: UserPlaylist {},\n            // current_playling_playlist: CurrentPlayingPlaylist {},\n            top_bar: TopBar::new(),\n            player: SongPlayer::new(backend.clone(), rx),\n            // backend,\n            help_mode: false,\n            exit: false,\n        }\n    }\n\n    /// Handles global keystrokes and state transitions.\n    fn handle_global_keystrokes(&mut self, key: KeyEvent) {\n        match self.state {\n            State::Global => match key.code {\n                KeyCode::Char('s') => self.state = State::Search,\n                KeyCode::Char('h') => self.state = State::History,\n                KeyCode::Char('p') => self.state = State::SongPlayer,\n                KeyCode::Char('?') => {\n                    self.help_mode = true;\n                    self.state = State::HelpMode;\n                }\n                KeyCode::Esc => {\n                    self.exit = true;\n                }\n                _ => (),\n            },\n            State::Search => match key.code {\n                KeyCode::Esc => self.state = State::Global,\n                _ => self.search.handle_keystrokes(key),\n            },\n            State::HelpMode => match key.code {\n                KeyCode::Esc => {\n                    self.state = State::Global;\n                    self.help_mode = false;\n                }\n                _ => (),\n            },\n            State::History => match key.code {\n                KeyCode::Esc => self.state = State::Global,\n                _ => self.history.handle_keystrokes(key),\n            },\n            State::SongPlayer => match key.code {\n                KeyCode::Esc => self.state = State::Global,\n                _ => self.player.handle_keystrokes(key),\n            },\n        }\n    }\n\n    /// Main render loop for updating the UI.\n    async fn render(mut self, mut terminal: DefaultTerminal) {\n        let mut redraw_interval = interval(Duration::from_millis(250)); // Redraw every 250ms\n\n        while !self.exit {\n            terminal\n                .draw(|frame| {\n                    let area = frame.area();\n                    let layout = Layout::default()\n                        .direction(ratatui::layout::Direction::Vertical)\n                        .constraints([\n                            Constraint::Percentage(10),\n                            Constraint::Percentage(75),\n                            Constraint::Percentage(15),\n                        ])\n                        .split(area);\n\n                    let middle_layout = Layout::default()\n                        .direction(ratatui::layout::Direction::Horizontal)\n                        .constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])\n                        .split(layout[1]);\n\n                    if !self.help_mode {\n                        self.top_bar\n                            .render(layout[0], frame.buffer_mut(), &self.state);\n                        self.search.render(middle_layout[0], frame.buffer_mut());\n                        self.history.render(middle_layout[1], frame.buffer_mut());\n                        self.player.render(layout[2], frame.buffer_mut());\n                    } else {\n                        let rows = vec![\n                            Row::new(vec![Cell::from(\"s\"), Cell::from(\"Search\")]),\n                            Row::new(vec![Cell::from(\"h\"), Cell::from(\"History\")]),\n                            Row::new(vec![Cell::from(\"p\"), Cell::from(\"Player\")]),\n                            Row::new(vec![Cell::from(\"?\"), Cell::from(\"Toggle Help Mode\")]),\n                            Row::new(vec![\n                                Cell::from(\"TAB (Search)\"),\n                                Cell::from(\"Toggle between search input and results\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"Esc (Global)\"),\n                                Cell::from(\"Quit application\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"Esc (Non-Global)\"),\n                                Cell::from(\"Switch to Global Mode\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"↑ / k(History/Search)\"),\n                                Cell::from(\"Navigate up in list\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"↓ / j(History/Search)\"),\n                                Cell::from(\"Navigate down in list\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"Space / ; (Player)\"),\n                                Cell::from(\"Pause current song\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"→ (Player)\"),\n                                Cell::from(\"Skip forward 5 seconds\"),\n                            ]),\n                            Row::new(vec![\n                                Cell::from(\"← (Player)\"),\n                                Cell::from(\"Rewind 5 seconds\"),\n                            ]),\n                        ];\n\n                        let help_table = Table::new(\n                            rows,\n                            [Constraint::Percentage(20), Constraint::Percentage(80)],\n                        )\n                        .block(Block::default().borders(Borders::ALL).title(\"Help\"))\n                        .header(Row::new(vec![Cell::from(\"Key\"), Cell::from(\"Action\")]));\n\n                        help_table.render(area, frame.buffer_mut());\n                    }\n                })\n                .unwrap();\n\n            tokio::select! {\n                _ = redraw_interval.tick() => {}\n                _ = async {\n                    if poll(Duration::from_millis(100)).unwrap() {\n                        if let Event::Key(key) = read().unwrap() {\n                            self.handle_global_keystrokes(key);\n                        }\n                    }\n                } => {}\n            }\n        }\n    }\n}\n\n/// Represents the top bar UI component.\nstruct TopBar;\n\nimpl TopBar {\n    fn new() -> Self {\n        Self\n    }\n    fn render(&mut self, area: Rect, buf: &mut Buffer, state: &State) {\n        let s = format!(\"Feather | Current Mode : {:?}\", state);\n        Paragraph::new(s)\n            .block(Block::default().borders(Borders::ALL))\n            .render(area, buf);\n    }\n}\n\n#[allow(unused)]\n/// Placeholder struct for user playlists.\nstruct UserPlaylist {}\n#[allow(unused)]\n/// Placeholder struct for currently playing playlist.\nstruct CurrentPlayingPlaylist {}\n"
  },
  {
    "path": "feather_frontend/src/player.rs",
    "content": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse ratatui::prelude::{Alignment, Buffer, Rect};\nuse ratatui::style::{Modifier, Style};\nuse ratatui::text::{Line, Span};\nuse ratatui::widgets::{Block, Borders, Paragraph, Widget};\nuse std::sync::{Arc, Mutex};\nuse std::time::Duration;\nuse tokio::sync::mpsc;\nuse tokio::task;\n\n#[derive(PartialEq, PartialOrd, Debug)]\nenum SongState {\n    Idle,              // No song is playing\n    Playing,           // A song is currently playing\n    Loading,           // Song is loading\n    ErrorPlayingoSong, // An error occurred while playing the song\n}\n\n#[derive(Clone)]\npub struct SongDetails {\n    song: Song,             // Information about the song\n    current_time: String,   // Current playback time (formatted as MM:SS)\n    total_duration: String, // Total duration of the song\n}\n\npub struct SongPlayer {\n    backend: Arc<Backend>,            // Backend reference for controlling playback\n    songstate: Arc<Mutex<SongState>>, // Current state of the player (Idle, Playing, etc.)\n    song_playing: Arc<Mutex<Option<SongDetails>>>, // Details of the currently playing song\n    rx: mpsc::Receiver<bool>,         // Receiver to listen for playback events\n}\n\nimpl SongPlayer {\n    pub fn new(backend: Arc<Backend>, rx: mpsc::Receiver<bool>) -> Self {\n        let player = Self {\n            backend,\n            songstate: Arc::new(Mutex::new(SongState::Idle)),\n            song_playing: Arc::new(Mutex::new(None)),\n            rx,\n        };\n        player.observe_time(); // Start observing playback time\n        player\n    }\n\n    // Function to continuously update the current playback time\n    fn observe_time(&self) {\n        let backend = Arc::clone(&self.backend);\n        let song_playing = Arc::clone(&self.song_playing);\n\n        tokio::task::spawn(async move {\n            loop {\n                // Try to get the current playback position from MPV\n                match backend.player.player.get_property::<f64>(\"time-pos\") {\n                    Ok(time) => {\n                        // Lock the song_playing mutex and update the current playback time\n                        if let Ok(mut song_lock) = song_playing.lock() {\n                            if let Some(song) = song_lock.as_mut() {\n                                song.current_time = format!(\"{:.0}\", time);\n                            }\n                        }\n                    }\n                    Err(_) => (), // Ignore errors (e.g., if MPV is not running)\n                }\n\n                tokio::time::sleep(Duration::from_millis(500)).await; // Update every 500ms\n            }\n        });\n    }\n\n    // Handle key presses for playback control\n    pub fn handle_keystrokes(&mut self, key: KeyEvent) {\n        if let Ok(state) = self.songstate.lock() {\n            if *state == SongState::Playing {\n                match key.code {\n                    KeyCode::Char(' ') | KeyCode::Char(';') => {\n                        // Toggle play/pause\n                        if let Ok(_) = self.backend.player.play_pause() {};\n                    }\n                    KeyCode::Right | KeyCode::Char('l') => {\n                        // Seek forward\n                        self.backend.player.seek_forward().ok();\n                    }\n                    KeyCode::Left | KeyCode::Char('j') => {\n                        // Seek backward\n                        self.backend.player.seek_backword().ok();\n                    }\n                    _ => (),\n                };\n            }\n        }\n    }\n\n    // Function to check whether a song is playing\n    fn check_playing(&mut self) {\n        let songstate = Arc::clone(&self.songstate);\n        let backend = Arc::clone(&self.backend);\n        let song_playing = Arc::clone(&self.song_playing);\n\n        task::spawn(async move {\n            const MAX_IDLE_COUNT: i32 = 5; // Max checks before considering it an error\n            let mut idle_count = 0;\n\n            // Initial delay before checking playback status\n            tokio::time::sleep(Duration::from_secs(1)).await;\n\n            loop {\n                match backend.player.is_playing() {\n                    Ok(true) => {\n                        if let Ok(mut state) = songstate.lock() {\n                            if let Ok(mut song_lock) = song_playing.lock() {\n                                if let Ok(song) = backend.song.lock() {\n                                    if let Some(value) = song.as_ref() {\n                                        let total_duration = backend\n                                            .player\n                                            .duration()\n                                            .parse::<f64>()\n                                            .map(|d| {\n                                                let total = d as i64;\n                                                format!(\"{:02}:{:02}\", total / 60, total % 60)\n                                            })\n                                            .unwrap_or_default();\n                                        *song_lock = Some(SongDetails {\n                                            song: value.clone(),\n                                            current_time: backend.player.get_current_time(),\n                                            total_duration,\n                                        });\n                                        *state = SongState::Playing;\n                                        return; // Exit once playing is confirmed\n                                    }\n                                }\n                            }\n                        }\n                        idle_count = 0; // Reset idle count since the song is playing\n                    }\n                    Ok(false) => {\n                        // Song is not playing, set state to Idle\n                        if let Ok(mut state) = songstate.lock() {\n                            *state = SongState::Idle;\n                        }\n                        idle_count += 1;\n                    }\n                    Err(_) => idle_count += 1, // Increase idle count if an error occurs\n                }\n\n                // If too many idle checks, assume an error occurred\n                if idle_count >= MAX_IDLE_COUNT {\n                    if let Ok(mut state) = songstate.lock() {\n                        if *state == SongState::Loading {\n                            *state = SongState::ErrorPlayingoSong;\n                        }\n                    }\n                }\n                tokio::time::sleep(Duration::from_secs(2)).await; // Check every 2 seconds\n            }\n        });\n    }\n\n    // Render the player UI\n    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {\n        // Check for playback event signals\n        if self.rx.try_recv().is_ok() {\n            if let Ok(mut state) = self.songstate.lock() {\n                *state = SongState::Loading;\n            }\n            self.check_playing(); // Start checking for playback status\n        }\n\n        let block = Block::default().borders(Borders::ALL);\n        let inner = block.inner(area);\n        block.render(area, buf);\n\n        if let Ok(state) = self.songstate.lock() {\n            let text = match *state {\n                SongState::Idle => vec![Line::from(\"No song is playing\")],\n                SongState::Playing => {\n                    if let Ok(song_playing) = self.song_playing.lock() {\n                        song_playing.as_ref().map_or_else(\n                            || vec![Line::from(\"Loading...\")],\n                            |song| {\n                                let current_time = song\n                                    .current_time\n                                    .parse::<i64>()\n                                    .map(|t| format!(\"{:02}:{:02}\", t / 60, t % 60))\n                                    .unwrap_or_default();\n                                vec![\n                                    Line::from(Span::styled(\n                                        song.song.song_name.clone(),\n                                        Style::default().add_modifier(Modifier::BOLD),\n                                    )),\n                                    Line::from(format!(\"{}/{}\", current_time, song.total_duration)),\n                                ]\n                            },\n                        )\n                    } else {\n                        vec![Line::from(\"Error accessing song details\")]\n                    }\n                }\n                SongState::Loading => {\n                    vec![Line::from(\"Loading Song\")]\n                }\n                SongState::ErrorPlayingoSong => {\n                    vec![Line::from(\"Error Playing Song\")]\n                }\n            };\n            Paragraph::new(text)\n                .alignment(Alignment::Center)\n                .render(inner, buf);\n        }\n    }\n}\n"
  },
  {
    "path": "feather_frontend/src/search.rs",
    "content": "use crate::backend::{Backend, Song};\nuse crossterm::event::{KeyCode, KeyEvent};\nuse feather::{ArtistName, SongId, SongName};\nuse ratatui::{\n    buffer::Buffer,\n    layout::{Constraint, Layout, Rect},\n    style::{Color, Style},\n    text::Span,\n    widgets::{\n        Block, Borders, List, ListItem, ListState, Paragraph, Scrollbar, ScrollbarState,\n        StatefulWidget, Widget,\n    },\n};\nuse std::sync::Arc;\nuse tokio::{\n    sync::mpsc,\n    time::{Duration, sleep},\n};\nuse tui_textarea::TextArea;\n\n// Defines possible states for the search interface\nenum SearchState {\n    SearchBar,     // When focused on input field\n    SearchResults, // When browsing search results\n}\n\npub struct Search<'a> {\n    textarea: TextArea<'a>, // Text input widget for search queries\n    state: SearchState,     // Current UI state\n    query: String,          // Current search query text\n    tx: mpsc::Sender<Result<Vec<((String, String), Vec<String>)>, String>>, // Sender for search results\n    rx: mpsc::Receiver<Result<Vec<((String, String), Vec<String>)>, String>>, // Receiver for search results\n    tx_player: mpsc::Sender<bool>, // Channel to communicate with player\n    backend: Arc<Backend>,         // Audio backend for search and playback\n    vertical_scroll_state: ScrollbarState, // Vertical scrollbar state\n    display_content: bool,         // Flag to show search results\n    results: Result<Option<Vec<((SongName, SongId), Vec<ArtistName>)>>, String>, // Search results or error\n    selected: usize,             // Index of selected result\n    selected_song: Option<Song>, // Currently selected song details\n    max_len: Option<usize>,      // Total number of search results\n}\n\nimpl Search<'_> {\n    // Constructor initializing the Search struct\n    pub fn new(backend: Arc<Backend>, tx_player: mpsc::Sender<bool>) -> Self {\n        let (tx, rx) = mpsc::channel(32); // Create channel for async search results\n        Self {\n            query: String::new(),\n            state: SearchState::SearchBar,\n            textarea: TextArea::default(),\n            tx,\n            rx,\n            tx_player,\n            backend,\n            vertical_scroll_state: ScrollbarState::default(),\n            display_content: false,\n            results: Ok(None),\n            selected: 0,\n            selected_song: None,\n            max_len: None,\n        }\n    }\n\n    // Handles keyboard input based on current state\n    pub fn handle_keystrokes(&mut self, key: KeyEvent) {\n        if let SearchState::SearchBar = self.state {\n            match key.code {\n                KeyCode::Tab => {\n                    // Switch to results state\n                    self.change_state();\n                }\n                KeyCode::Enter => {\n                    // Execute search\n                    self.display_content = false;\n                    self.selected = 0;\n                    let text = self.textarea.lines();\n                    if !text.is_empty() {\n                        self.query = text[0].trim().to_string();\n                        let tx = self.tx.clone();\n                        let query = self.query.clone();\n                        let backend = self.backend.clone();\n                        tokio::spawn(async move {\n                            // Async task for search\n                            sleep(Duration::from_millis(500)).await; // Debounce\n                            match backend.yt.search(&query).await {\n                                Ok(songs) => {\n                                    let _ = tx.send(Ok(songs)).await;\n                                }\n                                Err(e) => {\n                                    let _ = tx.send(Err(e)).await;\n                                }\n                            }\n                        });\n                    }\n                }\n                _ => {\n                    self.textarea.input(key);\n                } // Handle text input\n            }\n        } else {\n            // SearchResults state\n            match key.code {\n                KeyCode::Tab => {\n                    self.change_state();\n                } // Switch to search bar\n                KeyCode::Char('j') | KeyCode::Down => {\n                    // Move selection down\n                    self.selected = self.selected.saturating_add(1);\n                    if let Some(len) = self.max_len {\n                        self.selected = self.selected.min(len - 1);\n                    }\n                    self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);\n                }\n                KeyCode::Char('k') | KeyCode::Up => {\n                    // Move selection up\n                    self.selected = self.selected.saturating_sub(1);\n                    self.vertical_scroll_state = self.vertical_scroll_state.position(self.selected);\n                }\n                KeyCode::Enter => {\n                    // Play selected song\n                    if let Some(song) = self.selected_song.clone() {\n                        let backend = self.backend.clone();\n                        let tx_player = self.tx_player.clone();\n                        tokio::spawn(async move {\n                            let _ = backend.play_music(song).await.is_ok();\n                            let _ = tx_player.send(true).await;\n                        });\n                    }\n                }\n                _ => {}\n            }\n        }\n    }\n\n    // Toggles between search bar and results view\n    pub fn change_state(&mut self) {\n        match self.state {\n            SearchState::SearchResults => self.state = SearchState::SearchBar,\n            _ => self.state = SearchState::SearchResults,\n        }\n    }\n\n    // Renders the search UI\n    pub fn render(&mut self, area: Rect, buf: &mut Buffer) {\n        let chunks = Layout::default()\n            .direction(ratatui::layout::Direction::Vertical)\n            .constraints([\n                Constraint::Length(3), // Search bar height\n                Constraint::Min(0),    // Results area\n                Constraint::Length(3), // Bottom bar\n            ])\n            .split(area);\n        let searchbar_area = chunks[0];\n        let results_area = chunks[1];\n        let bottom_area = chunks[2];\n\n        // Check for new search results\n        if let Ok(response) = self.rx.try_recv() {\n            if let Ok(result) = response {\n                self.results = Ok(Some(result));\n            } else if let Err(e) = response {\n                self.results = Err(e);\n            }\n            self.display_content = true;\n        }\n\n        // Render search bar\n        let search_block = Block::default().title(\"Search Music\").borders(Borders::ALL);\n        self.textarea.set_cursor_line_style(Style::default());\n        self.textarea\n            .set_placeholder_text(\"Search Song or Playlist\");\n        self.textarea.set_style(Style::default().fg(Color::White));\n        self.textarea.set_block(search_block);\n        self.textarea.render(searchbar_area, buf);\n\n        // Render vertical scrollbar\n        let vertical_scrollbar =\n            Scrollbar::new(ratatui::widgets::ScrollbarOrientation::VerticalRight)\n                .begin_symbol(Some(\"↑\"))\n                .end_symbol(Some(\"↓\"));\n        vertical_scrollbar.render(results_area, buf, &mut self.vertical_scroll_state);\n\n        // Render search results if available\n        if self.display_content {\n            if let Ok(result) = self.results.clone() {\n                if let Some(r) = result {\n                    self.max_len = Some(r.len());\n                    let items: Vec<ListItem> = r\n                        .into_iter()\n                        .enumerate()\n                        .map(|(i, ((song, songid), artists))| {\n                            // Format results\n                            let style = if i == self.selected {\n                                self.selected_song =\n                                    Some(Song::new(song.clone(), songid.clone(), artists.clone()));\n                                Style::default().fg(Color::Yellow).bg(Color::Blue)\n                            } else {\n                                Style::default()\n                            };\n                            let text = format!(\"{} - {}\", song, artists.join(\", \"));\n                            ListItem::new(Span::styled(text, style))\n                        })\n                        .collect();\n\n                    let mut list_state = ListState::default();\n                    list_state.select(Some(self.selected));\n                    StatefulWidget::render(\n                        // Render results list\n                        List::new(items)\n                            .block(Block::default().title(\"Results\").borders(Borders::ALL))\n                            .highlight_symbol(\"▶\"),\n                        results_area,\n                        buf,\n                        &mut list_state,\n                    );\n                }\n            }\n        }\n\n        // Render bottom help bar\n        let bottom_bar = Paragraph::new(\"Press '?' for Help in Global Mode\")\n            .style(Style::default().fg(Color::White))\n            .block(Block::default().borders(Borders::ALL));\n        bottom_bar.render(bottom_area, buf); // Note: custom_area undefined, likely should be bottom_area\n\n        // Render outer border\n        let outer_block = Block::default().borders(Borders::ALL);\n        outer_block.render(area, buf);\n    }\n}\n"
  }
]