blowup 1.0.0

All tools related to improving my movie-watching experience.
Documentation
use blowup::sub::{align, fetch, shift};
use blowup::{config, config_cmd, download, tmdb, search, tracker};
use clap::{Parser, Subcommand};
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "blowup", about = "中文观影自动化流水线工具")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "搜索电影片源(优先 YIFY)")]
    Search {
        query: String,
        #[arg(long)]
        year: Option<u32>,
    },
    #[command(about = "通过 aria2c 下载种子/magnet")]
    Download {
        target: String,
        #[arg(long, default_value = ".")]
        output_dir: PathBuf,
    },
    #[command(subcommand, about = "字幕相关工具")]
    Sub(SubCommands),
    #[command(subcommand, about = "Tracker 列表管理")]
    Tracker(TrackerCommands),
    #[command(about = "通过 TMDB API 查询电影信息")]
    Info {
        query: String,
        #[arg(long)]
        year: Option<u32>,
    },
    #[command(subcommand, about = "管理 blowup 配置")]
    Config(ConfigCommands),
}

#[derive(Subcommand)]
enum SubCommands {
    #[command(about = "从 Assrt/OpenSubtitles 下载字幕")]
    Fetch {
        video: PathBuf,
        #[arg(long, default_value = "zh")]
        lang: String,
    },
    #[command(about = "用 alass 自动对齐字幕")]
    Align { video: PathBuf, srt: PathBuf },
    #[command(about = "从视频容器提取内嵌字幕流")]
    Extract {
        video: PathBuf,
        #[arg(long)]
        stream: Option<u32>,
    },
    #[command(about = "列出视频中的字幕流")]
    List { video: PathBuf },
    #[command(about = "手动偏移字幕时间戳(毫秒)")]
    Shift { srt: PathBuf, offset_ms: i64 },
}

#[derive(Subcommand)]
enum ConfigCommands {
    #[command(about = "设置配置项 (格式: section.field value)")]
    Set { key: String, value: String },
    #[command(about = "读取配置项 (格式: section.field)")]
    Get { key: String },
    #[command(about = "列出所有配置项")]
    List,
}

#[derive(Subcommand)]
enum TrackerCommands {
    #[command(about = "从远程源更新本地 tracker 列表")]
    Update {
        #[arg(long)]
        source: Option<String>,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let cfg = config::load_config();

    match cli.command {
        Commands::Search { query, year } => {
            let results = search::search_yify(&query, year).await?;
            for (i, r) in results.iter().enumerate() {
                println!(
                    "{}: {} ({}) [{}] seeds={}",
                    i + 1,
                    r.title,
                    r.year,
                    r.quality,
                    r.seeds
                );
                if let Some(m) = &r.magnet {
                    println!("   magnet: {}", m);
                }
                if let Some(u) = &r.torrent_url {
                    println!("   torrent: {}", u);
                }
            }
        }
        Commands::Download { target, output_dir } => {
            download::download(download::DownloadArgs {
                target: &target,
                output_dir: &output_dir,
                aria2c_bin: &cfg.tools.aria2c,
            })
            .await?;
        }
        Commands::Sub(sub_cmd) => match sub_cmd {
            SubCommands::Fetch { video, lang } => {
                fetch::fetch_subtitle(&video, &lang, fetch::SubSource::All, &cfg).await?;
            }
            SubCommands::Align { video, srt } => {
                align::align_subtitle(&video, &srt, Some(&cfg.tools.alass))?;
            }
            SubCommands::Extract { video, stream } => {
                blowup::sub::extract_sub_srt(&video, stream).await?;
            }
            SubCommands::List { video } => {
                blowup::sub::list_all_subtitle_stream(&video).await?;
            }
            SubCommands::Shift { srt, offset_ms } => {
                shift::shift_srt(&srt, offset_ms)?;
            }
        },
        Commands::Tracker(TrackerCommands::Update { source }) => {
            tracker::update_tracker_list(source).await?;
        }
        Commands::Info { query, year } => {
            let api_key = &cfg.tmdb.api_key;
            let movie = tmdb::query_tmdb(api_key, &query, year).await?;
            movie.print_info();
        }
        Commands::Config(config_cmd_args) => match config_cmd_args {
            ConfigCommands::Set { key, value } => {
                config_cmd::set_config(&key, &value)?;
            }
            ConfigCommands::Get { key } => {
                config_cmd::get_config(&key)?;
            }
            ConfigCommands::List => {
                config_cmd::list_config()?;
            }
        },
    }
    Ok(())
}