1use clap::{Args, CommandFactory, Parser, Subcommand};
2use serde_json::{Value, json};
3
4use crate::{api, auth, comments, crawler, insights, mcp, obscura, settings, 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 Api(api::ApiArgs),
22 Auth(auth::AuthArgs),
24 Comment(comments::CommentArgs),
26 Insights(insights::InsightsArgs),
28 Mcp,
30 Obscura(ObscuraArgs),
32 Subtitle(subtitles::SubtitleArgs),
34}
35
36#[derive(Debug, Args)]
37struct ObscuraArgs {
38 #[command(subcommand)]
39 command: ObscuraCommand,
40}
41
42#[derive(Debug, Subcommand)]
43enum ObscuraCommand {
44 Manifest,
46 Status {
48 #[arg(long, default_value = "obscura")]
50 binary: String,
51 },
52}
53
54pub fn run() -> Result<(), String> {
55 let cli = Cli::parse();
56 match cli.command {
57 Some(Command::Api(args)) => api::run(args),
58 Some(Command::Auth(args)) => auth::run(args),
59 Some(Command::Comment(args)) => comments::run(args),
60 Some(Command::Insights(args)) => insights::run(args),
61 Some(Command::Mcp) => mcp::run_stdio(),
62 Some(Command::Obscura(args)) => run_obscura(args.command),
63 Some(Command::Subtitle(args)) => subtitles::run(args),
64 None if cli.crawl.should_run() => crawler::run(cli.crawl),
65 None => {
66 Cli::command()
67 .print_long_help()
68 .map_err(|error| error.to_string())?;
69 println!();
70 Ok(())
71 }
72 }
73}
74
75fn run_obscura(command: ObscuraCommand) -> Result<(), String> {
76 match command {
77 ObscuraCommand::Manifest => {
78 print_json(&obscura::manifest(VERSION, &settings::settings_file()))
79 }
80 ObscuraCommand::Status { binary } => {
81 let data = settings::load().map_err(|error| error.to_string())?;
82 let openapi = settings::openapi(&data);
83 let authorized = !string_value(&openapi, "accessToken").is_empty()
84 && !string_value(&openapi, "openId").is_empty();
85 print_json(&json!({
86 "douyin": {
87 "version": VERSION,
88 "entrypoint": "douyin",
89 "configFile": settings::settings_file(),
90 "authorized": authorized
91 },
92 "obscura": obscura::status(&binary),
93 "next": {
94 "auth": ["douyin", "auth", "login"],
95 "machineStatus": ["douyin", "auth", "status", "--json"],
96 "manifest": ["douyin", "obscura", "manifest"]
97 }
98 }))
99 }
100 }
101}
102
103fn string_value<'a>(values: &'a serde_json::Map<String, Value>, key: &str) -> &'a str {
104 values.get(key).and_then(Value::as_str).unwrap_or("")
105}
106
107fn print_json(value: &Value) -> Result<(), String> {
108 println!(
109 "{}",
110 serde_json::to_string_pretty(value).map_err(|error| error.to_string())?
111 );
112 Ok(())
113}