From 697a525b55a63bac7fdb7010a7d2c60bd5ad2e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=B9=E7=A8=8B?= <1787003204@qq.com> Date: Mon, 25 May 2026 14:38:50 +0800 Subject: [PATCH] Add comment listing and reply commands (#1628) --- crates/biliup-cli/src/cli.rs | 32 ++++++ crates/biliup-cli/src/main.rs | 23 ++++- crates/biliup-cli/src/uploader.rs | 50 ++++++++++ crates/biliup/src/uploader/bilibili.rs | 129 +++++++++++++++++++++++++ 4 files changed, 233 insertions(+), 1 deletion(-) diff --git a/crates/biliup-cli/src/cli.rs b/crates/biliup-cli/src/cli.rs index 6a78edd..cf625f3 100644 --- a/crates/biliup-cli/src/cli.rs +++ b/crates/biliup-cli/src/cli.rs @@ -103,6 +103,38 @@ pub enum Commands { // #[clap()] vid: Vid, }, + /// 查看视频评论 + Comments { + /// vid为稿件 av 或 bv 号 + vid: Vid, + + /// 排序方式,0为按时间,2为按热度 + #[arg(long, default_value = "0")] + sort: u8, + + /// 页码 + #[arg(long, default_value = "1")] + pn: u32, + + /// 每页条数 + #[arg(long, default_value = "20")] + ps: u32, + }, + /// 回复视频评论,默认只打印将要回复的内容 + Reply { + /// vid为稿件 av 或 bv 号 + vid: Vid, + + /// 评论 rpid + rpid: u64, + + /// 回复内容 + message: String, + + /// 实际发送回复 + #[arg(long)] + execute: bool, + }, /// 输出flv元数据 DumpFlv { #[arg()] diff --git a/crates/biliup-cli/src/main.rs b/crates/biliup-cli/src/main.rs index e9e12c5..096893b 100644 --- a/crates/biliup-cli/src/main.rs +++ b/crates/biliup-cli/src/main.rs @@ -3,7 +3,9 @@ use time::macros::format_description; use biliup::uploader::util::SubmitOption; use biliup_cli::cli::{Cli, Commands, expand_path}; use biliup_cli::downloader::{download, generate_json}; -use biliup_cli::uploader::{append, list, login, renew, show, upload_by_command, upload_by_config}; +use biliup_cli::uploader::{ + append, comments, list, login, renew, reply, show, upload_by_command, upload_by_config, +}; use clap::Parser; @@ -99,6 +101,25 @@ async fn main() -> AppResult<()> { .await? } Commands::Show { vid } => show(user_cookie, vid, cli.proxy.as_deref()).await?, + Commands::Comments { vid, sort, pn, ps } => { + comments(user_cookie, vid, sort, pn, ps, cli.proxy.as_deref()).await? + } + Commands::Reply { + vid, + rpid, + message, + execute, + } => { + reply( + user_cookie, + vid, + rpid, + message, + execute, + cli.proxy.as_deref(), + ) + .await? + } Commands::DumpFlv { file_name } => { let file_name = expand_path(file_name); generate_json(file_name)? diff --git a/crates/biliup-cli/src/uploader.rs b/crates/biliup-cli/src/uploader.rs index a0d4d4c..9b5db45 100644 --- a/crates/biliup-cli/src/uploader.rs +++ b/crates/biliup-cli/src/uploader.rs @@ -263,6 +263,56 @@ pub async fn show(user_cookie: PathBuf, vid: Vid, proxy: Option<&str>) -> AppRes Ok(()) } +pub async fn comments( + user_cookie: PathBuf, + vid: Vid, + sort: u8, + pn: u32, + ps: u32, + proxy: Option<&str>, +) -> AppResult<()> { + let bilibili = login_by_cookies(user_cookie, proxy).await?; + let reply_list = bilibili + .comments(&vid, sort, pn, ps, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?; + + for reply in reply_list.replies.unwrap_or_default() { + println!("rpid={} uname={}", reply.rpid, reply.member.uname); + println!("{}", reply.content.message); + println!(); + } + + Ok(()) +} + +pub async fn reply( + user_cookie: PathBuf, + vid: Vid, + rpid: u64, + message: String, + execute: bool, + proxy: Option<&str>, +) -> AppResult<()> { + if !execute { + println!("dry-run: reply to {vid} rpid={rpid}"); + println!("{message}"); + println!("use --execute to send"); + return Ok(()); + } + + let bilibili = login_by_cookies(user_cookie, proxy).await?; + let ret = bilibili + .reply_comment(&vid, rpid, &message, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?; + println!( + "{}", + serde_json::to_string_pretty(&ret).change_context_lazy(|| AppError::Unknown)? + ); + Ok(()) +} + pub async fn list( user_cookie: PathBuf, is_pubing: bool, diff --git a/crates/biliup/src/uploader/bilibili.rs b/crates/biliup/src/uploader/bilibili.rs index b86099a..67b5a43 100644 --- a/crates/biliup/src/uploader/bilibili.rs +++ b/crates/biliup/src/uploader/bilibili.rs @@ -258,6 +258,40 @@ impl Display for Vid { } } +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ReplyMember { + pub uname: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ReplyContent { + pub message: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Reply { + pub rpid: u64, + pub mid: u64, + pub member: ReplyMember, + pub content: ReplyContent, + pub ctime: u64, +} + +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +pub struct ReplyPage { + pub count: Option, + pub num: Option, + pub size: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +pub struct ReplyList { + #[serde(default)] + pub page: ReplyPage, + #[serde(default)] + pub replies: Option>, +} + #[derive(Clone, Debug)] pub struct BiliBili { pub client: reqwest::Client, @@ -549,6 +583,101 @@ impl BiliBili { .await?) } + async fn aid_from_vid(&self, vid: &Vid) -> Result { + match vid { + Vid::Aid(aid) => Ok(*aid), + Vid::Bvid(bvid) => { + let res: ResponseData = self + .client + .get("https://api.bilibili.com/x/web-interface/view") + .query(&[("bvid", bvid)]) + .send() + .await? + .json() + .await?; + + match res { + ResponseData { + code: 0, + data: Some(data), + .. + } => data["aid"].as_u64().ok_or("aid error".into()), + res => Err(Kind::Custom(format!("{res:?}"))), + } + } + } + } + + /// 获取视频评论列表 + pub async fn comments( + &self, + vid: &Vid, + sort: u8, + pn: u32, + ps: u32, + _proxy: Option<&str>, + ) -> Result { + let oid = self.aid_from_vid(vid).await?; + let res: ResponseData = self + .client + .get("https://api.bilibili.com/x/v2/reply") + .query(&[ + ("type", "1".to_string()), + ("oid", oid.to_string()), + ("sort", sort.to_string()), + ("pn", pn.to_string()), + ("ps", ps.to_string()), + ]) + .send() + .await? + .json() + .await?; + + match res { + ResponseData { + code: 0, + data: Some(data), + .. + } => Ok(data), + res => Err(Kind::Custom(format!("{res:?}"))), + } + } + + /// 回复视频评论 + pub async fn reply_comment( + &self, + vid: &Vid, + rpid: u64, + message: &str, + _proxy: Option<&str>, + ) -> Result { + let oid = self.aid_from_vid(vid).await?; + let res: ResponseData = self + .client + .post("https://api.bilibili.com/x/v2/reply/add") + .form(&[ + ("type", "1".to_string()), + ("oid", oid.to_string()), + ("root", rpid.to_string()), + ("parent", rpid.to_string()), + ("message", message.to_string()), + ("csrf", self.get_csrf()?.to_string()), + ]) + .send() + .await? + .json() + .await?; + + match res { + ResponseData { + code: 0, + data: Some(data), + .. + } => Ok(data), + res => Err(Kind::Custom(format!("{res:?}"))), + } + } + pub async fn archive_pre(&self) -> Result { Ok(self .client