downloads-khinsider-com-dl

Download all songs at once from downloads.khinsider.com
git clone https://git.ea.contact/downloads-khinsider-com-dl
Log | Files | Refs | README

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 }