This commit is contained in:
Gvidas Juknevičius 2024-04-17 15:21:53 +03:00
parent e05b00609c
commit 0ea98848e5
17 changed files with 3330 additions and 3181 deletions

7
.gitignore vendored
View File

@ -1,3 +1,4 @@
/out /out
/music_mgr/target /music_mgr/target
/.venv /.venv
/config.json

View File

@ -1,13 +1,8 @@
{ {
"ytdlp": { "ytdlp": {
"path": "/usr/bin/yt-dlp", "path": "C:\\bin\\yt-dlp.exe"
"is_python": false
}, },
"spotdl": { "spotdl": {
"path": "/home/mcorange/.local/bin/spotdl", "path": "UNUSED"
"is_python": false
},
"python": {
"path": ""
} }
} }

File diff suppressed because it is too large Load Diff

3628
music_mgr/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,22 @@
[package] [package]
name = "music_mgr" name = "music_mgr"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anstyle = "1.0.6" anstyle = "1.0.6"
anyhow = "1.0.81" anyhow = "1.0.81"
camino = "1.1.6" camino = "1.1.6"
clap = { version = "4.5.4", features = ["derive"] } clap = { version = "4.5.4", features = ["derive"] }
env_logger = "0.11.3" env_logger = "0.11.3"
lazy_static = "1.4.0" lazy_static = "1.4.0"
libc = "0.2.153" libc = "0.2.153"
log = "0.4.21" log = "0.4.21"
reqwest = "0.12.3" reqwest = "0.12.3"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115" serde_json = "1.0.115"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "process", "sync"] } tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread", "process", "sync"] }
windows = { version = "0.56.0", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console"] } windows = { version = "0.56.0", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Console"] }
zip-extensions = "0.6.2" zip-extensions = "0.6.2"

145
music_mgr/manifest.json Normal file
View File

@ -0,0 +1,145 @@
{
"format": "m4a",
"genres": {
"pop": [
{"name": "Green Day - Basket Case", "url": "https://www.youtube.com/watch?v=wZ8eZRxFA-0"},
{"name": "Icona Pop - I Love It", "url": "https://www.youtube.com/watch?v=UxxajLWwzqY"}
],
"hip-hop": [
{"name": "Afroman - Because I Got High", "url": "https://www.youtube.com/watch?v=WeYsTmIzjkw"}
],
"rave": [
{"name": "EVERYTHING WHAT", "url": "https://www.youtube.com/watch?v=Gjdsq4kc5cA"},
{"name": "Tricky Disco", "url": "https://www.youtube.com/watch?v=t78qVdbAiXw"},
{"name": "DR. VODKA - Tricky Disco", "url": "https://www.youtube.com/watch?v=IknAUhl3i2o"}
],
"techno": [
{"name": "Dance For Me", "url": "https://www.youtube.com/watch?v=5DTSvGO_944"},
{"name": "Give It To Me", "url": "https://www.youtube.com/watch?v=upQe8EeSyZU"},
{"name": "Empire Of The Sun, southstar - We Are The People", "url": "https://www.youtube.com/watch?v=qguEGR5BK2k"},
{"name": "Beggin' (Techno)", "url": "https://www.youtube.com/watch?v=tXPs1FwW6lk"},
{"name": "Lily Allen - Not Fair", "url": "https://www.youtube.com/watch?v=WON_YIbeLis"},
{"name": "I WAS MADE FOR LOVIN' YOU (TECHNO)", "url": "https://www.youtube.com/watch?v=asVznhccYao"},
{"name": "Nicolas Julian - Applause", "url": "https://www.youtube.com/watch?v=-pXlrWVICAE"},
{"name": "08 Blumchen - Blaue Augen", "url": "https://www.youtube.com/watch?v=mE4PZcUfiwE"},
{"name": "MUTA - Party maker", "url": "https://www.youtube.com/watch?v=LT9VNK1aCXY"}
],
"electronic": [
{"name": "Zombie Nation - Kernkraft 400", "url": "https://www.youtube.com/watch?v=z5LW07FTJbI"},
{"name": "Benny Benassi - Satisfaction", "url": "https://www.youtube.com/watch?v=a0fkNdPiIL4"}
],
"rock": [
{"name": "Black Sabbath", "url": "https://www.youtube.com/watch?v=BOTIIw76qiE"}
],
"house": [
{"name": "Ralph Castelli - Morning Sex (Mochakk Remix)", "url": "https://www.youtube.com/watch?v=6bCwJ_TIDG4"},
{"name": "Billie Eilish - Bossa Nova (Lewii Edit)", "url": "https://www.youtube.com/watch?v=gNawHj2NCxA"},
{"name": "Fidde - I Only See Things I Dont Have", "url": "https://www.youtube.com/watch?v=vX_Ye_ZzI-Y"},
{"name": "Bauhouse - After Marvins Dance (Marvin Gaye's 'After The Dance' Edit)", "url": "https://www.youtube.com/watch?v=J-cgyYiExh8"},
{"name": "Men I Trust - Tailwhip (Lewii Edit)", "url": "https://www.youtube.com/watch?v=XhyM-JUWwWQ"},
{"name": "Sweely - Le Son Dancefloor", "url": "https://www.youtube.com/watch?v=5uEvZgmoG6Y"},
{"name": "THEOS - Rhodes Trip", "url": "https://www.youtube.com/watch?v=m7guRO0Uz_c"},
{"name": "Baltra - Tears Drop", "url": "https://www.youtube.com/watch?v=EXXMtKPfuzY"},
{"name": "Fidde - If Theres A Heaven I Wanna See It", "url": "https://www.youtube.com/watch?v=l2Nw7cIh7qg"},
{"name": "Unknown Artist - Kcik 23", "url": "https://www.youtube.com/watch?v=SnnqDdZJpzA"}
],
"lietuviskos": [
{"name": "Adomas Vysniauskas - As Judu", "url": "https://www.youtube.com/watch?v=dMm16TzZrjg"},
{"name": "RADVIS - KINO FILMAI", "url": "https://www.youtube.com/watch?v=vhAEkC3xNMo"},
{"name": "16Hz - Autostrada Vilnius - Kaunas", "url": "https://www.youtube.com/watch?v=ANS2TSegr40"},
{"name": "Zas - Zalias Pasas", "url": "https://www.youtube.com/watch?v=SZA7IjlCfyI"},
{"name": "Dzordana Butkute - Nebenoriu Laukt", "url": "https://www.youtube.com/watch?v=_AozFrAqNMk"},
{"name": "Juodas Garvezys (Remix)", "url": "https://www.youtube.com/watch?v=D-7qQbXHSAw"},
{"name": "morre - Kaip Diena", "url": "https://www.youtube.com/watch?v=6LDgLWCQSSM"},
{"name": "MC ENDRAY - AUDI", "url": "https://www.youtube.com/watch?v=oIjNoMGEuRg"},
{"name": "Mercy Dance - I Pajuri", "url": "https://www.youtube.com/watch?v=RPpkMh47l9w"},
{"name": "NL - Pasitusinam", "url": "https://www.youtube.com/watch?v=WhSFudvloog"},
{"name": "SixthBoi - Nevaidink", "url": "https://www.youtube.com/watch?v=nOTNnnrqTII"},
{"name": "Mr.Bullet - UZ MUS IR JUS", "url": "https://www.youtube.com/watch?v=85q_7jXEgH8"},
{"name": "Jovani, Karaliska Erdve - Is Leto Leidziasi Saule", "url": "https://www.youtube.com/watch?v=VqSu8iG1_DE"},
{"name": "Rondo - Margarita", "url": "https://www.youtube.com/watch?v=rF4w-Rxsiv4"},
{"name": "Radvis - TU ESI MELAGIS (Techno Extended)", "url": "https://www.youtube.com/watch?v=kmvvP7GW_bw"},
{"name": "Zas - Myliu kina", "url": "https://www.youtube.com/watch?v=ImFrfmi-qT8"},
{"name": "Zilvinas Zvagulis - Amerikonas grizo sunus", "url": "https://www.youtube.com/watch?v=UvzJEz5ADY8"},
{"name": "Raketa - I Kluba", "url": "https://www.youtube.com/watch?v=FkSjtpYN3EI"},
{"name": "Karaliska Erdve - Vakareja", "url": "https://www.youtube.com/watch?v=g0HmrlJ7fhE"},
{"name": "Tnn - Parukom", "url": "https://www.youtube.com/watch?v=v9pBZK2RIPI"},
{"name": "DJ Dalgis - Kauniete", "url": "https://www.youtube.com/watch?v=b3xPE9Iyuzc"},
{"name": "Andzikas - I gamta", "url": "https://www.youtube.com/watch?v=UyLdjC-hihM"},
{"name": "nemuno krantai - rytmecio rasos", "url": "https://www.youtube.com/watch?v=2-fGbsrofv4"},
{"name": "Tipo grupe - Lovoj Vezi", "url": "https://www.youtube.com/watch?v=M3zVMzWCy_c"},
{"name": "Kastanenda - Sombrero", "url": "https://www.youtube.com/watch?v=3Z3_4TknCfQ"},
{"name": "Elektra - Juda Tavo rankos", "url": "https://www.youtube.com/watch?v=k2RuDoudnOE"},
{"name": "Vilija ir Marijonas mikutavicius - Dabar Geriausi Musu Vakarai", "url": "https://www.youtube.com/watch?v=MPnZkEscWo0"},
{"name": "Parnesk alaus OG", "url": "https://www.youtube.com/watch?v=e7cB1JIlZ2k"},
{"name": "Eugenijus Ostapenko - Dviratukas", "url": "https://www.youtube.com/watch?v=ILFHZQK33Mw"},
{"name": "Ciulpuoneliai - Jau Nutilo Sirgaliai", "url": "https://www.youtube.com/watch?v=s8qIVA1U0C0"},
{"name": "Tweaxx - Mersas", "url": "https://www.youtube.com/watch?v=7ljAzgALPdA"},
{"name": "Dove - Naktinis Tusas", "url": "https://www.youtube.com/watch?v=pz-HEAwFEnk"},
{"name": "MAMA MANE RODYS PER FARUS", "url": "https://www.youtube.com/watch?v=F5HqXYRDZaE"},
{"name": "Kastaneda - Kelyje", "url": "https://www.youtube.com/watch?v=JVE6NQqKPL4"},
{"name": "NL - Juodas Golfas", "url": "https://www.youtube.com/watch?v=f2-ZmElSvPc"},
{"name": "DJ Dalgis - Zalia Siera", "url": "https://www.youtube.com/watch?v=nfentq_pez4"},
{"name": "L1GHT CASH - Whiskey Cola Lietuviskai (sultys degtinele) remix", "url": "https://www.youtube.com/watch?v=YVaqDaf1KXU"},
{"name": "Tipo grupe ir Kastaneda - Po stikliuka", "url": "https://www.youtube.com/watch?v=EtmE60nE7fI"},
{"name": "MG INTERNATIONAL - JUODA ORCHIDEJA", "url": "https://www.youtube.com/watch?v=HQvceFRBq9M"},
{"name": "Ganja - Truputi", "url": "https://www.youtube.com/watch?v=Pxve7CwiCHM"},
{"name": "Riaukenzo - Trys Trys Trys", "url": "https://www.youtube.com/watch?v=qJv6GRQCnCk"},
{"name": "Grupiokai - Degtine", "url": "https://www.youtube.com/watch?v=8SqbG2VmEFw"},
{"name": "Robertas Kupstas - Cia Mano Rojus", "url": "https://www.youtube.com/watch?v=xij_YeEInr8"},
{"name": "NIERKA - PENKTADIENIS", "url": "https://www.youtube.com/watch?v=h3TuZj_OAf0"},
{"name": "VAIKAI PO LELIJOM (REMIX)", "url": "https://www.youtube.com/watch?v=k1amBbsAZuo"},
{"name": "Vitalija Katunskyte - Robinzonas", "url": "https://www.youtube.com/watch?v=erDHG-QpbPY"},
{"name": "Rycka klipas", "url": "https://www.youtube.com/watch?v=nuTUDSQ3BBI"},
{"name": "Nezinau, Kodel...", "url": "https://www.youtube.com/watch?v=A-i2CkCnPoc"},
{"name": "NL - R1", "url": "https://www.youtube.com/watch?v=hSgav4fYnZ8"},
{"name": "DJ Dalgis - Negeriau", "url": "https://www.youtube.com/watch?v=c89YvG3MCcs"},
{"name": "Tipo Grupe - tipo daina", "url": "https://www.youtube.com/watch?v=PTIOaSjEgIU"},
{"name": "Depresinis feat. Deivas - 0,7", "url": "https://www.youtube.com/watch?v=rjwFjBgTzAA"},
{"name": "Depresinis & MERAKI2004 - VASARA ZJBS", "url": "https://www.youtube.com/watch?v=BD-pBjRy-5A"},
{"name": "Depresinis feat. Deivas - LEDUKAI", "url": "https://www.youtube.com/watch?v=R2-MtpkKgGI"},
{"name": "Depresinis feat. Deivas - Pavasaris", "url": "https://www.youtube.com/watch?v=yWWAucfQdN4"},
{"name": "Depresinis - LEDINE", "url": "https://www.youtube.com/watch?v=qugvChkXMLk"},
{"name": "Depresinis, Jypas - O Mazuti", "url": "https://www.youtube.com/watch?v=4t_DPbe2r3M"},
{"name": "AVA - Eik Tu NA", "url": "https://www.youtube.com/watch?v=yRf3ijaIgOg"},
{"name": "Judam Lietuvoj", "url": "https://www.youtube.com/watch?v=WDzWSEgSy5U"},
{"name": "16Hz - Baliavojam", "url": "https://www.youtube.com/watch?v=Ia-qERX8WLs"},
{"name": "Deivas - Klaipeda On Top", "url": "https://www.youtube.com/watch?v=g_h2M3e2OYU"},
{"name": "Depresinis - Volkswagina", "url": "https://www.youtube.com/watch?v=1lZR1VKsQHo"},
{"name": "SADBOY - Kaifuok", "url": "https://www.youtube.com/watch?v=vclryWgfy8I"},
{"name": "SADBOY - Blizgantys Naikai", "url": "https://www.youtube.com/watch?v=p5KsYJGcfOM"},
{"name": "SADBOY - 1001 Naktis", "url": "https://www.youtube.com/watch?v=mLJIjGvWmKI"},
{"name": "SADBOY - Deginam", "url": "https://www.youtube.com/watch?v=w3R0Aq1EGXg"},
{"name": "Wenona Waves - Topine Panele", "url": "https://www.youtube.com/watch?v=MPHuhmUomfE"},
{"name": "Andzikas - Virs debesu", "url": "https://www.youtube.com/watch?v=PHJcVGhxra8"},
{"name": "Grupe MX - 1.9 TDI", "url": "https://www.youtube.com/watch?v=8FBr5GQXsI8"},
{"name": "Patruliai - Kur Tu", "url": "https://www.youtube.com/watch?v=OPWhiu3cvj0"},
{"name": "Ka Tu Ka Vakare", "url": "https://www.youtube.com/watch?v=6SOS4ljHbJY"}
],
"lietuviskos/rave": [
{"name": "VainHouse - Malunas Prie Kelio", "url": "https://www.youtube.com/watch?v=bbwuNjDXCiM"},
{"name": "Sokoledas - Mano Skonis Sokolado (Matuze & Arnisxd Remix)", "url": "https://www.youtube.com/watch?v=hb41bfQxiM0"}
],
"rusiskos": [
{"name": "Topolini puh", "url": "https://www.youtube.com/watch?v=UUryvYF8tUs"},
{"name": "Raim & Artur feat. Zhenis - Diskoteka is 90 hit", "url": "https://www.youtube.com/watch?v=GfBhxlNhrn0"},
{"name": "Pimp Schwab - vse shto nas ne Ubivaet", "url": "https://www.youtube.com/watch?v=NTEXFyUE9Ww"},
{"name": "Dzaro and hansa - Visky Kola karaleva trans pola", "url": "https://www.youtube.com/watch?v=fflrMvZ2HtA"}
],
"noclue": [
{"name": "Bad Boys", "url": "https://www.youtube.com/watch?v=NTC7RD8xzCY"},
{"name": "DR. VODKA - DZIEWCZYNO Z TIKTOKA", "url": "https://www.youtube.com/watch?v=HLbw1WQt64o"},
{"name": "Maco Mamuko - Whiskey, Cola i Tequila", "url": "https://www.youtube.com/watch?v=aBrN0k0Phtc"}
],
"reggea": [
{"name": "Shaggy - It Wasn't Me", "url": "https://www.youtube.com/watch?v=ssVj50ombaM"}
],
"alt": [
{"name": "ROMANCEPLANET - FALL FROM THE SKY", "url": "https://www.youtube.com/watch?v=HMhzxzXBisw"},
{"name": "ROMANCEPLANET - PLAIN WHITE TEE", "url": "https://www.youtube.com/watch?v=tdVQbNwjGac"},
{"name": "ROMANCEPLANET - DANCE", "url": "https://www.youtube.com/watch?v=ircOfMb4gEw"}
]
}
}

View File

@ -1,46 +1,46 @@
use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{Manifest, ManifestSong}}; use crate::{config::ConfigWrapper, downloader::Downloader, manifest::{Manifest, ManifestSong}};
pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, genre: &Option<String>) -> anyhow::Result<()> { pub async fn add(cfg: &ConfigWrapper, manifest: &mut Manifest, downloader: &mut Downloader, url: &Option<String>, name: &Option<String>, genre: &Option<String>) -> anyhow::Result<()> {
log::debug!("Genre: {genre:?}"); log::debug!("Genre: {genre:?}");
log::debug!("url: {url:?}"); log::debug!("url: {url:?}");
log::debug!("name: {name:?}"); log::debug!("name: {name:?}");
let mut genres = manifest.genres.keys().map(|f| f.clone()).collect::<Vec<String>>(); let mut genres = manifest.genres.keys().map(|f| f.clone()).collect::<Vec<String>>();
genres.sort(); genres.sort();
let genre = genre.clone().unwrap_or_else( || { let genre = genre.clone().unwrap_or_else( || {
let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres); let g = crate::prompt::prompt_with_list_or_str("Enter song genre", &genres);
log::info!("Genre: {g}"); log::info!("Genre: {g}");
g g
}); });
let url = url.clone().unwrap_or_else( || let url = url.clone().unwrap_or_else( ||
crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)") crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)")
); );
let name = name.clone().unwrap_or_else( || let name = name.clone().unwrap_or_else( ||
crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}") crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}")
); );
manifest.add_song(genre.clone(), name.clone(), url.clone())?; manifest.add_song(genre.clone(), name.clone(), url.clone())?;
manifest.save()?; manifest.save()?;
let should_download = crate::prompt::prompt_bool("Download song now?", Some(false)); let should_download = crate::prompt::prompt_bool("Download song now?", Some(false));
if should_download { if should_download {
let song = &ManifestSong { let song = &ManifestSong {
name, name,
url, url,
}; };
downloader.download_song(cfg, song, &genre, &manifest.format()?).await?; downloader.download_song(cfg, song, &genre, &manifest.format()?).await?;
downloader.wait_for_procs(0).await?; downloader.wait_for_procs(0).await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,27 +1,28 @@
mod add; mod add;
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest}; use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> { pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> {
let mut downloader = Downloader::new(cfg.cfg.ytdlp.path.clone()); let mut downloader = Downloader::new();
match &cfg.cli.command { match &cfg.cli.command {
None | Some(CliCommand::Download) => { None | Some(CliCommand::Download) => {
if let Ok(count) = downloader.download_all(manifest, &cfg).await { match downloader.download_all(manifest, &cfg).await {
log::info!("Downloaded {count} songs"); Ok(count) => log::info!("Downloaded {count} songs"),
} else { Err(e) => {
log::error!("Failed to download songs"); log::error!("Failed to download songs: {e}");
return Ok(()); return Ok(());
} }
}, }
Some(c) => { },
match c { Some(c) => {
CliCommand::Download => unreachable!(), match c {
CliCommand::Add { url, name, genre } => add::add(cfg, manifest, &mut downloader, url, name, genre).await?, CliCommand::Download => unreachable!(),
} CliCommand::Add { url, name, genre } => add::add(cfg, manifest, &mut downloader, url, name, genre).await?,
} }
} }
}
Ok(())
Ok(())
} }

View File

@ -1,41 +1,40 @@
use camino::Utf8PathBuf; use camino::Utf8PathBuf;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crate::util::isatty;
#[derive(Debug, Parser, Default)]
#[derive(Debug, Parser, Default)] pub struct CliArgs {
pub struct CliArgs { /// Show more info
/// Show more info #[arg(long, short)]
#[arg(long, short)] pub debug: bool,
pub debug: bool,
/// Path to manifest
/// Path to manifest #[arg(long, short, default_value_t=Utf8PathBuf::from("./manifest.json"))]
#[arg(long, short, default_value_t=Utf8PathBuf::from("./manifest.json"))] pub manifest: Utf8PathBuf,
pub manifest: Utf8PathBuf,
/// Output directory
/// Output directory #[arg(long, short, default_value_t=Utf8PathBuf::from("./out"))]
#[arg(long, short, default_value_t=Utf8PathBuf::from("./out"))] pub output: Utf8PathBuf,
pub output: Utf8PathBuf,
/// Config path
/// Config path #[arg(long, short, default_value_t=Utf8PathBuf::from("./config.json"))]
#[arg(long, short, default_value_t=Utf8PathBuf::from("./config.json"))] pub config: Utf8PathBuf,
pub config: Utf8PathBuf,
#[command(subcommand)]
#[command(subcommand)] pub command: Option<CliCommand>,
pub command: Option<CliCommand>,
}
}
#[derive(Debug, Subcommand, Default)]
#[derive(Debug, Subcommand, Default)] pub enum CliCommand {
pub enum CliCommand { #[default]
#[default] Download,
Download, Add {
Add { #[arg(long, short)]
#[arg(long, short)] url: Option<String>,
url: Option<String>, #[arg(long, short)]
#[arg(long, short)] name: Option<String>,
name: Option<String>, #[arg(long, short)]
#[arg(long, short)] genre: Option<String>
genre: Option<String> }
} }
}

View File

@ -1,141 +1,133 @@
pub mod cli; pub mod cli;
use std::path::PathBuf; use std::path::PathBuf;
use clap::Parser; use clap::Parser;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use anyhow::Result; use anyhow::Result;
use crate::util::{self, dl_to_file, isatty}; use crate::util::{self, isatty};
use self::cli::CliArgs; use self::cli::CliArgs;
const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip"; // const YTDLP_DL_URL: &'static str = "https://github.com/yt-dlp/yt-dlp/archive/refs/heads/master.zip";
const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip"; // const SPOTDL_DL_URL: &'static str = "https://github.com/spotDL/spotify-downloader/archive/refs/heads/master.zip";
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ConfigWrapper { pub struct ConfigWrapper {
pub cfg: Config, pub cfg: Config,
pub cli: cli::CliArgs, pub cli: cli::CliArgs,
pub isatty: bool pub isatty: bool
} }
#[derive(Debug, Serialize, Deserialize, Default)] #[derive(Debug, Serialize, Deserialize, Default)]
pub struct Config { pub struct Config {
pub ytdlp: ConfigYtdlp, pub ytdlp: ConfigYtdlp,
pub spotdl: ConfigSpotdl, pub spotdl: ConfigSpotdl,
pub python: ConfigPython, }
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[derive(Debug, Serialize, Deserialize, Default)] pub struct ConfigYtdlp {
pub struct ConfigYtdlp { pub path: PathBuf,
pub path: PathBuf, }
pub is_python: bool,
} #[derive(Debug, Serialize, Deserialize, Default)]
pub struct ConfigSpotdl {
#[derive(Debug, Serialize, Deserialize, Default)] pub path: PathBuf,
pub struct ConfigSpotdl { }
pub path: PathBuf,
pub is_python: bool
} impl ConfigWrapper {
pub async fn parse() -> Result<Self> {
#[derive(Debug, Serialize, Deserialize, Default)] let mut s = Self::default();
pub struct ConfigPython { s.cli = cli::CliArgs::parse();
pub path: PathBuf, crate::logger::init_logger(s.cli.debug);
} s.cfg = Config::parse(&s.cli).await?;
s.isatty = isatty();
Ok(s)
impl ConfigWrapper { }
pub async fn parse() -> Result<Self> { }
let mut s = Self::default();
s.cli = cli::CliArgs::parse(); impl Config {
crate::logger::init_logger(s.cli.debug); pub async fn parse(cli: &CliArgs) -> Result<Self> {
s.cfg = Config::parse(&s.cli).await?; if !cli.config.exists() {
s.isatty = isatty(); log::info!("Config doesnt exist");
Ok(s) return Self::setup_config(&cli).await;
} }
}
let data = std::fs::read_to_string(&cli.config)?;
impl Config { let data: Self = serde_json::from_str(&data)?;
pub async fn parse(cli: &CliArgs) -> Result<Self> { Ok(data)
if !cli.config.exists() { }
log::info!("Config doesnt exist");
return Self::setup_config(&cli).await; async fn setup_config(cli: &CliArgs) -> Result<Self> {
} let mut s = Self::default();
let mut error = false;
let data = std::fs::read_to_string(&cli.config)?;
let data: Self = serde_json::from_str(&data)?; match util::is_program_in_path("yt-dlp") {
Ok(data) Some(p) => {
} s.ytdlp.path = p;
},
async fn setup_config(cli: &CliArgs) -> Result<Self> {
let mut s = Self::default(); None => {
error = true;
let bin_dir = cli.output.clone().into_std_path_buf().join(".bin/"); log::error!("could not find yt-dlp, please install it.");
let mut python_needed = false; log::info!(" - With winget (Windows only) (recommended):");
log::info!(" - Most new windows versions have winget installed, if not, instructions here: https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget");
match util::is_program_in_path("yt-dlp") { log::info!(" - run `winget install yt-dlp`");
Some(p) => { log::info!(" - With chocolatey (Windows only):");
s.ytdlp.path = p; log::info!(" - Make sure you have chocolatey installed - https://chocolatey.org/install");
s.ytdlp.is_python = false; log::info!(" - run `choco install yt-dlp` as Admin");
}, log::info!(" - With pip (from python) (Cross platform)");
log::info!(" - Make sure you have python installed");
None => { log::info!(" - pip install yt-dlp");
python_needed = true; log::info!(" - Using your distro's package manager (Unix/BSD only) (Not recommended)")
s.ytdlp.is_python = true; }
s.ytdlp.path = bin_dir.join("ytdlp"); }
dl_to_file(YTDLP_DL_URL, s.ytdlp.path.join("ytdlp.zip")).await?;
zip_extensions::zip_extract(&s.ytdlp.path.join("ytdlp.zip"), &s.ytdlp.path)?; match util::is_program_in_path("spotdl") {
} Some(p) => {
} s.spotdl.path = p;
},
match util::is_program_in_path("spotdl") {
Some(p) => { None => {
s.spotdl.path = p; let res = crate::prompt::prompt_bool("Spotdl is not installed but if you dont need to download music from spotify you dont need it, skip it?", None);
s.spotdl.is_python = false; if res {
}, s.spotdl.path = PathBuf::from("UNUSED");
} else {
None => { error = true;
python_needed = true; log::error!("could not find spotdl, please install it. ");
s.spotdl.is_python = true; log::info!(" - With pip (from python) (Cross platform) (recommended)");
s.spotdl.path = bin_dir.join("ytdlp"); log::info!(" - Make sure you have python installed - https://www.python.org/downloads/");
dl_to_file(SPOTDL_DL_URL, s.spotdl.path.join("spotdl.zip")).await?; log::info!(" - pip install spotdl");
zip_extensions::zip_extract(&s.spotdl.path.join("spotdl.zip"), &s.ytdlp.path)?; }
} }
} }
match util::is_program_in_path("ffmpeg") {
let python_paths = &[ Some(_) => (),
util::is_program_in_path("python"),
util::is_program_in_path("python3") None => {
]; error = true;
log::error!("could not find ffmpeg, please install it.");
if python_needed { log::info!(" - With winget (Windows only) (recommended):");
let mut found = false; log::info!(" - Most new windows versions have winget installed, if not, instructions here: https://learn.microsoft.com/en-us/windows/package-manager/winget/#install-winget");
for p in python_paths { log::info!(" - run `winget install --id=Gyan.FFmpeg -e`");
match p { log::info!(" - With chocolatey (Windows only):");
Some(p) => { log::info!(" - Make sure you have chocolatey installed - https://chocolatey.org/install");
s.python.path = p.clone(); log::info!(" - run `choco install ffmpeg` as Admin");
found = true; }
break }
}
None => { if !error {
} s.save(cli.config.clone().into_std_path_buf())?;
} }
}
Ok(s)
if !found { }
panic!("Python needs to be installed for this to work, or install ytdlp and spotdl manually, (dont forget to delete the config file after doing so)");
} fn save(&self, path: PathBuf) -> anyhow::Result<()> {
} let data = serde_json::to_string_pretty(self)?;
std::fs::write(path, data)?;
Ok(())
s.save(cli.config.clone().into_std_path_buf())?; }
Ok(s) }
}
fn save(&self, path: PathBuf) -> anyhow::Result<()> {
let data = serde_json::to_string_pretty(self)?;
std::fs::write(path, data)?;
Ok(())
}
}

View File

@ -1,15 +1,15 @@
#[cfg(target_family="windows")] #[cfg(target_family="windows")]
mod constants { mod constants {
pub const PATH_VAR_SEP: &'static str = ";"; pub const PATH_VAR_SEP: &'static str = ";";
pub const EXEC_EXT: &'static str = "exe"; pub const EXEC_EXT: &'static str = "exe";
} }
#[cfg(target_family="unix")] #[cfg(target_family="unix")]
mod constants { mod constants {
pub const PATH_VAR_SEP: &'static str = ":"; pub const PATH_VAR_SEP: &'static str = ":";
pub const EXEC_EXT: &'static str = ""; pub const EXEC_EXT: &'static str = "";
} }
pub use constants::*; pub use constants::*;

View File

@ -1,117 +1,132 @@
use std::{collections::HashMap, path::PathBuf, process::Stdio}; use std::{collections::HashMap, path::PathBuf, process::Stdio};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::Level; use log::Level;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};
use crate::{config::ConfigWrapper, manifest::{Manifest, ManifestSong}}; use crate::{config::ConfigWrapper, manifest::{Manifest, ManifestSong}};
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Proc { struct Proc {
url: String, url: String,
path: String, path: String,
finished: bool finished: bool
} }
lazy_static!( lazy_static!(
static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new())); static ref PROCESSES: Mutex<RwLock<HashMap<usize, Proc>>> = Mutex::new(RwLock::new(HashMap::new()));
); );
pub struct Downloader { pub struct Downloader {
count: usize, count: usize,
ytdlp_path: PathBuf, id_itr: usize,
id_itr: usize, }
}
impl Downloader {
impl Downloader { pub fn new() -> Self {
pub fn new(ytdlp_path: PathBuf) -> Self { Self {
Self { count: 0,
count: 0, id_itr: 0,
ytdlp_path, }
id_itr: 0, }
}
} pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
let format = manifest.format()?;
pub async fn download_all(&mut self, manifest: &Manifest, cfg: &ConfigWrapper) -> anyhow::Result<usize> {
let format = manifest.format()?; for (genre, songs) in &manifest.genres {
for song in songs {
for (genre, songs) in &manifest.genres { self.download_song(cfg, &song, &genre, &format).await?;
for song in songs { self.wait_for_procs(10).await?;
self.download_song(cfg, &song, &genre, &format).await?; }
self.wait_for_procs(10).await?; }
} self.wait_for_procs(0).await?;
} Ok(self.count)
self.wait_for_procs(0).await?; }
Ok(self.count)
} pub async fn download_song(&mut self, cfg: &ConfigWrapper, song: &ManifestSong, genre: &String, format: &String) -> anyhow::Result<()> {
let path = format!("{}/{genre}/{}.{}", cfg.cli.output, song.name, &format);
pub async fn download_song(&mut self, cfg: &ConfigWrapper, song: &ManifestSong, genre: &String, format: &String) -> anyhow::Result<()> {
let path = format!("{}/{genre}/{}.{}", cfg.cli.output, song.name, &format); if PathBuf::from(&path).exists() {
log::debug!("File {path} exists, skipping");
if PathBuf::from(&path).exists() { return Ok(())
log::debug!("File {path} exists, skipping"); }
return Ok(())
} log::debug!("File {path} doesnt exist, downloading");
let mut cmd = tokio::process::Command::new(&self.ytdlp_path); let mut cmd = if song.url.contains("youtube.com") || song.url.contains("youtu.be") {
let cmd = cmd.args([ log::debug!("Song {} is from yotube", song.url);
"-x", let mut cmd = tokio::process::Command::new(&cfg.cfg.ytdlp.path);
"--audio-format", cmd.args([
format.as_str(), "-x",
"-o", "--audio-format",
path.as_str(), format.as_str(),
song.url.as_str() "-o",
]); path.as_str(),
song.url.as_str()
let cmd = if log::max_level() < Level::Debug { ]);
cmd.stdout(Stdio::null()).stderr(Stdio::null()) cmd
} else {cmd}; } else {
let mut cmd = tokio::process::Command::new(&cfg.cfg.spotdl.path);
let mut proc = cmd.spawn()?; cmd.args([
let id = self.id_itr; "-x",
"--audio-format",
tokio::spawn(async move { format.as_str(),
let id = id; "-o",
proc.wait().await path.as_str(),
.expect("child process encountered an error"); song.url.as_str()
PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true; ]);
}); cmd
};
log::info!("Downloading {path}");
PROCESSES.lock().await.write().await.insert(id, Proc { if log::max_level() < Level::Debug {
url: song.url.clone(), cmd.stdout(Stdio::null()).stderr(Stdio::null());
path, };
finished: false,
}); let mut proc = cmd.spawn()?;
self.id_itr += 1; let id = self.id_itr;
Ok(())
} tokio::spawn(async move {
let id = id;
pub async fn wait_for_procs(&mut self, until: usize) -> anyhow::Result<()> { proc.wait().await
// NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible .expect("child process encountered an error");
// NOTE: So its also kinda really slow PROCESSES.lock().await.write().await.get_mut(&id).unwrap().finished = true;
loop { });
{
if PROCESSES.lock().await.read().await.len() <= until { log::info!("Downloading {path}");
return Ok(()); PROCESSES.lock().await.write().await.insert(id, Proc {
} url: song.url.clone(),
} path,
finished: false,
let procs = { });
PROCESSES.lock().await.read().await.clone() self.id_itr += 1;
}; Ok(())
}
for (idx, proc) in procs {
if proc.finished { pub async fn wait_for_procs(&mut self, until: usize) -> anyhow::Result<()> {
{ // NOTE: This looks really fucked because i dont want to deadlock the processes so i lock PROCESSES for as little as possible
PROCESSES.lock().await.write().await.remove(&idx); // NOTE: So its also kinda really slow
} loop {
log::info!("Finished downloading {}", proc.path); {
self.count += 1; if PROCESSES.lock().await.read().await.len() <= until {
} return Ok(());
} }
} }
#[allow(unreachable_code)] //? rust_analizer not smart enough for this
Ok(()) let procs = {
} PROCESSES.lock().await.read().await.clone()
};
for (idx, proc) in procs {
if proc.finished {
{
PROCESSES.lock().await.write().await.remove(&idx);
}
log::info!("Finished downloading {}", proc.path);
self.count += 1;
}
}
}
#[allow(unreachable_code)] //? rust_analizer not smart enough for this
Ok(())
}
} }

View File

@ -1,14 +1,14 @@
use log::LevelFilter; use log::LevelFilter;
pub fn init_logger(debug: bool) { pub fn init_logger(debug: bool) {
let level = if debug { let level = if debug {
LevelFilter::Debug LevelFilter::Debug
} else { } else {
LevelFilter::Info LevelFilter::Info
}; };
env_logger::builder() env_logger::builder()
.format_timestamp(None) .format_timestamp(None)
.filter_level(level) .filter_level(level)
.init(); .init();
} }

View File

@ -1,30 +1,30 @@
use config::ConfigWrapper; use config::ConfigWrapper;
// TODO: Possibly use https://docs.rs/ytextract/latest/ytextract/ instead of ytdlp // TODO: Possibly use https://docs.rs/ytextract/latest/ytextract/ instead of ytdlp
mod manifest; mod manifest;
mod logger; mod logger;
mod downloader; mod downloader;
mod util; mod util;
mod commands; mod commands;
mod prompt; mod prompt;
mod config; mod config;
mod constants; mod constants;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let Ok(cfg) = ConfigWrapper::parse().await else { let Ok(cfg) = ConfigWrapper::parse().await else {
return; return;
}; };
let mut manifest = match manifest::Manifest::from_path(&cfg.cli.manifest.as_std_path()) { let mut manifest = match manifest::Manifest::from_path(&cfg.cli.manifest.as_std_path()) {
Ok(m) => m, Ok(m) => m,
Err(e) => { Err(e) => {
log::error!("Failed to parse manifest file {}: {e}", cfg.cli.manifest); log::error!("Failed to parse manifest file {}: {e}", cfg.cli.manifest);
return; return;
} }
}; };
let _ = commands::command_run(&cfg, &mut manifest).await; let _ = commands::command_run(&cfg, &mut manifest).await;
} }

View File

@ -1,76 +1,76 @@
use std::{collections::HashMap, fs::read_to_string, path::{Path, PathBuf}}; use std::{collections::HashMap, fs::read_to_string, path::{Path, PathBuf}};
use anyhow::bail; use anyhow::bail;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const ALLOWED_FORMATS: &[&'static str] = &["m4a", "aac", "flac", "mp3", "vaw"]; const ALLOWED_FORMATS: &[&'static str] = &["m4a", "aac", "flac", "mp3", "vaw"];
type Genre = String; type Genre = String;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest { pub struct Manifest {
#[serde(skip)] #[serde(skip)]
path: PathBuf, path: PathBuf,
format: String, format: String,
pub genres: HashMap<Genre, Vec<ManifestSong>> pub genres: HashMap<Genre, Vec<ManifestSong>>
} }
impl Manifest { impl Manifest {
pub fn format(&self) -> anyhow::Result<String> { pub fn format(&self) -> anyhow::Result<String> {
if !ALLOWED_FORMATS.contains(&self.format.as_str()) { if !ALLOWED_FORMATS.contains(&self.format.as_str()) {
log::error!("Unknown format, allowed formats: {}", ALLOWED_FORMATS.join(", ")); log::error!("Unknown format, allowed formats: {}", ALLOWED_FORMATS.join(", "));
bail!("") bail!("")
} }
Ok(self.format.clone()) Ok(self.format.clone())
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestSong { pub struct ManifestSong {
pub name: String, pub name: String,
pub url: String pub url: String
} }
impl Manifest { impl Manifest {
fn from_string(s: String) -> anyhow::Result<Self> { fn from_string(s: String) -> anyhow::Result<Self> {
let s = serde_json::from_str(&s)?; let s = serde_json::from_str(&s)?;
Ok(s) Ok(s)
} }
pub fn from_path(p: &Path) -> anyhow::Result<Self> { pub fn from_path(p: &Path) -> anyhow::Result<Self> {
let data = read_to_string(p)?; let data = read_to_string(p)?;
let mut s = Self::from_string(data)?; let mut s = Self::from_string(data)?;
s.path = p.to_path_buf(); s.path = p.to_path_buf();
Ok(s) Ok(s)
} }
pub fn add_song(&mut self, genre: String, name: String, url: String) -> anyhow::Result<()> { pub fn add_song(&mut self, genre: String, name: String, url: String) -> anyhow::Result<()> {
if !self.genres.contains_key(&genre) { if !self.genres.contains_key(&genre) {
self.genres.insert(genre.clone(), Vec::new()); self.genres.insert(genre.clone(), Vec::new());
} }
let Some(genre_ref) = self.genres.get_mut(&genre) else { let Some(genre_ref) = self.genres.get_mut(&genre) else {
log::error!("Invalid genre '{}'", genre); log::error!("Invalid genre '{}'", genre);
bail!("Invalid genre") bail!("Invalid genre")
}; };
genre_ref.push(ManifestSong { genre_ref.push(ManifestSong {
name, name,
url, url,
}); });
Ok(()) Ok(())
} }
pub fn save(&self) -> anyhow::Result<()> { pub fn save(&self) -> anyhow::Result<()> {
let data = serde_json::to_string_pretty(self)?; let data = serde_json::to_string_pretty(self)?;
std::fs::write(&self.path, data)?; std::fs::write(&self.path, data)?;
Ok(()) Ok(())
} }
} }

View File

@ -1,148 +1,149 @@
use std::{collections::HashMap, io::Write}; use std::{collections::HashMap, io::Write};
pub fn simple_prompt(p: &str) -> String { pub fn simple_prompt(p: &str) -> String {
print!("{c}prompt{r}: {p} > ", print!("{c}prompt{r}: {p} > ",
c=anstyle::AnsiColor::Cyan.render_fg(), c=anstyle::AnsiColor::Cyan.render_fg(),
r=anstyle::Reset.render() r=anstyle::Reset.render()
); );
// I dont care if it fails // I dont care if it fails
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();
let mut buf = String::new(); let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf); let _ = std::io::stdin().read_line(&mut buf);
buf.trim().to_string() buf.trim().to_string()
} }
pub fn prompt_with_list(p: &str, options: &[&str]) -> usize { #[allow(dead_code)]
println!("{c}prompt{r}: {p}", pub fn prompt_with_list(p: &str, options: &[&str]) -> usize {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p}",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
);
for (i, op) in options.iter().enumerate() {
println!(" - {}: {}", i, op); for (i, op) in options.iter().enumerate() {
} println!(" - {}: {}", i, op);
}
print!("> ");
// I dont care if it fails print!("> ");
let _ = std::io::stdout().flush(); // I dont care if it fails
let _ = std::io::stdout().flush();
let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf); let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf);
if let Ok(num) = buf.parse::<usize>() {
if num <= options.len() { if let Ok(num) = buf.parse::<usize>() {
return num; if num <= options.len() {
} else { return num;
return prompt_with_list(p, options); } else {
} return prompt_with_list(p, options);
} else { }
return prompt_with_list(p, options); } else {
} return prompt_with_list(p, options);
} }
}
pub fn prompt_with_list_or_str(p: &str, options: &[String]) -> String {
println!("{c}prompt{r}: {p} (select with number or input text)", pub fn prompt_with_list_or_str(p: &str, options: &[String]) -> String {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p} (select with number or input text)",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
);
for (i, op) in options.iter().enumerate() {
println!(" - {}: {}", i, op); for (i, op) in options.iter().enumerate() {
} println!(" - {}: {}", i, op);
}
print!("> ");
// I dont care if it fails print!("> ");
let _ = std::io::stdout().flush(); // I dont care if it fails
let _ = std::io::stdout().flush();
let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf); let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf);
if let Ok(num) = buf.trim().parse::<usize>() {
if let Some(g) = options.get(num) { if let Ok(num) = buf.trim().parse::<usize>() {
return g.clone(); if let Some(g) = options.get(num) {
} else { return g.clone();
return prompt_with_list_or_str(p, options); } else {
} return prompt_with_list_or_str(p, options);
} else { }
return buf.trim().to_string(); } else {
} return buf.trim().to_string();
} }
}
pub fn prompt_with_map(p: &str, options: HashMap<&str, &str>) -> String { #[allow(dead_code)]
println!("{c}prompt{r}: {p}", pub fn prompt_with_map(p: &str, options: HashMap<&str, &str>) -> String {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p}",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
);
let mut keys = Vec::new();
let mut keys = Vec::new();
for (k, v) in &options {
println!(" - {}: {}", k, v); for (k, v) in &options {
keys.push(k.trim().to_lowercase()) println!(" - {}: {}", k, v);
} keys.push(k.trim().to_lowercase())
}
print!("> ");
print!("> ");
// I dont care if it fails
let _ = std::io::stdout().flush(); // I dont care if it fails
let _ = std::io::stdout().flush();
let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf); let mut buf = String::new();
if !keys.contains(&buf.trim().to_lowercase()) { let _ = std::io::stdin().read_line(&mut buf);
return prompt_with_map(p, options); if !keys.contains(&buf.trim().to_lowercase()) {
} return prompt_with_map(p, options);
buf.trim().to_string() }
} buf.trim().to_string()
}
pub fn prompt_bool(p: &str, default: Option<bool>) -> bool {
if default == Some(true) { pub fn prompt_bool(p: &str, default: Option<bool>) -> bool {
println!("{c}prompt{r}: {p} (Y/n)", if default == Some(true) {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p} (Y/n)",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
} else if default == Some(false) { );
println!("{c}prompt{r}: {p} (y/N)", } else if default == Some(false) {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p} (y/N)",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
} else { );
println!("{c}prompt{r}: {p} (y/n)", } else {
c=anstyle::AnsiColor::Cyan.render_fg(), println!("{c}prompt{r}: {p} (y/n)",
r=anstyle::Reset.render() c=anstyle::AnsiColor::Cyan.render_fg(),
); r=anstyle::Reset.render()
} );
print!("> "); }
print!("> ");
// I dont care if it fails
let _ = std::io::stdout().flush(); // I dont care if it fails
let _ = std::io::stdout().flush();
let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf); let mut buf = String::new();
let _ = std::io::stdin().read_line(&mut buf);
if buf.trim().is_empty() {
match default { if buf.trim().is_empty() {
Some(true) => return true, match default {
Some(false) => return false, Some(true) => return true,
None => { Some(false) => return false,
return prompt_bool(p, default); None => {
} return prompt_bool(p, default);
} }
} }
}
match buf.to_lowercase().trim() {
"y" => true, match buf.to_lowercase().trim() {
"n" => false, "y" => true,
c => { "n" => false,
log::error!("'{c}' is invalid, type y (yes) or n (no)"); c => {
return prompt_bool(p, default); log::error!("'{c}' is invalid, type y (yes) or n (no)");
} return prompt_bool(p, default);
} }
} }
}

View File

@ -1,54 +1,54 @@
use std::{io::Write, path::PathBuf}; use std::path::PathBuf;
use crate::constants; use crate::constants;
pub fn is_program_in_path(program: &str) -> Option<PathBuf> { pub fn is_program_in_path(program: &str) -> Option<PathBuf> {
if let Ok(path) = std::env::var("PATH") { if let Ok(path) = std::env::var("PATH") {
for p in path.split(constants::PATH_VAR_SEP) { for p in path.split(constants::PATH_VAR_SEP) {
let exec_path = PathBuf::from(p).join(program).with_extension(constants::EXEC_EXT); let exec_path = PathBuf::from(p).join(program).with_extension(constants::EXEC_EXT);
if std::fs::metadata(&exec_path).is_ok() { if std::fs::metadata(&exec_path).is_ok() {
return Some(exec_path); return Some(exec_path);
} }
} }
} }
None None
} }
#[cfg(target_family="unix")] #[cfg(target_family="unix")]
pub fn isatty() -> bool { pub fn isatty() -> bool {
use std::{ffi::c_int, os::fd::AsRawFd}; use std::{ffi::c_int, os::fd::AsRawFd};
unsafe { unsafe {
let fd = std::io::stdin().as_raw_fd(); let fd = std::io::stdin().as_raw_fd();
libc::isatty(fd as c_int) == 1 libc::isatty(fd as c_int) == 1
} }
} }
#[cfg(target_family="windows")] #[cfg(target_family="windows")]
pub fn isatty() -> bool { pub fn isatty() -> bool {
unsafe { unsafe {
use windows::Win32::System::Console; use windows::Win32::System::Console;
use Console::{CONSOLE_MODE, STD_OUTPUT_HANDLE}; use Console::{CONSOLE_MODE, STD_OUTPUT_HANDLE};
let Ok(handle) = Console::GetStdHandle(STD_OUTPUT_HANDLE) else { let Ok(handle) = Console::GetStdHandle(STD_OUTPUT_HANDLE) else {
return false; return false;
}; };
let mut out = CONSOLE_MODE(0); let mut out = CONSOLE_MODE(0);
let ret = Console::GetConsoleMode(handle, &mut out); let ret = Console::GetConsoleMode(handle, &mut out);
ret.is_ok() ret.is_ok()
} }
} }
pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> { // pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> {
log::info!("Downloading {} -> {:?}", url, p); // log::info!("Downloading {} -> {:?}", url, p);
let ytdlp_req = reqwest::get(url).await?.bytes().await?; // let ytdlp_req = reqwest::get(url).await?.bytes().await?;
log::debug!("Downloading {:?} finished, writing to file", p); // log::debug!("Downloading {:?} finished, writing to file", p);
let mut fd = std::fs::File::create(&p)?; // let mut fd = std::fs::File::create(&p)?;
fd.write(&ytdlp_req)?; // fd.write(&ytdlp_req)?;
log::debug!("Finished writing {:?}", p); // log::debug!("Finished writing {:?}", p);
Ok(()) // Ok(())
} // }