biliget 0.6.9

简单的B站视频下载工具 支持免登录下载B站高清视频
use crate::downloader::download::download_file;
use crate::extract::bilibili::get_download_url;
use crate::extract::bvid::get_bvid_from_url;
use crate::processer::process::{ProcessError, ProcessOption, process};
use crate::progress::cli_bar::CliProgressBar;
use crate::util::path::get_paths;
use crate::util::temp::{add_temp_file, drop_temp_file};
use clap::Parser;
use std::error::Error;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio_util::sync::CancellationToken;

mod cli;
mod downloader;
mod extract;
mod macros;
mod processer;
mod progress;
mod util;

#[tokio::main]
async fn main() {
    let cli = cli::Cli::parse();
    let cancel = CancellationToken::new();

    let cancel_handler = cancel.clone();
    tokio::spawn(async move {
        if tokio::signal::ctrl_c().await.is_ok() {
            println!("清理喵...");
            cancel_handler.cancel();
        }
    });

    tokio::select! {
        _ = cancel.cancelled() => {
            exit_ok();
        }
        res = app(cli, cancel.clone()) => {
            match res {
                Ok(_) => exit_ok(),
                Err(e) => exit_err(e),
            }
        }
    }
}

async fn app(cli: cli::Cli, cancel: CancellationToken) -> Result<(), Box<dyn Error>> {
    let bvid = get_bvid_from_url(&cli.url).ok_or_else(|| "获取bvid失败喵".to_string())?;

    let (video_url, audio_url, mut title, headers) = get_download_url(&bvid).await?;
    if title.is_empty() {
        title = "downloaded_video".to_string();
    }

    println!("\n视频标题: {}", title);
    println!("BVID: {}\n", bvid);

    let (output_file, video_temp_file, audio_temp_file) = get_paths(&title, &cli);
    println!("准备下载到: {}", output_file.display());

    if output_file.try_exists().unwrap_or(false) {
        println!("注意喵!目标文件已存在 继续下载将覆盖同名文件");
    }

    println!("按回车继续喵...");
    wait_for_enter(cancel.clone()).await?;

    println!("开始下载喵...");

    let audio_task = download_file(
        &audio_url,
        &audio_temp_file,
        &headers,
        CliProgressBar::new("下载音频喵..."),
        cancel.clone(),
    );

    if !cli.only_audio {
        add_temp_file(&video_temp_file);
        add_temp_file(&audio_temp_file);

        let video_task = download_file(
            &video_url,
            &video_temp_file,
            &headers,
            CliProgressBar::new("下载视频喵..."),
            cancel.clone(),
        );

        let (v_res, a_res) = tokio::join!(video_task, audio_task);
        v_res?;
        a_res?;
        println!("下载完成喵...");
    } else {
        add_temp_file(&audio_temp_file);
        audio_task.await?;
        println!("下载完成喵...");
    }

    let process_message = if cli.only_audio {
        "音频处理喵..."
    } else {
        "视频合并喵..."
    };
    let process_option = ProcessOption {
        video_file: if cli.only_audio {
            None
        } else {
            Some(&video_temp_file)
        },
        audio_file: Some(&audio_temp_file),
        output_file: &output_file,
        only_audio: cli.only_audio,
    };

    process(
        process_option,
        CliProgressBar::new(process_message),
        cancel.clone(),
    )
    .await
    .map_err(|e| {
        if matches!(e, ProcessError::Cancelled()) && output_file.exists() {
            let _ = std::fs::remove_file(&output_file);
        }
        e
    })?;

    println!("\n下到了: {}", output_file.display());
    Ok(())
}

async fn wait_for_enter(cancel: CancellationToken) -> Result<(), Box<dyn Error>> {
    let mut stdin = BufReader::new(tokio::io::stdin());
    let mut line = String::new();
    tokio::select! {
        _ = stdin.read_line(&mut line) => Ok(()),
        _ = cancel.cancelled() => Err("取消操作喵".into()),
    }
}

fn exit_ok() {
    drop_temp_file();
    println!("拜拜喵");
    std::process::exit(0);
}

fn exit_err(err: Box<dyn Error>) {
    drop_temp_file();
    eprintln!("{}", err);
    std::process::exit(1);
}