use crate::cache::Cache;
use crate::config::{COMMAND, Config, Quality};
use kodik_parser::{Response, reqwest::Client};
use log::LevelFilter;
use std::io::Write;
use std::io::{self, BufWriter};
use std::process::{Command, ExitCode, Stdio};
mod cache;
mod config;
mod logging;
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
mod tests;
#[must_use]
pub async fn run(args: Vec<String>) -> ExitCode {
if args.len() < 2 {
eprint!("{}", COMMAND.help());
return ExitCode::FAILURE;
}
let mut config = match Config::build(args) {
Ok(config) => {
logging::setup_logging(config.level_filter);
config
}
Err(err) => {
logging::setup_logging(LevelFilter::Info);
log::error!("{err}");
return ExitCode::FAILURE;
}
};
if config.help {
eprint!("{}", COMMAND.help());
return ExitCode::FAILURE;
}
let mut cache_opt = Cache::load();
if let Some(cache) = cache_opt.as_ref() {
cache.apply(&mut config);
}
let client = Client::new();
let use_lazy = config.lazy || config.player.is_some();
let mut idx = 0;
while idx < config.urls.len() {
let Some(url) = config.urls.get(idx) else {
break;
};
if url.starts_with("https://shiki") {
match kodik_shiki::run(
&client,
url,
config.cookie.as_deref(),
config.translation_title.as_deref(),
config.translation_type.0.as_ref(),
config.episode,
)
.await
{
Ok(video_result) => match video_result {
kodik_shiki::VideoResult::Episodes(episodes) => {
let episode_count = episodes.len();
config.urls.splice(idx..=idx, episodes);
idx += episode_count;
}
kodik_shiki::VideoResult::Film(film) => {
if let Some(url_ref) = config.urls.get_mut(idx) {
*url_ref = film;
}
idx += 1;
}
},
Err(e) => {
log::error!("{e}");
return ExitCode::FAILURE;
}
}
} else {
idx += 1;
}
}
let exit_code = if use_lazy {
run_lazy(&client, config.urls, config.quality, config.player).await
} else {
run_parallel(&client, config.urls, config.quality).await
};
if let Some(cache) = cache_opt.as_mut()
&& cache.is_changed(config.cookie.as_deref())
{
log::warn!("Updating cache... in {}", cache.path.display());
cache.update(config.cookie.as_deref());
cache.save();
}
exit_code
}
async fn run_parallel(client: &Client, urls: Vec<String>, quality: Quality) -> ExitCode {
let results = {
let mut set = tokio::task::JoinSet::new();
for (idx, url) in urls.into_iter().enumerate() {
let client = client.clone();
set.spawn(async move {
let result = kodik_parser::parse(&client, &url).await;
(idx, result)
});
}
let mut results = set.join_all().await;
results.sort_by(|a, b| a.0.cmp(&b.0));
results
};
let stdout = io::stdout();
let mut handle = BufWriter::new(stdout.lock());
for (_, res) in results {
let kodik_response = match res {
Ok(r) => r,
Err(e) => {
log::error!("{e}");
return ExitCode::FAILURE;
}
};
let Some(link) = get_link(&kodik_response, quality) else {
log::error!("no playable links found for this video");
return ExitCode::FAILURE;
};
if let Err(e) = writeln!(handle, "{link}") {
log::error!("{e}");
return ExitCode::FAILURE;
}
}
if let Err(e) = handle.flush() {
log::error!("{e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
async fn run_lazy(
client: &Client,
urls: Vec<String>,
quality: Quality,
player: Option<String>,
) -> ExitCode {
for url in urls {
let kodik_response = match kodik_parser::parse(client, &url).await {
Ok(r) => r,
Err(e) => {
log::error!("{e}");
return ExitCode::FAILURE;
}
};
let Some(link) = get_link(&kodik_response, quality) else {
log::error!("no playable links found for this video");
return ExitCode::FAILURE;
};
if let Some(player) = &player {
if let Err(e) = spawn_player(player, link) {
log::error!("{e}");
return ExitCode::FAILURE;
}
} else if let Err(e) = writeln!(io::stdout(), "{link}") {
log::error!("{e}");
return ExitCode::FAILURE;
}
}
ExitCode::SUCCESS
}
fn get_link(response: &Response, quality: Quality) -> Option<&str> {
match quality {
Quality::P360 => response
.links
.quality_360
.first()
.map(|link| link.src.as_str()),
Quality::P480 => response
.links
.quality_480
.first()
.map(|link| link.src.as_str()),
Quality::P720 => response
.links
.quality_720
.first()
.map(|link| link.src.as_str()),
}
}
fn spawn_player(player: &str, link: &str) -> Result<(), String> {
let mut parts = player.split_whitespace();
let mut program = parts.next().ok_or("empty player")?;
program = if cfg!(target_os = "windows") && program == "mpv" {
"mpv.com"
} else {
program
};
let mut cmd = Command::new(program);
for arg in parts {
cmd.arg(arg);
}
cmd.arg(link)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.and_then(|mut child| child.wait().map(|_| ()))
.map_err(|e| format!("failed to spawn player '{program}': {e}"))
}