main.rs (5069B)
1 use downloads_khinsider_com_dl as lib; 2 use lib::{config::Config, event::Event}; 3 4 use anyhow::Result; 5 use clap::Parser; 6 use crossterm::{cursor, execute, terminal::{Clear, ClearType}}; 7 use std::collections::HashMap; 8 use std::io::Write; 9 10 #[derive(Parser)] 11 #[command(about = "Download game soundtracks from downloads.khinsider.com")] 12 struct Args { 13 /// Link to the album page on downloads.khinsider.com (example https://downloads.khinsider.com/game-soundtracks/album/synthetik-2-windows-gamerip-2021) 14 url: String, 15 16 /// Download flacs. Download MP3 if not set 17 #[arg(short = 'f', long = "flac")] 18 flac: bool, 19 20 /// Download images into "images" directory 21 #[arg(short = 'i', long = "images")] 22 images: bool, 23 } 24 25 #[tokio::main] 26 async fn main() -> Result<()> { 27 let config = parse_args()?; 28 let (tx, rx) = std::sync::mpsc::channel::<Event>(); 29 let handle = tokio::task::spawn_blocking(move || { 30 let mut state = ProgressState::default(); 31 for event in rx { 32 handle_event(event, &mut state); 33 } 34 }); 35 36 lib::run(config, tx).await?; 37 handle.await?; 38 Ok(()) 39 } 40 41 fn parse_args() -> Result<Config> { 42 let args = Args::parse(); 43 Ok(Config { 44 url: args.url, 45 flac: args.flac, 46 images: args.images, 47 }) 48 } 49 50 struct SlotInfo { 51 name: String, 52 downloaded: usize, 53 total: Option<usize>, 54 } 55 56 impl SlotInfo { 57 fn format_line(&self) -> String { 58 match self.total { 59 Some(total) if total > 0 => { 60 let pct = (self.downloaded * 100 / total) as u8; 61 format!("{pct:3}% {}", self.name) 62 } 63 _ => format!(" {}", self.name), 64 } 65 } 66 } 67 68 #[derive(Default)] 69 struct ProgressState { 70 total: usize, 71 completed: usize, 72 failed: usize, 73 slots: Vec<Option<SlotInfo>>, 74 id_to_slot: HashMap<usize, usize>, 75 lines_printed: usize, 76 } 77 78 impl ProgressState { 79 fn alloc_slot(&mut self, id: usize, name: String) { 80 let slot = self.slots.iter().position(|s| s.is_none()).unwrap_or_else(|| { 81 self.slots.push(None); 82 self.slots.len() - 1 83 }); 84 self.slots[slot] = Some(SlotInfo { name, downloaded: 0, total: None }); 85 self.id_to_slot.insert(id, slot); 86 } 87 88 fn update_progress(&mut self, id: usize, downloaded: usize, total: Option<usize>) { 89 if let Some(&slot) = self.id_to_slot.get(&id) { 90 if let Some(Some(info)) = self.slots.get_mut(slot) { 91 info.downloaded = downloaded; 92 info.total = total; 93 } 94 } 95 } 96 97 fn free_slot(&mut self, id: usize) { 98 if let Some(&slot) = self.id_to_slot.get(&id) { 99 self.slots[slot] = None; 100 self.id_to_slot.remove(&id); 101 } 102 } 103 104 fn render(&mut self) { 105 let mut stdout = std::io::stdout(); 106 107 if self.lines_printed > 0 { 108 let _ = execute!(stdout, cursor::MoveUp(self.lines_printed as u16)); 109 } 110 111 // Summary line 112 let _ = execute!(stdout, Clear(ClearType::CurrentLine)); 113 if self.failed > 0 { 114 println!("[{}/{}] complete, {} failed", self.completed, self.total, self.failed); 115 } else { 116 println!("[{}/{}] complete", self.completed, self.total); 117 } 118 119 // One line per slot 120 for slot in &self.slots { 121 let _ = execute!(stdout, Clear(ClearType::CurrentLine)); 122 match slot { 123 Some(info) => println!(" {}", info.format_line()), 124 None => println!(), 125 } 126 } 127 128 let _ = stdout.flush(); 129 self.lines_printed = 1 + self.slots.len(); 130 } 131 } 132 133 fn handle_event(event: Event, state: &mut ProgressState) { 134 match event { 135 Event::GetPageStarted => println!("Fetching page..."), 136 Event::GetPageCompleted => println!("Page fetched."), 137 Event::TotalDownloads(n) => { 138 state.total = n; 139 state.render(); 140 } 141 Event::DlStarted { id, name } => { 142 state.alloc_slot(id, name); 143 state.render(); 144 } 145 Event::DlProgress { id, downloaded, total } => { 146 state.update_progress(id, downloaded, total); 147 state.render(); 148 } 149 Event::DlCompleted { id } => { 150 state.completed += 1; 151 state.free_slot(id); 152 state.render(); 153 } 154 Event::DlFailed { id, error } => { 155 state.failed += 1; 156 state.free_slot(id); 157 // Print error above the progress block 158 if state.lines_printed > 0 { 159 let _ = execute!( 160 std::io::stdout(), 161 cursor::MoveUp(state.lines_printed as u16) 162 ); 163 state.lines_printed = 0; 164 } 165 println!("Failed: {error}"); 166 state.render(); 167 } 168 } 169 }