Refractored downloader
Implemented add command Created config and merged it with cli
This commit is contained in:
1154
music_mgr/Cargo.lock
generated
1154
music_mgr/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,9 @@ env_logger = "0.11.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.153"
|
||||
log = "0.4.21"
|
||||
reqwest = "0.12.3"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_json = "1.0.115"
|
||||
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"] }
|
||||
zip-extensions = "0.6.2"
|
||||
|
||||
38
music_mgr/src/commands/add.rs
Normal file
38
music_mgr/src/commands/add.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
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<()> {
|
||||
|
||||
let genres = manifest.genres.keys().map(|f| f.clone()).collect::<Vec<String>>();
|
||||
|
||||
let genre = genre.clone().unwrap_or(
|
||||
crate::prompt::prompt_with_list_or_str("Enter song genre", &genres)
|
||||
);
|
||||
|
||||
log::debug!("Genre: {genre}");
|
||||
|
||||
let url = url.clone().unwrap_or(
|
||||
crate::prompt::simple_prompt("Enter song youtube url, make sure its not a playlist, (yt only for now)")
|
||||
);
|
||||
|
||||
let name = name.clone().unwrap_or(
|
||||
crate::prompt::simple_prompt("Enter song name with like this: {Author} - {Song name}")
|
||||
);
|
||||
|
||||
manifest.add_song(genre.clone(), name.clone(), url.clone())?;
|
||||
manifest.save()?;
|
||||
|
||||
let should_download = crate::prompt::prompt_bool("Download song now?", Some(true));
|
||||
|
||||
if should_download {
|
||||
let song = &ManifestSong {
|
||||
name,
|
||||
url,
|
||||
};
|
||||
|
||||
downloader.download_song(cfg, song, &genre, &manifest.format()?).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,22 +1,27 @@
|
||||
use crate::{cli::{CliArgs, CliCommand}, downloader::Downloader, manifest::Manifest, util};
|
||||
mod add;
|
||||
|
||||
use crate::{config::{cli::CliCommand, ConfigWrapper}, downloader::Downloader, manifest::Manifest};
|
||||
|
||||
|
||||
pub async fn command_run(cli: &CliArgs, manifest: &Manifest) {
|
||||
let mut downloader = Downloader::new(util::get_ytdlp_path());
|
||||
match &cli.command {
|
||||
|
||||
pub async fn command_run(cfg: &ConfigWrapper, manifest: &mut Manifest) -> anyhow::Result<()> {
|
||||
let mut downloader = Downloader::new(cfg.cfg.ytdlp.path.clone());
|
||||
match &cfg.cli.command {
|
||||
None | Some(CliCommand::Download) => {
|
||||
if let Ok(count) = downloader.download_all(manifest, &cli).await {
|
||||
if let Ok(count) = downloader.download_all(manifest, &cfg).await {
|
||||
log::info!("Downloaded {count} songs");
|
||||
} else {
|
||||
log::error!("Failed to download songs");
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Some(c) => {
|
||||
match c {
|
||||
CliCommand::Download => unreachable!(),
|
||||
CliCommand::Add { .. } => todo!(),
|
||||
CliCommand::Add { url, name, genre } => add::add(cfg, manifest, &mut downloader, url, name, genre).await?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::util::isatty;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[derive(Debug, Parser, Default)]
|
||||
pub struct CliArgs {
|
||||
/// Show more info
|
||||
#[arg(long, short)]
|
||||
@@ -17,11 +17,13 @@ pub struct CliArgs {
|
||||
#[arg(long, short, default_value_t=Utf8PathBuf::from("./out"))]
|
||||
pub output: Utf8PathBuf,
|
||||
|
||||
/// Config path
|
||||
#[arg(long, short, default_value_t=Utf8PathBuf::from("./config.json"))]
|
||||
pub config: Utf8PathBuf,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub command: Option<CliCommand>,
|
||||
|
||||
#[clap(skip)]
|
||||
pub is_tty: bool
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand, Default)]
|
||||
@@ -29,15 +31,8 @@ pub enum CliCommand {
|
||||
#[default]
|
||||
Download,
|
||||
Add {
|
||||
url: String,
|
||||
name: String,
|
||||
genre: String
|
||||
url: Option<String>,
|
||||
name: Option<String>,
|
||||
genre: Option<String>
|
||||
}
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
pub fn populate_extra(&mut self) -> &mut Self{
|
||||
self.is_tty = isatty();
|
||||
self
|
||||
}
|
||||
}
|
||||
132
music_mgr/src/config/mod.rs
Normal file
132
music_mgr/src/config/mod.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
pub mod cli;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use anyhow::Result;
|
||||
use crate::util::{self, dl_to_file, isatty};
|
||||
|
||||
use self::cli::CliArgs;
|
||||
|
||||
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";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigWrapper {
|
||||
pub cfg: Config,
|
||||
pub cli: cli::CliArgs,
|
||||
pub isatty: bool
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
pub ytdlp: ConfigYtdlp,
|
||||
pub spotdl: ConfigSpotdl,
|
||||
pub python: ConfigPython,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ConfigYtdlp {
|
||||
pub path: PathBuf,
|
||||
pub is_python: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ConfigSpotdl {
|
||||
pub path: PathBuf,
|
||||
pub is_python: bool
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct ConfigPython {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
impl ConfigWrapper {
|
||||
pub async fn parse() -> Result<Self> {
|
||||
let mut s = Self::default();
|
||||
s.cli = cli::CliArgs::parse();
|
||||
crate::logger::init_logger(s.cli.debug);
|
||||
s.cfg = Config::parse(&s.cli).await?;
|
||||
s.isatty = isatty();
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub async fn parse(cli: &CliArgs) -> Result<Self> {
|
||||
if !cli.config.exists() {
|
||||
return Self::setup_config(&cli).await;
|
||||
}
|
||||
|
||||
let data = std::fs::read_to_string(&cli.config)?;
|
||||
let data: Self = serde_json::from_str(&data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn setup_config(cli: &CliArgs) -> Result<Self> {
|
||||
let mut s = Self::default();
|
||||
|
||||
let bin_dir = cli.output.clone().into_std_path_buf().join(".bin/");
|
||||
let mut python_needed = false;
|
||||
|
||||
match util::is_program_in_path("yt-dlp") {
|
||||
Some(p) => {
|
||||
s.ytdlp.path = p;
|
||||
s.ytdlp.is_python = false;
|
||||
},
|
||||
|
||||
None => {
|
||||
python_needed = true;
|
||||
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;
|
||||
s.spotdl.is_python = false;
|
||||
},
|
||||
|
||||
None => {
|
||||
python_needed = true;
|
||||
s.spotdl.is_python = true;
|
||||
s.spotdl.path = bin_dir.join("ytdlp");
|
||||
dl_to_file(SPOTDL_DL_URL, s.spotdl.path.join("spotdl.zip")).await?;
|
||||
zip_extensions::zip_extract(&s.spotdl.path.join("spotdl.zip"), &s.ytdlp.path)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let python_paths = &[
|
||||
util::is_program_in_path("python"),
|
||||
util::is_program_in_path("python3")
|
||||
];
|
||||
|
||||
if python_needed {
|
||||
let mut found = false;
|
||||
for p in python_paths {
|
||||
match p {
|
||||
Some(p) => {
|
||||
s.python.path = p.clone();
|
||||
found = true;
|
||||
break
|
||||
}
|
||||
None => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
15
music_mgr/src/constants.rs
Normal file
15
music_mgr/src/constants.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
#[cfg(target_family="windows")]
|
||||
mod constants {
|
||||
pub const PATH_VAR_SEP: &'static str = ";";
|
||||
pub const EXEC_EXT: &'static str = "exe";
|
||||
}
|
||||
|
||||
#[cfg(target_family="unix")]
|
||||
mod constants {
|
||||
pub const PATH_VAR_SEP: &'static str = ":";
|
||||
pub const EXEC_EXT: &'static str = "";
|
||||
}
|
||||
|
||||
|
||||
pub use constants::*;
|
||||
@@ -4,7 +4,7 @@ use lazy_static::lazy_static;
|
||||
use log::Level;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use crate::{cli::CliArgs, manifest::Manifest};
|
||||
use crate::{config::ConfigWrapper, manifest::{Manifest, ManifestSong}};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -20,12 +20,12 @@ lazy_static!(
|
||||
|
||||
pub struct Downloader {
|
||||
count: usize,
|
||||
ytdlp_path: String,
|
||||
ytdlp_path: PathBuf,
|
||||
id_itr: usize,
|
||||
}
|
||||
|
||||
impl Downloader {
|
||||
pub fn new(ytdlp_path: String) -> Self {
|
||||
pub fn new(ytdlp_path: PathBuf) -> Self {
|
||||
Self {
|
||||
count: 0,
|
||||
ytdlp_path,
|
||||
@@ -33,12 +33,12 @@ impl Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_all(&mut self, manifest: &Manifest, cli: &CliArgs) -> anyhow::Result<usize> {
|
||||
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 {
|
||||
self.download_song(format!("{}/{genre}/{}.{}", cli.output, song.name, &format), &format, &song.url).await?;
|
||||
self.download_song(cfg, &song, &genre, &format).await?;
|
||||
self.wait_for_procs(10).await?;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,9 @@ impl Downloader {
|
||||
Ok(self.count)
|
||||
}
|
||||
|
||||
async fn download_song(&mut self, path: String, audio_format: &String, url: &String) -> anyhow::Result<()> {
|
||||
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");
|
||||
return Ok(())
|
||||
@@ -55,10 +57,10 @@ impl Downloader {
|
||||
let cmd = cmd.args([
|
||||
"-x",
|
||||
"--audio-format",
|
||||
audio_format.as_str(),
|
||||
format.as_str(),
|
||||
"-o",
|
||||
path.as_str(),
|
||||
url.as_str()
|
||||
song.url.as_str()
|
||||
]);
|
||||
|
||||
let cmd = if log::max_level() < Level::Debug {
|
||||
@@ -77,7 +79,7 @@ impl Downloader {
|
||||
|
||||
log::info!("Downloading {path}");
|
||||
PROCESSES.lock().await.write().await.insert(id, Proc {
|
||||
url: url.clone(),
|
||||
url: song.url.clone(),
|
||||
path,
|
||||
finished: false,
|
||||
});
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
use clap::Parser;
|
||||
use config::ConfigWrapper;
|
||||
|
||||
|
||||
// TODO: Possibly use https://docs.rs/ytextract/latest/ytextract/ instead of ytdlp
|
||||
|
||||
use crate::cli::CliArgs;
|
||||
|
||||
mod cli;
|
||||
mod manifest;
|
||||
mod logger;
|
||||
mod downloader;
|
||||
mod util;
|
||||
mod commands;
|
||||
mod prompt;
|
||||
mod config;
|
||||
mod constants;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut cli_args = CliArgs::parse();
|
||||
cli_args.populate_extra();
|
||||
logger::init_logger(cli_args.debug);
|
||||
let Ok(cfg) = ConfigWrapper::parse().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let manifest = match manifest::Manifest::from_path(&cli_args.manifest.as_std_path()) {
|
||||
let mut manifest = match manifest::Manifest::from_path(&cfg.cli.manifest.as_std_path()) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::error!("Failed to parse manifest file {}: {e}", cli_args.manifest);
|
||||
log::error!("Failed to parse manifest file {}: {e}", cfg.cli.manifest);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
commands::command_run(&cli_args, &manifest).await;
|
||||
let _ = commands::command_run(&cfg, &mut manifest).await;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, fs::read_to_string, path::Path};
|
||||
use std::{collections::HashMap, fs::read_to_string, path::{Path, PathBuf}};
|
||||
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -11,6 +11,8 @@ type Genre = String;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Manifest {
|
||||
#[serde(skip)]
|
||||
path: PathBuf,
|
||||
format: String,
|
||||
pub genres: HashMap<Genre, Vec<ManifestSong>>
|
||||
}
|
||||
@@ -35,13 +37,35 @@ pub struct ManifestSong {
|
||||
|
||||
|
||||
impl Manifest {
|
||||
pub fn from_string(s: String) -> anyhow::Result<Self> {
|
||||
fn from_string(s: String) -> anyhow::Result<Self> {
|
||||
let s = serde_json::from_str(&s)?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn from_path(p: &Path) -> anyhow::Result<Self> {
|
||||
let data = read_to_string(p)?;
|
||||
Self::from_string(data)
|
||||
let mut s = Self::from_string(data)?;
|
||||
s.path = p.to_path_buf();
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn add_song(&mut self, genre: String, name: String, url: String) -> anyhow::Result<()> {
|
||||
let Some(genre_ref) = self.genres.get_mut(&genre) else {
|
||||
log::error!("Invalid genre '{}'", genre);
|
||||
bail!("Invalid genre")
|
||||
};
|
||||
|
||||
genre_ref.push(ManifestSong {
|
||||
name,
|
||||
url,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let data = serde_json::to_string_pretty(self)?;
|
||||
std::fs::write(&self.path, data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ use std::{collections::HashMap, io::Write};
|
||||
|
||||
pub fn simple_prompt(p: &str) -> String {
|
||||
|
||||
print!("{c}prompt{r}: {p}",
|
||||
c=anstyle::AnsiColor::Magenta.render_fg(),
|
||||
r=anstyle::Style::new().render_reset()
|
||||
print!("{c}prompt{r}: {p} > ",
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
|
||||
// I dont care if it fails
|
||||
@@ -15,13 +15,13 @@ pub fn simple_prompt(p: &str) -> String {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut buf);
|
||||
|
||||
buf
|
||||
buf.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn prompt_with_options(p: &str, options: &[&str]) -> usize {
|
||||
pub fn prompt_with_list(p: &str, options: &[&str]) -> usize {
|
||||
println!("{c}prompt{r}: {p}",
|
||||
c=anstyle::AnsiColor::Magenta.render_fg(),
|
||||
r=anstyle::Style::new().render_reset()
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
|
||||
for (i, op) in options.iter().enumerate() {
|
||||
@@ -39,19 +39,47 @@ pub fn prompt_with_options(p: &str, options: &[&str]) -> usize {
|
||||
if num <= options.len() {
|
||||
return num;
|
||||
} else {
|
||||
log::error!("Number not in range");
|
||||
return prompt_with_options(p, options);
|
||||
return prompt_with_list(p, options);
|
||||
}
|
||||
} else {
|
||||
log::error!("Not a number");
|
||||
return prompt_with_options(p, options);
|
||||
return prompt_with_list(p, options);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_with_named_options(p: &str, options: HashMap<&str, &str>) -> String {
|
||||
pub fn prompt_with_list_or_str(p: &str, options: &[String]) -> String {
|
||||
println!("{c}prompt{r}: {p} (select with number or input text)",
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
|
||||
for (i, op) in options.iter().enumerate() {
|
||||
println!(" - {}: {}", i, op);
|
||||
}
|
||||
|
||||
print!("> ");
|
||||
// I dont care if it fails
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
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) {
|
||||
return g.clone();
|
||||
} else {
|
||||
return prompt_with_list_or_str(p, options);
|
||||
}
|
||||
} else {
|
||||
return buf.trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn prompt_with_map(p: &str, options: HashMap<&str, &str>) -> String {
|
||||
println!("{c}prompt{r}: {p}",
|
||||
c=anstyle::AnsiColor::Magenta.render_fg(),
|
||||
r=anstyle::Style::new().render_reset()
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
|
||||
let mut keys = Vec::new();
|
||||
@@ -69,7 +97,52 @@ pub fn prompt_with_named_options(p: &str, options: HashMap<&str, &str>) -> Strin
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut buf);
|
||||
if !keys.contains(&buf.trim().to_lowercase()) {
|
||||
return prompt_with_named_options(p, options);
|
||||
return prompt_with_map(p, options);
|
||||
}
|
||||
buf.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn prompt_bool(p: &str, default: Option<bool>) -> bool {
|
||||
if default == Some(true) {
|
||||
println!("{c}prompt{r}: {p} (Y/n)",
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
} else if default == Some(false) {
|
||||
println!("{c}prompt{r}: {p} (y/N)",
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
} else {
|
||||
println!("{c}prompt{r}: {p} (y/n)",
|
||||
c=anstyle::AnsiColor::Cyan.render_fg(),
|
||||
r=anstyle::Reset.render()
|
||||
);
|
||||
}
|
||||
print!("> ");
|
||||
|
||||
// I dont care if it fails
|
||||
let _ = std::io::stdout().flush();
|
||||
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::stdin().read_line(&mut buf);
|
||||
|
||||
if buf.trim().is_empty() {
|
||||
match default {
|
||||
Some(true) => return true,
|
||||
Some(false) => return false,
|
||||
None => {
|
||||
return prompt_bool(p, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match buf.to_lowercase().trim() {
|
||||
"y" => true,
|
||||
"n" => false,
|
||||
c => {
|
||||
log::error!("'{c}' is invalid, type y (yes) or n (no)");
|
||||
return prompt_bool(p, default);
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
fn is_program_in_path(program: &str) -> Option<String> {
|
||||
use crate::constants;
|
||||
|
||||
|
||||
|
||||
|
||||
pub fn is_program_in_path(program: &str) -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("PATH") {
|
||||
for p in path.split(":") {
|
||||
let p_str = format!("{}/{}", p, program);
|
||||
if std::fs::metadata(&p_str).is_ok() {
|
||||
return Some(p_str);
|
||||
for p in path.split(constants::PATH_VAR_SEP) {
|
||||
let exec_path = PathBuf::from(p).join(program).with_extension(constants::EXEC_EXT);
|
||||
if std::fs::metadata(&exec_path).is_ok() {
|
||||
return Some(exec_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn get_ytdlp_path() -> String {
|
||||
if let Some(p) = is_program_in_path("yt-dlp") {
|
||||
return p;
|
||||
}
|
||||
// TODO: Download yt-dlp to ./.bin/yt-dlp if doesnt exist
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(target_family="unix")]
|
||||
pub fn isatty() -> bool {
|
||||
use std::{ffi::c_int, os::fd::AsRawFd};
|
||||
@@ -45,4 +41,14 @@ pub fn isatty() -> bool {
|
||||
|
||||
ret.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dl_to_file(url: &str, p: PathBuf) -> anyhow::Result<()> {
|
||||
log::info!("Downloading {} -> {:?}", url, p);
|
||||
let ytdlp_req = reqwest::get(url).await?.bytes().await?;
|
||||
log::debug!("Downloading {:?} finished, writing to file", p);
|
||||
let mut fd = std::fs::File::create(&p)?;
|
||||
fd.write(&ytdlp_req)?;
|
||||
log::debug!("Finished writing {:?}", p);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user