Skip to main content

douyin_cli/
cli.rs

1use clap::{Args, CommandFactory, Parser, Subcommand};
2use serde_json::{Value, json};
3
4use crate::{api, auth, comments, crawler, insights, mcp, obscura, settings, stats, subtitles};
5
6const VERSION: &str = env!("CARGO_PKG_VERSION");
7
8#[derive(Debug, Parser)]
9#[command(name = "douyin", version, about = "抖音 CLI(Rust)")]
10#[command(long_about = "通用抖音命令行工具的 Rust 实现。更多命令见:douyin COMMAND --help")]
11struct Cli {
12    #[command(flatten)]
13    crawl: crawler::CrawlArgs,
14    #[command(subcommand)]
15    command: Option<Command>,
16}
17
18#[derive(Debug, Subcommand)]
19enum Command {
20    /// 调用抖音开放平台官方 OpenAPI
21    Api(api::ApiArgs),
22    /// 管理授权
23    Auth(auth::AuthArgs),
24    /// 抓取作品评论区
25    Comment(comments::CommentArgs),
26    /// 离线分析热词、热梗与需求发现
27    Insights(insights::InsightsArgs),
28    /// 通过 stdio 启动抖音 MCP 服务器
29    Mcp,
30    /// Obscura 集成辅助命令
31    Obscura(ObscuraArgs),
32    /// 离线统计采集作品表现,不分析媒体画面/声音
33    Stats(stats::StatsArgs),
34    /// 从本地视频/音频生成字幕
35    Subtitle(subtitles::SubtitleArgs),
36}
37
38#[derive(Debug, Args)]
39struct ObscuraArgs {
40    #[command(subcommand)]
41    command: ObscuraCommand,
42}
43
44#[derive(Debug, Subcommand)]
45enum ObscuraCommand {
46    /// 输出 Obscura 集成 manifest
47    Manifest,
48    /// 检查本地 Obscura 集成状态
49    Status {
50        /// Obscura 可执行文件名
51        #[arg(long, default_value = "obscura")]
52        binary: String,
53    },
54}
55
56pub fn run() -> Result<(), String> {
57    let cli = Cli::parse();
58    match cli.command {
59        Some(Command::Api(args)) => api::run(args),
60        Some(Command::Auth(args)) => auth::run(args),
61        Some(Command::Comment(args)) => comments::run(args),
62        Some(Command::Insights(args)) => insights::run(args),
63        Some(Command::Mcp) => mcp::run_stdio(),
64        Some(Command::Obscura(args)) => run_obscura(args.command),
65        Some(Command::Stats(args)) => stats::run(args),
66        Some(Command::Subtitle(args)) => subtitles::run(args),
67        None if cli.crawl.should_run() => crawler::run(cli.crawl),
68        None => {
69            Cli::command()
70                .print_long_help()
71                .map_err(|error| error.to_string())?;
72            println!();
73            Ok(())
74        }
75    }
76}
77
78fn run_obscura(command: ObscuraCommand) -> Result<(), String> {
79    match command {
80        ObscuraCommand::Manifest => {
81            print_json(&obscura::manifest(VERSION, &settings::settings_file()))
82        }
83        ObscuraCommand::Status { binary } => {
84            let data = settings::load().map_err(|error| error.to_string())?;
85            let openapi = settings::openapi(&data);
86            let authorized = !string_value(&openapi, "accessToken").is_empty()
87                && !string_value(&openapi, "openId").is_empty();
88            print_json(&json!({
89                "douyin": {
90                    "version": VERSION,
91                    "entrypoint": "douyin",
92                    "configFile": settings::settings_file(),
93                    "authorized": authorized
94                },
95                "obscura": obscura::status(&binary),
96                "next": {
97                    "auth": ["douyin", "auth", "login"],
98                    "machineStatus": ["douyin", "auth", "status", "--json"],
99                    "manifest": ["douyin", "obscura", "manifest"]
100                }
101            }))
102        }
103    }
104}
105
106fn string_value<'a>(values: &'a serde_json::Map<String, Value>, key: &str) -> &'a str {
107    values.get(key).and_then(Value::as_str).unwrap_or("")
108}
109
110fn print_json(value: &Value) -> Result<(), String> {
111    println!(
112        "{}",
113        serde_json::to_string_pretty(value).map_err(|error| error.to_string())?
114    );
115    Ok(())
116}