use crate::{
automation::{Prompter, SelectOptions},
cookie::{CookieJar, CookieParam},
downloader::{self, Decrypter},
};
use anyhow::Result;
use clap::Args;
use cookie::Cookie;
use reqwest::{
Proxy, Url,
blocking::Client,
header::{HeaderMap, HeaderName, HeaderValue},
};
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
type CookieParams = Vec<CookieParam>;
#[derive(Args, Clone, Debug)]
pub struct Save {
#[arg(required = true)]
pub input: String,
#[arg(long)]
pub base_url: Option<Url>,
#[arg(short, long)]
pub directory: Option<PathBuf>,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub parse: bool,
#[arg(long, default_value = "copy")]
pub subs_codec: String,
#[arg(short, long, help_heading = "Automation Options")]
pub interactive: bool,
#[arg(long, help_heading = "Automation Options")]
pub interactive_raw: bool,
#[arg(short, long, help_heading = "Automation Options")]
pub list_streams: bool,
#[arg(
short,
long,
help_heading = "Automation Options",
default_value = "v=best:s=en",
long_help = "Filters to be applied for automatic stream selection.\n\nSYNTAX: `v={}:a={}:s={}` where `{}` (in priority order) can contain\n|> all: select all streams.\n|> skip: skip all streams or select inverter.\n|> 1,2: ids obtained by --list-streams flag.\n|> 1080p,1280x720: stream resolution.\n|> en,fr: stream language.\n\nEXAMPLES:\n|> v=skip:a=skip:s=all (download all sub streams)\n|> a:en:s=en (prefer en lang)\n|> v=1080p:a=all:s=skip (1080p with all audio streams)"
)]
pub select_streams: String,
#[arg(long, help_heading = "Client Options", default_value = "[]", hide_default_value = true, value_parser = cookie_parser)]
pub cookies: CookieParams,
#[arg(long, help_heading = "Client Options", num_args = 2, value_names = &["KEY", "VALUE"])]
pub header: Vec<String>,
#[arg(long, help_heading = "Client Options")]
pub no_certificate_checks: bool,
#[arg(long, help_heading = "Client Options", value_parser = proxy_address_parser)]
pub proxy: Option<Proxy>,
#[arg(long, help_heading = "Client Options", default_value = "", hide_default_value = true, value_parser = query_parser)]
pub query: HashMap<String, String>,
#[arg(long, help_heading = "Client Options", num_args = 2, value_names = &["SET_COOKIE", "URL"])]
pub set_cookie: Vec<String>,
#[arg(
long,
help_heading = "Client Options",
default_value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
)]
pub user_agent: String,
#[arg(long, help_heading = "Decrypt Options", value_name = "KID:KEY;...", default_value = "", hide_default_value = true, value_parser = keys_parser)]
pub keys: Decrypter,
#[arg(long, help_heading = "Decrypt Options")]
pub no_decrypt: bool,
#[arg(long, help_heading = "Download Options", default_value_t = 15)]
pub retries: u8,
#[arg(long, help_heading = "Download Options")]
pub no_merge: bool,
#[arg(short, long, help_heading = "Download Options", default_value_t = 5, value_parser = clap::value_parser!(u8).range(1..=16))]
pub threads: u8,
}
impl Save {
fn client(&self) -> Result<Client> {
let mut client_builder = Client::builder()
.cookie_store(true)
.danger_accept_invalid_certs(self.no_certificate_checks)
.user_agent(&self.user_agent)
.timeout(std::time::Duration::from_secs(60));
if !self.header.is_empty() {
let mut headers = HeaderMap::new();
for i in (0..self.header.len()).step_by(2) {
headers.insert(
self.header[i].parse::<HeaderName>()?,
self.header[i + 1].parse::<HeaderValue>()?,
);
}
client_builder = client_builder.default_headers(headers);
}
if let Some(proxy) = &self.proxy {
client_builder = client_builder.proxy(proxy.clone());
}
let mut jar = CookieJar::new();
if !self.set_cookie.is_empty() {
for i in (0..self.set_cookie.len()).step_by(2) {
jar.add_cookie_str(&self.set_cookie[i], &self.set_cookie[i + 1].parse::<Url>()?);
}
}
for cookie in &self.cookies {
if let Some(url) = &cookie.url {
jar.add_cookie_str(&format!("{}", cookie.as_cookie()), &url.parse::<Url>()?);
} else {
jar.add_cookie(cookie.as_cookie());
}
}
let client = client_builder.cookie_provider(Arc::new(jar)).build()?;
Ok(client)
}
pub fn execute(self) -> Result<()> {
let client = self.client()?;
let prompter = Prompter {
interactive: self.interactive,
interactive_raw: self.interactive_raw,
};
let meta = downloader::fetch_playlist(
self.base_url.clone(),
&client,
&self.input,
&prompter,
&self.query,
)?;
if self.list_streams {
downloader::list_all_streams(&meta)?;
} else if self.parse {
let playlist =
downloader::parse_all_streams(self.base_url.clone(), &client, &meta, &self.query)?;
serde_json::to_writer(std::io::stdout(), &playlist)?;
} else {
let streams = downloader::parse_selected_streams(
self.base_url.clone(),
&client,
&meta,
&prompter,
&self.query,
SelectOptions::parse(&self.select_streams),
)?;
downloader::download(
self.base_url,
client,
self.keys,
self.directory,
self.no_decrypt,
self.no_merge,
self.output,
self.query,
streams,
self.subs_codec,
self.retries,
self.threads,
)?;
}
Ok(())
}
}
fn cookie_parser(s: &str) -> Result<CookieParams, String> {
if Path::new(s).exists() {
Ok(serde_json::from_slice::<CookieParams>(
&std::fs::read(s).map_err(|_| format!("could not read {s}."))?,
)
.map_err(|_| "could not deserialize cookies from json file.")?)
} else if let Ok(cookies) = serde_json::from_str::<CookieParams>(s) {
Ok(cookies)
} else {
let mut cookies = vec![];
for cookie in Cookie::split_parse(s) {
match cookie {
Ok(x) => cookies.push(CookieParam::new(x.name(), x.value())),
Err(_) => return Err("could not split parse cookies.".to_owned()),
}
}
Ok(cookies)
}
}
fn keys_parser(s: &str) -> Result<Decrypter, String> {
if s.is_empty() {
return Ok(Decrypter::None);
}
let mut kid_key_pairs = HashMap::new();
for pair in s.split(';') {
if let Some((kid, key)) = pair.split_once(':') {
let kid = kid.to_ascii_lowercase().replace('-', "");
let key = key.to_ascii_lowercase().replace('-', "");
if kid.len() == 32
&& key.len() == 32
&& kid.chars().all(|c| c.is_ascii_hexdigit())
&& key.chars().all(|c| c.is_ascii_hexdigit())
{
kid_key_pairs.insert(kid, key);
} else {
return Err("invalid kid key format used.".to_owned());
}
}
}
Ok(Decrypter::Mp4Decrypt(kid_key_pairs))
}
fn proxy_address_parser(s: &str) -> Result<Proxy, String> {
Proxy::all(s).map_err(|x| x.to_string())
}
fn query_parser(s: &str) -> Result<HashMap<String, String>, String> {
let mut queries = HashMap::new();
if s.is_empty() {
return Ok(queries);
}
for pair in s.split('&') {
let mut parts = pair.splitn(2, '=');
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
queries.insert(key.to_owned(), value.to_owned());
}
}
Ok(queries)
}