From 838ace7896f517558816091a8108c8bd337a0978 Mon Sep 17 00:00:00 2001 From: Chikage Date: Sun, 15 Oct 2023 14:45:57 +0900 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90qbittorrent=E6=9C=BA=E5=99=A8?= =?UTF-8?q?=E4=BA=BA=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 26 ++ matrix_bot/Cargo.toml | 3 +- matrix_bot/src/main.rs | 15 +- matrix_bot_core/src/lib.rs | 4 +- matrix_bot_core/src/matrix/client.rs | 2 +- matrix_bot_core/src/matrix/room.rs | 48 +++- plugins/qbittorrent/Cargo.toml | 9 +- plugins/qbittorrent/src/lib.rs | 59 ++++- plugins/qbittorrent/src/matrix.rs | 132 ++++++++++ plugins/qbittorrent/src/qbit/binary.rs | 5 +- plugins/qbittorrent/src/qbit/mod.rs | 2 + plugins/qbittorrent/src/qbit/ops.rs | 314 +++++++++++++++++++++++ plugins/qbittorrent/src/setting.rs | 90 ++++--- plugins/qbittorrent/src/upload/gofile.rs | 135 ++++++++++ plugins/qbittorrent/src/upload/mod.rs | 1 + 15 files changed, 781 insertions(+), 64 deletions(-) create mode 100644 plugins/qbittorrent/src/matrix.rs create mode 100644 plugins/qbittorrent/src/qbit/ops.rs create mode 100644 plugins/qbittorrent/src/upload/gofile.rs create mode 100644 plugins/qbittorrent/src/upload/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 59be3ff..14b0d52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1867,6 +1867,7 @@ dependencies = [ "env_logger", "log", "matrix_bot_core", + "qbittorrent", "tokio", "webhook", "yande_popular", @@ -2399,11 +2400,16 @@ dependencies = [ "env_logger", "log", "matrix_bot_core", + "once_cell", "qbit-rs", + "regex", "reqwest", "serde", + "serde_json", "tokio", "toml 0.8.2", + "url", + "walkdir", ] [[package]] @@ -2582,6 +2588,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -2740,6 +2747,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.22" @@ -3560,6 +3576,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" diff --git a/matrix_bot/Cargo.toml b/matrix_bot/Cargo.toml index f1322f0..86cde94 100644 --- a/matrix_bot/Cargo.toml +++ b/matrix_bot/Cargo.toml @@ -20,7 +20,8 @@ clap = { version = "4.4.6", features = ["derive", "env"] } # Optional dependencies yande_popular = { path = "../plugins/yande_popular", optional = true } webhook = { path = "../plugins/webhook", optional = true } +qbittorrent = { path = "../plugins/qbittorrent", optional = true } [features] default = ["full"] -full = ["yande_popular", "webhook"] +full = ["yande_popular", "webhook", "qbittorrent"] diff --git a/matrix_bot/src/main.rs b/matrix_bot/src/main.rs index 344d8c7..29b13a6 100644 --- a/matrix_bot/src/main.rs +++ b/matrix_bot/src/main.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::Parser; use matrix_bot_core::{ matrix::{self, client::Client}, - sdk::SyncSettings, + matrix_sdk::config::SyncSettings, }; #[derive(Parser, Debug)] @@ -106,5 +106,18 @@ pub fn load_plugins(client: &Client, settings_folder: impl AsRef) -> Resul }); }); }; + + #[cfg(feature = "qbittorrent")] + { + let client = client.clone(); + let settings_folder = settings_folder.as_ref().to_path_buf(); + tokio::spawn(async move { + qbittorrent::run(client, settings_folder) + .await + .unwrap_or_else(|e| { + log::error!("qbittorrent stop: {}", e); + }); + }); + }; Ok(()) } diff --git a/matrix_bot_core/src/lib.rs b/matrix_bot_core/src/lib.rs index fbe4f2a..6c64558 100644 --- a/matrix_bot_core/src/lib.rs +++ b/matrix_bot_core/src/lib.rs @@ -1,4 +1,2 @@ pub mod matrix; -pub mod sdk { - pub use matrix_sdk::config::SyncSettings; -} +pub use matrix_sdk; diff --git a/matrix_bot_core/src/matrix/client.rs b/matrix_bot_core/src/matrix/client.rs index e0ac7eb..039ad4f 100644 --- a/matrix_bot_core/src/matrix/client.rs +++ b/matrix_bot_core/src/matrix/client.rs @@ -30,7 +30,7 @@ impl Client { let homeserver_url = Url::parse(homeserver_url).expect("Couldn't parse the homeserver URL"); std::fs::create_dir_all(&db_path)?; - std::fs::create_dir_all(&session_file.as_ref().parent().unwrap())?; + std::fs::create_dir_all(session_file.as_ref().parent().unwrap())?; let mut client = matrix_sdk::Client::builder() .homeserver_url(&homeserver_url) diff --git a/matrix_bot_core/src/matrix/room.rs b/matrix_bot_core/src/matrix/room.rs index fcd34b4..15480aa 100644 --- a/matrix_bot_core/src/matrix/room.rs +++ b/matrix_bot_core/src/matrix/room.rs @@ -27,18 +27,16 @@ impl Room { if let Some(room) = room { Ok(Room(room)) + } else if let Some(room) = client.get_invited_room(room_id.try_into()?) { + room.accept_invitation() + .await + .map_err(|e| anyhow!("Can't accept invitation:{e}"))?; + let room = client + .get_joined_room(room_id.try_into()?) + .ok_or(anyhow!("Can't find room {}", room_id))?; + Ok(Room(room)) } else { - if let Some(room) = client.get_invited_room(room_id.try_into()?) { - room.accept_invitation() - .await - .map_err(|e| anyhow!("Can't accept invitation:{e}"))?; - let room = client - .get_joined_room(room_id.try_into()?) - .ok_or(anyhow!("Can't find room {}", room_id))?; - Ok(Room(room)) - } else { - Err(anyhow!("Can't find room {}", room_id)) - } + Err(anyhow!("Can't find room {}", room_id)) } } @@ -95,10 +93,32 @@ impl Room { Ok(()) } + pub async fn send_html(&self, msg: &str, html_msg: &str) -> Result<()> { + let msg = RoomMessageEventContent::text_html(msg, html_msg); + + self.0.send(msg, None).await?; + + Ok(()) + } + + pub async fn send_relates_html(&self, msg: &str, html_msg: &str, event_id: &str) -> Result<()> { + let msg = RoomMessageEventContent::text_html(msg, html_msg); + + let event_id = OwnedEventId::try_from(event_id)?; + let timeline_event = self.0.event(&event_id).await?; + let event_content = timeline_event.event.deserialize_as::()?; + let original_message = event_content.as_original().unwrap(); + let msg = msg.make_reply_to(original_message); + + self.0.send(msg, None).await?; + + Ok(()) + } + pub async fn send_relates_msg( &self, msg: &str, - event_id: &OwnedEventId, + event_id: &str, is_markdown: bool, ) -> Result<()> { let msg = if is_markdown { @@ -106,7 +126,9 @@ impl Room { } else { RoomMessageEventContent::text_plain(msg) }; - let timeline_event = self.0.event(event_id).await?; + + let event_id = OwnedEventId::try_from(event_id)?; + let timeline_event = self.0.event(&event_id).await?; let event_content = timeline_event.event.deserialize_as::()?; let original_message = event_content.as_original().unwrap(); let msg = msg.make_reply_to(original_message); diff --git a/plugins/qbittorrent/Cargo.toml b/plugins/qbittorrent/Cargo.toml index 90070d6..1a480aa 100644 --- a/plugins/qbittorrent/Cargo.toml +++ b/plugins/qbittorrent/Cargo.toml @@ -10,9 +10,14 @@ matrix_bot_core = { path = "../../matrix_bot_core" } log = "0.4.14" toml = "0.8.2" serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1.33.0", default-features = false, features = [] } +tokio = { version = "1.33.0", default-features = false, features = ["macros"] } qbit-rs = { version = "0.3.7" } -reqwest = { version = "0.11.6", features = ["blocking"] } +reqwest = { version = "0.11.6", features = ["blocking", "multipart", "json"] } +regex = "1.10.0" +once_cell = "1.8.0" +url = "2.4.1" +serde_json = "1.0.68" +walkdir = "2.4.0" [dev-dependencies] env_logger = "0.10.0" diff --git a/plugins/qbittorrent/src/lib.rs b/plugins/qbittorrent/src/lib.rs index 8cea2c2..5599aca 100644 --- a/plugins/qbittorrent/src/lib.rs +++ b/plugins/qbittorrent/src/lib.rs @@ -1,16 +1,67 @@ +use std::{collections::HashMap, sync::OnceLock}; + use anyhow::Result; -use matrix_bot_core::matrix::client::Client; +use matrix_bot_core::matrix::{client::Client, room::Room}; +use qbit_rs::Qbit; +use setting::RoomSetting; + +use crate::{ + qbit::ops::{expire_torrents, upload_torrents}, + setting::Setting, +}; +mod matrix; mod qbit; mod setting; +mod upload; + +static ROOM_MAP: OnceLock> = OnceLock::new(); +static API: OnceLock = OnceLock::new(); #[allow(unused_variables)] pub async fn run(client: Client, plugin_folder: impl AsRef) -> Result<()> { log::info!("start yande_popular"); - let setting = setting::get_or_init(plugin_folder)?; + let setting = Setting::get_or_init(plugin_folder)?; + + #[cfg(target_os = "linux")] + let _child: std::process::Child; + #[cfg(target_os = "linux")] + if setting.use_internal_qbit { + let runtime_folder = PathBuf::from("data/plugins/qbittorrent/runtime"); + let port = setting.qbit_url.split(":").last(); + let port = port.unwrap_or("80").parse().unwrap_or(80); + _child = qbit::binary::run(&runtime_folder, port)?; + } + + let room = setting.to_hashmap(&client).await?; + ROOM_MAP + .set(room) + .map_err(|_| anyhow::anyhow!("ROOM_MAP OnceLock double set"))?; + + let api = qbit::ops::login(&setting.qbit_user, &setting.qbit_pass, &setting.qbit_url).await?; + API.set(api) + .map_err(|_| anyhow::anyhow!("API OnceLock double set"))?; + + for (room, setting) in ROOM_MAP.get().unwrap().values() { + matrix::add_listener(room); + } + + loop { + let (expire, upload) = qbit::ops::scan_torrent(API.get().unwrap()).await?; + + expire_torrents(API.get().unwrap(), &expire) + .await + .unwrap_or_else(|e| { + log::error!("expire torrent failed: {}", e); + }); - + upload_torrents(API.get().unwrap(), &upload) + .await + .unwrap_or_else(|e| { + log::error!("upload torrent failed: {}", e); + }); - Ok(()) + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + } } diff --git a/plugins/qbittorrent/src/matrix.rs b/plugins/qbittorrent/src/matrix.rs new file mode 100644 index 0000000..07aed37 --- /dev/null +++ b/plugins/qbittorrent/src/matrix.rs @@ -0,0 +1,132 @@ +use matrix_bot_core::{ + matrix, + matrix_sdk::{ + self, + ruma::events::room::message::{ + MessageType, OriginalSyncRoomMessageEvent, Relation, TextMessageEventContent, + }, + }, +}; + +use crate::{ + qbit::ops::{add_torrent, show_status}, + API, ROOM_MAP, +}; + +pub fn add_listener(room: &matrix::room::Room) { + room.0.add_event_handler( + |event: OriginalSyncRoomMessageEvent, room: matrix_sdk::room::Room| async move { + if let matrix_sdk::room::Room::Joined(room) = room { + let msg_body = match event.content.msgtype { + MessageType::Text(TextMessageEventContent { body, .. }) => body, + _ => return, + }; + + let msg_body = msg_body.trim(); + + if msg_body.is_empty() { + return; + } + if msg_body.starts_with("!download") { + let link = msg_body + .trim_start_matches("!download") + .trim() + .split_ascii_whitespace() + .next() + .unwrap_or_default(); + + let room_id = room.room_id().as_str(); + + let map = ROOM_MAP.get().and_then(|map| map.get(room_id)); + + if let Some((room, setting)) = map { + let result = add_torrent( + API.get().unwrap(), + link, + setting.download_path.as_path(), + room_id, + event.event_id.as_str(), + ) + .await; + + match result { + Ok(_) => { + room.send_relates_msg("添加成功", event.event_id.as_str(), false) + .await + .unwrap_or_else(|e| { + log::error!("send message failed: {}", e); + }); + } + Err(e) => { + room.send_relates_msg( + &format!("添加失败: {}", e), + event.event_id.as_str(), + false, + ) + .await + .unwrap_or_else(|e| { + log::error!("send message failed: {}", e); + }); + } + } + } + } + } + }, + ); + + room.0.add_event_handler( + |event: OriginalSyncRoomMessageEvent, room: matrix_sdk::room::Room| async move { + if let matrix_sdk::room::Room::Joined(room) = room { + let msg_body = match event.content.msgtype { + MessageType::Text(TextMessageEventContent { body, .. }) => body, + _ => return, + }; + + let msg_body = msg_body.trim(); + + if msg_body.is_empty() { + return; + } + if msg_body.starts_with("!status") { + let room_id = room.room_id().as_str(); + + let map = ROOM_MAP.get().and_then(|map| map.get(room_id)); + + let reply_event_id = event.content.relates_to.as_ref().and_then(|r| { + if let Relation::Reply { in_reply_to } = r { + let event_id = in_reply_to.event_id.as_str(); + Some(event_id.to_string()) + } else { + None + } + }); + + if let Some((room, _setting)) = map { + let msg = show_status(API.get().unwrap(), room_id, reply_event_id).await; + match msg { + Ok((msg, html_msg)) => { + room.send_relates_html(&msg, &html_msg, event.event_id.as_str()) + .await + .unwrap_or_else(|e| { + log::error!("send message failed: {}", e); + }); + } + Err(e) => { + room.send_relates_msg( + &format!("获取失败: {}", e), + event.event_id.as_str(), + false, + ) + .await + .unwrap_or_else(|e| { + log::error!("send message failed: {}", e); + }); + } + } + } + } + } + }, + ); +} diff --git a/plugins/qbittorrent/src/qbit/binary.rs b/plugins/qbittorrent/src/qbit/binary.rs index 9a074b0..09c7f41 100644 --- a/plugins/qbittorrent/src/qbit/binary.rs +++ b/plugins/qbittorrent/src/qbit/binary.rs @@ -5,7 +5,6 @@ use std::{ process::{Child, Stdio}, }; -#[allow(dead_code)] fn get_download_link() -> Result { if cfg!(target_os = "linux") != true { return Err(anyhow::anyhow!("only support linux")); @@ -24,7 +23,7 @@ fn get_download_link() -> Result { } Ok(link) } -#[allow(dead_code)] + fn download_binary(path: impl AsRef) -> Result<()> { let link = get_download_link()?; let resp = reqwest::blocking::get(link)?; @@ -38,7 +37,7 @@ fn download_binary(path: impl AsRef) -> Result<()> { } Ok(()) } -#[allow(dead_code)] + pub fn run(runtime_folder: impl AsRef, port: u16) -> Result { let binary_path = runtime_folder.as_ref().join("qbittorrent-nox"); if !binary_path.exists() { diff --git a/plugins/qbittorrent/src/qbit/mod.rs b/plugins/qbittorrent/src/qbit/mod.rs index 4150e4d..b3e78cc 100644 --- a/plugins/qbittorrent/src/qbit/mod.rs +++ b/plugins/qbittorrent/src/qbit/mod.rs @@ -1 +1,3 @@ +#[cfg(target_os = "linux")] pub mod binary; +pub mod ops; diff --git a/plugins/qbittorrent/src/qbit/ops.rs b/plugins/qbittorrent/src/qbit/ops.rs new file mode 100644 index 0000000..950cf39 --- /dev/null +++ b/plugins/qbittorrent/src/qbit/ops.rs @@ -0,0 +1,314 @@ +use std::{collections::HashMap, path::Path}; + +use anyhow::{Ok, Result}; +use once_cell::sync::Lazy; +use qbit_rs::{ + model::{Credential, GetTorrentListArg, State}, + Qbit, +}; +use regex::Regex; + +use crate::{upload, ROOM_MAP}; + +static MAGNET_REGEX: Lazy = + Lazy::new(|| Regex::new(r"^(magnet:[\?xt\=\w\:\&\;\+\%.]+)").unwrap()); + +pub async fn login(user: &str, pass: &str, url: &str) -> Result { + let credential = Credential::new(user, pass); + let api = Qbit::new(url, credential); + api.login(false).await?; + + Ok(api) +} + +// 如果是磁力链接,直接添加,并且如果连接中有名称,成功添加后返回名称 +pub async fn add_torrent( + api: &Qbit, + magnet: &str, + save_path: impl AsRef, + room: &str, + event: &str, +) -> Result<()> { + if !MAGNET_REGEX.is_match(magnet) { + return Err(anyhow::anyhow!("invalid magnet url")); + } + + let url = url::Url::parse(magnet)?; + let xt = url + .query_pairs() + .find(|(k, _)| k == "xt") + .map(|(_, v)| v.to_string()); + + // 检测是否已经添加过 + let torrents = api.get_torrent_list(GetTorrentListArg::default()).await?; + for torrent in torrents { + if let Some(torrent_url) = torrent.magnet_uri { + let torrent_url = url::Url::parse(&torrent_url)?; + let torrent_xt = torrent_url + .query_pairs() + .find(|(k, _)| k == "xt") + .map(|(_, v)| v.to_string()); + if xt == torrent_xt { + return Err(anyhow::anyhow!("torrent already exists")); + } + } + } + + let url = qbit_rs::model::TorrentSource::Urls { + urls: vec![url].into(), + }; + + let save_path = if save_path.as_ref().is_absolute() { + save_path.as_ref().to_string_lossy().to_string() + } else { + std::env::current_dir()? + .join(save_path.as_ref()) + .to_string_lossy() + .to_string() + }; + + let arg = qbit_rs::model::AddTorrentArg { + source: url, + savepath: Some(save_path), + tags: Some(event.to_string()), + category: Some(room.to_string()), + ..Default::default() + }; + + api.add_torrent(arg).await?; + + Ok(()) +} + +type ExpireTorrents = HashMap; +type UploadTorrents = HashMap; +pub async fn scan_torrent(api: &Qbit) -> Result<(ExpireTorrents, UploadTorrents)> { + let torrents = api.get_torrent_list(GetTorrentListArg::default()).await?; + let now_timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let mut expire_torrents = HashMap::new(); + let mut upload_torrents = HashMap::new(); + for torrent in torrents { + if now_timestamp as i64 - torrent.added_on.unwrap_or(0) > 60 * 60 * 24 * 7 + && !matches!( + &torrent.state, + Some(qbit_rs::model::State::PausedUP) + | Some(qbit_rs::model::State::StalledUP) + | Some(qbit_rs::model::State::Uploading) + ) + && torrent.hash.is_some() + && torrent + .category + .as_ref() + .is_some_and(|c| c.starts_with('!')) + { + expire_torrents.insert(torrent.hash.as_ref().unwrap().clone(), torrent.clone()); + } + + if matches!( + &torrent.state, + Some(qbit_rs::model::State::PausedUP) + | Some(qbit_rs::model::State::StalledUP) + | Some(qbit_rs::model::State::Uploading) + ) && torrent + .category + .as_ref() + .is_some_and(|c| c.starts_with('!')) + { + if let Some(hash) = &torrent.hash { + upload_torrents.insert(hash.clone(), torrent.clone()); + } + } + } + + Ok((expire_torrents, upload_torrents)) +} + +pub async fn expire_torrents(api: &Qbit, torrents: &ExpireTorrents) -> Result<()> { + for (hash, torrent) in torrents { + let event_id = torrent.tags.as_ref(); + let room_id = torrent.category.as_ref(); + + api.delete_torrents(vec![hash.clone()], true).await?; + if room_id.is_some() { + let room_id = room_id.unwrap(); + + let room = ROOM_MAP.get().unwrap().get(room_id); + + if let Some((room, _)) = room { + if let Some(name) = torrent.name.as_ref() { + let msg = format!("文件名:{} \n下载超时,已删除。", name); + match event_id { + Some(event_id) => { + room.send_relates_msg(&msg, event_id, true).await?; + api.delete_tags(vec![event_id.clone()]).await?; + } + None => { + room.send_msg(&msg, true).await?; + } + } + } else { + let msg = "下载超时,已删除。".to_string(); + match event_id { + Some(event_id) => { + room.send_relates_msg(&msg, event_id, false).await?; + api.delete_tags(vec![event_id.clone()]).await?; + } + None => { + room.send_msg(&msg, false).await?; + } + } + } + } + } + log::info!("delete torrent: {}", &hash); + } + + Ok(()) +} + +pub async fn upload_torrents(api: &Qbit, torrents: &UploadTorrents) -> Result<()> { + for (hash, torrent) in torrents { + log::info!("upload torrent: {}", &hash); + let event_id = torrent.tags.as_ref(); + let room_id = torrent.category.as_ref(); + + let file_path = torrent.content_path.as_ref().unwrap().clone(); + let file_path = std::path::Path::new(&file_path); + + let download_page = upload::gofile::upload(file_path).await?; + if event_id.is_some() && room_id.is_some() { + let room_id = room_id.unwrap(); + + let room = ROOM_MAP.get().unwrap().get(room_id); + + if let Some((room, _)) = room { + if let Some(name) = torrent.name.as_ref() { + let msg = format!( + "文件名:{} \n下载完成,[点击下载]({})。", + name, download_page + ); + match event_id { + Some(event_id) => { + room.send_relates_msg(&msg, event_id, true).await?; + api.delete_tags(vec![event_id.clone()]).await?; + } + None => { + room.send_msg(&msg, true).await?; + } + } + } else { + let msg = format!("下载完成,[点击下载]({})。", download_page); + match event_id { + Some(event_id) => { + room.send_relates_msg(&msg, event_id, false).await?; + api.delete_tags(vec![event_id.clone()]).await?; + } + None => { + room.send_msg(&msg, false).await?; + } + } + } + } + api.delete_torrents(vec![hash.clone()], true).await?; + } + } + + Ok(()) +} + +pub async fn show_status( + api: &Qbit, + room_id: &str, + event_id: Option, +) -> Result<(String, String)> { + let arg = GetTorrentListArg { + category: Some(room_id.to_string()), + tag: event_id, + ..Default::default() + }; + + let torrents = api.get_torrent_list(arg).await?; + let mut vec = Vec::new(); + for torrent in torrents { + let name = torrent.name.unwrap_or_default(); + let state = match torrent.state { + Some(State::PausedUP) => "下载完成", + Some(State::StalledUP) => "下载完成", + Some(State::Uploading) => "上传中", + Some(State::PausedDL) => "暂停", + Some(State::StalledDL) => "下载中(无连接)", + Some(State::Downloading) => "下载中", + Some(State::CheckingDL) => "检查中", + Some(State::CheckingUP) => "检查中", + Some(State::QueuedDL) => "等待下载", + Some(State::QueuedUP) => "等待上传", + Some(State::MetaDL) => "元数据下载中", + Some(State::MissingFiles) => "缺少文件", + Some(State::Unknown) => "未知", + Some(State::Error) => "错误", + Some(State::ForcedUP) => "强制上传", + Some(State::ForcedDL) => "强制下载", + Some(State::Allocating) => "分配空间", + Some(State::CheckingResumeData) => "检查恢复数据", + Some(State::Moving) => "移动中", + None => "未知", + }; + + let progress = torrent.progress.unwrap_or_default(); + let progress = format!("{:.2}%", progress * 100.0); + + vec.push((name, state, progress)); + } + + let mut msg = String::new(); + for (name, state, progress) in &vec { + msg.push_str(&format!("{} {} {}\n", name, state, progress)); + } + + let mut html_msg = String::from( + " + + + + + + + + ", + ); + for (name, state, progress) in vec { + html_msg.push_str(&format!( + " + + + + ", + name, state, progress + )); + } + html_msg.push_str("
名称状态进度
{}{}{}
"); + + Ok((msg, html_msg)) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[tokio::test] + async fn test_show_status() { + let api = login("admin", "adminadmin", "http://127.0.0.1:8080") + .await + .unwrap(); + let status = api + .get_torrent_list(GetTorrentListArg::default()) + .await + .unwrap(); + println!("{:#?}", status); + } +} diff --git a/plugins/qbittorrent/src/setting.rs b/plugins/qbittorrent/src/setting.rs index 0cf1e97..57317ba 100644 --- a/plugins/qbittorrent/src/setting.rs +++ b/plugins/qbittorrent/src/setting.rs @@ -1,56 +1,74 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; use anyhow::Result; +use matrix_bot_core::matrix::{client::Client, room::Room}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] pub struct Setting { - pub room_setting: Vec, + pub room: Vec, pub qbit_user: String, pub qbit_pass: String, pub qbit_url: String, + #[cfg(target_os = "linux")] pub use_internal_qbit: bool, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, Hash, Eq, PartialEq)] pub struct RoomSetting { pub download_path: PathBuf, - pub file_size_limit: u128, pub room_id: String, pub db_path: PathBuf, } -pub fn get_or_init(path: impl AsRef) -> Result { - let setting_path = path.as_ref().join("qbittorrent.toml"); - // load setting, if not exists, create it and exit - let setting = if !setting_path.exists() { - log::info!("create setting file: {}", setting_path.to_string_lossy()); - let setting = Setting { - room_setting: vec![RoomSetting { - download_path: path.as_ref().join("qbittorrent").join("download"), - file_size_limit: 0, - room_id: "".to_string(), - db_path: path.as_ref().join("qbittorrent").join("db"), - }], - qbit_user: "admin".to_string(), - qbit_pass: "adminadmin".to_string(), - qbit_url: "http://127.0.0.1:8080".to_string(), - use_internal_qbit: true, +impl Setting { + pub fn get_or_init(path: impl AsRef) -> Result { + let setting_path = path.as_ref().join("qbittorrent.toml"); + // load setting, if not exists, create it and exit + let setting = if !setting_path.exists() { + log::info!("create setting file: {}", setting_path.to_string_lossy()); + let setting = Setting { + room: vec![RoomSetting { + download_path: path.as_ref().join("qbittorrent").join("download"), + room_id: "".to_string(), + db_path: path.as_ref().join("qbittorrent").join("db"), + }], + qbit_user: "admin".to_string(), + qbit_pass: "adminadmin".to_string(), + qbit_url: "http://127.0.0.1:8080".to_string(), + #[cfg(target_os = "linux")] + use_internal_qbit: true, + }; + let toml = toml::to_string_pretty(&setting).unwrap(); + std::fs::write(&setting_path, toml)?; + log::error!( + "please edit setting file: {}", + setting_path.to_string_lossy() + ); + return Err(anyhow::anyhow!( + "please edit setting file: {}", + setting_path.to_string_lossy() + )); + } else { + log::info!("load setting file: {}", setting_path.to_string_lossy()); + let toml = std::fs::read_to_string(&setting_path)?; + toml::from_str(&toml)? }; - let toml = toml::to_string_pretty(&setting).unwrap(); - std::fs::write(&setting_path, toml)?; - log::error!( - "please edit setting file: {}", - setting_path.to_string_lossy() - ); - return Err(anyhow::anyhow!( - "please edit setting file: {}", - setting_path.to_string_lossy() - )); - } else { - log::info!("load setting file: {}", setting_path.to_string_lossy()); - let toml = std::fs::read_to_string(&setting_path)?; - toml::from_str(&toml)? - }; - Ok(setting) + Ok(setting) + } + + pub async fn to_hashmap( + &self, + client: &Client, + ) -> Result> { + let mut hashmap = HashMap::new(); + for setting in &self.room { + let room = Room::new(client, &setting.room_id).await?; + hashmap.insert(setting.room_id.to_string(), (room, setting.clone())); + } + Ok(hashmap) + } } diff --git a/plugins/qbittorrent/src/upload/gofile.rs b/plugins/qbittorrent/src/upload/gofile.rs new file mode 100644 index 0000000..905f0ae --- /dev/null +++ b/plugins/qbittorrent/src/upload/gofile.rs @@ -0,0 +1,135 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, path::Path}; +use walkdir::WalkDir; + +#[derive(Debug, Deserialize, Serialize)] +struct ServerResponse { + status: String, + data: Server, +} + +#[derive(Debug, Deserialize, Serialize)] +struct Server { + server: String, +} + +pub async fn upload>(file_path: P) -> Result { + // curl https://api.gofile.io/getServer + // {"status":"ok","data":{"server":"srv-store3"}} + let resp = reqwest::get("https://api.gofile.io/getServer").await?; + let resp = resp.json::().await?; + let status = resp.status.as_str(); + if status != "ok" { + return Err(anyhow::anyhow!("Failed to get server:{:?}", resp)); + } + let server = resp.data.server; + + if file_path.as_ref().is_dir() { + let mut token = None; + let mut folder_id = None; + let mut download_page = String::new(); + for entry in WalkDir::new(&file_path) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| { + e.file_type().is_file() + && !e.file_name().to_string_lossy().to_string().starts_with('.') + }) + { + let file_name = entry.file_name().to_string_lossy().to_string(); + let file = std::fs::read(entry.path())?; + let upload = UploadParams { + server: server.clone(), + token: token.clone(), + folder_id: folder_id.clone(), + file, + file_name, + }; + let upload = upload_file(upload).await?; + if upload.guest_token.is_some() { + token = upload.guest_token; + } + if folder_id.is_none() { + folder_id = Some(upload.parent_folder); + } + download_page = upload.download_page; + } + if download_page.is_empty() { + return Err(anyhow::anyhow!( + "Failed to upload file:{:?}", + file_path.as_ref() + )); + } + Ok(download_page) + } else { + let file_name = file_path + .as_ref() + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + let file = std::fs::read(file_path)?; + let upload = UploadParams { + server, + token: None, + folder_id: None, + file, + file_name, + }; + let upload = upload_file(upload).await?; + Ok(upload.download_page) + } +} + +#[derive(Debug)] +struct UploadParams { + server: String, + token: Option, + folder_id: Option, + file: Vec, + file_name: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct UploadResponse { + status: String, + data: UploadInfo, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct UploadInfo { + guest_token: Option, + download_page: String, + code: String, + parent_folder: String, + file_id: String, + file_name: String, + md5: String, +} +async fn upload_file(parameter: UploadParams) -> Result { + // curl -F file=@someFile.txt https://store1.gofile.io/uploadFile + let url = format!("https://{}.gofile.io/uploadFile", parameter.server); + + // 一定要有file_name方法,且参数不能为空,否则数据上传失败 + let part = + reqwest::multipart::Part::bytes(Cow::from(parameter.file)).file_name(parameter.file_name); + let mut form = reqwest::multipart::Form::new().part("file", part); + if let Some(token) = parameter.token { + form = form.text("token", token); + } + if let Some(folder_id) = parameter.folder_id { + form = form.text("folderId", folder_id); + } + let client = reqwest::ClientBuilder::new().build()?; + + let resp = client.post(url).multipart(form).send().await?; + let resp = resp.json::().await?; + let status = resp.status.as_str(); + if status != "ok" { + return Err(anyhow::anyhow!("Failed to upload file:{:?}", resp)); + } + + Ok(resp.data) +} diff --git a/plugins/qbittorrent/src/upload/mod.rs b/plugins/qbittorrent/src/upload/mod.rs new file mode 100644 index 0000000..4ee67b6 --- /dev/null +++ b/plugins/qbittorrent/src/upload/mod.rs @@ -0,0 +1 @@ +pub mod gofile;