diff --git a/Cargo.lock b/Cargo.lock index add00a8..35b2a53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,7 +359,6 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" name = "biliup" version = "0.2.4" dependencies = [ - "anyhow", "async-trait", "base64", "byteorder", @@ -395,14 +394,17 @@ dependencies = [ name = "biliup-cli" version = "0.2.4" dependencies = [ - "anyhow", + "async-channel", "async-trait", "axum", + "axum-login", "biliup", + "bon", "bytes", "chrono", "clap", "dialoguer", + "error-stack", "futures", "glob", "humantime", @@ -410,7 +412,9 @@ dependencies = [ "indexmap", "indicatif", "mime_guess", + "nom 8.0.0", "ormlite", + "password-auth", "qrcode", "regex", "reqwest", @@ -424,9 +428,13 @@ dependencies = [ "time", "tokio", "toml", + "tower", "tower-http", + "tower-sessions", + "tower-sessions-sqlx-store", "tracing", "tracing-subscriber", + "url", ] [[package]] @@ -3617,14 +3625,14 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stream-gears" -version = "1.1.4" +version = "1.1.5" dependencies = [ - "anyhow", "async-channel", "async-trait", "axum", "axum-login", "biliup", + "biliup-cli", "bon", "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e12c841..a546b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "3" [workspace.package] -version = "1.1.4" +version = "1.1.5" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } @@ -23,7 +23,6 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["local-time"] } tracing-appender = "0.2" -anyhow = "1.0" thiserror = "2" error-stack = "0.6.0" diff --git a/README.md b/README.md index 71b6422..f709d04 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ ## 💴捐赠 + [爱发电 »](https://afdian.com/a/biliup) ## ⭐Stars diff --git a/crates/biliup-cli/Cargo.toml b/crates/biliup-cli/Cargo.toml index 20bc174..59e4a44 100644 --- a/crates/biliup-cli/Cargo.toml +++ b/crates/biliup-cli/Cargo.toml @@ -23,7 +23,7 @@ bytes = { workspace = true } futures = { workspace = true } clap = { version = "4", features = ["derive"] } time = { workspace = true } -anyhow = { workspace = true } + glob = "0.3.2" qrcode = "0.14" indicatif = "0.17.11" @@ -33,7 +33,7 @@ chrono = { workspace = true, features = ["serde"] } indexmap = { version = "2.8.0" } async-trait = { workspace = true } humantime = "2.2.0" -axum = { version = "0.8" } +axum = { version = "0.8", features = ["macros"] } tower-http = { version = "0.6.2", features = ["cors"] } reqwest = { workspace = true } sqlx = { version = "0.8", features = [ "runtime-tokio-rustls", "sqlite" ] } @@ -44,7 +44,18 @@ tempfile = "3.14" rust-embed = "8.7.2" mime_guess = "2.0.5" ormlite = { version = "0.24.1", features = ["sqlite", "chrono"] } - +url = { workspace = true } +error-stack = { workspace = true } +bon = { workspace = true } +axum-login = "0.18.0" +password-auth = "1.0.0" +tower = "0.5.2" +tower-sessions = { version = "0.14.0", default-features = false, features = [ + "signed", +] } +tower-sessions-sqlx-store = { version = "0.15.0", features = ["sqlite"] } +async-channel = { version = "2.5.0" } +nom = "8" [features] default = [] python-bridge = [] diff --git a/crates/biliup-cli/src/downloader.rs b/crates/biliup-cli/src/downloader.rs index 9fcddb6..7172848 100644 --- a/crates/biliup-cli/src/downloader.rs +++ b/crates/biliup-cli/src/downloader.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use biliup::downloader::extractor::find_extractor; use biliup::downloader::flv_parser::{ CodecId, SoundFormat, TagData, aac_audio_packet_header, avc_video_packet_header, header, @@ -8,10 +7,11 @@ use biliup::downloader::flv_writer; use biliup::downloader::flv_writer::{FlvTag, TagDataHeader}; use biliup::downloader::httpflv::map_parse_err; use biliup::downloader::util::Segmentable; +use biliup_cli::server::errors::{AppError, AppResult}; use bytes::{Buf, BufMut, Bytes, BytesMut}; +use error_stack::ResultExt; use std::io::{BufReader, BufWriter, ErrorKind, Read}; use std::path::PathBuf; - use tracing::{error, info, warn}; pub async fn download( @@ -19,22 +19,27 @@ pub async fn download( output: String, split_size: Option, split_time: Option, -) -> Result<()> { +) -> AppResult<()> { let segmentable = Segmentable::new(split_time.map(|t| t.into()), split_size); let client = Default::default(); if let Some(extractor) = find_extractor(url) { - let mut site = extractor.get_site(url, client).await?; - site.download(&output, segmentable, None).await?; + let mut site = extractor + .get_site(url, client) + .await + .change_context_lazy(|| AppError::Unknown)?; + site.download(&output, segmentable, None) + .await + .change_context_lazy(|| AppError::Unknown)?; } else { warn!("not find extractor for {url}") } Ok(()) } -pub fn generate_json(mut file_name: PathBuf) -> Result<()> { +pub fn generate_json(mut file_name: PathBuf) -> AppResult<()> { // let args: Vec = env::args().collect(); // let file_name = &args[1]; - let flv_file = std::fs::File::open(&file_name)?; + let flv_file = std::fs::File::open(&file_name).change_context_lazy(|| AppError::Unknown)?; let buf_reader = BufReader::new(flv_file); let mut reader = Reader::new(buf_reader); @@ -43,10 +48,13 @@ pub fn generate_json(mut file_name: PathBuf) -> Result<()> { let mut video_tag_count = 0; let mut tag_count = 0; let _err_count = 0; - let flv_header = reader.read_frame(9)?; + let flv_header = reader + .read_frame(9) + .change_context_lazy(|| AppError::Unknown)?; // file_name.parent().and_then(|p|p + file_name.file_name()+".json"); // Vec::clear() - let (_, header) = map_parse_err(header(&flv_header), "flv header")?; + let (_, header) = map_parse_err(header(&flv_header), "flv header") + .change_context_lazy(|| AppError::Unknown)?; let mut os_string = file_name.extension().unwrap_or_default().to_owned(); os_string.push(".json"); file_name.set_extension(os_string); @@ -56,13 +64,22 @@ pub fn generate_json(mut file_name: PathBuf) -> Result<()> { .create_new(true) .write(true) .open(&file_name) - .with_context(|| format!("file name: {}", file_name.canonicalize().unwrap().display()))?; + .change_context_lazy(|| { + AppError::Custom(format!( + "file name: {}", + file_name.canonicalize().unwrap().display() + )) + })?; let mut writer = BufWriter::new(file); - flv_writer::to_json(&mut writer, &header)?; + flv_writer::to_json(&mut writer, &header).change_context_lazy(|| AppError::Unknown)?; loop { - let _previous_tag_size = reader.read_frame(4)?; + let _previous_tag_size = reader + .read_frame(4) + .change_context_lazy(|| AppError::Unknown)?; - let t_header = reader.read_frame(11)?; + let t_header = reader + .read_frame(11) + .change_context_lazy(|| AppError::Unknown)?; if t_header.is_empty() { break; } @@ -74,7 +91,9 @@ pub fn generate_json(mut file_name: PathBuf) -> Result<()> { } }; tag_count += 1; - let bytes = reader.read_frame(tag_header.data_size as usize)?; + let bytes = reader + .read_frame(tag_header.data_size as usize) + .change_context_lazy(|| AppError::Unknown)?; let (i, flv_tag_data) = match map_parse_err( tag_data(tag_header.tag_type, tag_header.data_size as usize)(&bytes), "tag data", @@ -144,7 +163,7 @@ pub fn generate_json(mut file_name: PathBuf) -> Result<()> { } } }; - flv_writer::to_json(&mut writer, &flv_tag)?; + flv_writer::to_json(&mut writer, &flv_tag).change_context_lazy(|| AppError::Unknown)?; } info!("tag count: {tag_count}"); info!("script tag count: {script_tag_count}"); diff --git a/crates/biliup-cli/src/lib.rs b/crates/biliup-cli/src/lib.rs index 214d3c5..ab04306 100644 --- a/crates/biliup-cli/src/lib.rs +++ b/crates/biliup-cli/src/lib.rs @@ -1,13 +1,17 @@ pub mod server; -use anyhow::{Context, Result}; - -use crate::server::api::router::ApplicationController; +// use crate::server::api::router::ApplicationController; +use crate::server::app::ApplicationController; +use crate::server::core::download_manager::ActorHandle; +use crate::server::errors::{AppError, AppResult}; use crate::server::infrastructure::connection_pool::ConnectionManager; +use crate::server::infrastructure::repositories; use crate::server::infrastructure::service_register::ServiceRegister; +use error_stack::ResultExt; use std::net::ToSocketAddrs; +use std::sync::{Arc, RwLock}; -pub async fn run(addr: (&str, u16)) -> Result<()> { +pub async fn run(addr: (&str, u16)) -> AppResult<()> { // let config = Arc::new(AppConfig::parse()); tracing::info!( @@ -17,12 +21,23 @@ pub async fn run(addr: (&str, u16)) -> Result<()> { .await .expect("could not initialize the database connection pool"); - let service_register = ServiceRegister::new(conn_pool); + let config = Arc::new(RwLock::new(repositories::get_config(&conn_pool).await?)); + let actor_handle = Arc::new(ActorHandle::new( + config.read().unwrap().pool1_size, + config.read().unwrap().pool2_size, + )); + let vec = Vec::new(); + + let service_register = ServiceRegister::new(conn_pool, config.clone(), actor_handle, vec); tracing::info!("migrations successfully ran, initializing axum server..."); - let addr = addr.to_socket_addrs()?.next().unwrap(); + let addr = addr + .to_socket_addrs() + .change_context(AppError::Unknown)? + .next() + .unwrap(); ApplicationController::serve(&addr, service_register) .await - .context("could not initialize application routes")?; + .attach("could not initialize application routes")?; Ok(()) } diff --git a/crates/biliup-cli/src/main.rs b/crates/biliup-cli/src/main.rs index fb055b3..f4e0c9b 100644 --- a/crates/biliup-cli/src/main.rs +++ b/crates/biliup-cli/src/main.rs @@ -2,7 +2,6 @@ mod cli; mod downloader; mod uploader; -use anyhow::Result; use time::macros::format_description; use crate::cli::{Cli, Commands}; @@ -12,11 +11,12 @@ use biliup::uploader::util::SubmitOption; use clap::Parser; +use biliup_cli::server::errors::AppResult; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> AppResult<()> { // a builder for `FmtSubscriber`. // let subscriber = FmtSubscriber::builder() // // all spans/events with a level higher than TRACE (e.g, debug, info, warn, etc.) diff --git a/crates/biliup-cli/src/server.rs b/crates/biliup-cli/src/server.rs new file mode 100644 index 0000000..5092773 --- /dev/null +++ b/crates/biliup-cli/src/server.rs @@ -0,0 +1,19 @@ +pub mod api; +pub mod app; +pub mod config; +pub mod core; +pub mod errors; +pub mod infrastructure; +mod router; +pub mod util; + +use error_stack::ResultExt; +use futures::TryFutureExt; + +use std::net::ToSocketAddrs; +// use tokio::sync::mpsc::Receiver; +use crate::server::errors::{AppError, AppResult}; +use crate::server::infrastructure::connection_pool::ConnectionManager; +use crate::server::infrastructure::repositories; +use crate::server::infrastructure::service_register::ServiceRegister; +use tracing::info; diff --git a/crates/stream-gears/src/server/api/auth.rs b/crates/biliup-cli/src/server/api/auth.rs similarity index 99% rename from crates/stream-gears/src/server/api/auth.rs rename to crates/biliup-cli/src/server/api/auth.rs index 840ac9a..219929d 100644 --- a/crates/stream-gears/src/server/api/auth.rs +++ b/crates/biliup-cli/src/server/api/auth.rs @@ -30,7 +30,7 @@ mod post { use tracing::log::info; /// Handler for the "POST /signup" endpoint. - pub(crate) async fn signup( + pub async fn signup( mut auth_session: AuthSession, Json(creds): Json, ) -> impl IntoResponse { diff --git a/crates/biliup-cli/src/server/api/bilibili_endpoints.rs b/crates/biliup-cli/src/server/api/bilibili_endpoints.rs index d9abfb1..25edb11 100644 --- a/crates/biliup-cli/src/server/api/bilibili_endpoints.rs +++ b/crates/biliup-cli/src/server/api/bilibili_endpoints.rs @@ -1,51 +1,72 @@ -use crate::server::errors::{AppError, AppResult}; +use crate::server::errors::{AppError, report_to_response}; use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::repositories::models::Configuration; +use crate::server::infrastructure::models::Configuration; use axum::extract::{Query, State}; +use axum::response::Response; use axum::{Extension, Json}; use biliup::client::StatelessClient; use biliup::uploader::credential::login_by_cookies; use bytes::Bytes; +use error_stack::{Report, ResultExt}; use ormlite::Model; use std::collections::HashMap; +use tracing::info; pub async fn archive_pre_endpoint( Query(params): Query>, State(pool): State, -) -> AppResult> { +) -> Result, Response> { let configurations = Configuration::select() .where_("key = 'bilibili-cookies'") .fetch_all(&pool) - .await?; + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; for cookies in configurations { if let Ok(bili) = login_by_cookies(cookies.value, None).await { - return Ok(Json(bili.archive_pre().await?)); + return Ok(Json( + bili.archive_pre() + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + )); } } - Err(AppError::InternalServerErrorWithContext( + Err(report_to_response(Report::from(AppError::Custom( "无可用 cookie 文件".to_string(), - )) + )))) } pub async fn get_myinfo_endpoint( Query(params): Query>, -) -> AppResult> { - println!("{}", ¶ms["user"]); - let bili = login_by_cookies(¶ms["user"], None).await?; - Ok(Json(bili.my_info().await?)) +) -> Result, Response> { + let bili = login_by_cookies(¶ms["user"], None) + .await + .change_context(AppError::Custom(params["user"].to_string())) + .map_err(report_to_response)?; + Ok(Json( + bili.my_info() + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + )) } pub async fn get_proxy_endpoint( - Extension(client): Extension, + State(client): State, Query(params): Query>, -) -> AppResult { - println!("{}", ¶ms["url"]); +) -> Result { + info!(params = ¶ms["url"]); // let bili = login_by_cookies(¶ms["user"]).await?; - Ok(client + client .client .get(¶ms["url"]) .send() - .await? + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)? .bytes() - .await?) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response) } diff --git a/crates/biliup-cli/src/server/api/endpoints.rs b/crates/biliup-cli/src/server/api/endpoints.rs index 36c8f1d..fe6a979 100644 --- a/crates/biliup-cli/src/server/api/endpoints.rs +++ b/crates/biliup-cli/src/server/api/endpoints.rs @@ -1,45 +1,235 @@ -use crate::server::core::download_actor::DownloadActorHandle; -use crate::server::core::live_streamers::{ - AddLiveStreamerDto, DynLiveStreamersService, LiveStreamerDto, LiveStreamerEntity, -}; -use crate::server::core::upload_streamers::{DynUploadStreamersRepository, StudioEntity}; -use crate::server::core::users::User; -use crate::server::errors::{AppError, AppResult}; - +use crate::server::config::Config; +use crate::server::errors::{AppError, AppResult, report_to_response}; use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::repositories::models::{ - Configuration, FileItem, LiveStreamer, StreamerInfo, UploadStreamer, +use crate::server::infrastructure::context::Worker; +use crate::server::infrastructure::dto::LiveStreamerResponse; +use crate::server::infrastructure::models::{ + Configuration, FileItem, InsertConfiguration, InsertLiveStreamer, InsertUploadStreamer, + LiveStreamer, StreamerInfo, UploadStreamer, }; +use crate::server::infrastructure::repositories::{del_streamer, get_all_streamer}; +use crate::server::infrastructure::service_register::ServiceRegister; use axum::extract::{Path, State}; -use axum::{Extension, Json}; -use ormlite::Model; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::{Json, debug_handler}; +use biliup::credential::{Credential, LoginInfo}; +use error_stack::{Report, ResultExt}; +use ormlite::{Insert, Model}; use serde_json::json; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, UNIX_EPOCH}; +use tokio::fs; +use tokio::io::AsyncWriteExt; +use tracing::info; pub async fn get_streamers_endpoint( State(pool): State, -) -> AppResult>> { - let live_streamers = LiveStreamer::select().fetch_all(&pool).await?; + State(workers): State>>>>, +) -> Result>, Response> { + let live_streamers = get_all_streamer(&pool).await.map_err(report_to_response)?; + info!( + "get_streamers_endpoint found {} live streamers", + live_streamers.len() + ); + Ok(Json( + live_streamers + .into_iter() + .map(|x| LiveStreamerResponse { + status: workers + .read() + .unwrap() + .iter() + .find_map(|worker| { + if worker.id == x.id { + Some(*worker.downloader_status.read().unwrap()) + } else { + None + } + }) + .unwrap_or_default(), + inner: x, + }) + .collect(), + )) +} +pub async fn post_streamers_endpoint( + State(service_register): State, + State(pool): State, + Json(payload): Json, +) -> Result, Response> { + let Some(manager) = service_register.get_manager(&payload.url) else { + info!("not supported url: {}", &payload.url); + return Err((StatusCode::BAD_REQUEST, "Not supported url").into_response()); + }; + let monitor = manager.ensure_monitor(); + + /// You can insert the model directly. + let live_streamers = payload + .insert(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + service_register + .add_room(live_streamers.id, &live_streamers.url, monitor) + .await + .map_err(report_to_response)?; + + info!(workers=?service_register.workers, "successfully inserted new live streamers"); + Ok(Json(live_streamers)) +} + +pub async fn put_streamers_endpoint( + State(service_register): State, + State(pool): State, + Json(payload): Json, +) -> Result, Response> { + let streamer = payload + .update_all_fields(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + let Some(manager) = service_register.get_manager(&streamer.url) else { + info!("not supported url: {}", &streamer.url); + return Err((StatusCode::BAD_REQUEST, "Not supported url").into_response()); + }; + let monitor = manager.ensure_monitor(); + + service_register + .del_room(streamer.id) + .await + .map_err(report_to_response)?; + + service_register + .add_room(streamer.id, &streamer.url, monitor) + .await + .map_err(report_to_response)?; + + info!(workers=?streamer, "successfully update live streamers"); + Ok(Json(streamer)) +} + +pub async fn delete_streamers_endpoint( + State(service_register): State, + State(pool): State, + State(workers): State>>>>, + Path(id): Path, +) -> Result, Response> { + service_register + .del_room(id) + .await + .map_err(report_to_response)?; + let live_streamers = del_streamer(&pool, id).await.map_err(report_to_response)?; + info!(workers=?service_register.workers, "successfully inserted new live streamers"); Ok(Json(live_streamers)) } pub async fn get_configuration( - State(pool): State, -) -> AppResult> { - let configurations = Configuration::select() - .where_("key = 'config'") - .fetch_one(&pool) - .await?; + State(config): State>>, +) -> Result, Response> { + Ok(Json(config.read().unwrap().clone())) +} - Ok(Json(serde_json::from_str(&configurations.value)?)) +// #[axum_macros::debug_handler(state = ServiceRegister)] +pub async fn put_configuration( + State(config): State>>, + State(pool): State, + Json(json_data): Json, +) -> Result, Response> { + // 将 JSON 序列化为 TEXT 存库 + let value_txt = serde_json::to_string(&json_data) + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + let mut tx = pool + .begin() + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + // 最多取 2 条判断是否多行 + let ids: Vec = + sqlx::query_scalar::<_, i64>("SELECT id FROM configuration WHERE key = ?1 LIMIT 2") + .bind("config") + .fetch_all(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + let saved: Configuration = if ids.is_empty() { + // 插入 + sqlx::query("INSERT INTO configuration (key, value) VALUES (?1, ?2)") + .bind("config") + .bind(&value_txt) + .execute(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + // 取 last_insert_rowid 并读回整行 + let id: i64 = sqlx::query_scalar::<_, i64>("SELECT last_insert_rowid()") + .fetch_one(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + sqlx::query_as::<_, Configuration>("SELECT id, key, value FROM configuration WHERE id = ?1") + .bind(id) + .fetch_one(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)? + } else if ids.len() == 1 { + // 更新 + let id = ids[0]; + sqlx::query("UPDATE configuration SET value = ?1 WHERE id = ?2") + .bind(&value_txt) + .bind(id) + .execute(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + sqlx::query_as::<_, Configuration>("SELECT id, key, value FROM configuration WHERE id = ?1") + .bind(id) + .fetch_one(&mut *tx) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)? + } else { + // 多行报错 + return Err(report_to_response(Report::new(AppError::Custom( + format!("有多个空间配置同时存在 (key='config'): {} 行", ids.len()).to_string(), + )))); + }; + + tx.commit() + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + // 提交后从 DB 重新加载配置 + let saved_config: Config = serde_json::from_str(&saved.value) + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + *config.write().unwrap() = saved_config; + Ok(Json(config.read().unwrap().clone())) } pub async fn get_streamer_info( // Extension(streamers_service): Extension, State(pool): State, -) -> AppResult>> { - let streamer_infos = StreamerInfo::select().fetch_all(&pool).await?; - let file_items = FileItem::select().fetch_all(&pool).await?; +) -> Result>, Response> { + let streamer_infos = StreamerInfo::select() + .fetch_all(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + let file_items = FileItem::select() + .fetch_all(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; println!("{:?}", file_items); Ok(Json(streamer_infos)) @@ -48,83 +238,88 @@ pub async fn get_streamer_info( pub async fn get_upload_streamers_endpoint( // Extension(streamers_service): Extension, State(pool): State, -) -> AppResult>> { - let uploader_streamers = UploadStreamer::select().fetch_all(&pool).await?; +) -> Result>, Response> { + let uploader_streamers = UploadStreamer::select() + .fetch_all(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; Ok(Json(uploader_streamers)) } +pub async fn add_upload_streamer_endpoint( + // Extension(streamers_service): Extension, + State(pool): State, + Json(upload_streamer): Json, +) -> Result, Response> { + if upload_streamer.id.is_none() { + Ok(Json( + serde_json::to_value( + ormlite::Insert::insert(upload_streamer, &pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + ) + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + )) + } else { + Ok(Json( + serde_json::to_value( + upload_streamer + .update_all_fields(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + ) + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + )) + } +} + pub async fn get_upload_streamer_endpoint( State(pool): State, Path(id): Path, -) -> AppResult> { +) -> Result, Response> { let uploader_streamers = UploadStreamer::select() .where_("id = ?") .bind(id) .fetch_one(&pool) - .await?; + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; Ok(Json(uploader_streamers)) } - -pub async fn get_streamer_endpoint( - Extension(streamers_service): Extension, - Extension(download_actor_handle): Extension, - Path(id): Path, -) -> AppResult> { - Ok(Json(streamers_service.get_streamer_by_id(id).await?)) -} - -pub async fn add_streamer_endpoint( - Extension(streamers_service): Extension, - Extension(download_actor_handle): Extension, - Json(request): Json, -) -> AppResult> { - download_actor_handle.add_streamer(&request.url); - Ok(Json(streamers_service.add_streamer(request).await?)) -} - -pub async fn delete_streamer_endpoint( - Extension(download_actor_handle): Extension, - Path(id): Path, -) -> AppResult> { - Ok(Json(())) -} - -pub async fn update_streamer_endpoint( - Extension(download_actor_handle): Extension, - Json(request): Json, -) -> AppResult> { - download_actor_handle.update_streamer(&request.url); - Err(AppError::InvalidLoginAttempt) -} - -pub async fn add_upload_streamer_endpoint( - Extension(streamers_service): Extension, - Json(request): Json, -) -> AppResult> { - Ok(Json(streamers_service.create_streamer(request).await?)) -} - pub async fn delete_template_endpoint( - Extension(streamers_service): Extension, + State(pool): State, Path(id): Path, -) -> AppResult> { - Ok(Json(streamers_service.delete_streamer(id).await?)) -} - -pub async fn update_template_endpoint( - Extension(streamers_service): Extension, - Json(request): Json, -) -> AppResult> { - Ok(Json(streamers_service.update_streamer(request).await?)) +) -> Result, Response> { + let uploader_streamers = UploadStreamer::select() + .where_("id = ?") + .bind(id) + .fetch_one(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + Ok(Json( + uploader_streamers + .delete(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?, + )) } pub async fn get_users_endpoint( State(pool): State, -) -> AppResult>> { +) -> Result>, Response> { let configurations = Configuration::select() .where_("key = 'bilibili-cookies'") .fetch_all(&pool) - .await?; + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; let mut res = Vec::new(); for cookies in configurations { res.push(json!({ @@ -137,10 +332,112 @@ pub async fn get_users_endpoint( Ok(Json(res)) } -pub async fn add_user_endpoint(Json(request): Json) -> AppResult> { - Err(AppError::InternalServerError) +pub async fn add_user_endpoint( + State(pool): State, + Json(user): Json, +) -> Result, Response> { + let res = user + .insert(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + Ok(Json(res)) } -pub async fn delete_user_endpoint(Path(id): Path) -> AppResult> { +pub async fn delete_user_endpoint( + Path(id): Path, + State(pool): State, +) -> Result, Response> { + let x = sqlx::query("DELETE FROM configuration WHERE id = ?") + .bind(id) + .execute(&pool) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + info!("{:?}", x); Ok(Json(())) } + +pub async fn get_qrcode() -> Result, Response> { + let qrcode = Credential::new(None) + .get_qrcode() + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + Ok(Json(qrcode)) +} + +pub async fn login_by_qrcode( + Json(value): Json, +) -> Result, Response> { + let info = tokio::time::timeout( + Duration::from_secs(300), + Credential::new(None).login_by_qrcode(value), + // std::future::pending::>(), + ) + .await + .change_context(AppError::Custom("deadline has elapsed".to_string())) + .map_err(report_to_response)? + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + // extract mid + let mid = info.token_info.mid; + let filename = format!("data/{}.json", mid); + + let mut file = fs::File::create(&filename) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + file.write_all(&serde_json::to_vec_pretty(&info).unwrap()) + .await + .change_context(AppError::Unknown) + .map_err(report_to_response)?; + + Ok(Json(json!({ "filename": filename }))) +} + +pub async fn get_videos() -> Result>, Response> { + let media_extensions = [".mp4", ".flv", ".3gp", ".webm", ".mkv", ".ts"]; + let blacklist = ["next-env.d.ts"]; + + let mut file_list = Vec::new(); + let mut index = 1; + + // **use tokio::fs::read_dir** + if let Ok(mut entries) = fs::read_dir("../../../../..").await { + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + let file_name = entry.file_name().to_string_lossy().into_owned(); + + if blacklist.contains(&file_name.as_str()) { + continue; + } + + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if media_extensions + .iter() + .any(|allowed| &ext == &allowed.trim_start_matches('.')) + { + if let Ok(metadata) = entry.metadata().await { + let mtime = metadata + .modified() + .ok() + .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_secs()) + .unwrap_or(0); + + file_list.push(serde_json::json!({ + "key": index, + "name": file_name, + "updateTime": mtime, + "size": metadata.len(), + })); + index += 1; + } + } + } + } + } + Ok(Json(file_list)) +} diff --git a/crates/biliup-cli/src/server/api/mod.rs b/crates/biliup-cli/src/server/api/mod.rs new file mode 100644 index 0000000..15df7f3 --- /dev/null +++ b/crates/biliup-cli/src/server/api/mod.rs @@ -0,0 +1,4 @@ +pub mod auth; +pub mod bilibili_endpoints; +pub mod endpoints; +pub mod spa; diff --git a/crates/biliup-cli/src/server/api/router.rs b/crates/biliup-cli/src/server/api/router.rs deleted file mode 100644 index 66f315c..0000000 --- a/crates/biliup-cli/src/server/api/router.rs +++ /dev/null @@ -1,125 +0,0 @@ -use biliup::client::StatelessClient; - -use crate::server::api::endpoints::{ - add_streamer_endpoint, add_upload_streamer_endpoint, add_user_endpoint, - delete_streamer_endpoint, delete_template_endpoint, delete_user_endpoint, get_configuration, - get_streamer_endpoint, get_streamer_info, get_streamers_endpoint, get_upload_streamer_endpoint, - get_upload_streamers_endpoint, get_users_endpoint, update_streamer_endpoint, - update_template_endpoint, -}; -use crate::server::core::download_actor::DownloadActorHandle; - -use crate::server::infrastructure::service_register::ServiceRegister; - -use anyhow::Context; -use axum::routing::{delete, get, post, put}; -use axum::{Extension, Router, http}; - -use crate::server::api::bilibili_endpoints::{ - archive_pre_endpoint, get_myinfo_endpoint, get_proxy_endpoint, -}; -use crate::server::core::main_loop::spawn_main_loop; -use axum::http::HeaderValue; -use std::net::SocketAddr; - -use crate::server::api::spa::static_handler; -use tower_http::cors::{AllowMethods, CorsLayer}; -use tracing::info; - -pub struct ApplicationController; - -impl ApplicationController { - pub async fn serve(addr: &SocketAddr, service_register: ServiceRegister) -> anyhow::Result<()> { - let client = StatelessClient::default(); - // let vec = service_register.streamers_service.get_streamers().await?; - let (main_loop, _) = spawn_main_loop(); - let actor_handle = DownloadActorHandle::new(vec![], client.clone()); - // build our application with a route - let app = Router::new() - // `GET /` goes to `root` - .route("/v1/streamers", get(get_streamers_endpoint)) - .route("/v1/configuration", get(get_configuration)) - .route("/v1/streamer-info", get(get_streamer_info)) - .route("/v1/upload/streamers", get(get_upload_streamers_endpoint)) - .route( - "/v1/upload/streamers/{id}", - delete(delete_template_endpoint) - .put(update_template_endpoint) - .get(get_upload_streamer_endpoint), - ) - .route("/v1/streamers/{id}", get(get_streamer_endpoint)) - .route("/v1/streamers/{id}", delete(delete_streamer_endpoint)) - .route("/v1/streamers/{id}", put(update_streamer_endpoint)) - .route("/v1/streamers", post(add_streamer_endpoint)) - // .route( - // "/v1/upload/streamers/:id", - // , - // ) - // .route("/v1/upload/streamers/:id", ) - .route("/v1/upload/streamers", post(add_upload_streamer_endpoint)) - .route("/v1/users", get(get_users_endpoint).post(add_user_endpoint)) - .route("/v1/users/{id}", delete(delete_user_endpoint)) - .route("/bili/archive/pre", get(archive_pre_endpoint)) - .route("/bili/space/myinfo", get(get_myinfo_endpoint)) - .route("/bili/proxy", get(get_proxy_endpoint)) - .fallback(static_handler) - // .route("/", get(root)) - .layer( - // see https://docs.rs/tower-http/latest/tower_http/cors/index.html - // for more details - // - // pay attention that for some request types like posting content-type: application/json - // it is required to add ".allow_headers([http::header::CONTENT_TYPE])" - // or see this issue https://github.com/tokio-rs/axum/issues/849 - CorsLayer::new() - .allow_headers([http::header::CONTENT_TYPE]) - .allow_origin("http://localhost:3000".parse::().unwrap()) - .allow_methods(AllowMethods::any()), - ) - .layer(Extension(actor_handle)) - // .layer(Extension(client.clone())) - .layer(Extension(client.clone())) - .with_state(service_register); - // `POST /users` goes to `create_user` - // .route("/users", post(create_user)); - // let mut test = Cycle::new(indexmap![ - // "1".to_string() => StreamStatus::Idle, - // "2".to_string() => StreamStatus::Idle, - // "3".to_string() => StreamStatus::Idle, - // "4".to_string() => StreamStatus::Idle, - // ]); - // let testget = test.clone(); - - // tokio::spawn(async move { - // tokio::time::sleep(Duration::from_secs(31)).await; - // actor_handle.remove_streamer("https://www.huya.com/superrabbit"); - // println!("yesssss") - // }); - // let mut n = 0; - // loop { - // let string = testget.get(&mut n); - // println!("yoooo {string:?}"); - // tokio::time::sleep(Duration::from_secs(2)).await; - // } - // extensions.get().and_then(|actor| { - // actor - // }); - // println!("nonono"); - // Ok::<_, anyhow::Error>(()) - - // tokio::spawn(async move { - // tokio::time::sleep(Duration::from_secs(10)).await; - // test.replace(indexmap!["10".to_string() => StreamStatus::Idle]); - // }); - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - info!("routes initialized, listening on {}", addr); - let listener = tokio::net::TcpListener::bind(addr).await?; - - axum::serve(listener, app) - .await - .context("error while starting API server")?; - - Ok(()) - } -} diff --git a/crates/biliup-cli/src/server/api/spa.rs b/crates/biliup-cli/src/server/api/spa.rs index e9fee46..16f1e39 100644 --- a/crates/biliup-cli/src/server/api/spa.rs +++ b/crates/biliup-cli/src/server/api/spa.rs @@ -27,11 +27,12 @@ pub async fn static_handler(uri: Uri) -> impl IntoResponse { ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() } None => { - if path.contains('.') { - return not_found().await; - } - - index_html().await + // if path.contains('.') { + // return not_found().await; + // } + // + // index_html().await + not_found().await } } } diff --git a/crates/stream-gears/src/server/app.rs b/crates/biliup-cli/src/server/app.rs similarity index 100% rename from crates/stream-gears/src/server/app.rs rename to crates/biliup-cli/src/server/app.rs diff --git a/crates/stream-gears/src/server/config.rs b/crates/biliup-cli/src/server/config.rs similarity index 79% rename from crates/stream-gears/src/server/config.rs rename to crates/biliup-cli/src/server/config.rs index ab2ee3f..b0fbd14 100644 --- a/crates/stream-gears/src/server/config.rs +++ b/crates/biliup-cli/src/server/config.rs @@ -1,11 +1,7 @@ use crate::server::core::downloader::DownloaderType; +use crate::server::errors::{AppError, AppResult}; use crate::server::infrastructure::models::hook_step::HookStep; -use anyhow::{Context, bail}; -use pyo3::prelude::PyDictMethods; -use pyo3::sync::OnceLockExt; -use pyo3::types::{PyAnyMethods, PyDict}; -use pyo3::{Bound, FromPyObject, PyAny, PyResult, Python, pyclass, pyfunction, pymethods}; -use pythonize::pythonize; +use error_stack::bail; use serde::{Deserialize, Serialize}; use std::cell::LazyCell; use std::ops::Deref; @@ -255,7 +251,7 @@ pub struct Credit { pub uid: String, } -#[derive(bon::Builder, FromPyObject, Debug, Clone, Serialize, Deserialize, Default)] +#[derive(bon::Builder, Debug, Clone, Serialize, Deserialize, Default)] pub struct UserConfig { // B 站 #[serde(default)] @@ -324,67 +320,10 @@ fn default_check_sourcecode() -> u64 { } impl Config { - pub fn load_or_create>(path: P) -> anyhow::Result { - bail!("load_or_create: {:?}", path.as_ref().display()) + pub fn load_or_create>(path: P) -> AppResult { + bail!(AppError::Custom(format!( + "load_or_create: {:?}", + path.as_ref().display() + ))) } } - -// 进程级全局单例(安全):OnceLock + Arc + RwLock -pub(crate) static CONFIG: LazyLock>> = LazyLock::new(|| { - Arc::new(RwLock::new( - Config::builder().streamers(Default::default()).build(), - )) -}); - -fn cfg_arc() -> &'static Arc> { - &*CONFIG -} - -#[pyclass] -struct ConfigState { - // 用 PyObject 存,方便保持任意 Python 对象 - map: Arc>, -} - -#[pymethods] -impl ConfigState { - /// 获取:config.get("k", default=None) - /// - 若 key 存在,返回保存的对象 - /// - 若不存在,返回 default(默认 None) - #[pyo3(signature = (key, default=None))] - fn get<'py>( - &self, - py: Python<'py>, - key: &str, - default: Option>, - ) -> PyResult> { - let guard = self.map.read().unwrap(); - // serde_json::to_value(guard.deref()) - if let Some(bound) = pythonize(py, guard.deref())? - .extract::>()? - .get_item(key)? - { - if bound.is_none() - && let Some(d) = default - { - return Ok(d); - } - return Ok(bound); - }; - let Some(default) = default else { - return Err(pyo3::exceptions::PyAttributeError::new_err(format!( - "object has no attribute '{key}'" - ))); - }; - Ok(default) - } -} - -#[pyfunction] -pub fn config_bindings(py: Python<'_>) -> PyResult { - let state = ConfigState { - map: cfg_arc().clone(), - }; - // pythonize(py, &config) - Ok(state) -} diff --git a/crates/biliup-cli/src/server/config_manager.rs b/crates/biliup-cli/src/server/config_manager.rs deleted file mode 100644 index 5fc48d2..0000000 --- a/crates/biliup-cli/src/server/config_manager.rs +++ /dev/null @@ -1,464 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use tokio::fs; -use tracing::{info, warn}; - -/// 配置格式枚举 -#[derive(Debug, Clone)] -pub enum ConfigFormat { - Yaml, - Toml, - Json, -} - -/// 流配置结构,对应Python版本的streamers配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StreamerConfig { - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub filename_prefix: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub time_range: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub excluded_keywords: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub uploader: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tid: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub copyright: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub cover_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub preprocessor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub downloaded_processor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub postprocessor: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub segment_processor: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub opt_args: Option>, - - // 其他字段作为额外的键值对存储 - #[serde(flatten)] - pub extra_fields: HashMap, -} - -/// 后处理器配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum PostprocessorConfig { - Simple(String), - Complex { - #[serde(skip_serializing_if = "Option::is_none")] - rm: Option, - #[serde(skip_serializing_if = "Option::is_none")] - mv: Option, - #[serde(skip_serializing_if = "Option::is_none")] - run: Option, - #[serde(skip_serializing_if = "Option::is_none")] - webhook: Option, - }, -} - -/// 用户配置结构,对应Python版本的user配置 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub cookies: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub access_token: Option, -} - -/// 主配置结构,对应Python版本的config -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BiliUpConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub streamers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub user: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub threads: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub lines: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pool1_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub pool2_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub event_loop_interval: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub check_sourcecode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub delay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_upload_limit: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub filtering_threshold: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub downloader: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub segment_time: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub file_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub filename_prefix: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub use_live_cover: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub segment_processor_parallel: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub dolby: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub hires: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub charging_pay: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub no_reprint: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub submit_api: Option, - - // 日志配置 - #[serde(skip_serializing_if = "Option::is_none")] - pub logging: Option>, - - // 其他字段作为额外的键值对存储 - #[serde(flatten)] - pub extra_fields: HashMap, -} - -impl Default for BiliUpConfig { - fn default() -> Self { - Self { - streamers: None, - user: None, - threads: Some(2), - lines: Some("AUTO".to_string()), - pool1_size: Some(5), - pool2_size: Some(3), - event_loop_interval: Some(30), - check_sourcecode: Some(15), - delay: Some(0), - max_upload_limit: Some(999), - filtering_threshold: Some(0), - downloader: Some("stream-gears".to_string()), - segment_time: Some("01:00:00".to_string()), - file_size: None, - filename_prefix: None, - use_live_cover: Some(false), - segment_processor_parallel: Some(false), - dolby: Some(0), - hires: Some(0), - charging_pay: Some(0), - no_reprint: Some(0), - submit_api: None, - logging: None, - extra_fields: HashMap::new(), - } - } -} - -/// 配置管理器,对应Python版本的Config类 -pub struct ConfigManager { - config: BiliUpConfig, - config_path: Option, - format: Option, -} - -impl ConfigManager { - pub fn new() -> Self { - Self { - config: BiliUpConfig::default(), - config_path: None, - format: None, - } - } - - /// 从文件加载配置,对应Python的load方法 - pub async fn load>(&mut self, path: Option

) -> Result<()> { - let config_path = if let Some(path) = path { - path.as_ref().to_path_buf() - } else { - // 自动检测配置文件 - self.find_config_file().await? - }; - - info!("Loading config from: {:?}", config_path); - - let content = fs::read_to_string(&config_path).await?; - let format = self.detect_format(&config_path)?; - - self.config = match format { - ConfigFormat::Yaml => serde_yaml::from_str(&content)?, - ConfigFormat::Toml => toml::from_str(&content)?, - ConfigFormat::Json => serde_json::from_str(&content)?, - }; - - self.config_path = Some(config_path); - self.format = Some(format); - - info!("Config loaded successfully"); - Ok(()) - } - - /// 保存配置到文件,对应Python的save方法 - pub async fn save(&self) -> Result<()> { - let config_path = self - .config_path - .as_ref() - .ok_or_else(|| anyhow::anyhow!("No config path set"))?; - let format = self - .format - .as_ref() - .ok_or_else(|| anyhow::anyhow!("No config format set"))?; - - let content = match format { - ConfigFormat::Yaml => serde_yaml::to_string(&self.config)?, - ConfigFormat::Toml => toml::to_string_pretty(&self.config)?, - ConfigFormat::Json => serde_json::to_string_pretty(&self.config)?, - }; - - fs::write(config_path, content).await?; - info!("Config saved to: {:?}", config_path); - Ok(()) - } - - /// 保存到指定路径,对应Python的dump方法 - pub async fn dump>(&self, path: P) -> Result<()> { - let path = path.as_ref(); - let format = self.detect_format(path)?; - - // 创建备份 - if path.exists() { - let backup_path = format!( - "{}.backup.{}", - path.to_string_lossy(), - chrono::Utc::now().format("%Y%m%d%H%M%S") - ); - if let Err(e) = fs::copy(path, &backup_path).await { - warn!("Failed to create backup: {}", e); - } else { - info!("Created backup: {}", backup_path); - } - } - - let content = match format { - ConfigFormat::Yaml => serde_yaml::to_string(&self.config)?, - ConfigFormat::Toml => toml::to_string_pretty(&self.config)?, - ConfigFormat::Json => serde_json::to_string_pretty(&self.config)?, - }; - - fs::write(path, content).await?; - info!("Config dumped to: {:?}", path); - Ok(()) - } - - /// 自动查找配置文件 - async fn find_config_file(&self) -> Result { - let candidates = vec!["config.yaml", "config.yml", "config.toml", "config.json"]; - - for candidate in candidates { - let path = PathBuf::from(candidate); - if path.exists() { - return Ok(path); - } - } - - Err(anyhow::anyhow!("No config file found")) - } - - /// 检测配置文件格式 - fn detect_format>(&self, path: P) -> Result { - let path = path.as_ref(); - let extension = path - .extension() - .and_then(|s| s.to_str()) - .ok_or_else(|| anyhow::anyhow!("Cannot determine file format"))?; - - match extension.to_lowercase().as_str() { - "yaml" | "yml" => Ok(ConfigFormat::Yaml), - "toml" => Ok(ConfigFormat::Toml), - "json" => Ok(ConfigFormat::Json), - _ => Err(anyhow::anyhow!("Unsupported config format: {}", extension)), - } - } - - /// 获取配置的只读引用 - pub fn get_config(&self) -> &BiliUpConfig { - &self.config - } - - /// 获取配置的可变引用 - pub fn get_config_mut(&mut self) -> &mut BiliUpConfig { - &mut self.config - } - - /// 获取流配置 - pub fn get_streamer_config(&self, name: &str) -> Option<&StreamerConfig> { - self.config.streamers.as_ref()?.get(name) - } - - /// 设置流配置 - pub fn set_streamer_config(&mut self, name: String, config: StreamerConfig) { - if self.config.streamers.is_none() { - self.config.streamers = Some(HashMap::new()); - } - if let Some(ref mut streamers) = self.config.streamers { - streamers.insert(name, config); - } - } - - /// 删除流配置 - pub fn remove_streamer_config(&mut self, name: &str) -> Option { - self.config.streamers.as_mut()?.remove(name) - } - - /// 获取所有流名称 - pub fn get_streamer_names(&self) -> Vec { - self.config - .streamers - .as_ref() - .map(|streamers| streamers.keys().cloned().collect()) - .unwrap_or_default() - } - - /// 获取配置值(通用方法) - pub fn get(&self, key: &str) -> Option - where - T: for<'de> Deserialize<'de> + Clone, - { - // 这是一个简化版本,实际实现需要处理嵌套的键路径 - // 例如:get("streamers.streamer1.url") - None - } - - /// 从cookies文件加载,对应Python的load_cookies - pub async fn load_cookies>(&mut self, path: P) -> Result<()> { - let content = fs::read_to_string(path).await?; - let cookies_data: serde_json::Value = serde_json::from_str(&content)?; - - if let Some(cookie_info) = cookies_data.get("cookie_info") - && let Some(cookies) = cookie_info.get("cookies").and_then(|c| c.as_array()) - { - let mut cookie_map = HashMap::new(); - for cookie in cookies { - if let (Some(name), Some(value)) = ( - cookie.get("name").and_then(|n| n.as_str()), - cookie.get("value").and_then(|v| v.as_str()), - ) { - cookie_map.insert(name.to_string(), value.to_string()); - } - } - - if self.config.user.is_none() { - self.config.user = Some(UserConfig { - cookies: None, - access_token: None, - }); - } - if let Some(ref mut user) = self.config.user { - user.cookies = Some(cookie_map); - } - } - - if let Some(token_info) = cookies_data.get("token_info") - && let Some(access_token) = token_info.get("access_token").and_then(|t| t.as_str()) - && let Some(ref mut user) = self.config.user - { - user.access_token = Some(access_token.to_string()); - } - - Ok(()) - } - - /// 合并配置,用于处理覆盖字段 - pub fn merge(&mut self, other: BiliUpConfig) { - // TODO: 实现深度合并逻辑 - if other.streamers.is_some() { - self.config.streamers = other.streamers; - } - if other.user.is_some() { - self.config.user = other.user; - } - // 合并其他字段... - } -} - -impl Default for ConfigManager { - fn default() -> Self { - Self::new() - } -} - -// 全局配置管理器实例 - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::NamedTempFile; - use tokio::io::AsyncWriteExt; - - #[tokio::test] - async fn test_config_yaml_loading() { - let yaml_content = r#" -streamers: - test_streamer: - url: "https://live.bilibili.com/12345" - format: "mp4" - tags: ["测试", "直播"] -threads: 4 -pool1_size: 8 -"#; - - let mut temp_file = NamedTempFile::new().unwrap(); - - let mut manager = ConfigManager::new(); - manager.load(Some(temp_file.path())).await.unwrap(); - - let config = manager.get_config(); - assert_eq!(config.threads, Some(4)); - assert_eq!(config.pool1_size, Some(8)); - - let streamer = config - .streamers - .as_ref() - .unwrap() - .get("test_streamer") - .unwrap(); - assert_eq!(streamer.url, "https://live.bilibili.com/12345"); - assert_eq!(streamer.format, Some("mp4".to_string())); - } - - #[tokio::test] - async fn test_config_toml_loading() { - let toml_content = r#" -threads = 4 -pool1_size = 8 - -[streamers.test_streamer] -url = "https://live.bilibili.com/12345" -format = "mp4" -tags = ["测试", "直播"] -"#; - - let mut temp_file = NamedTempFile::new().unwrap(); - - let mut manager = ConfigManager::new(); - manager.load(Some(temp_file.path())).await.unwrap(); - - let config = manager.get_config(); - assert_eq!(config.threads, Some(4)); - assert_eq!(config.pool1_size, Some(8)); - } -} diff --git a/crates/biliup-cli/src/server/core.rs b/crates/biliup-cli/src/server/core.rs deleted file mode 100644 index 1252701..0000000 --- a/crates/biliup-cli/src/server/core.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub mod download_actor; -pub mod live_streamers; -pub mod main_loop; -pub mod upload_actor; -pub mod upload_streamers; -pub mod users; -pub mod util; - -/// Status of the live stream -pub enum LiveStatus { - /// Stream is online. - Online, - /// Stream is offline. - Offline, - /// The status of the stream could not be determined. - Unknown, -} - -/// Status of the live stream -#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, PartialEq)] -pub enum StreamStatus { - /// Stream is online. - Working, - /// Stream is offline. - Inspecting, - /// The status of the stream could not be determined. - #[default] - Pending, - Idle, -} diff --git a/crates/biliup-cli/src/server/core/download_actor.rs b/crates/biliup-cli/src/server/core/download_actor.rs deleted file mode 100644 index 773e828..0000000 --- a/crates/biliup-cli/src/server/core/download_actor.rs +++ /dev/null @@ -1,210 +0,0 @@ -use biliup::client::StatelessClient; - -use crate::server::core::StreamStatus; -use crate::server::core::live_streamers::LiveStreamerDto; -use crate::server::core::util::{AnyMap, Cycle}; -use biliup::downloader::extractor::{SiteDefinition, find_extractor}; - -use indexmap::indexmap; - -use std::collections::HashMap; -use std::error::Error; -use std::ops::DerefMut; -use std::sync::{Arc, RwLock}; -use std::time::Duration; -use tokio::task::JoinHandle; -use tracing::debug; - -async fn start_monitor( - task: Cycle, - extractor: &(dyn SiteDefinition + Send + Sync), - client: StatelessClient, - // live_streamers_service: DynLiveStreamersService, -) { - let n = &mut 0; - loop { - let (url, status) = task.get(n); - status != StreamStatus::Working; - - match status { - StreamStatus::Working => {} - StreamStatus::Inspecting => {} - StreamStatus::Pending => {} - StreamStatus::Idle => { - task.change(&url, StreamStatus::Inspecting); - } - } - - match (extractor.get_site(&url, client.clone()).await, status) { - (Ok(site), StreamStatus::Idle | StreamStatus::Inspecting) => { - println!("Idle\n {url} \n{site}"); - // let (filename, split_size, split_time) = if let Ok(LiveStreamerDto { - // filename, - // split_size, - // split_time, - // .. - // }) = - // live_streamers_service.get_streamer_by_url(&url).await - // { - // (filename, split_size, split_time.map(Duration::from_secs)) - // } else { - // ("./video/%Y-%m-%d/%H_%M_%S{title}".to_string(), None, None) - // }; - // let live_streamers_service = live_streamers_service.clone(); - // { - // let client = client.clone(); - // let url = url.clone(); - // let task = task.clone(); - // logging_spawn(async move { - // let hook = live_streamers_service - // .get_studio_by_url(&url) - // .await - // .unwrap_or_default() - // .map(|studio| -> Box { - // let handle = UploadActorHandle::new(client, studio); - // Box::new(move |file_name| { - // if let Ok(metadata) = std::fs::metadata(file_name) - // .map_err(|err| error!("{}", err)) - // { - // if metadata.len() > 10 * 1024 * 1024 { - // info!("开始上传: {}", file_name); - // handle.send_file_path(file_name); - // } - // } - // }) - // }); - // if hook.is_none() { - // debug!(url = %url, "upload template not set."); - // } - // - // let segmentable = Segmentable::new(split_time, split_size); - // // let segmentable = Segmentable::new( None, Some(16*1024*1024)); - // site.download(&filename, segmentable, hook).await?; - // task.change(&url, StreamStatus::Idle); - // Ok::<_, Box>(()) - // }); - // } - task.change(&url, StreamStatus::Working); - } - (Ok(_site), StreamStatus::Pending) => { - println!("Pending"); - } - (Ok(_site), StreamStatus::Working) => { - debug!("Working"); - } - (Err(e), _) => { - task.change(&url, StreamStatus::Idle); - debug!(url, "{e}") - } - } - tokio::time::sleep(Duration::from_secs(30)).await; - } -} - -#[derive(Clone)] -struct DownloadActor { - client: StatelessClient, -} - -impl DownloadActor { - fn new(client: StatelessClient) -> Self { - Self { client } - } - - fn run( - &mut self, - list: Vec, - extensions: StreamActorMap, - // client: StatelessClient, - ) { - for streamer in list { - // let Some(extractor) = find_extractor(&streamer.url) else { continue; }; - let mut guard = extensions.write().unwrap(); - self.add_streamer(guard.deref_mut(), streamer.url) - } - println!("{:?}", extensions); - } - - fn add_streamer( - &self, - map: &mut AnyMap<(Cycle, JoinHandle<()>)>, - url: String, - // client: StatelessClient, - ) { - let Some(extractor) = find_extractor(&url) else { - return; - }; - let _entry = map - .entry(extractor.as_any().type_id()) - .and_modify(|(cy, _)| cy.insert(url.clone(), StreamStatus::Idle)) - .or_insert_with(|| { - let cycle = Cycle::new(indexmap![url => StreamStatus::Idle]); - let task = cycle.clone(); - let client = self.client.clone(); - let handle = - tokio::spawn(async move { start_monitor(task, extractor, client).await }); - (cycle, handle) - }); - } -} - -type StreamActorMap = Arc, JoinHandle<()>)>>>; - -#[derive(Clone)] -pub struct DownloadActorHandle { - platform_map: StreamActorMap, - // client: StatelessClient, - actor: DownloadActor, -} - -impl DownloadActorHandle { - pub fn new(list: Vec, client: StatelessClient) -> Self { - let mut actor = DownloadActor::new(client); - let platform_map = Arc::new(RwLock::new(HashMap::default())); - let platform = Arc::clone(&platform_map); - // let client_c = client.clone(); - actor.run(list, platform); - Self { - platform_map, - actor, - } - } - - pub fn add_streamer(&self, url: &str) { - self.actor.add_streamer( - self.platform_map.write().unwrap().deref_mut(), - url.to_string(), - // self.client.clone(), - ); - } - - pub fn get_streamers(&self) -> HashMap { - let read_guard = self.platform_map.read().unwrap(); - let mut map = HashMap::new(); - for (key, val) in read_guard.iter() { - map.extend(val.0.get_all()); - } - map - } - - pub fn update_streamer(&self, url: &str) { - self.remove_streamer(url); - self.add_streamer(url); - } - - pub fn remove_streamer(&self, url: &str) { - find_extractor(url).and_then(|extractor| { - self.platform_map - .read() - .unwrap() - .get(&extractor.as_any().type_id()) - .and_then(|(cy, join_handle)| { - let mut guard = cy.write(); - if guard.len() <= 1 { - join_handle.abort() - } - guard.swap_remove(url) - }) - }); - } -} diff --git a/crates/stream-gears/src/server/core/download_manager.rs b/crates/biliup-cli/src/server/core/download_manager.rs similarity index 96% rename from crates/stream-gears/src/server/core/download_manager.rs rename to crates/biliup-cli/src/server/core/download_manager.rs index 5830e88..c8ae1bc 100644 --- a/crates/stream-gears/src/server/core/download_manager.rs +++ b/crates/biliup-cli/src/server/core/download_manager.rs @@ -3,6 +3,7 @@ use crate::server::core::monitor::{Monitor, RoomsHandle}; use crate::server::core::plugin::{DownloadPlugin, StreamInfo}; use crate::server::errors::{AppError, AppResult}; use crate::server::infrastructure::context::{Worker, WorkerStatus}; +use crate::server::infrastructure::models::hook_step::process_video; use async_channel::{Receiver, Sender, bounded}; use biliup::bilibili::{BiliBili, Studio, Video}; use biliup::client::StatelessClient; @@ -19,7 +20,6 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use tokio::task::JoinHandle; use tracing::{error, info, warn}; -use crate::server::infrastructure::models::hook_step::process_video; pub struct DownloadManager { pub monitor: Mutex>>, @@ -39,7 +39,7 @@ impl DownloadManager { } } - pub(crate) fn ensure_monitor(&self) -> Arc { + pub fn ensure_monitor(&self) -> Arc { // Monitor::new(self.plugin.name(), url) self.monitor .lock() @@ -91,10 +91,10 @@ impl DActor { move |event: SegmentEvent| { if event.segment_index == 0 { match sender.force_send(UploaderMessage::SegmentEvent( - event, - rx.clone(), - room.clone(), - )) { + event, + rx.clone(), + room.clone(), + )) { Ok(Some(ret)) => { warn!(SegmentEvent = ?ret, "replace an existing message in the channel"); } @@ -175,7 +175,8 @@ impl UActor { &line, &event.file_path, ) - .await.map_err(|e| error!(e=?e)) + .await + .map_err(|e| error!(e=?e)) { videos.push(video); } @@ -189,7 +190,8 @@ impl UActor { &line, &se.file_path, ) - .await.map_err(|e| error!(e=?e)) + .await + .map_err(|e| error!(e=?e)) { videos.push(video); } @@ -254,7 +256,9 @@ impl UActor { let streamer = worker.get_streamer().await.unwrap(); if let Some(post_processor) = streamer.postprocessor { for video_path in &video_paths { - process_video(video_path, &post_processor).await.map_err(|e| error!(e=?e)); + process_video(video_path, &post_processor) + .await + .map_err(|e| error!(e=?e)); } } } diff --git a/crates/stream-gears/src/server/core/downloader.rs b/crates/biliup-cli/src/server/core/downloader.rs similarity index 100% rename from crates/stream-gears/src/server/core/downloader.rs rename to crates/biliup-cli/src/server/core/downloader.rs diff --git a/crates/stream-gears/src/server/core/downloader/ffmpeg_downloader.rs b/crates/biliup-cli/src/server/core/downloader/ffmpeg_downloader.rs similarity index 100% rename from crates/stream-gears/src/server/core/downloader/ffmpeg_downloader.rs rename to crates/biliup-cli/src/server/core/downloader/ffmpeg_downloader.rs diff --git a/crates/stream-gears/src/server/core/downloader/stream_gears.rs b/crates/biliup-cli/src/server/core/downloader/stream_gears.rs similarity index 100% rename from crates/stream-gears/src/server/core/downloader/stream_gears.rs rename to crates/biliup-cli/src/server/core/downloader/stream_gears.rs diff --git a/crates/biliup-cli/src/server/core/live_streamers.rs b/crates/biliup-cli/src/server/core/live_streamers.rs deleted file mode 100644 index aadd880..0000000 --- a/crates/biliup-cli/src/server/core/live_streamers.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::server::core::StreamStatus; -use async_trait::async_trait; -use biliup::uploader::bilibili::Studio; -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use std::sync::Arc; - -/// Similar to above, we want to keep a reference count across threads so we can manage our connection pool. -pub type DynLiveStreamersRepository = Arc; -pub type DynDownloadRecordsRepository = Arc; -pub type DynVideosRepository = Arc; - -#[async_trait] -pub trait LiveStreamersRepository { - async fn create_streamer( - &self, - entity: AddLiveStreamerDto, - ) -> anyhow::Result; - async fn delete_streamer(&self, id: i64) -> anyhow::Result<()>; - async fn update_streamer( - &self, - entity: LiveStreamerEntity, - ) -> anyhow::Result; - async fn get_streamers(&self) -> anyhow::Result>; - async fn get_streamer_by_url(&self, url: &str) -> anyhow::Result; - async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result; -} - -#[async_trait] -pub trait DownloadRecordsRepository { - async fn create(&self, entity: DownloadRecords) -> anyhow::Result; - // async fn delete(&self, id: i64) -> anyhow::Result<()>; - // async fn update(&self, entity: DownloadRecords) -> anyhow::Result; - async fn get_all(&self) -> anyhow::Result>; - // async fn get_by_id(&self, id: i64) -> anyhow::Result; -} - -#[async_trait] -pub trait VideosRepository { - async fn create(&self, entity: Videos) -> anyhow::Result; - // async fn delete(&self, id: i64) -> anyhow::Result<()>; - async fn update(&self, entity: Videos) -> anyhow::Result; - // async fn get_all(&self) -> anyhow::Result>; - async fn get_by_id(&self, id: i64) -> anyhow::Result; -} - -#[derive(Serialize, Deserialize, FromRow)] -pub struct LiveStreamerEntity { - pub id: i64, - pub url: String, - pub remark: String, - pub filename: String, - pub split_time: Option, - pub split_size: Option, - pub upload_id: Option, -} - -#[derive(FromRow)] -pub struct DownloadRecords { - pub id: i64, - pub title: String, - pub live_id: i64, - pub upload_id: Option, - pub status: String, -} - -#[derive(FromRow)] -pub struct Videos { - pub id: i64, - pub start_time: chrono::DateTime, - pub end_time: chrono::DateTime, - pub path: String, - pub record_id: i64, - pub status: String, -} - -impl LiveStreamerEntity { - pub fn into_dto(self) -> LiveStreamerDto { - LiveStreamerDto { - id: self.id, - url: self.url, - remark: self.remark, - filename: self.filename, - split_time: self.split_time.map(|t| t as u64), - split_size: self.split_size.map(|s| s as u64), - status: Default::default(), - } - } -} - -/// A reference counter for our user service allows us safely pass instances user utils -/// around which themselves depend on the user repostiory, and ultimately, our Posgres connection pool. -pub type DynLiveStreamersService = Arc; - -#[async_trait] -pub trait LiveStreamersService { - async fn add_streamer(&self, request: AddLiveStreamerDto) -> anyhow::Result; - async fn get_streamer_by_url(&self, url: &str) -> anyhow::Result; - async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result; - async fn get_streamers(&self) -> anyhow::Result>; - async fn get_studio_by_url(&self, url: &str) -> anyhow::Result>; -} - -#[derive(Serialize, Deserialize, Default, Debug)] -pub struct AddLiveStreamerDto { - pub url: String, - pub remark: String, - pub filename: String, - pub split_time: Option, - pub split_size: Option, - pub upload_id: Option, -} - -#[derive(Serialize, Deserialize, Default, Debug)] -pub struct LiveStreamerDto { - pub id: i64, - pub url: String, - pub remark: String, - pub filename: String, - pub split_time: Option, - pub split_size: Option, - pub status: StreamStatus, -} diff --git a/crates/biliup-cli/src/server/core/main_loop.rs b/crates/biliup-cli/src/server/core/main_loop.rs deleted file mode 100644 index 6f656a9..0000000 --- a/crates/biliup-cli/src/server/core/main_loop.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::server::core::StreamStatus; -use crate::server::core::live_streamers::Videos; -use crate::server::core::util::Cycle; -use anyhow::Result; -use biliup::downloader::extractor::Site; -use tokio::sync::mpsc::Sender; -use tokio::sync::mpsc::{Receiver, channel}; -use tokio::task::JoinHandle; - -/// This struct is used by client actors to send messages to the main loop. The -/// message type is `ToServer`. -#[derive(Clone, Debug)] -pub struct ServerHandle { - chan: Sender, -} -impl ServerHandle { - pub async fn send(&mut self, msg: ToMain) { - if self.chan.send(msg).await.is_err() { - panic!("Main loop has shut down."); - } - } -} - -/// The message type used when a client actor sends messages to the main loop. -pub enum ToMain { - NewRecording(Site, Cycle), - FileClosed(Videos), - // FatalError(io::Error), -} - -pub fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) { - let (send, recv) = channel(64); - - let handle = ServerHandle { chan: send }; - - let join = tokio::spawn(async move { - let res = main_loop(recv).await; - match res { - Ok(()) => {} - Err(err) => { - eprintln!("Oops {}.", err); - } - } - }); - - (handle, join) -} - -async fn main_loop(mut recv: Receiver) -> Result<()> { - while let Some(msg) = recv.recv().await { - match msg { - ToMain::NewRecording(_site, _task) => {} - ToMain::FileClosed(_) => {} - } - } - - Ok(()) -} - -// async fn recording( -// url: &str, -// mut site: Site, -// task: Cycle, -// client: StatelessClient, -// live_streamers_service: DynLiveStreamersService, -// ) { -// println!("Idle\n {url} \n{site}"); -// let (filename, split_size, split_time) = if let Ok(LiveStreamerDto { -// filename, -// split_size, -// split_time, -// .. -// }) = -// live_streamers_service.get_streamer_by_url(url).await -// { -// (filename, split_size, split_time.map(Duration::from_secs)) -// } else { -// ("./video/%Y-%m-%d/%H_%M_%S{title}".to_string(), None, None) -// }; -// -// logging_spawn({ -// // let client = client.clone(); -// let url = url.to_string(); -// let task = task.clone(); -// // let live_streamers_service = live_streamers_service.clone(); -// async move { -// let hook = live_streamers_service -// .get_studio_by_url(&url) -// .await -// .unwrap_or_default() -// .map(|studio| -> Box { -// let handle = UploadActorHandle::new(client, studio); -// Box::new(move |file_name| { -// if let Ok(metadata) = -// std::fs::metadata(file_name).map_err(|err| error!("{}", err)) -// { -// if metadata.len() > 10 * 1024 * 1024 { -// info!("开始上传: {}", file_name); -// handle.send_file_path(file_name); -// } -// } -// }) -// }); -// if hook.is_none() { -// debug!(url = %url, "upload template not set."); -// } -// let segmentable = Segmentable::new(split_time, split_size); -// // let segmentable = Segmentable::new( None, Some(16*1024*1024)); -// site.download(&filename, segmentable, hook).await?; -// task.change(&url, StreamStatus::Idle); -// Ok::<_, Box>(()) -// } -// }); -// task.change(url, StreamStatus::Working); -// } diff --git a/crates/stream-gears/src/server/core/mod.rs b/crates/biliup-cli/src/server/core/mod.rs similarity index 100% rename from crates/stream-gears/src/server/core/mod.rs rename to crates/biliup-cli/src/server/core/mod.rs diff --git a/crates/stream-gears/src/server/core/monitor.rs b/crates/biliup-cli/src/server/core/monitor.rs similarity index 100% rename from crates/stream-gears/src/server/core/monitor.rs rename to crates/biliup-cli/src/server/core/monitor.rs diff --git a/crates/biliup-cli/src/server/core/plugin.rs b/crates/biliup-cli/src/server/core/plugin.rs new file mode 100644 index 0000000..3436f9c --- /dev/null +++ b/crates/biliup-cli/src/server/core/plugin.rs @@ -0,0 +1,61 @@ +use crate::server::core::download_manager::{ActorHandle, DownloadManager}; +use crate::server::core::downloader::ffmpeg_downloader::FfmpegDownloader; +use crate::server::core::downloader::stream_gears::StreamGears; +use crate::server::core::downloader::{DownloadConfig, Downloader, DownloaderType}; +use crate::server::errors::{AppError, AppResult}; +use crate::server::infrastructure::context::Worker; +use crate::server::util::{Recorder, media_ext_from_url, parse_time}; +use async_trait::async_trait; +use biliup::downloader::util::Segmentable; +use error_stack::{Report, ResultExt}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use time::OffsetDateTime; +use tracing::{debug, info}; + +// Stream information structures +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StreamInfo { + pub name: String, + pub url: String, + pub raw_stream_url: String, + pub title: String, + #[serde(with = "time::serde::rfc3339")] + pub date: OffsetDateTime, // 保存 Python 的时间对象(如 time.struct_time) + // pub end_time: PyObject, // 同上 + pub live_cover_path: Option, + pub platform: String, + pub stream_headers: HashMap, + pub suffix: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum StreamStatus { + Live { stream_info: StreamInfo }, + Offline, + Unknown, +} + +#[async_trait] +pub trait DownloadBase: Send + Sync { + async fn check_stream(&self) -> Result>; + async fn get_stream_info(&self) -> Result>; + async fn download(&self, output_path: impl AsRef) -> Result<(), Report>; + fn should_record(&self, room_title: &str) -> bool; + fn get_platform_name(&self) -> &'static str; +} + +#[async_trait] +pub trait DownloadPlugin { + fn matches(&self, url: &str) -> bool; + async fn check_status(&self, url: &str) -> Result>; + async fn create_downloader( + &self, + stream_info: &StreamInfo, + worker: &Worker, + ) -> AppResult>; + + fn name(&self) -> &str; +} diff --git a/crates/biliup-cli/src/server/core/upload_actor.rs b/crates/biliup-cli/src/server/core/upload_actor.rs deleted file mode 100644 index bddd352..0000000 --- a/crates/biliup-cli/src/server/core/upload_actor.rs +++ /dev/null @@ -1,156 +0,0 @@ -use biliup::client::StatelessClient; -use biliup::error::Kind; -use biliup::uploader::VideoFile; -use biliup::uploader::bilibili::{BiliBili, Studio, Vid, Video}; -use biliup::uploader::credential::login_by_cookies; -use biliup::uploader::line::Line; -use futures::StreamExt; -use std::path::{Path, PathBuf}; - -use std::time::Instant; -use tokio::sync::mpsc; -use tracing::{error, info}; - -struct UploadActor { - receiver: mpsc::UnboundedReceiver, - client: StatelessClient, - studio: Studio, - vid: Option, -} -enum ActorMessage { - Upload { path: PathBuf }, -} - -impl UploadActor { - fn new( - studio: Studio, - client: StatelessClient, - receiver: mpsc::UnboundedReceiver, - ) -> Self { - UploadActor { - receiver, - client, - studio, - vid: None, - } - } - - async fn upload( - &self, - video_paths: &[&Path], - bili: &BiliBili, - line: Line, - limit: usize, - ) -> biliup::error::Result> { - let mut videos = Vec::new(); - for video_path in video_paths { - println!("{:?}", video_path.canonicalize()?.to_str()); - info!("{line:?}"); - let video_file = VideoFile::new(video_path)?; - let total_size = video_file.total_size; - let file_name = video_file.file_name.clone(); - let uploader = line.pre_upload(bili, video_file).await?; - - let instant = Instant::now(); - - let video = uploader - .upload(self.client.clone(), limit, |vs| { - vs.map(|vs| { - let chunk = vs?; - let len = chunk.len(); - Ok((chunk, len)) - }) - }) - .await?; - let t = instant.elapsed().as_millis(); - info!( - "Upload completed: {file_name} => cost {:.2}s, {:.2} MB/s.", - t as f64 / 1000., - total_size as f64 / 1000. / t as f64 - ); - videos.push(video); - } - Ok(videos) - } - - async fn handle_message(&mut self, msg: ActorMessage) -> biliup::error::Result<()> { - match msg { - ActorMessage::Upload { path } => { - let bili = login_by_cookies("cookies.json", None).await?; - let videos = self - .upload(&[path.as_path()], &bili, Default::default(), 3) - .await?; - - if let Some(vid) = &self.vid { - let mut studio = bili.studio_data(vid, None).await?; - studio.videos.extend(videos); - bili.edit(&studio, None).await?; - } else { - let studio = &mut self.studio; - studio.videos.extend(videos); - if studio.title.is_empty() { - studio.title = path - .file_stem() - .and_then(|fname| fname.to_str()) - .unwrap_or_default() - .to_string(); - } - if studio.tag.is_empty() { - studio.tag = bili - .recommend_tag(studio.tid, &studio.title, "") - .await? - .get(0) - .and_then(|tag| tag.get("tag")) - .and_then(|tag| tag.as_str()) - .unwrap_or("录播") - .to_string(); - } - let result = bili.submit(studio, None).await?; - self.vid = Some( - result - .data - .as_ref() - .and_then(|data| data.get("bvid")) - .and_then(|vid| vid.as_str()) - .map(|vid| Vid::Bvid(String::from(vid))) - .ok_or_else(|| Kind::Custom(format!("{:?}", result)))?, - ); - } - } - } - Ok(()) - } -} - -async fn run_download_actor(mut actor: UploadActor) { - while let Some(msg) = actor.receiver.recv().await { - match actor.handle_message(msg).await { - Ok(_) => {} - Err(e) => { - error!("{}", e) - } - }; - } -} - -#[derive(Clone)] -pub struct UploadActorHandle { - sender: mpsc::UnboundedSender, -} - -impl UploadActorHandle { - pub fn new(client: StatelessClient, studio: Studio) -> Self { - let (sender, receiver) = mpsc::unbounded_channel(); - let actor = UploadActor::new(studio, client, receiver); - tokio::spawn(run_download_actor(actor)); - - Self { sender } - } - - pub fn send_file_path>(&self, path: T) { - let msg = ActorMessage::Upload { - path: PathBuf::from(path.as_ref()), - }; - let _ = self.sender.send(msg); - } -} diff --git a/crates/biliup-cli/src/server/core/upload_streamers.rs b/crates/biliup-cli/src/server/core/upload_streamers.rs deleted file mode 100644 index 2a784d5..0000000 --- a/crates/biliup-cli/src/server/core/upload_streamers.rs +++ /dev/null @@ -1,129 +0,0 @@ -use async_trait::async_trait; -use biliup::uploader::bilibili::Studio; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use std::sync::Arc; - -pub type DynUploadStreamersRepository = Arc; -pub type DynUploadRecordsRepository = Arc; - -#[async_trait] -pub trait UploadStreamersRepository { - async fn create_streamer(&self, studio: StudioEntity) -> anyhow::Result; - async fn delete_streamer(&self, id: i64) -> anyhow::Result<()>; - async fn update_streamer(&self, studio: StudioEntity) -> anyhow::Result; - async fn get_streamers(&self) -> anyhow::Result>; - async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result; -} - -#[async_trait] -pub trait UploadRecordsRepository { - async fn create(&self, entity: UploadRecords) -> anyhow::Result; - async fn delete(&self, id: i64) -> anyhow::Result<()>; - // async fn update(&self, entity: UploadRecords) -> anyhow::Result; - async fn get_all(&self) -> anyhow::Result>; - async fn get_by_id(&self, id: i64) -> anyhow::Result; -} - -// #[serde(default)] -#[derive(FromRow, Serialize, Deserialize)] -pub struct StudioEntity { - #[serde(default)] - pub id: i64, - pub template_name: String, - pub user: i64, - pub copyright: u8, - pub source: String, - pub tid: u16, - pub cover: String, - pub title: String, - pub desc: String, - pub dynamic: String, - pub tag: String, - pub dtime: Option, - pub interactive: u8, - pub mission_id: Option, - pub dolby: u8, - pub lossless_music: u8, - pub no_reprint: u8, - pub charging_pay: u8, - pub up_selection_reply: bool, - pub up_close_reply: bool, - pub up_close_danmu: bool, -} - -impl StudioEntity { - pub fn into_dto(self) -> Studio { - Studio { - copyright: self.copyright, - source: self.source, - tid: self.tid, - cover: self.cover, - title: self.title, - desc_format_id: 0, - desc: self.desc, - dynamic: self.dynamic, - subtitle: Default::default(), - tag: self.tag, - videos: vec![], - desc_v2: None, - dtime: self.dtime, - open_subtitle: false, - interactive: self.interactive, - mission_id: self.mission_id, - dolby: self.dolby, - lossless_music: self.lossless_music, - no_reprint: self.no_reprint, - charging_pay: self.charging_pay, - aid: None, - up_selection_reply: self.up_selection_reply, - up_close_reply: self.up_close_reply, - up_close_danmu: self.up_close_danmu, - extra_fields: None, - } - } - - // pub fn into_profile(self, following: bool) -> ProfileDto { - // ProfileDto { - // username: self.username, - // bio: self.bio, - // image: self.image, - // following, - // } - // } -} - -#[derive(FromRow, Serialize, Deserialize)] -pub struct UploadRecords { - pub id: i64, - pub identity: String, - pub status: String, -} - -impl Default for StudioEntity { - fn default() -> Self { - StudioEntity { - id: 0, - template_name: "".to_string(), - user: 0, - copyright: 1, - source: "".to_string(), - tid: 0, - cover: "".to_string(), - title: "".to_string(), - desc: "".to_string(), - dynamic: "".to_string(), - tag: "".to_string(), - dtime: None, - interactive: 0, - mission_id: None, - dolby: 0, - lossless_music: 0, - no_reprint: 0, - charging_pay: 0, - up_selection_reply: false, - up_close_reply: false, - up_close_danmu: false, - } - } -} diff --git a/crates/biliup-cli/src/server/core/users.rs b/crates/biliup-cli/src/server/core/users.rs deleted file mode 100644 index 6a664da..0000000 --- a/crates/biliup-cli/src/server/core/users.rs +++ /dev/null @@ -1,23 +0,0 @@ -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; -use std::sync::Arc; - -pub type DynUsersRepository = Arc; - -#[derive(FromRow, Serialize, Deserialize)] -pub struct User { - pub id: i64, - pub name: String, - pub value: String, - pub platform: String, -} - -#[async_trait] -pub trait UsersRepository { - async fn create_user(&self, user: User) -> anyhow::Result; - async fn get_users(&self) -> anyhow::Result>; - async fn delete_user(&self, id: i64) -> anyhow::Result<()>; - async fn update_user(&self, user: User) -> anyhow::Result; - async fn get_user_by_id(&self, id: i64) -> anyhow::Result; -} diff --git a/crates/biliup-cli/src/server/core/util.rs b/crates/biliup-cli/src/server/core/util.rs deleted file mode 100644 index 5dc7071..0000000 --- a/crates/biliup-cli/src/server/core/util.rs +++ /dev/null @@ -1,114 +0,0 @@ -use indexmap::IndexMap; -use std::any::TypeId; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::hash::{BuildHasherDefault, Hasher}; -use std::sync::{Arc, RwLock, RwLockWriteGuard}; -use tokio::task::JoinHandle; -use tracing::error; - -pub type AnyMap = HashMap>; - -// With TypeIds as keys, there's no need to hash them. They are already hashes -// themselves, coming from the compiler. The IdHasher just holds the u64 of -// the TypeId, and then returns it, instead of doing any bit fiddling. -#[derive(Default)] -pub struct IdHasher(u64); - -impl Hasher for IdHasher { - #[inline] - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, _: &[u8]) { - unreachable!("TypeId calls write_u64"); - } - - #[inline] - fn write_u64(&mut self, id: u64) { - self.0 = id; - } -} - -#[derive(Clone)] -pub struct Cycle { - map: Arc>>, -} - -impl Cycle { - pub fn new(map: IndexMap) -> Self { - if map.is_empty() { - unreachable!("list must not be empty") - } - Self { - map: Arc::new(RwLock::new(map)), - } - } - - pub fn get(&self, i: &mut usize) -> (String, T) { - match self.map.read().unwrap().get_index(*i) { - Some((k, v)) => { - *i += 1; - (k.clone(), *v) - } - None => { - *i = 0; - self.get(i) - } - } - } - - pub fn get_all(&self) -> IndexMap { - let read_guard = self.map.read().unwrap(); - read_guard.clone() - } - - pub fn replace(&mut self, map: IndexMap) { - if map.is_empty() { - unreachable!("list must not be empty") - } - *self.map.write().unwrap() = map; - } - - pub fn write(&self) -> RwLockWriteGuard<'_, IndexMap> { - self.map.write().unwrap() - } - - pub fn change(&self, key: &str, value: T) { - self.write() - .entry(String::from(key)) - .and_modify(|mt| *mt = value); - } - - pub fn insert(&self, key: String, value: T) { - self.map.write().unwrap().insert(key, value); - } -} - -impl Debug for Cycle { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{{")?; - let mut temp = Vec::new(); - for (k, v) in self.map.read().unwrap().iter() { - temp.push(format!(" {{ {}: {:?} }}", k, v)); - } - write!(f, "{}", temp.join(","))?; - write!(f, " }}") - } -} - -pub fn logging_spawn(future: T) -> JoinHandle -where - T: Future>> + Send + 'static, - T::Output: Send + 'static, -{ - tokio::spawn(async move { - future.await.map_err(|e| { - error!("spawn error: {}", e); - e - }) - }) -} diff --git a/crates/biliup-cli/src/server/errors.rs b/crates/biliup-cli/src/server/errors.rs index 89a4675..3849e61 100644 --- a/crates/biliup-cli/src/server/errors.rs +++ b/crates/biliup-cli/src/server/errors.rs @@ -1,103 +1,34 @@ -use std::borrow::Cow; -use std::{collections::HashMap, fmt::Debug}; - -use axum::response::Response; -use axum::{Json, http::StatusCode, response::IntoResponse}; +use axum::Json; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use error_stack::Report; use serde::{Deserialize, Serialize}; - +use std::collections::HashMap; use thiserror::Error; use tracing::log::error; -pub type AppResult = Result; - -pub type ConduitErrorMap = HashMap, Vec>>; - #[derive(Error, Debug)] pub enum AppError { - #[error("authentication is required to access this resource")] - Unauthorized, - #[error("username or password is incorrect")] - InvalidLoginAttempt, - #[error("user does not have privilege to access this resource")] - Forbidden, + #[error("Unknown Error")] + Unknown, + #[error("{0}")] - NotFound(String), - #[error("{0}")] - ApplicationStartup(String), - #[error("{0}")] - BadRequest(String), - #[error("unexpected error has occurred")] - InternalServerError, - #[error("{0}")] - InternalServerErrorWithContext(String), - #[error("{0}")] - ObjectConflict(String), - #[error("unprocessable request has occurred")] - UnprocessableEntity { errors: ConduitErrorMap }, - #[error(transparent)] - AxumJsonRejection(#[from] axum::extract::rejection::JsonRejection), - #[error(transparent)] - AnyhowError(#[from] anyhow::Error), - #[error(transparent)] - DownloadError(#[from] biliup::downloader::error::Error), - #[error(transparent)] - UploadError(#[from] biliup::error::Kind), - #[error(transparent)] - ReqwestError(#[from] reqwest::Error), - #[error(transparent)] - OrmliteError(#[from] ormlite::Error), - #[error(transparent)] - SerdeJsonError(#[from] serde_json::Error), + Custom(String), } -impl IntoResponse for AppError { - fn into_response(self) -> Response { - error!("AppError{}", AppError = self); - let (status, error_message) = match self { - Self::InternalServerErrorWithContext(err) => (StatusCode::INTERNAL_SERVER_ERROR, err), - Self::NotFound(err) => (StatusCode::NOT_FOUND, err), - Self::ObjectConflict(err) => (StatusCode::CONFLICT, err), - Self::InvalidLoginAttempt => ( - StatusCode::BAD_REQUEST, - Self::InvalidLoginAttempt.to_string(), - ), - Self::Unauthorized => (StatusCode::UNAUTHORIZED, Self::Unauthorized.to_string()), - Self::AnyhowError(err) => 'e: { - for cause in err.chain() { - if let Some(error) = cause.downcast_ref::>() - { - println!("121212cause: {}", error.message()); - break 'e (StatusCode::BAD_REQUEST, error.to_string()); - } - println!("{}", cause); - } - (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) - } - AppError::Forbidden - | AppError::ApplicationStartup(_) - | AppError::BadRequest(_) - | AppError::InternalServerError - | AppError::UnprocessableEntity { .. } - | AppError::AxumJsonRejection(_) - | AppError::DownloadError(_) - | AppError::UploadError(_) - | AppError::SerdeJsonError(_) - | AppError::ReqwestError(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - String::from("unexpected error occurred"), - ), - AppError::OrmliteError(ormlite) => ( - StatusCode::INTERNAL_SERVER_ERROR, - String::from("OrmliteError occurred"), - ), - }; +pub fn report_to_response(report: impl Into>) -> Response { + let report = report.into(); + let (status, error_message) = match report.downcast_ref::() { + Some(AppError::Unknown) => (StatusCode::INTERNAL_SERVER_ERROR, report.to_string()), + Some(AppError::Custom(msg)) => (StatusCode::INTERNAL_SERVER_ERROR, msg.into()), + _ => (StatusCode::INTERNAL_SERVER_ERROR, report.to_string()), + }; + tracing::error!(error = ?report); + // I'm not a fan of the error specification, so for the sake of consistency, + // serialize singular errors as a map of vectors similar to the 422 validation responses + let body = Json(ApiError::new(error_message)); - // I'm not a fan of the error specification, so for the sake of consistency, - // serialize singular errors as a map of vectors similar to the 422 validation responses - let body = Json(ApiError::new(error_message)); - - (status, body).into_response() - } + (status, body).into_response() } #[derive(Debug, Deserialize, Serialize)] @@ -121,3 +52,5 @@ impl ApiError { Self { errors, message } } } + +pub type AppResult = Result>; diff --git a/crates/biliup-cli/src/server/event_manager.rs b/crates/biliup-cli/src/server/event_manager.rs deleted file mode 100644 index 8b3901a..0000000 --- a/crates/biliup-cli/src/server/event_manager.rs +++ /dev/null @@ -1,251 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{RwLock, mpsc}; -use tracing::{debug, error, info}; - -/// 事件类型枚举,对应Python版本的事件 -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum EventType { - PreDownload, - Download, - Downloaded, - Upload, - Uploaded, -} - -/// 事件数据结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum BiliUpEvent { - PreDownload { name: String, url: String }, - Download { name: String, url: String }, - Downloaded { stream_info: StreamInfo }, - Upload { stream_info: StreamInfo }, - Uploaded { files: Vec }, -} - -/// 流信息结构,对应Python版本的stream_info -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StreamInfo { - pub name: String, - pub url: String, - pub title: Option, - pub date: chrono::DateTime, - pub end_time: Option>, - pub live_cover_path: Option, - pub is_download: bool, - pub platform: String, - pub database_row_id: Option, -} - -/// 文件信息结构 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FileInfo { - pub video: String, - pub danmaku: Option, -} - -/// 事件处理器trait -#[async_trait] -pub trait EventHandler: Send + Sync { - async fn handle(&self, event: BiliUpEvent) -> Result>; - fn event_type(&self) -> EventType; -} - -/// 事件管理器上下文,对应Python版本的context -#[derive(Debug, Default)] -pub struct EventContext { - pub url_upload_count: HashMap, - pub upload_filename: Vec, - pub file_upload_count: HashMap, - pub url_status: HashMap, // 0: idle, 1: downloading, 2: stopped -} - -/// 事件管理器,对应Python版本的EventManager -pub struct EventManager { - context: Arc>, - handlers: Arc>>>>, - event_tx: mpsc::UnboundedSender, - event_rx: Arc>>>, -} - -impl EventManager { - pub fn new() -> Self { - let (event_tx, event_rx) = mpsc::unbounded_channel(); - - Self { - context: Arc::new(RwLock::new(EventContext::default())), - handlers: Arc::new(RwLock::new(HashMap::new())), - event_tx, - event_rx: Arc::new(RwLock::new(Some(event_rx))), - } - } - - /// 注册事件处理器 - pub async fn register_handler(&self, handler: Box) { - let event_type = handler.event_type(); - let mut handlers = self.handlers.write().await; - handlers - .entry(event_type) - .or_insert_with(Vec::new) - .push(handler); - } - - /// 发送事件 - pub async fn send_event(&self, event: BiliUpEvent) -> Result<()> { - self.event_tx - .send(event) - .map_err(|e| anyhow::anyhow!("Failed to send event: {}", e))?; - Ok(()) - } - - /// 获取上下文的只读访问 - pub async fn get_context(&self) -> tokio::sync::RwLockReadGuard { - self.context.read().await - } - - /// 获取上下文的写访问 - pub async fn get_context_mut(&self) -> tokio::sync::RwLockWriteGuard { - self.context.write().await - } - - /// 启动事件循环 - pub async fn start(&self) -> Result<()> { - let mut event_rx = { - let mut rx_guard = self.event_rx.write().await; - rx_guard - .take() - .ok_or_else(|| anyhow::anyhow!("Event manager already started"))? - }; - - info!("Event manager started"); - - while let Some(event) = event_rx.recv().await { - if let Err(e) = self.process_event(event).await { - error!("Failed to process event: {}", e); - } - } - - Ok(()) - } - - /// 处理单个事件 - async fn process_event(&self, event: BiliUpEvent) -> Result<()> { - let event_type = match &event { - BiliUpEvent::PreDownload { .. } => EventType::PreDownload, - BiliUpEvent::Download { .. } => EventType::Download, - BiliUpEvent::Downloaded { .. } => EventType::Downloaded, - BiliUpEvent::Upload { .. } => EventType::Upload, - BiliUpEvent::Uploaded { .. } => EventType::Uploaded, - }; - - debug!("Processing event: {:?}", event_type); - - let handlers = self.handlers.read().await; - if let Some(event_handlers) = handlers.get(&event_type) { - for handler in event_handlers { - match handler.handle(event.clone()).await { - Ok(Some(next_event)) => { - // 如果处理器返回了新事件,继续发送 - if let Err(e) = self.send_event(next_event).await { - error!("Failed to send follow-up event: {}", e); - } - } - Ok(None) => { - // 事件处理完成,无后续事件 - } - Err(e) => { - error!("Handler failed to process event: {}", e); - } - } - } - } - - Ok(()) - } - - /// 更新URL状态 - pub async fn update_url_status(&self, url: &str, status: i32) { - let mut context = self.context.write().await; - context.url_status.insert(url.to_string(), status); - } - - /// 获取URL状态 - pub async fn get_url_status(&self, url: &str) -> i32 { - let context = self.context.read().await; - context.url_status.get(url).copied().unwrap_or(0) - } - - /// 增加上传计数 - pub async fn increment_upload_count(&self, url: &str) { - let mut context = self.context.write().await; - *context.url_upload_count.entry(url.to_string()).or_insert(0) += 1; - } - - /// 减少上传计数 - pub async fn decrement_upload_count(&self, url: &str) { - let mut context = self.context.write().await; - if let Some(count) = context.url_upload_count.get_mut(url) { - *count = (*count - 1).max(0); - } - } - - /// 检查是否正在上传 - pub async fn is_uploading(&self, url: &str) -> bool { - let context = self.context.read().await; - context.url_upload_count.get(url).copied().unwrap_or(0) > 0 - } -} - -impl Default for EventManager { - fn default() -> Self { - Self::new() - } -} - -// 全局事件管理器实例 -#[cfg(test)] -mod tests { - use super::*; - use tokio::time::{Duration, sleep}; - - struct TestHandler { - event_type: EventType, - } - - #[async_trait] - impl EventHandler for TestHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - println!("Handling event: {:?}", event); - Ok(None) - } - - fn event_type(&self) -> EventType { - self.event_type.clone() - } - } - - #[tokio::test] - async fn test_event_manager() { - let event_manager = EventManager::new(); - - // 注册处理器 - let handler = Box::new(TestHandler { - event_type: EventType::PreDownload, - }); - event_manager.register_handler(handler).await; - - // 发送事件 - let event = BiliUpEvent::PreDownload { - name: "test".to_string(), - url: "http://test.com".to_string(), - }; - - event_manager.send_event(event).await.unwrap(); - - // 等待处理 - sleep(Duration::from_millis(100)).await; - } -} diff --git a/crates/biliup-cli/src/server/handlers.rs b/crates/biliup-cli/src/server/handlers.rs deleted file mode 100644 index 8ba0ffc..0000000 --- a/crates/biliup-cli/src/server/handlers.rs +++ /dev/null @@ -1,357 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use chrono::Utc; -use tokio::fs; -use tracing::{debug, error, info, warn}; - -use crate::server::core::live_streamers::DynLiveStreamersService; -use crate::server::event_manager::{BiliUpEvent, EventHandler, EventType, FileInfo, StreamInfo}; - -/// 预下载处理器,对应Python的pre_processor -pub struct PreDownloadHandler { - live_streamers_service: DynLiveStreamersService, -} - -impl PreDownloadHandler { - pub fn new(live_streamers_service: DynLiveStreamersService) -> Self { - Self { - live_streamers_service, - } - } - - async fn execute_preprocessor(&self, name: &str, url: &str) -> Result<()> { - // TODO: 实现预处理器执行逻辑 - // 这里应该调用配置中的preprocessor命令 - info!("Executing preprocessor for {} - {}", name, url); - Ok(()) - } -} - -#[async_trait] -impl EventHandler for PreDownloadHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - if let BiliUpEvent::PreDownload { name, url } = event { - // 检查URL状态 - - debug!("{} - {} 开播了准备下载", name, url); - - // 执行预处理器 - if let Err(e) = self.execute_preprocessor(&name, &url).await { - error!("Preprocessor execution failed: {}", e); - } - - // 发送下载事件 - return Ok(Some(BiliUpEvent::Download { name, url })); - } - Ok(None) - } - - fn event_type(&self) -> EventType { - EventType::PreDownload - } -} - -/// 下载处理器,对应Python的process -pub struct DownloadHandler { - live_streamers_service: DynLiveStreamersService, -} - -impl DownloadHandler { - pub fn new(live_streamers_service: DynLiveStreamersService) -> Self { - Self { - live_streamers_service, - } - } - - async fn perform_download(&self, name: &str, url: &str) -> Result { - // TODO: 实现实际的下载逻辑 - // 这里应该调用biliup_download等价函数 - - info!("Starting download for {} - {}", name, url); - - // 模拟下载过程 - let stream_info = StreamInfo { - name: name.to_string(), - url: url.to_string(), - title: Some(format!("{} 的直播", name)), - date: Utc::now(), - end_time: None, - live_cover_path: None, - is_download: false, - platform: "unknown".to_string(), - database_row_id: None, - }; - - Ok(stream_info) - } - - async fn check_time_range(&self, name: &str) -> bool { - // TODO: 实现时间范围检查 - // 对应Python版本的check_timerange函数 - true - } -} - -#[async_trait] -impl EventHandler for DownloadHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - if let BiliUpEvent::Download { name, url } = event { - // 设置下载状态 - let result = async { - let stream_info = self.perform_download(&name, &url).await?; - Ok::(stream_info) - } - .await; - - // 重置下载状态 - let final_status = if self.check_time_range(&name).await { - 0 - } else { - 2 - }; - - match result { - Ok(stream_info) => { - return Ok(Some(BiliUpEvent::Downloaded { stream_info })); - } - Err(e) => { - error!("下载错误: {} - {}", name, e); - } - } - } - Ok(None) - } - - fn event_type(&self) -> EventType { - EventType::Download - } -} - -/// 下载完成处理器,对应Python的processed -pub struct DownloadedHandler {} - -impl Default for DownloadedHandler { - fn default() -> Self { - Self::new() - } -} - -impl DownloadedHandler { - pub fn new() -> Self { - Self {} - } - - async fn execute_downloaded_processor(&self, stream_info: &StreamInfo) -> Result<()> { - // TODO: 实现下载后处理器执行 - info!("Executing downloaded processor for {}", stream_info.name); - Ok(()) - } - - async fn get_file_list(&self, name: &str) -> Vec { - // TODO: 实现文件列表获取,对应Python的UploadBase.file_list - vec![] - } -} - -#[async_trait] -impl EventHandler for DownloadedHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - if let BiliUpEvent::Downloaded { stream_info } = event { - let name = &stream_info.name; - let url = &stream_info.url; - - // 执行下载后处理器 - if let Err(e) = self.execute_downloaded_processor(&stream_info).await { - error!("Downloaded processor execution failed: {}", e); - } - - // 发送上传事件 - return Ok(Some(BiliUpEvent::Upload { stream_info })); - } - Ok(None) - } - - fn event_type(&self) -> EventType { - EventType::Downloaded - } -} - -/// 上传处理器,对应Python的process_upload -pub struct UploadHandler { - live_streamers_service: DynLiveStreamersService, -} - -impl UploadHandler { - pub fn new(live_streamers_service: DynLiveStreamersService) -> Self { - Self { - live_streamers_service, - } - } - - async fn get_file_list(&self, name: &str) -> Vec { - // TODO: 实现文件列表获取 - vec![] - } - - async fn perform_upload(&self, stream_info: &StreamInfo) -> Result> { - // TODO: 实现上传逻辑 - info!("Starting upload for {}", stream_info.name); - Ok(vec![]) - } - - async fn execute_webhook(&self, stream_info: &StreamInfo, error: Option<&str>) -> Result<()> { - // TODO: 实现webhook执行 - Ok(()) - } -} - -#[async_trait] -impl EventHandler for UploadHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - if let BiliUpEvent::Upload { stream_info } = event { - let url = &stream_info.url; - let name = &stream_info.name; - - // 检查是否已在上传 - - // 增加上传计数 - - let result = async { - let file_list = self.get_file_list(name).await; - if file_list.is_empty() { - debug!("无需上传"); - return Ok(vec![]); - } - - // 检查上传延迟 - - // TODO: 实现延迟检测逻辑 - - info!("开始上传: {}", name); - self.perform_upload(&stream_info).await - } - .await; - - // 减少上传计数 - match result { - Ok(files) => { - // 执行webhook - if let Err(e) = self.execute_webhook(&stream_info, None).await { - error!("Webhook execution failed: {}", e); - } - - if !files.is_empty() { - return Ok(Some(BiliUpEvent::Uploaded { files })); - } - } - Err(e) => { - error!("上传错误: {} - {}", name, e); - - // 执行错误webhook - if let Err(webhook_err) = self - .execute_webhook(&stream_info, Some(&e.to_string())) - .await - { - error!("Error webhook execution failed: {}", webhook_err); - } - } - } - } - Ok(None) - } - - fn event_type(&self) -> EventType { - EventType::Upload - } -} - -/// 上传完成处理器,对应Python的uploaded函数 -pub struct UploadedHandler {} - -impl Default for UploadedHandler { - fn default() -> Self { - Self::new() - } -} - -impl UploadedHandler { - pub fn new() -> Self { - Self {} - } - - async fn execute_postprocessor(&self, files: &[FileInfo]) -> Result<()> { - // TODO: 实现后处理器执行逻辑 - info!("Executing postprocessor for {} files", files.len()); - - for file in files { - // 处理每个文件的后处理 - self.process_file(&file.video).await?; - if let Some(ref danmaku) = file.danmaku { - self.process_file(danmaku).await?; - } - } - - Ok(()) - } - - async fn process_file(&self, file_path: &str) -> Result<()> { - // TODO: 实现文件后处理(移动、删除、运行命令等) - info!("Processing file: {}", file_path); - Ok(()) - } - - async fn remove_files(&self, files: &[FileInfo]) -> Result<()> { - for file in files { - if let Err(e) = fs::remove_file(&file.video).await { - warn!("Failed to remove file {}: {}", file.video, e); - } else { - info!("删除 - {}", file.video); - } - - if let Some(ref danmaku) = file.danmaku { - if let Err(e) = fs::remove_file(danmaku).await { - warn!("Failed to remove danmaku file {}: {}", danmaku, e); - } else { - info!("删除 - {}", danmaku); - } - } - } - Ok(()) - } -} - -#[async_trait] -impl EventHandler for UploadedHandler { - async fn handle(&self, event: BiliUpEvent) -> Result> { - if let BiliUpEvent::Uploaded { files } = event { - // 执行后处理器 - if let Err(e) = self.execute_postprocessor(&files).await { - error!("Postprocessor execution failed: {}", e); - } - - // TODO: 根据配置决定是否删除文件 - // if let Err(e) = self.remove_files(&files).await { - // error!("Failed to remove files: {}", e); - // } - } - Ok(None) - } - - fn event_type(&self) -> EventType { - EventType::Uploaded - } -} - -/// 处理器注册器,用于注册所有处理器 -pub struct HandlerRegistry; - -impl HandlerRegistry { - pub async fn register_all_handlers( - live_streamers_service: DynLiveStreamersService, - ) -> Result<()> { - // 注册所有处理器 - - info!("All event handlers registered successfully"); - Ok(()) - } -} diff --git a/crates/biliup-cli/src/server/infrastructure.rs b/crates/biliup-cli/src/server/infrastructure.rs deleted file mode 100644 index 2aa3678..0000000 --- a/crates/biliup-cli/src/server/infrastructure.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod connection_pool; -pub mod repositories; -pub mod service_register; diff --git a/crates/biliup-cli/src/server/infrastructure/connection_pool.rs b/crates/biliup-cli/src/server/infrastructure/connection_pool.rs index 1d6e80e..6b3161c 100644 --- a/crates/biliup-cli/src/server/infrastructure/connection_pool.rs +++ b/crates/biliup-cli/src/server/infrastructure/connection_pool.rs @@ -1,7 +1,8 @@ -use anyhow::{Context, Result}; -use ormlite::model::*; +use crate::server::errors::{AppError, AppResult}; +use error_stack::ResultExt; use sqlx::sqlite::SqlitePoolOptions; use sqlx::{Pool, Sqlite}; +use std::path::Path; use tracing::info; pub type ConnectionPool = Pool; @@ -9,18 +10,27 @@ pub type ConnectionPool = Pool; pub struct ConnectionManager; impl ConnectionManager { - pub async fn new_pool(path: &str) -> Result { + pub async fn new_pool(path: &str) -> AppResult { + // 创建所有父级目录(如果不存在) + if let Some(parent) = Path::new(path).parent() { + std::fs::create_dir_all(parent) + .change_context(AppError::Unknown) + .attach_lazy(|| path.to_string())?; // 创建 data/ 目录 + } let db_url = format!("sqlite://{path}"); /// Start by making a database connection. std::fs::OpenOptions::new() .write(true) .create(true) - .open(path)?; + .open(path) + .change_context(AppError::Unknown)?; let pool = SqlitePoolOptions::new() .max_connections(2) .connect(&db_url) .await - .context("error while initializing the database connection pool")?; + .change_context(AppError::Custom( + "error while initializing the database connection pool".to_string(), + ))?; /// Query builder syntax closely follows SQL syntax, translated into chained function calls. @@ -28,7 +38,9 @@ impl ConnectionManager { sqlx::migrate!() .run(&pool) .await - .context("error while running database migrations")?; + .change_context(AppError::Custom( + "error while running database migrations".to_string(), + ))?; Ok(pool) } diff --git a/crates/stream-gears/src/server/infrastructure/context.rs b/crates/biliup-cli/src/server/infrastructure/context.rs similarity index 99% rename from crates/stream-gears/src/server/infrastructure/context.rs rename to crates/biliup-cli/src/server/infrastructure/context.rs index 6e6bc8a..37dd336 100644 --- a/crates/stream-gears/src/server/infrastructure/context.rs +++ b/crates/biliup-cli/src/server/infrastructure/context.rs @@ -28,7 +28,7 @@ impl Context { #[derive(Debug)] pub struct Worker { - pub(crate) id: i64, + pub id: i64, pub url: String, pub context: Context, pub downloader_status: RwLock, diff --git a/crates/stream-gears/src/server/infrastructure/dto.rs b/crates/biliup-cli/src/server/infrastructure/dto.rs similarity index 100% rename from crates/stream-gears/src/server/infrastructure/dto.rs rename to crates/biliup-cli/src/server/infrastructure/dto.rs diff --git a/crates/stream-gears/src/server/infrastructure/mod.rs b/crates/biliup-cli/src/server/infrastructure/mod.rs similarity index 76% rename from crates/stream-gears/src/server/infrastructure/mod.rs rename to crates/biliup-cli/src/server/infrastructure/mod.rs index 18928ec..4eed25d 100644 --- a/crates/stream-gears/src/server/infrastructure/mod.rs +++ b/crates/biliup-cli/src/server/infrastructure/mod.rs @@ -3,5 +3,5 @@ pub mod context; pub mod dto; pub mod models; pub mod repositories; -pub(crate) mod service_register; +pub mod service_register; pub mod users; diff --git a/crates/stream-gears/src/server/infrastructure/models.rs b/crates/biliup-cli/src/server/infrastructure/models.rs similarity index 100% rename from crates/stream-gears/src/server/infrastructure/models.rs rename to crates/biliup-cli/src/server/infrastructure/models.rs diff --git a/crates/stream-gears/src/server/infrastructure/models/hook_step.rs b/crates/biliup-cli/src/server/infrastructure/models/hook_step.rs similarity index 96% rename from crates/stream-gears/src/server/infrastructure/models/hook_step.rs rename to crates/biliup-cli/src/server/infrastructure/models/hook_step.rs index 327d582..70d9197 100644 --- a/crates/stream-gears/src/server/infrastructure/models/hook_step.rs +++ b/crates/biliup-cli/src/server/infrastructure/models/hook_step.rs @@ -105,7 +105,10 @@ impl HookStep { } /// 处理所有后处理器 -pub async fn process_video(video_path: &Path, processors: &[HookStep]) -> Result<(), Box> { +pub async fn process_video( + video_path: &Path, + processors: &[HookStep], +) -> Result<(), Box> { for processor in processors { processor.execute(video_path).await?; } diff --git a/crates/biliup-cli/src/server/infrastructure/repositories.rs b/crates/biliup-cli/src/server/infrastructure/repositories.rs index 7c07222..832c9f9 100644 --- a/crates/biliup-cli/src/server/infrastructure/repositories.rs +++ b/crates/biliup-cli/src/server/infrastructure/repositories.rs @@ -1,7 +1,67 @@ -pub mod download_records_repository; -pub mod live_streamers_repository; -pub mod models; -pub mod upload_records_repository; -pub mod upload_streamers_repository; -pub mod users_repository; -pub mod videos_repository; +use crate::server::config::Config; +use crate::server::errors::{AppError, AppResult}; +use crate::server::infrastructure::connection_pool::ConnectionPool; +use crate::server::infrastructure::models::{ + Configuration, InsertConfiguration, LiveStreamer, UploadStreamer, +}; +use error_stack::ResultExt; +use ormlite::{Insert, Model}; + +pub async fn get_streamer(pool: &ConnectionPool, id: i64) -> AppResult { + LiveStreamer::select() + .where_("id = ?") + .bind(id) + .fetch_one(pool) + .await + .change_context(AppError::Unknown) +} + +pub async fn del_streamer(pool: &ConnectionPool, id: i64) -> AppResult { + let streamer = get_streamer(pool, id).await?; + streamer + .clone() + .delete(pool) + .await + .change_context(AppError::Unknown)?; + Ok(streamer) +} +pub async fn get_all_streamer(pool: &ConnectionPool) -> AppResult> { + LiveStreamer::select() + .fetch_all(pool) + .await + .change_context(AppError::Unknown) +} + +pub async fn get_config(pool: &ConnectionPool) -> AppResult { + let configuration = Configuration::select() + .where_("key = 'config'") + .fetch_optional(pool) + .await + .change_context(AppError::Unknown)?; + if let Some(configuration) = configuration { + let json: Config = + serde_json::from_str(&configuration.value).change_context(AppError::Unknown)?; + Ok(json) + } else { + let config = Config::builder().streamers(Default::default()).build(); + Ok(config) + } +} + +pub async fn insert_config(pool: &ConnectionPool, config: &Config) -> AppResult { + let configuration = InsertConfiguration { + key: "config".to_string(), + value: serde_json::to_string(config).unwrap(), + } + .insert(pool) + .await + .change_context(AppError::Unknown)?; + Ok(configuration) +} + +pub async fn get_all_uploader(pool: &ConnectionPool) -> AppResult> { + UploadStreamer::select() + .fetch_all(pool) + .await + .change_context(AppError::Unknown) +} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/download_records_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/download_records_repository.rs deleted file mode 100644 index 802bda8..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/download_records_repository.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::server::core::live_streamers::{DownloadRecords, DownloadRecordsRepository}; - -use crate::server::infrastructure::connection_pool::ConnectionPool; -use async_trait::async_trait; - -pub struct SqliteDownloadRecordsRepository { - pool: ConnectionPool, -} - -impl SqliteDownloadRecordsRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} - -#[async_trait] -impl DownloadRecordsRepository for SqliteDownloadRecordsRepository { - async fn create(&self, entity: DownloadRecords) -> anyhow::Result { - todo!() - } - - async fn get_all(&self) -> anyhow::Result> { - todo!() - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/live_streamers_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/live_streamers_repository.rs deleted file mode 100644 index 58d8ef9..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/live_streamers_repository.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::server::infrastructure::connection_pool::ConnectionPool; - -#[derive(Clone)] -pub struct SqliteLiveStreamersRepository { - pool: ConnectionPool, -} - -impl SqliteLiveStreamersRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/models.rs b/crates/biliup-cli/src/server/infrastructure/repositories/models.rs deleted file mode 100644 index 38f7d78..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/models.rs +++ /dev/null @@ -1,79 +0,0 @@ -use chrono::NaiveDateTime; -use ormlite::Model; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Model, Debug, Clone, Serialize, Deserialize)] -#[ormlite(table = "uploadstreamers")] -pub struct UploadStreamer { - pub id: i64, - pub template_name: String, - pub title: Option, - pub tid: Option, - pub copyright: Option, - pub copyright_source: Option, - pub cover_path: Option, - pub description: Option, - pub dynamic: Option, - pub dtime: Option, - pub dolby: Option, - pub hires: Option, - pub charging_pay: Option, - pub no_reprint: Option, - pub uploader: Option, - pub user_cookie: Option, - pub tags: Value, // not null - pub credits: Option, - pub up_selection_reply: Option, - pub up_close_reply: Option, - pub up_close_danmu: Option, - pub extra_fields: Option, - pub is_only_self: Option, -} - -#[derive(Model, Debug, Clone, Serialize, Deserialize)] -#[ormlite(table = "streamerinfo")] -pub struct StreamerInfo { - pub id: i64, - pub name: String, - pub url: String, - pub title: String, - pub date: NaiveDateTime, - pub live_cover_path: String, -} - -#[derive(Model, Debug, Clone, Serialize, Deserialize)] -#[ormlite(table = "livestreamers")] -pub struct LiveStreamer { - pub id: i64, - pub url: String, - pub remark: String, - pub filename_prefix: Option, - pub time_range: Option, - pub upload_streamers_id: Option, // FK,可空 - pub format: Option, - #[ormlite(column = "override")] - pub override_: Option, // "override" 为关键字,字段名避让 - pub preprocessor: Option, - pub segment_processor: Option, - pub downloaded_processor: Option, - pub postprocessor: Option, - pub opt_args: Option, - pub excluded_keywords: Option, -} - -#[derive(Model, Debug, Clone, Serialize, Deserialize)] -#[ormlite(table = "filelist")] -pub struct FileItem { - pub id: i64, - pub file: String, - pub streamer_info_id: i64, // FK not null -} - -#[derive(Model, Debug, Clone, Serialize, Deserialize)] -#[ormlite(table = "configuration")] -pub struct Configuration { - pub id: i64, - pub key: String, - pub value: String, // TEXT -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/upload_records_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/upload_records_repository.rs deleted file mode 100644 index 340d050..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/upload_records_repository.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::server::core::upload_streamers::{UploadRecords, UploadRecordsRepository}; - -use crate::server::infrastructure::connection_pool::ConnectionPool; -use async_trait::async_trait; - -#[derive(Clone)] -pub struct SqliteUploadRecordsRepository { - pool: ConnectionPool, -} - -impl SqliteUploadRecordsRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} - -#[async_trait] -impl UploadRecordsRepository for SqliteUploadRecordsRepository { - async fn create(&self, _entity: UploadRecords) -> anyhow::Result { - todo!() - } - - async fn delete(&self, _id: i64) -> anyhow::Result<()> { - todo!() - } - - async fn get_all(&self) -> anyhow::Result> { - todo!() - } - - async fn get_by_id(&self, _id: i64) -> anyhow::Result { - todo!() - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/upload_streamers_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/upload_streamers_repository.rs deleted file mode 100644 index ab8afc0..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/upload_streamers_repository.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::server::infrastructure::connection_pool::ConnectionPool; - -#[derive(Clone)] -pub struct SqliteUploadStreamersRepository { - pool: ConnectionPool, -} - -impl SqliteUploadStreamersRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/users_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/users_repository.rs deleted file mode 100644 index 052ee77..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/users_repository.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::server::infrastructure::connection_pool::ConnectionPool; - -#[derive(Clone)] -pub struct SqliteUsersStreamersRepository { - pool: ConnectionPool, -} - -impl SqliteUsersStreamersRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/repositories/videos_repository.rs b/crates/biliup-cli/src/server/infrastructure/repositories/videos_repository.rs deleted file mode 100644 index 52f7632..0000000 --- a/crates/biliup-cli/src/server/infrastructure/repositories/videos_repository.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::server::core::live_streamers::{Videos, VideosRepository}; - -use crate::server::infrastructure::connection_pool::ConnectionPool; -use async_trait::async_trait; - -#[derive(Clone)] -pub struct SqliteVideosRepository { - pool: ConnectionPool, -} - -impl SqliteVideosRepository { - pub fn new(pool: ConnectionPool) -> Self { - Self { pool } - } -} - -#[async_trait] -impl VideosRepository for SqliteVideosRepository { - async fn create(&self, entity: Videos) -> anyhow::Result { - todo!() - } - - async fn update(&self, entity: Videos) -> anyhow::Result { - todo!() - } - - async fn get_by_id(&self, id: i64) -> anyhow::Result { - todo!() - } -} diff --git a/crates/biliup-cli/src/server/infrastructure/service_register.rs b/crates/biliup-cli/src/server/infrastructure/service_register.rs index f73abe9..3000110 100644 --- a/crates/biliup-cli/src/server/infrastructure/service_register.rs +++ b/crates/biliup-cli/src/server/infrastructure/service_register.rs @@ -1,51 +1,120 @@ -use crate::server::core::live_streamers::{DynDownloadRecordsRepository, DynVideosRepository}; -use crate::server::core::upload_streamers::DynUploadRecordsRepository; +use crate::server::config; +use crate::server::config::Config; +use crate::server::core::download_manager::{ActorHandle, DownloadManager}; +use crate::server::core::monitor::Monitor; +use crate::server::core::plugin; +use crate::server::errors::{AppError, AppResult}; use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::repositories::download_records_repository::SqliteDownloadRecordsRepository; -use crate::server::infrastructure::repositories::upload_records_repository::SqliteUploadRecordsRepository; -use crate::server::infrastructure::repositories::videos_repository::SqliteVideosRepository; +use crate::server::infrastructure::context::{Context, Worker}; +use crate::server::infrastructure::repositories; use axum::extract::FromRef; -use std::sync::Arc; +use biliup::client::StatelessClient; +use biliup::credential::Credential; +use error_stack::bail; +use ormlite::Model; +use std::sync::{Arc, RwLock}; use tracing::info; -#[derive(Clone)] +#[derive(FromRef, Clone)] pub struct ServiceRegister { pub pool: ConnectionPool, - pub upload_records_repository: DynUploadRecordsRepository, - pub videos_repository: DynVideosRepository, - pub download_records_repository: DynDownloadRecordsRepository, + pub workers: Arc>>>, + pub managers: Arc>, + pub config: Arc>, + pub actor_handle: Arc, + pub client: StatelessClient, } /// A simple service container responsible for managing the various services our API endpoints will pull from through axum extensions. impl ServiceRegister { - pub fn new(pool: ConnectionPool) -> Self { + pub fn new( + pool: ConnectionPool, + config: Arc>, + actor_handle: Arc, + download_manager: Vec, + ) -> Self { info!("initializing utility services..."); + let client = StatelessClient::default(); + + info!(config=?config); info!("utility services initialized, building feature services..."); - let upload_records_repository = Arc::new(SqliteUploadRecordsRepository::new(pool.clone())) - as DynUploadRecordsRepository; - - let videos_repository = - Arc::new(SqliteVideosRepository::new(pool.clone())) as DynVideosRepository; - - let download_records_repository = - Arc::new(SqliteDownloadRecordsRepository::new(pool.clone())) - as DynDownloadRecordsRepository; - info!("feature services successfully initialized!"); ServiceRegister { pool, - upload_records_repository, - videos_repository, - download_records_repository, + workers: Arc::new(Default::default()), + managers: Arc::new(download_manager), + config: config.clone(), + actor_handle, + client, } } + + pub fn get_manager(&self, url: &str) -> Option<&DownloadManager> { + self.managers + .iter() + .find(|&manager| manager.matches(url)) + .map(|v| v as _) + } + + pub async fn add_room( + &self, + id: i64, + url: &str, + monitor: Arc, + ) -> AppResult> { + let worker = Arc::new(Worker::new( + id, + url, + Context::new(self.pool.clone(), self.client.clone()), + )); + monitor.rooms_handle.add(worker.clone()).await; + self.workers.write().unwrap().push(worker); + info!("add {url} success"); + Ok(Some(())) + } + + pub async fn del_room(&self, id: i64) -> AppResult<()> { + let Some(i) = self.workers.read().unwrap().iter().position(|x| x.id == id) else { + return Err(error_stack::Report::new(AppError::Unknown)); + }; + + let removed = self.workers.write().unwrap().swap_remove(i); + let url = &removed.url; + let Some(manager) = self.get_manager(url) else { + info!("not found url: {url}"); + bail!(AppError::Unknown) + }; + let monitor = manager.ensure_monitor(); + let len = monitor.rooms_handle.del(id).await; + info!("{id} removed, remained len {len}"); + if len == 0 { + *manager.monitor.lock().unwrap() = None; + } + + Ok(()) + } } -impl FromRef for ConnectionPool { - fn from_ref(app_state: &ServiceRegister) -> ConnectionPool { - app_state.pool.clone() - } -} +// impl FromRef for ConnectionPool { +// fn from_ref(app_state: &ServiceRegister) -> ConnectionPool { +// app_state.pool.clone() +// } +// } + +// pub(crate) async fn add_room(&self, id: i64, pool: ConnectionPool) -> Arc { +// let arc = self.ensure_monitor().rooms_handle.clone(); +// let worker = Arc::new(Worker::new(id, pool, arc.clone())); +// arc.add(worker.clone()).await; +// worker +// } +// +// pub async fn del_room(&self, id: i64) { +// let len = self.ensure_monitor().rooms_handle.del(id).await; +// info!("{id} removed, remained len {len}"); +// if len == 0 { +// *self.monitor.lock().unwrap() = None; +// } +// } diff --git a/crates/stream-gears/src/server/infrastructure/users.rs b/crates/biliup-cli/src/server/infrastructure/users.rs similarity index 98% rename from crates/stream-gears/src/server/infrastructure/users.rs rename to crates/biliup-cli/src/server/infrastructure/users.rs index eb65119..f21000c 100644 --- a/crates/stream-gears/src/server/infrastructure/users.rs +++ b/crates/biliup-cli/src/server/infrastructure/users.rs @@ -72,7 +72,7 @@ impl Backend { Ok(user.is_some()) } - pub(crate) async fn create_user(&mut self, creds: Credentials) -> AppResult { + pub async fn create_user(&mut self, creds: Credentials) -> AppResult { // create the new user account... // 验证输入 if creds.username.is_empty() || creds.password.is_empty() { diff --git a/crates/biliup-cli/src/server/integration.rs b/crates/biliup-cli/src/server/integration.rs deleted file mode 100644 index cf8b1ac..0000000 --- a/crates/biliup-cli/src/server/integration.rs +++ /dev/null @@ -1,181 +0,0 @@ -use anyhow::Result; -use tokio::task::JoinHandle; -use tracing::{error, info}; - -use crate::server::{ - core::live_streamers::DynLiveStreamersService, plugin_registry::initialize_plugins, -}; - -/// 集成管理器,负责协调所有组件 -pub struct IntegrationManager { - event_loop_handle: Option>, -} - -impl IntegrationManager { - pub fn new() -> Self { - Self { - event_loop_handle: None, - } - } - - /// 初始化整个系统,对应Python版本的启动流程 - pub async fn initialize(&mut self) -> Result<()> { - info!("Initializing BiliUp Rust server..."); - - // 1. 初始化配置管理器 - self.initialize_config().await?; - - // 2. 初始化插件注册表 - initialize_plugins().await?; - - // 7. 初始化直播源监控 - - info!("BiliUp Rust server initialized successfully"); - Ok(()) - } - - /// 初始化配置管理器 - async fn initialize_config(&self) -> Result<()> { - info!("Loading configuration..."); - - // 尝试加载配置文件 - - // 尝试加载cookies文件 - - info!("Configuration loaded"); - Ok(()) - } - - /// 启动事件循环 - async fn start_event_loop(&mut self) -> Result<()> { - info!("Starting event loop..."); - - info!("Event loop started"); - Ok(()) - } - - /// 初始化直播源监控 - async fn initialize_live_monitoring( - &self, - live_streamers_service: DynLiveStreamersService, - ) -> Result<()> { - info!("Starting live stream monitoring..."); - - // 从配置中获取直播源列表 - - info!("Live stream monitoring initialized"); - Ok(()) - } - - /// 为单个直播源启动监控 - async fn start_streamer_monitoring( - &self, - name: &str, - config: &crate::server::config_manager::StreamerConfig, - plugin_name: &str, - ) -> Result<()> { - // 创建监控任务 - let name = name.to_string(); - let url = config.url.clone(); - let plugin_name = plugin_name.to_string(); - - tokio::spawn(async move { - loop { - // 检查流状态 - match Self::check_single_stream(&name, &url, &plugin_name).await { - Ok(true) => { - info!("Stream {} is online, starting download", name); - - // 发送预下载事件 - } - Ok(false) => { - // 流不在线,继续监控 - } - Err(e) => { - error!("Failed to check stream {}: {}", name, e); - } - } - - // 等待下一次检查(从配置获取间隔) - let interval = std::time::Duration::from_secs(30); // TODO: 从配置读取 - tokio::time::sleep(interval).await; - } - }); - - Ok(()) - } - - /// 检查单个流的状态 - async fn check_single_stream(name: &str, url: &str, plugin_name: &str) -> Result { - // TODO: 使用插件注册表中的插件检查流状态 - // 这里需要实际调用对应插件的check_stream方法 - - // 模拟检查 - Ok(false) - } - - /// 停止所有服务 - pub async fn shutdown(&mut self) -> Result<()> { - info!("Shutting down BiliUp server..."); - - // 停止事件循环 - if let Some(handle) = self.event_loop_handle.take() { - handle.abort(); - info!("Event loop stopped"); - } - - // TODO: 停止其他服务 - - info!("BiliUp server shutdown complete"); - Ok(()) - } - - /// 重新加载配置 - pub async fn reload_config(&self) -> Result<()> { - info!("Reloading configuration..."); - - info!("Configuration reloaded"); - Ok(()) - } -} - -impl Default for IntegrationManager { - fn default() -> Self { - Self::new() - } -} - -/// 系统状态结构 -#[derive(Debug, serde::Serialize)] -pub struct SystemStatus { - pub running: bool, - pub streamers_count: usize, - pub downloaders_count: usize, - pub uploaders_count: usize, - pub event_loop_running: bool, -} - -/// 全局集成管理器实例 - -/// 便捷函数:初始化整个系统 - -/// 便捷函数:获取系统状态 - -/// 便捷函数:关闭系统 - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_integration_manager_initialization() { - let mut manager = IntegrationManager::new(); - - // 注意:这个测试可能需要数据库环境 - // 在实际测试中,应该使用测试数据库或模拟 - match manager.initialize().await { - Ok(_) => println!("Integration manager initialized successfully"), - Err(e) => println!("Integration manager initialization failed: {}", e), - } - } -} diff --git a/crates/biliup-cli/src/server/mod.rs b/crates/biliup-cli/src/server/mod.rs deleted file mode 100644 index e9bbd43..0000000 --- a/crates/biliup-cli/src/server/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub mod errors; - -pub mod api { - pub mod bilibili_endpoints; - pub mod endpoints; - pub mod router; - pub mod spa; -} - -pub mod core; - -pub mod infrastructure; - -// 新增的模块 -pub mod config_manager; -pub mod event_manager; -pub mod handlers; -pub mod integration; -pub mod plugin_registry; diff --git a/crates/biliup-cli/src/server/plugin_registry.rs b/crates/biliup-cli/src/server/plugin_registry.rs deleted file mode 100644 index cf2f1a9..0000000 --- a/crates/biliup-cli/src/server/plugin_registry.rs +++ /dev/null @@ -1,388 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{debug, info, warn}; - -use crate::server::event_manager::StreamInfo; - -/// 插件类型枚举 -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub enum PluginType { - Downloader, - Uploader, -} - -/// 插件信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginInfo { - pub name: String, - pub plugin_type: PluginType, - pub valid_url_pattern: String, - pub priority: i32, -} - -/// 下载器插件trait,对应Python版本的DownloadBase -#[async_trait] -pub trait DownloaderPlugin: Send + Sync { - fn name(&self) -> &str; - fn valid_url_pattern(&self) -> &str; - fn priority(&self) -> i32 { - 0 - } - - /// 检查流状态,对应Python的acheck_stream - async fn check_stream(&self, url: &str, is_check: bool) -> Result; - - /// 获取流信息 - async fn get_stream_info(&self, url: &str) -> Result>; - - /// 开始下载,对应Python的start方法 - async fn start_download( - &self, - name: &str, - url: &str, - kwargs: HashMap, - ) -> Result; - - /// 检查是否应该录制,对应Python的should_record - fn should_record(&self, stream_info: &StreamInfo) -> bool { - // TODO: 实现关键词和时间范围检查 - true - } -} - -/// 上传器插件trait,对应Python版本的UploadBase -#[async_trait] -pub trait UploaderPlugin: Send + Sync { - fn name(&self) -> &str; - fn priority(&self) -> i32 { - 0 - } - - /// 上传文件列表 - async fn upload(&self, files: Vec, stream_info: &StreamInfo) -> Result>; - - /// 获取上传配置 - fn get_upload_config(&self) -> HashMap { - HashMap::new() - } -} - -/// Python插件包装器,用于调用Python插件 -#[cfg(feature = "python-bridge")] -pub struct PythonPluginWrapper { - name: String, - plugin_type: PluginType, - module_path: String, - class_name: String, - // Python对象引用将在实际实现时添加 -} - -#[cfg(feature = "python-bridge")] -impl PythonPluginWrapper { - pub fn new( - name: String, - plugin_type: PluginType, - module_path: String, - class_name: String, - ) -> Self { - Self { - name, - plugin_type, - module_path, - class_name, - } - } -} - -#[cfg(feature = "python-bridge")] -#[async_trait] -impl DownloaderPlugin for PythonPluginWrapper { - fn name(&self) -> &str { - &self.name - } - - fn valid_url_pattern(&self) -> &str { - // TODO: 从Python模块获取VALID_URL_BASE - "" - } - - async fn check_stream(&self, url: &str, is_check: bool) -> Result { - // TODO: 使用PyO3调用Python插件的acheck_stream方法 - Ok(false) - } - - async fn get_stream_info(&self, url: &str) -> Result> { - // TODO: 从Python插件获取流信息 - Ok(None) - } - - async fn start_download( - &self, - name: &str, - url: &str, - kwargs: HashMap, - ) -> Result { - // TODO: 调用Python插件的start方法 - Err(anyhow::anyhow!("Not implemented")) - } -} - -/// 插件注册表 -pub struct PluginRegistry { - downloaders: Arc>>>, - uploaders: Arc>>>, - url_patterns: Arc>>, // 正则表达式和插件名的映射 -} - -impl PluginRegistry { - pub fn new() -> Self { - Self { - downloaders: Arc::new(RwLock::new(HashMap::new())), - uploaders: Arc::new(RwLock::new(HashMap::new())), - url_patterns: Arc::new(RwLock::new(Vec::new())), - } - } - - /// 注册下载器插件 - pub async fn register_downloader(&self, plugin: Box) -> Result<()> { - let name = plugin.name().to_string(); - let pattern = plugin.valid_url_pattern().to_string(); - - info!("Registering downloader plugin: {}", name); - - // 编译正则表达式 - if let Ok(regex) = regex::Regex::new(&pattern) { - let mut patterns = self.url_patterns.write().await; - patterns.push((regex, name.clone())); - } else { - warn!("Invalid URL pattern for plugin {}: {}", name, pattern); - } - - let mut downloaders = self.downloaders.write().await; - downloaders.insert(name, plugin); - - Ok(()) - } - - /// 注册上传器插件 - pub async fn register_uploader(&self, plugin: Box) -> Result<()> { - let name = plugin.name().to_string(); - info!("Registering uploader plugin: {}", name); - - let mut uploaders = self.uploaders.write().await; - uploaders.insert(name, plugin); - - Ok(()) - } - - /// 根据URL找到对应的下载器插件 - pub async fn find_downloader(&self, url: &str) -> Option { - let patterns = self.url_patterns.read().await; - - for (regex, plugin_name) in patterns.iter() { - if regex.is_match(url) { - debug!("Found downloader plugin {} for URL: {}", plugin_name, url); - return Some(plugin_name.clone()); - } - } - - None - } - - /// 获取下载器插件 - pub async fn get_downloader( - &self, - name: &str, - ) -> Option>>> { - let downloaders = self.downloaders.read().await; - if downloaders.contains_key(name) { - Some(downloaders) - } else { - None - } - } - - /// 获取上传器插件 - pub async fn get_uploader( - &self, - name: &str, - ) -> Option>>> { - let uploaders = self.uploaders.read().await; - if uploaders.contains_key(name) { - Some(uploaders) - } else { - None - } - } - - /// 列出所有插件 - pub async fn list_plugins(&self) -> (Vec, Vec) { - let downloaders = self.downloaders.read().await; - let uploaders = self.uploaders.read().await; - - ( - downloaders.keys().cloned().collect(), - uploaders.keys().cloned().collect(), - ) - } - - /// 从Python插件目录加载插件 - #[cfg(feature = "python-bridge")] - pub async fn load_python_plugins(&self, plugin_dir: &str) -> Result<()> { - use std::fs; - use std::path::Path; - - info!("Loading Python plugins from: {}", plugin_dir); - - let plugin_path = Path::new(plugin_dir); - if !plugin_path.exists() { - return Err(anyhow::anyhow!( - "Plugin directory does not exist: {}", - plugin_dir - )); - } - - // 扫描Python文件 - for entry in fs::read_dir(plugin_path)? { - let entry = entry?; - let path = entry.path(); - - if path.extension().and_then(|s| s.to_str()) == Some("py") { - if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) { - // 跳过特殊文件 - if file_name.starts_with("__") { - continue; - } - - // TODO: 实际的Python插件加载逻辑 - info!("Found Python plugin file: {}", file_name); - - // 创建Python插件包装器 - let plugin = PythonPluginWrapper::new( - file_name.to_string(), - PluginType::Downloader, - format!("biliup.plugins.{}", file_name), - "Plugin".to_string(), - ); - - // self.register_downloader(Box::new(plugin)).await?; - } - } - } - - Ok(()) - } - - /// 检查所有URL的流状态,对应Python的batch_check - pub async fn batch_check(&self, urls: &[String]) -> Result> { - let mut online_urls = Vec::new(); - - for url in urls { - if let Some(plugin_name) = self.find_downloader(url).await { - let downloaders = self.downloaders.read().await; - if let Some(plugin) = downloaders.get(&plugin_name) { - match plugin.check_stream(url, true).await { - Ok(true) => { - debug!("Stream online: {}", url); - online_urls.push(url.clone()); - } - Ok(false) => { - debug!("Stream offline: {}", url); - } - Err(e) => { - warn!("Failed to check stream {}: {}", url, e); - } - } - } - } - } - - Ok(online_urls) - } -} - -impl Default for PluginRegistry { - fn default() -> Self { - Self::new() - } -} - -// 全局插件注册表实例 -/// 插件初始化函数,对应Python版本的插件发现机制 -pub async fn initialize_plugins() -> Result<()> { - // TODO: 注册内置的Rust插件 - info!("Registering built-in plugins..."); - - // TODO: 加载Python插件(如果启用了python-bridge特性) - #[cfg(feature = "python-bridge")] - { - info!("Loading Python plugins..."); - let python_plugin_dir = - std::env::var("BILIUP_PLUGIN_DIR").unwrap_or_else(|_| "biliup/plugins".to_string()); - - if let Err(e) = registry.load_python_plugins(&python_plugin_dir).await { - warn!("Failed to load Python plugins: {}", e); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - struct TestDownloader { - name: String, - } - - #[async_trait] - impl DownloaderPlugin for TestDownloader { - fn name(&self) -> &str { - &self.name - } - - fn valid_url_pattern(&self) -> &str { - r"https://test\.com/.*" - } - - async fn check_stream(&self, _url: &str, _is_check: bool) -> Result { - Ok(true) - } - - async fn get_stream_info(&self, _url: &str) -> Result> { - Ok(None) - } - - async fn start_download( - &self, - _name: &str, - _url: &str, - _kwargs: HashMap, - ) -> Result { - Err(anyhow::anyhow!("Test plugin")) - } - } - - #[tokio::test] - async fn test_plugin_registry() { - let registry = PluginRegistry::new(); - - let plugin = Box::new(TestDownloader { - name: "test".to_string(), - }); - - registry.register_downloader(plugin).await.unwrap(); - - let plugin_name = registry.find_downloader("https://test.com/stream").await; - assert_eq!(plugin_name, Some("test".to_string())); - - let (downloaders, uploaders) = registry.list_plugins().await; - assert_eq!(downloaders.len(), 1); - assert_eq!(uploaders.len(), 0); - } -} diff --git a/crates/stream-gears/src/server/router.rs b/crates/biliup-cli/src/server/router.rs similarity index 100% rename from crates/stream-gears/src/server/router.rs rename to crates/biliup-cli/src/server/router.rs diff --git a/crates/stream-gears/src/server/util.rs b/crates/biliup-cli/src/server/util.rs similarity index 100% rename from crates/stream-gears/src/server/util.rs rename to crates/biliup-cli/src/server/util.rs diff --git a/crates/biliup-cli/src/uploader.rs b/crates/biliup-cli/src/uploader.rs index d67bc9b..35373a2 100644 --- a/crates/biliup-cli/src/uploader.rs +++ b/crates/biliup-cli/src/uploader.rs @@ -1,5 +1,4 @@ use crate::cli::UploadLine; -use anyhow::{Context, Result, anyhow}; use biliup::client::StatelessClient; use biliup::error::Kind; use biliup::uploader::bilibili::{BiliBili, Studio, Vid, Video}; @@ -7,11 +6,13 @@ use biliup::uploader::credential::{Credential, LoginInfo}; use biliup::uploader::line::Probe; use biliup::uploader::util::SubmitOption; use biliup::uploader::{VideoFile, credential, line, load_config}; +use biliup_cli::server::errors::{AppError, AppResult}; use bytes::{Buf, Bytes}; use clap::ValueEnum; use dialoguer::Input; use dialoguer::Select; use dialoguer::theme::ColorfulTheme; +use error_stack::ResultExt; use futures::{Stream, StreamExt}; use image::Luma; use indicatif::{ProgressBar, ProgressStyle}; @@ -26,7 +27,7 @@ use std::task::Poll; use std::time::Instant; use tracing::{info, warn}; -pub async fn login(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { +pub async fn login(user_cookie: PathBuf, proxy: Option<&str>) -> AppResult<()> { let client = Credential::new(proxy); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("选择一种登录方式") @@ -37,7 +38,8 @@ pub async fn login(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { .item("浏览器登录") .item("网页Cookie登录1") .item("网页Cookie登录2") - .interact()?; + .interact() + .change_context_lazy(|| AppError::Unknown)?; let info = match selection { 0 => login_by_password(client).await?, 1 => login_by_sms(client).await?, @@ -47,20 +49,25 @@ pub async fn login(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { 5 => login_by_webqr_cookies(client).await?, _ => panic!(), }; - let file = std::fs::File::create(user_cookie)?; - serde_json::to_writer_pretty(&file, &info)?; + let file = std::fs::File::create(user_cookie).change_context_lazy(|| AppError::Unknown)?; + serde_json::to_writer_pretty(&file, &info).change_context_lazy(|| AppError::Unknown)?; info!("登录成功,数据保存在{:?}", file); Ok(()) } -pub async fn renew(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { +pub async fn renew(user_cookie: PathBuf, proxy: Option<&str>) -> AppResult<()> { let client = Credential::new(proxy); let mut file = fopen_rw(user_cookie)?; - let login_info: LoginInfo = serde_json::from_reader(&file)?; - let new_info = client.renew_tokens(login_info).await?; - file.rewind()?; - file.set_len(0)?; - serde_json::to_writer_pretty(std::io::BufWriter::new(&file), &new_info)?; + let login_info: LoginInfo = + serde_json::from_reader(&file).change_context_lazy(|| AppError::Unknown)?; + let new_info = client + .renew_tokens(login_info) + .await + .change_context_lazy(|| AppError::Unknown)?; + file.rewind().change_context_lazy(|| AppError::Unknown)?; + file.set_len(0).change_context_lazy(|| AppError::Unknown)?; + serde_json::to_writer_pretty(std::io::BufWriter::new(&file), &new_info) + .change_context_lazy(|| AppError::Unknown)?; info!("{new_info:?}"); Ok(()) } @@ -73,7 +80,7 @@ pub async fn upload_by_command( limit: usize, submit: SubmitOption, proxy: Option<&str>, -) -> Result<()> { +) -> AppResult<()> { let bili = login_by_cookies(user_cookie, proxy).await?; if studio.title.is_empty() { studio.title = video_path[0] @@ -86,8 +93,14 @@ pub async fn upload_by_command( studio.videos = upload(&video_path, &bili, line, limit).await?; match submit { - SubmitOption::BCutAndroid => bili.submit_by_bcut_android(&studio, proxy).await?, - _ => bili.submit_by_app(&studio, proxy).await?, + SubmitOption::BCutAndroid => bili + .submit_by_bcut_android(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, + _ => bili + .submit_by_app(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, }; Ok(()) @@ -98,13 +111,16 @@ pub async fn upload_by_config( user_cookie: PathBuf, submit_override: Option, proxy: Option<&str>, -) -> Result<()> { +) -> AppResult<()> { // println!("number of concurrent futures: {limit}"); let bilibili = login_by_cookies(user_cookie, proxy).await?; - let config = load_config(&config)?; + let config = load_config(&config).change_context_lazy(|| AppError::Unknown)?; for (filename_patterns, mut studio) in config.streamers { let mut paths = Vec::new(); - for entry in glob::glob(&filename_patterns)?.filter_map(Result::ok) { + for entry in glob::glob(&filename_patterns) + .change_context_lazy(|| AppError::Unknown)? + .filter_map(Result::ok) + { paths.push(entry); } if paths.is_empty() { @@ -126,8 +142,14 @@ pub async fn upload_by_config( // 命令行参数优先,如果没有提供则使用配置文件中的设置 let submit_option = submit_override.clone().unwrap_or(config.submit.clone()); match submit_option { - SubmitOption::BCutAndroid => bilibili.submit_by_bcut_android(&studio, proxy).await?, - _ => bilibili.submit_by_app(&studio, proxy).await?, + SubmitOption::BCutAndroid => bilibili + .submit_by_bcut_android(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, + _ => bilibili + .submit_by_app(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, }; } Ok(()) @@ -141,23 +163,38 @@ pub async fn append( limit: usize, submit: SubmitOption, proxy: Option<&str>, -) -> Result<()> { +) -> AppResult<()> { let bilibili = login_by_cookies(user_cookie, proxy).await?; let mut uploaded_videos = upload(&video_path, &bilibili, line, limit).await?; - let mut studio = bilibili.studio_data(&vid, proxy).await?; + let mut studio = bilibili + .studio_data(&vid, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?; studio.videos.append(&mut uploaded_videos); match submit { - SubmitOption::App => bilibili.edit_by_app(&studio, proxy).await?, - _ => bilibili.edit_by_web(&studio).await?, + SubmitOption::App => bilibili + .edit_by_app(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, + _ => bilibili + .edit_by_web(&studio) + .await + .change_context_lazy(|| AppError::Unknown)?, }; // studio.edit(&login_info).await?; Ok(()) } -pub async fn show(user_cookie: PathBuf, vid: Vid, proxy: Option<&str>) -> Result<()> { +pub async fn show(user_cookie: PathBuf, vid: Vid, proxy: Option<&str>) -> AppResult<()> { let bilibili = login_by_cookies(user_cookie, proxy).await?; - let video_info = bilibili.video_data(&vid, proxy).await?; - println!("{}", serde_json::to_string_pretty(&video_info)?); + let video_info = bilibili + .video_data(&vid, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?; + println!( + "{}", + serde_json::to_string_pretty(&video_info).change_context_lazy(|| AppError::Unknown)? + ); Ok(()) } @@ -169,7 +206,7 @@ pub async fn list( proxy: Option<&str>, from_page: u32, max_pages: Option, -) -> Result<()> { +) -> AppResult<()> { let status = match (is_pubing, pubed, not_pubed) { (true, false, false) => "is_pubing", (false, true, false) => "pubed", @@ -184,23 +221,26 @@ pub async fn list( let bilibili = login_by_cookies(user_cookie, proxy).await?; bilibili .recent_archives(status, from_page, max_pages) - .await? + .await + .change_context_lazy(|| AppError::Unknown)? .iter() .for_each(|arc| println!("{}", arc.to_string_pretty())); Ok(()) } -async fn login_by_cookies(user_cookie: PathBuf, proxy: Option<&str>) -> Result { +async fn login_by_cookies(user_cookie: PathBuf, proxy: Option<&str>) -> AppResult { let result = credential::login_by_cookies(&user_cookie, proxy).await; Ok(match result { - Err(Kind::IO(_)) => result.with_context(|| { - String::from("open cookies file: ") + &user_cookie.to_string_lossy() + Err(Kind::IO(_)) => result.change_context_lazy(|| { + AppError::Custom(String::from("open cookies file: ") + &user_cookie.to_string_lossy()) })?, _ => { - let bili = result?; + let bili = result.change_context_lazy(|| AppError::Unknown)?; info!( "user: {}", - bili.my_info().await?["data"]["name"] + bili.my_info() + .await + .change_context_lazy(|| AppError::Unknown)?["data"]["name"] .as_str() .unwrap_or_default() ); @@ -209,14 +249,15 @@ async fn login_by_cookies(user_cookie: PathBuf, proxy: Option<&str>) -> Result Result<()> { +pub async fn cover_up(studio: &mut Studio, bili: &BiliBili) -> AppResult<()> { if !studio.cover.is_empty() { let url = bili .cover_up( &std::fs::read(Path::new(&studio.cover)) - .with_context(|| format!("cover: {}", studio.cover))?, + .change_context_lazy(|| AppError::Custom(format!("cover: {}", studio.cover)))?, ) - .await?; + .await + .change_context_lazy(|| AppError::Unknown)?; info!("{url}"); studio.cover = url; } @@ -228,7 +269,7 @@ pub async fn upload( bili: &BiliBili, line: Option, limit: usize, -) -> Result> { +) -> AppResult> { info!("number of concurrent futures: {limit}"); let mut videos = Vec::new(); let client = StatelessClient::default(); @@ -253,15 +294,19 @@ pub async fn upload( // let line = line::kodo(); for video_path in video_path { info!("{line:?}"); - let video_file = VideoFile::new(video_path) - .with_context(|| format!("file {}", video_path.to_string_lossy()))?; + let video_file = VideoFile::new(video_path).change_context_lazy(|| { + AppError::Custom(format!("file {}", video_path.to_string_lossy())) + })?; let total_size = video_file.total_size; let file_name = video_file.file_name.clone(); - let uploader = line.pre_upload(bili, video_file).await?; + let uploader = line + .pre_upload(bili, video_file) + .await + .change_context_lazy(|| AppError::Unknown)?; //Progress bar let pb = ProgressBar::new(total_size); pb.set_style(ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")?); + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").change_context_lazy(|| AppError::Unknown)?); // pb.enable_steady_tick(Duration::from_secs(1)); // pb.tick() @@ -276,7 +321,8 @@ pub async fn upload( Ok((Progressbar::new(chunk, pb), len)) }) }) - .await?; + .await + .change_context_lazy(|| AppError::Unknown)?; pb.finish_and_clear(); let t = instant.elapsed().as_millis(); info!( @@ -289,24 +335,31 @@ pub async fn upload( Ok(videos) } -pub async fn login_by_password(credential: Credential) -> Result { +pub async fn login_by_password(credential: Credential) -> AppResult { let username: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入账号") - .interact()?; + .interact() + .change_context_lazy(|| AppError::Unknown)?; let password: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入密码") - .interact()?; - Ok(credential.login_by_password(&username, &password).await?) + .interact() + .change_context_lazy(|| AppError::Unknown)?; + Ok(credential + .login_by_password(&username, &password) + .await + .change_context_lazy(|| AppError::Unknown)?) } -pub async fn login_by_sms(credential: Credential) -> Result { +pub async fn login_by_sms(credential: Credential) -> AppResult { let country_code: u32 = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入手机国家代码") .default(86) - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; let phone: u64 = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入手机号") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; let res = credential .send_sms_handle_recaptcha(phone, country_code, |url| async move { println!("{url}"); @@ -324,16 +377,24 @@ pub async fn login_by_sms(credential: Credential) -> Result { Ok((challenge, valiate)) }) - .await?; + .await + .change_context_lazy(|| AppError::Unknown)?; let input: u32 = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入验证码") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; // println!("{}", payload); - Ok(credential.login_by_sms(input, res).await?) + Ok(credential + .login_by_sms(input, res) + .await + .change_context_lazy(|| AppError::Unknown)?) } -pub async fn login_by_qrcode(credential: Credential) -> Result { - let value = credential.get_qrcode().await?; +pub async fn login_by_qrcode(credential: Credential) -> AppResult { + let value = credential + .get_qrcode() + .await + .change_context_lazy(|| AppError::Unknown)?; let code = QrCode::new( value["data"]["url"] .as_str() @@ -354,43 +415,58 @@ pub async fn login_by_qrcode(credential: Credential) -> Result { ); // Save the image. image.save("qrcode.png").unwrap(); - Ok(credential.login_by_qrcode(value).await?) + Ok(credential + .login_by_qrcode(value) + .await + .change_context_lazy(|| AppError::Unknown)?) } -pub async fn login_by_browser(credential: Credential) -> Result { - let value = credential.get_qrcode().await?; +pub async fn login_by_browser(credential: Credential) -> AppResult { + let value = credential + .get_qrcode() + .await + .change_context_lazy(|| AppError::Unknown)?; println!( "{}", value["data"]["url"] .as_str() - .ok_or_else(|| anyhow!("{}", value))? + .ok_or_else(|| AppError::Custom(value.to_string()))? ); println!("请复制此链接至浏览器中完成登录"); - Ok(credential.login_by_qrcode(value).await?) + Ok(credential + .login_by_qrcode(value) + .await + .change_context_lazy(|| AppError::Unknown)?) } -pub async fn login_by_web_cookies(credential: Credential) -> Result { +pub async fn login_by_web_cookies(credential: Credential) -> AppResult { let sess_data: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入SESSDATA") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; let bili_jct: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入bili_jct") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; Ok(credential .login_by_web_cookies(&sess_data, &bili_jct) - .await?) + .await + .change_context_lazy(|| AppError::Unknown)?) } -pub async fn login_by_webqr_cookies(credential: Credential) -> Result { +pub async fn login_by_webqr_cookies(credential: Credential) -> AppResult { let sess_data: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入SESSDATA") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; let dede_user_id: String = Input::with_theme(&ColorfulTheme::default()) .with_prompt("请输入DedeUserID") - .interact_text()?; + .interact_text() + .change_context_lazy(|| AppError::Unknown)?; Ok(credential .login_by_web_qrcode(&sess_data, &dede_user_id) - .await?) + .await + .change_context_lazy(|| AppError::Unknown)?) } impl From for Body { @@ -400,13 +476,15 @@ impl From for Body { } #[inline] -pub fn fopen_rw>(path: P) -> Result { +pub fn fopen_rw>(path: P) -> AppResult { let path = path.as_ref(); std::fs::File::options() .read(true) .write(true) .open(path) - .with_context(|| String::from("open cookies file: ") + &path.to_string_lossy()) + .change_context_lazy(|| { + AppError::Custom(String::from("open cookies file: ") + &path.to_string_lossy()) + }) } #[derive(Clone)] @@ -420,7 +498,7 @@ impl Progressbar { Self { bytes, pb } } - pub fn progress(&mut self) -> Result> { + pub fn progress(&mut self) -> AppResult> { let pb = &self.pb; let content_bytes = &mut self.bytes; @@ -442,7 +520,7 @@ impl Progressbar { } impl Stream for Progressbar { - type Item = Result; + type Item = AppResult; fn poll_next( mut self: Pin<&mut Self>, diff --git a/crates/biliup/Cargo.toml b/crates/biliup/Cargo.toml index cbe5454..c10d376 100644 --- a/crates/biliup/Cargo.toml +++ b/crates/biliup/Cargo.toml @@ -32,7 +32,6 @@ reqwest-middleware = { version = "0.4", features = ["json"] } reqwest-retry = "0.7.0" clap = { version = "4", features = ["derive"], optional = true } time = "0.3" -anyhow = { workspace = true } m3u8-rs = "6" nom = "8" chrono = { workspace = true } diff --git a/crates/biliup/src/downloader/hls.rs b/crates/biliup/src/downloader/hls.rs index cb8266a..deba246 100644 --- a/crates/biliup/src/downloader/hls.rs +++ b/crates/biliup/src/downloader/hls.rs @@ -153,11 +153,10 @@ impl Drop for TsFile { #[cfg(test)] mod tests { - use anyhow::Result; use reqwest::Url; #[test] - fn test_url() -> Result<()> { + fn test_url() -> Result<(), Box> { let url = Url::parse("h://host.path/to/remote/resource.m3u8")?; let scheme = url.scheme(); let new_url = url.join("http://path.host/remote/resource.ts")?; @@ -167,7 +166,7 @@ mod tests { } #[test] - fn it_works() -> Result<()> { + fn it_works() -> Result<(), Box> { // download( // "test.ts")?; Ok(()) diff --git a/crates/biliup/src/downloader/httpflv.rs b/crates/biliup/src/downloader/httpflv.rs index f355c5e..5cf923e 100644 --- a/crates/biliup/src/downloader/httpflv.rs +++ b/crates/biliup/src/downloader/httpflv.rs @@ -281,11 +281,10 @@ impl Connection { #[cfg(test)] mod tests { - use anyhow::Result; use bytes::{Buf, BufMut, BytesMut}; #[test] - fn byte_it_works() -> Result<()> { + fn byte_it_works() -> Result<(), Box> { let mut bb = bytes::BytesMut::with_capacity(10); println!("chunk {:?}", bb.chunk()); println!("capacity {}", bb.capacity()); @@ -317,7 +316,7 @@ mod tests { } #[test] - fn it_works() -> Result<()> { + fn it_works() -> Result<(), Box> { // download( // "test.flv")?; Ok(()) diff --git a/crates/biliup/src/downloader/util.rs b/crates/biliup/src/downloader/util.rs index abfa0e6..90afe06 100644 --- a/crates/biliup/src/downloader/util.rs +++ b/crates/biliup/src/downloader/util.rs @@ -290,11 +290,10 @@ pub fn format_filename(file_name: &str) -> String { #[cfg(test)] mod tests { use super::*; - use anyhow::Result; use std::path::{Path, PathBuf}; #[test] - fn it_works() -> Result<()> { + fn it_works() -> Result<(), Box> { let mut p = PathBuf::from("/feel/the"); p.set_extension("force"); @@ -307,7 +306,7 @@ mod tests { } #[test] - fn test_segmentation_logic() -> Result<()> { + fn test_segmentation_logic() -> Result<(), Box> { // 测试时间分割 let mut seg = Segmentable::new(Some(Duration::from_secs(10)), None); assert!(!seg.needed()); diff --git a/crates/biliup/src/error.rs b/crates/biliup/src/error.rs index dcd47e4..b47554b 100644 --- a/crates/biliup/src/error.rs +++ b/crates/biliup/src/error.rs @@ -33,9 +33,6 @@ pub enum Kind { #[error(transparent)] SerdeUrl(#[from] serde_urlencoded::ser::Error), // source and Display delegate to anyhow::Error - #[error(transparent)] - AnyhowError(#[from] anyhow::Error), - #[error("need recaptcha")] NeedRecaptcha(String), } diff --git a/crates/stream-gears/Cargo.toml b/crates/stream-gears/Cargo.toml index 9db009d..499b1a8 100644 --- a/crates/stream-gears/Cargo.toml +++ b/crates/stream-gears/Cargo.toml @@ -25,11 +25,12 @@ tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing-appender = { workspace = true } -anyhow = { workspace = true } + thiserror = { workspace = true } futures = { workspace = true } async-channel = { version = "2.5.0" } biliup = { path = "../biliup"} +biliup-cli = { path = "../biliup-cli"} url = { workspace = true } time = { workspace = true } chrono = { workspace = true } diff --git a/crates/stream-gears/src/lib.rs b/crates/stream-gears/src/lib.rs index d97710b..21883c4 100644 --- a/crates/stream-gears/src/lib.rs +++ b/crates/stream-gears/src/lib.rs @@ -1,9 +1,8 @@ extern crate core; mod login; -mod uploader; - mod server; +mod uploader; use pyo3::prelude::*; use time::macros::format_description; @@ -86,12 +85,12 @@ pub async fn download_with_hook( segment: Segmentable, file_name_hook: CallbackFn, proxy: Option<&str>, -) -> anyhow::Result<()> { +) -> PyResult<()> { let client = StatelessClient::new(headers, proxy); - let response = client.retryable(url).await?; + let response = client.retryable(url).await.unwrap(); let mut connection = Connection::new(response); // let buf = &mut [0u8; 9]; - let bytes = connection.read_frame(9).await?; + let bytes = connection.read_frame(9).await.unwrap(); // response.read_exact(buf)?; // let out = File::create(format!("{}.flv", file_name)).expect("Unable to create file."); // let mut writer = BufWriter::new(out); @@ -111,7 +110,7 @@ pub async fn download_with_hook( Err(e) => { error!("{e}"); let file = LifecycleFile::with_hook(file_name, "ts", file_name_hook); - hls::download(url, &client, file, segment).await?; + hls::download(url, &client, file, segment).await.unwrap(); } } Ok(()) @@ -233,8 +232,7 @@ fn download_with_callback( ) { Ok(res) => Ok(res), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( - "{}, {}", - err.root_cause(), + "{}", err ))), } @@ -249,8 +247,7 @@ fn login_by_cookies(file: String, proxy: Option) -> PyResult { match result { Ok(_) => Ok(true), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( - "{}, {}", - err.root_cause(), + "{}", err ))), } @@ -302,8 +299,8 @@ fn login_by_qrcode(ret: String, proxy: Option) -> PyResult { let info = Credential::new(proxy.as_deref()) .login_by_qrcode(serde_json::from_str(&ret).unwrap()) .await?; - let res = serde_json::to_string_pretty(&info)?; - Ok::<_, anyhow::Error>(res) + let res = serde_json::to_string_pretty(&info).unwrap(); + Ok::<_, biliup::error::Kind>(res) }) .map_err(|err| pyo3::exceptions::PyRuntimeError::new_err(format!("{:#?}", err))) } @@ -441,8 +438,7 @@ fn upload( Ok(_) => Ok(()), // Ok(_) => { }, Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( - "{}, {}", - err.root_cause(), + "{}", err ))), } @@ -471,7 +467,7 @@ fn stream_gears(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(login_by_web_cookies, m)?)?; m.add_function(wrap_pyfunction!(login_by_web_qrcode, m)?)?; m.add_function(wrap_pyfunction!(server::main_loop, m)?)?; - m.add_function(wrap_pyfunction!(server::config::config_bindings, m)?)?; + m.add_function(wrap_pyfunction!(server::config_bindings, m)?)?; m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/crates/stream-gears/src/login.rs b/crates/stream-gears/src/login.rs index 6d884c1..ff90aa1 100644 --- a/crates/stream-gears/src/login.rs +++ b/crates/stream-gears/src/login.rs @@ -1,9 +1,12 @@ -use anyhow::Result; use biliup::uploader::bilibili::BiliBili; use biliup::uploader::credential::Credential; +use biliup_cli::server::errors::{AppError, AppResult}; +use error_stack::ResultExt; -pub async fn login_by_cookies(file: &str, proxy: Option<&str>) -> Result { - let login_info = biliup::uploader::credential::login_by_cookies(file, proxy).await?; +pub async fn login_by_cookies(file: &str, proxy: Option<&str>) -> AppResult { + let login_info = biliup::uploader::credential::login_by_cookies(file, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?; Ok(login_info) } @@ -11,20 +14,33 @@ pub async fn send_sms( country_code: u32, phone: u64, proxy: Option<&str>, -) -> Result { - let ret = Credential::new(proxy).send_sms(phone, country_code).await?; +) -> AppResult { + let ret = Credential::new(proxy) + .send_sms(phone, country_code) + .await + .change_context_lazy(|| AppError::Unknown)?; Ok(ret) } -pub async fn login_by_sms(code: u32, res: serde_json::Value, proxy: Option<&str>) -> Result { - let info = Credential::new(proxy).login_by_sms(code, res).await?; - let file = std::fs::File::create("cookies.json")?; - serde_json::to_writer_pretty(&file, &info)?; +pub async fn login_by_sms( + code: u32, + res: serde_json::Value, + proxy: Option<&str>, +) -> AppResult { + let info = Credential::new(proxy) + .login_by_sms(code, res) + .await + .change_context_lazy(|| AppError::Unknown)?; + let file = std::fs::File::create("cookies.json").change_context_lazy(|| AppError::Unknown)?; + serde_json::to_writer_pretty(&file, &info).change_context_lazy(|| AppError::Unknown)?; Ok(true) } -pub async fn get_qrcode(proxy: Option<&str>) -> Result { - let qrcode = Credential::new(proxy).get_qrcode().await?; +pub async fn get_qrcode(proxy: Option<&str>) -> AppResult { + let qrcode = Credential::new(proxy) + .get_qrcode() + .await + .change_context_lazy(|| AppError::Unknown)?; Ok(qrcode) } @@ -32,12 +48,13 @@ pub async fn login_by_web_cookies( sess_data: &str, bili_jct: &str, proxy: Option<&str>, -) -> Result { +) -> AppResult { let info = Credential::new(proxy) .login_by_web_cookies(sess_data, bili_jct) - .await?; - let file = std::fs::File::create("cookies.json")?; - serde_json::to_writer_pretty(&file, &info)?; + .await + .change_context_lazy(|| AppError::Unknown)?; + let file = std::fs::File::create("cookies.json").change_context_lazy(|| AppError::Unknown)?; + serde_json::to_writer_pretty(&file, &info).change_context_lazy(|| AppError::Unknown)?; Ok(true) } @@ -45,11 +62,12 @@ pub async fn login_by_web_qrcode( sess_data: &str, dede_user_id: &str, proxy: Option<&str>, -) -> Result { +) -> AppResult { let info = Credential::new(proxy) .login_by_web_qrcode(sess_data, dede_user_id) - .await?; - let file = std::fs::File::create("cookies.json")?; - serde_json::to_writer_pretty(&file, &info)?; + .await + .change_context_lazy(|| AppError::Unknown)?; + let file = std::fs::File::create("cookies.json").change_context_lazy(|| AppError::Unknown)?; + serde_json::to_writer_pretty(&file, &info).change_context_lazy(|| AppError::Unknown)?; Ok(true) } diff --git a/crates/stream-gears/src/server.rs b/crates/stream-gears/src/server.rs index 3a21859..6239910 100644 --- a/crates/stream-gears/src/server.rs +++ b/crates/stream-gears/src/server.rs @@ -1,25 +1,315 @@ -mod api; -mod app; -pub(crate) mod config; -mod core; -mod errors; -pub mod infrastructure; -mod router; -mod util; - -use error_stack::ResultExt; +use crate::construct_headers; +use async_trait::async_trait; +use biliup::downloader::util::Segmentable; +use biliup_cli::server::app::ApplicationController; +use biliup_cli::server::config; +use biliup_cli::server::config::Config; +use biliup_cli::server::core::download_manager::{ActorHandle, DownloadManager}; +use biliup_cli::server::core::downloader::ffmpeg_downloader::FfmpegDownloader; +use biliup_cli::server::core::downloader::stream_gears::StreamGears; +use biliup_cli::server::core::downloader::{DownloadConfig, Downloader, DownloaderType}; +use biliup_cli::server::core::plugin::{DownloadPlugin, StreamInfo, StreamStatus}; +use biliup_cli::server::errors::{AppError, AppResult}; +use biliup_cli::server::infrastructure::connection_pool::ConnectionManager; +use biliup_cli::server::infrastructure::context::Worker; +use biliup_cli::server::infrastructure::repositories; +use biliup_cli::server::infrastructure::service_register::ServiceRegister; +use biliup_cli::server::util::{Recorder, media_ext_from_url, parse_time}; +use error_stack::{Report, ResultExt}; +use fancy_regex::Regex; use futures::TryFutureExt; use pyo3::exceptions::PyRuntimeError; -use pyo3::prelude::PyListMethods; -use pyo3::{PyResult, Python, pyfunction}; +use pyo3::prelude::PyDictMethods; +use pyo3::prelude::{PyAnyMethods, PyListMethods, PyModule}; +use pyo3::sync::OnceLockExt; +use pyo3::types::PyDict; +use pyo3::types::{PyList, PyType}; +use pyo3::{Bound, Py, PyAny, PyResult, Python}; +use pyo3::{FromPyObject, pyclass, pyfunction, pymethods}; +use pythonize::pythonize; +use std::collections::HashMap; use std::net::ToSocketAddrs; -// use tokio::sync::mpsc::Receiver; -use crate::server::app::ApplicationController; -use crate::server::errors::{AppError, AppResult}; -use crate::server::infrastructure::connection_pool::ConnectionManager; -use crate::server::infrastructure::repositories; -use crate::server::infrastructure::service_register::ServiceRegister; -use tracing::info; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::{Arc, LazyLock, RwLock}; +use time::OffsetDateTime; +use tracing::{debug, info}; + +#[derive(Debug)] +pub struct PyPlugin { + plugin: Arc>, + pattern: Regex, + name: String, +} + +impl PyPlugin { + pub fn from_pytype(class: &Bound) -> PyResult { + let pattern = class.getattr("VALID_URL_BASE")?.to_string(); + // info!("{pattern}"); + let re = Regex::new(&pattern).unwrap(); + let plugin = class.clone(); + let name: String = class.getattr("__name__")?.extract()?; + // info!("发现插件类: {name}"); + Ok(Self { + plugin: Arc::new(plugin.unbind()), + pattern: re, + name, + }) + } +} + +#[async_trait] +impl DownloadPlugin for PyPlugin { + fn matches(&self, url: &str) -> bool { + if self.pattern.is_match(url).unwrap() { + // 找到匹配的部分 + if let Some(mat) = self.pattern.find(url).unwrap() { + debug!(" 匹配内容: {}", mat.as_str()); + return true; + } + } + false + } + + async fn check_status(&self, url: &str) -> Result> { + info!("Checking status"); + match call_via_threads(self.plugin.clone(), url) + .await + .change_context(AppError::Unknown)? + { + Some(info) => Ok(StreamStatus::Live { stream_info: info }), + None => Ok(StreamStatus::Offline), + } + } + + async fn create_downloader( + &self, + stream_info: &StreamInfo, + worker: &Worker, + ) -> AppResult> { + let config = worker.get_config().await?; + let streamer = worker.get_streamer().await?; + // info!(stream_info=?stream_info, "Create downloader"); + let raw_stream_url = &stream_info.raw_stream_url; + let suffix = streamer + .format + .unwrap_or_else(|| stream_info.suffix.clone()); + println!("suffix: {suffix}"); + let recorder = Recorder::new( + streamer.filename_prefix, + &streamer.remark, + &stream_info.title, + &suffix, + ); + + match config.downloader { + Some(DownloaderType::Ffmpeg) => { + let config = DownloadConfig { + format: suffix, + segment_time: config.segment_time, + file_size: Some(config.file_size), // 2GB + headers: stream_info.stream_headers.clone(), + extra_args: vec![], + downloader_type: DownloaderType::FfmpegInternal, + filename_prefix: recorder.generate_filename(), + }; + + Ok(Box::new(FfmpegDownloader::new( + raw_stream_url, + config, + PathBuf::from("./downloads"), + ))) + } + // Some(DownloaderType::StreamGears) => { + // + // }, + _ => Ok(Box::new(StreamGears::new( + raw_stream_url, + construct_headers(&stream_info.stream_headers), + recorder.filename_template(), + Segmentable::new( + config.segment_time.as_deref().map(parse_time), + Some(config.file_size), + ), + None, + ))), + } + } + + fn name(&self) -> &str { + &self.name + } +} + +async fn call_via_threads(obj: Arc>, url: &str) -> PyResult> { + let url = url.to_string(); + // obj. + tokio::task::spawn_blocking(move || { + Python::attach(|py| -> PyResult> { + // 从 biliup.util 获取 loop(按你项目里真实的名字来取) + let util = PyModule::import(py, "biliup.common.util")?; + // 下面两行二选一(取决于 biliup.util 的 API): + // let loop_obj: Py = util.getattr("loop")?.into_py(py); + // 或: + // let loop_obj: Py = util.call_method0("get_loop")?.into_py(py); + + // 这里假设是直接暴露了 util.loop + let loop_obj = util.getattr("loop")?; + + let asyncio = PyModule::import(py, "asyncio")?; + + // 生成协程 self.acheck_stream() + let instance = obj.bind(py).call1(("fname", url))?; + let coro = instance.call_method0("acheck_stream")?; + + // 调度到指定 loop + let fut = asyncio + .getattr("run_coroutine_threadsafe")? + .call1((coro, loop_obj))?; + + let res = fut.call_method0("result")?; + let is_live = res.unbind().extract(py)?; + let info = if is_live { + Some(stream_info_from_py(py, &instance)?) + } else { + None + }; + Ok(info) + }) + }) + .await + .expect("spawn_blocking panicked") +} + +pub fn from_py(actor_handle: Arc) -> PyResult> { + let classes: Vec = Python::attach(|py| -> PyResult> { + let plugins = py.import("biliup.plugins")?; + let decorators = py.import("biliup.engine.decorators")?; + // 获取 Plugin 类 + let plugin_class = decorators.getattr("Plugin")?; + + let instance = plugin_class.call1((plugins,))?; + + // 如果要获取类属性(而不是实例属性) + let bound = plugin_class.getattr("download_plugins")?; + let plugin_list: &Bound = bound.downcast()?; + + plugin_list + .iter() + .map(|x| { + let download = x.downcast::()?; + let py_plugin = PyPlugin::from_pytype(download)?; + Ok(DownloadManager::new(py_plugin, actor_handle.clone())) + }) + .collect::>() + // 基类 DownloadBase + // py.import("biliup.engine.download")?.getattr("DownloadBase") + })?; + // println!("类属性值: {:?}", class_attr); + + // let download_base: &Bound = download_base.downcast()?; + + Ok(classes) +} + +/// 从 Python 的 `self`(Bound)与 start_time / end_time 构造 StreamInfo +/// end_time 的语义与 Python 中一致:若未提供或为“假值”,则使用 time.localtime() +pub fn stream_info_from_py(py: Python<'_>, self_obj: &Bound<'_, PyAny>) -> PyResult { + // 从 self 上获取属性并抽取为 Rust 类型 + let name: String = self_obj.getattr("fname")?.extract()?; + let url: String = self_obj.getattr("url")?.extract()?; + let raw_stream_url: String = self_obj.getattr("raw_stream_url")?.extract()?; + let title: String = self_obj.getattr("room_title")?.extract()?; + let live_cover_path: Option = self_obj.getattr("live_cover_path")?.extract()?; + let is_download: bool = self_obj.getattr("is_download")?.extract()?; + let platform: String = self_obj.getattr("platform")?.extract()?; + let stream_headers: HashMap = self_obj.getattr("stream_headers")?.extract()?; + self_obj.call_method1("update_headers", (&stream_headers,))?; + + // date 直接使用传入的 start_time(保留为 Python 对象) + let date = OffsetDateTime::now_utc(); + // end_time: 若传入 None 或“假值”,则使用 time.localtime() + // let end_time_obj: PyObject = match end_time { + // Some(et) if et.is_true()? => et.to_object(py), + // _ => { + // let time_mod = py.import("time")?; + // let lt = time_mod.getattr("localtime")?.call0()?; + // lt.to_object(py) + // } + // };self.update_headers(self.stream_headers) + + Ok(StreamInfo { + name, + url, + suffix: media_ext_from_url(&raw_stream_url).unwrap(), + raw_stream_url, + title, + date, + live_cover_path, + platform, + stream_headers, + }) +} + +#[pyclass] +struct ConfigState { + // 用 PyObject 存,方便保持任意 Python 对象 + map: Arc>, +} + +#[pymethods] +impl ConfigState { + /// 获取:config.get("k", default=None) + /// - 若 key 存在,返回保存的对象 + /// - 若不存在,返回 default(默认 None) + #[pyo3(signature = (key, default=None))] + fn get<'py>( + &self, + py: Python<'py>, + key: &str, + default: Option>, + ) -> PyResult> { + let guard = self.map.read().unwrap(); + // serde_json::to_value(guard.deref()) + if let Some(bound) = pythonize(py, guard.deref())? + .extract::>()? + .get_item(key)? + { + if bound.is_none() + && let Some(d) = default + { + return Ok(d); + } + return Ok(bound); + }; + let Some(default) = default else { + return Err(pyo3::exceptions::PyAttributeError::new_err(format!( + "object has no attribute '{key}'" + ))); + }; + Ok(default) + } +} + +#[pyfunction] +pub fn config_bindings(py: Python<'_>) -> PyResult { + let state = ConfigState { + map: cfg_arc().clone(), + }; + // pythonize(py, &config) + Ok(state) +} + +// 进程级全局单例(安全):OnceLock + Arc + RwLock +pub static CONFIG: LazyLock>> = LazyLock::new(|| { + Arc::new(RwLock::new( + Config::builder().streamers(Default::default()).build(), + )) +}); + +fn cfg_arc() -> &'static Arc> { + &*CONFIG +} #[pyfunction] pub fn main_loop(py: Python<'_>) -> PyResult<()> { @@ -56,12 +346,17 @@ async fn _main() -> AppResult<()> { let conn_pool = ConnectionManager::new_pool("data/data.sqlite3") .await .expect("could not initialize the database connection pool"); - *config::CONFIG.write().unwrap() = repositories::get_config(&conn_pool).await?; - let service_register = ServiceRegister::new(conn_pool, config::CONFIG.clone()); + + *CONFIG.write().unwrap() = repositories::get_config(&conn_pool).await?; + let actor_handle = Arc::new(ActorHandle::new( + CONFIG.read().unwrap().pool1_size, + CONFIG.read().unwrap().pool2_size, + )); + let vec = from_py(actor_handle.clone()).unwrap(); + + let service_register = ServiceRegister::new(conn_pool, CONFIG.clone(), actor_handle, vec); let all_streamer = repositories::get_all_streamer(&service_register.pool).await?; - let all_uploader = repositories::get_all_uploader(&service_register.pool).await?; - // let mut monitors = Vec::new(); for streamer in all_streamer { // workers.push(Arc::new(Worker::new(streamer.id, service_register.pool.clone()))); diff --git a/crates/stream-gears/src/server/api/bilibili_endpoints.rs b/crates/stream-gears/src/server/api/bilibili_endpoints.rs deleted file mode 100644 index 25edb11..0000000 --- a/crates/stream-gears/src/server/api/bilibili_endpoints.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::server::errors::{AppError, report_to_response}; -use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::models::Configuration; -use axum::extract::{Query, State}; -use axum::response::Response; -use axum::{Extension, Json}; -use biliup::client::StatelessClient; -use biliup::uploader::credential::login_by_cookies; -use bytes::Bytes; -use error_stack::{Report, ResultExt}; -use ormlite::Model; -use std::collections::HashMap; -use tracing::info; - -pub async fn archive_pre_endpoint( - Query(params): Query>, - State(pool): State, -) -> Result, Response> { - let configurations = Configuration::select() - .where_("key = 'bilibili-cookies'") - .fetch_all(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - for cookies in configurations { - if let Ok(bili) = login_by_cookies(cookies.value, None).await { - return Ok(Json( - bili.archive_pre() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - )); - } - } - Err(report_to_response(Report::from(AppError::Custom( - "无可用 cookie 文件".to_string(), - )))) -} - -pub async fn get_myinfo_endpoint( - Query(params): Query>, -) -> Result, Response> { - let bili = login_by_cookies(¶ms["user"], None) - .await - .change_context(AppError::Custom(params["user"].to_string())) - .map_err(report_to_response)?; - Ok(Json( - bili.my_info() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - )) -} - -pub async fn get_proxy_endpoint( - State(client): State, - Query(params): Query>, -) -> Result { - info!(params = ¶ms["url"]); - // let bili = login_by_cookies(¶ms["user"]).await?; - client - .client - .get(¶ms["url"]) - .send() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)? - .bytes() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response) -} diff --git a/crates/stream-gears/src/server/api/endpoints.rs b/crates/stream-gears/src/server/api/endpoints.rs deleted file mode 100644 index c599bde..0000000 --- a/crates/stream-gears/src/server/api/endpoints.rs +++ /dev/null @@ -1,443 +0,0 @@ -use crate::server::config::Config; -use crate::server::errors::{AppError, AppResult, report_to_response}; -use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::context::Worker; -use crate::server::infrastructure::dto::LiveStreamerResponse; -use crate::server::infrastructure::models::{ - Configuration, FileItem, InsertConfiguration, InsertLiveStreamer, InsertUploadStreamer, - LiveStreamer, StreamerInfo, UploadStreamer, -}; -use crate::server::infrastructure::repositories::{del_streamer, get_all_streamer}; -use crate::server::infrastructure::service_register::ServiceRegister; -use axum::extract::{Path, State}; -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use axum::{Json, debug_handler}; -use biliup::credential::{Credential, LoginInfo}; -use error_stack::{Report, ResultExt}; -use ormlite::{Insert, Model}; -use serde_json::json; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, UNIX_EPOCH}; -use tokio::fs; -use tokio::io::AsyncWriteExt; -use tracing::info; - -pub async fn get_streamers_endpoint( - State(pool): State, - State(workers): State>>>>, -) -> Result>, Response> { - let live_streamers = get_all_streamer(&pool).await.map_err(report_to_response)?; - info!( - "get_streamers_endpoint found {} live streamers", - live_streamers.len() - ); - Ok(Json( - live_streamers - .into_iter() - .map(|x| LiveStreamerResponse { - status: workers - .read() - .unwrap() - .iter() - .find_map(|worker| { - if worker.id == x.id { - Some(*worker.downloader_status.read().unwrap()) - } else { - None - } - }) - .unwrap_or_default(), - inner: x, - }) - .collect(), - )) -} - -pub async fn post_streamers_endpoint( - State(service_register): State, - State(pool): State, - Json(payload): Json, -) -> Result, Response> { - let Some(manager) = service_register.get_manager(&payload.url) else { - info!("not supported url: {}", &payload.url); - return Err((StatusCode::BAD_REQUEST, "Not supported url").into_response()); - }; - let monitor = manager.ensure_monitor(); - - /// You can insert the model directly. - let live_streamers = payload - .insert(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - service_register - .add_room(live_streamers.id, &live_streamers.url, monitor) - .await - .map_err(report_to_response)?; - - info!(workers=?service_register.workers, "successfully inserted new live streamers"); - Ok(Json(live_streamers)) -} - -pub async fn put_streamers_endpoint( - State(service_register): State, - State(pool): State, - Json(payload): Json, -) -> Result, Response> { - let streamer = payload - .update_all_fields(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - let Some(manager) = service_register.get_manager(&streamer.url) else { - info!("not supported url: {}", &streamer.url); - return Err((StatusCode::BAD_REQUEST, "Not supported url").into_response()); - }; - let monitor = manager.ensure_monitor(); - - service_register - .del_room(streamer.id) - .await - .map_err(report_to_response)?; - - service_register - .add_room(streamer.id, &streamer.url, monitor) - .await - .map_err(report_to_response)?; - - info!(workers=?streamer, "successfully update live streamers"); - Ok(Json(streamer)) -} - -pub async fn delete_streamers_endpoint( - State(service_register): State, - State(pool): State, - State(workers): State>>>>, - Path(id): Path, -) -> Result, Response> { - service_register - .del_room(id) - .await - .map_err(report_to_response)?; - let live_streamers = del_streamer(&pool, id).await.map_err(report_to_response)?; - info!(workers=?service_register.workers, "successfully inserted new live streamers"); - Ok(Json(live_streamers)) -} - -pub async fn get_configuration( - State(config): State>>, -) -> Result, Response> { - Ok(Json(config.read().unwrap().clone())) -} - -// #[axum_macros::debug_handler(state = ServiceRegister)] -pub async fn put_configuration( - State(config): State>>, - State(pool): State, - Json(json_data): Json, -) -> Result, Response> { - // 将 JSON 序列化为 TEXT 存库 - let value_txt = serde_json::to_string(&json_data) - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - let mut tx = pool - .begin() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - // 最多取 2 条判断是否多行 - let ids: Vec = - sqlx::query_scalar::<_, i64>("SELECT id FROM configuration WHERE key = ?1 LIMIT 2") - .bind("config") - .fetch_all(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - let saved: Configuration = if ids.is_empty() { - // 插入 - sqlx::query("INSERT INTO configuration (key, value) VALUES (?1, ?2)") - .bind("config") - .bind(&value_txt) - .execute(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - // 取 last_insert_rowid 并读回整行 - let id: i64 = sqlx::query_scalar::<_, i64>("SELECT last_insert_rowid()") - .fetch_one(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - sqlx::query_as::<_, Configuration>("SELECT id, key, value FROM configuration WHERE id = ?1") - .bind(id) - .fetch_one(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)? - } else if ids.len() == 1 { - // 更新 - let id = ids[0]; - sqlx::query("UPDATE configuration SET value = ?1 WHERE id = ?2") - .bind(&value_txt) - .bind(id) - .execute(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - sqlx::query_as::<_, Configuration>("SELECT id, key, value FROM configuration WHERE id = ?1") - .bind(id) - .fetch_one(&mut *tx) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)? - } else { - // 多行报错 - return Err(report_to_response(Report::new(AppError::Custom( - format!("有多个空间配置同时存在 (key='config'): {} 行", ids.len()).to_string(), - )))); - }; - - tx.commit() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - // 提交后从 DB 重新加载配置 - let saved_config: Config = serde_json::from_str(&saved.value) - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - *config.write().unwrap() = saved_config; - Ok(Json(config.read().unwrap().clone())) -} - -pub async fn get_streamer_info( - // Extension(streamers_service): Extension, - State(pool): State, -) -> Result>, Response> { - let streamer_infos = StreamerInfo::select() - .fetch_all(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - let file_items = FileItem::select() - .fetch_all(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - println!("{:?}", file_items); - - Ok(Json(streamer_infos)) -} - -pub async fn get_upload_streamers_endpoint( - // Extension(streamers_service): Extension, - State(pool): State, -) -> Result>, Response> { - let uploader_streamers = UploadStreamer::select() - .fetch_all(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - Ok(Json(uploader_streamers)) -} - -pub async fn add_upload_streamer_endpoint( - // Extension(streamers_service): Extension, - State(pool): State, - Json(upload_streamer): Json, -) -> Result, Response> { - if upload_streamer.id.is_none() { - Ok(Json( - serde_json::to_value( - ormlite::Insert::insert(upload_streamer, &pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - ) - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - )) - } else { - Ok(Json( - serde_json::to_value( - upload_streamer - .update_all_fields(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - ) - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - )) - } -} - -pub async fn get_upload_streamer_endpoint( - State(pool): State, - Path(id): Path, -) -> Result, Response> { - let uploader_streamers = UploadStreamer::select() - .where_("id = ?") - .bind(id) - .fetch_one(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - Ok(Json(uploader_streamers)) -} -pub async fn delete_template_endpoint( - State(pool): State, - Path(id): Path, -) -> Result, Response> { - let uploader_streamers = UploadStreamer::select() - .where_("id = ?") - .bind(id) - .fetch_one(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - Ok(Json( - uploader_streamers - .delete(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?, - )) -} - -pub async fn get_users_endpoint( - State(pool): State, -) -> Result>, Response> { - let configurations = Configuration::select() - .where_("key = 'bilibili-cookies'") - .fetch_all(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - let mut res = Vec::new(); - for cookies in configurations { - res.push(json!({ - "id": cookies.id, - "name": cookies.value, - "value": cookies.value, - "platform": cookies.key, - })) - } - Ok(Json(res)) -} - -pub async fn add_user_endpoint( - State(pool): State, - Json(user): Json, -) -> Result, Response> { - let res = user - .insert(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - Ok(Json(res)) -} - -pub async fn delete_user_endpoint( - Path(id): Path, - State(pool): State, -) -> Result, Response> { - let x = sqlx::query("DELETE FROM configuration WHERE id = ?") - .bind(id) - .execute(&pool) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - info!("{:?}", x); - Ok(Json(())) -} - -pub async fn get_qrcode() -> Result, Response> { - let qrcode = Credential::new(None) - .get_qrcode() - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - Ok(Json(qrcode)) -} - -pub async fn login_by_qrcode( - Json(value): Json, -) -> Result, Response> { - let info = tokio::time::timeout( - Duration::from_secs(300), - Credential::new(None).login_by_qrcode(value), - // std::future::pending::>(), - ) - .await - .change_context(AppError::Custom("deadline has elapsed".to_string())) - .map_err(report_to_response)? - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - // extract mid - let mid = info.token_info.mid; - let filename = format!("data/{}.json", mid); - - let mut file = fs::File::create(&filename) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - file.write_all(&serde_json::to_vec_pretty(&info).unwrap()) - .await - .change_context(AppError::Unknown) - .map_err(report_to_response)?; - - Ok(Json(json!({ "filename": filename }))) -} - -pub async fn get_videos() -> Result>, Response> { - let media_extensions = [".mp4", ".flv", ".3gp", ".webm", ".mkv", ".ts"]; - let blacklist = ["next-env.d.ts"]; - - let mut file_list = Vec::new(); - let mut index = 1; - - // **use tokio::fs::read_dir** - if let Ok(mut entries) = fs::read_dir(".").await { - while let Ok(Some(entry)) = entries.next_entry().await { - let path = entry.path(); - let file_name = entry.file_name().to_string_lossy().into_owned(); - - if blacklist.contains(&file_name.as_str()) { - continue; - } - - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if media_extensions - .iter() - .any(|allowed| &ext == &allowed.trim_start_matches('.')) - { - if let Ok(metadata) = entry.metadata().await { - let mtime = metadata - .modified() - .ok() - .and_then(|time| time.duration_since(UNIX_EPOCH).ok()) - .map(|d| d.as_secs()) - .unwrap_or(0); - - file_list.push(serde_json::json!({ - "key": index, - "name": file_name, - "updateTime": mtime, - "size": metadata.len(), - })); - index += 1; - } - } - } - } - } - Ok(Json(file_list)) -} diff --git a/crates/stream-gears/src/server/api/mod.rs b/crates/stream-gears/src/server/api/mod.rs deleted file mode 100644 index f8294e6..0000000 --- a/crates/stream-gears/src/server/api/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod auth; -pub(crate) mod bilibili_endpoints; -pub mod endpoints; -pub(crate) mod spa; diff --git a/crates/stream-gears/src/server/api/spa.rs b/crates/stream-gears/src/server/api/spa.rs deleted file mode 100644 index 16f1e39..0000000 --- a/crates/stream-gears/src/server/api/spa.rs +++ /dev/null @@ -1,49 +0,0 @@ -use axum::http::{StatusCode, Uri, header}; -use axum::response::{Html, IntoResponse, Response}; -use rust_embed::Embed; - -static INDEX_HTML: &str = "index.html"; - -#[derive(Embed)] -#[folder = "../../out/"] -struct Assets; - -pub async fn static_handler(uri: Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/'); - - if path.is_empty() || path == INDEX_HTML { - return index_html().await; - } - - let guess_html = path.to_owned() + ".html"; - if let Some(html) = Assets::get(&guess_html) { - return Html(html.data).into_response(); - } - - match Assets::get(path) { - Some(content) => { - let mime = mime_guess::from_path(path).first_or_octet_stream(); - - ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() - } - None => { - // if path.contains('.') { - // return not_found().await; - // } - // - // index_html().await - not_found().await - } - } -} - -async fn index_html() -> Response { - match Assets::get(INDEX_HTML) { - Some(content) => Html(content.data).into_response(), - None => not_found().await, - } -} - -async fn not_found() -> Response { - (StatusCode::NOT_FOUND, "404").into_response() -} diff --git a/crates/stream-gears/src/server/core/plugin.rs b/crates/stream-gears/src/server/core/plugin.rs deleted file mode 100644 index f71ce82..0000000 --- a/crates/stream-gears/src/server/core/plugin.rs +++ /dev/null @@ -1,285 +0,0 @@ -use crate::construct_headers; -use crate::server::core::download_manager::{ActorHandle, DownloadManager}; -use crate::server::core::downloader::ffmpeg_downloader::FfmpegDownloader; -use crate::server::core::downloader::stream_gears::StreamGears; -use crate::server::core::downloader::{DownloadConfig, Downloader, DownloaderType}; -use crate::server::errors::{AppError, AppResult}; -use crate::server::infrastructure::context::Worker; -use crate::server::util::{Recorder, media_ext_from_url, parse_time}; -use async_trait::async_trait; -use biliup::downloader::util::Segmentable; -use error_stack::{Report, ResultExt}; -use fancy_regex::Regex; -use pyo3::prelude::{PyAnyMethods, PyListMethods, PyModule}; -use pyo3::types::{PyList, PyType}; -use pyo3::{Bound, Py, PyAny, PyResult, Python}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use time::OffsetDateTime; -use tracing::{debug, info}; - -// Stream information structures -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StreamInfo { - pub name: String, - pub url: String, - pub raw_stream_url: String, - pub title: String, - #[serde(with = "time::serde::rfc3339")] - pub date: OffsetDateTime, // 保存 Python 的时间对象(如 time.struct_time) - // pub end_time: PyObject, // 同上 - pub live_cover_path: Option, - pub platform: String, - pub stream_headers: HashMap, - pub suffix: String, -} - -impl StreamInfo { - /// 从 Python 的 `self`(Bound)与 start_time / end_time 构造 StreamInfo - /// end_time 的语义与 Python 中一致:若未提供或为“假值”,则使用 time.localtime() - pub fn from_py(py: Python<'_>, self_obj: &Bound<'_, PyAny>) -> PyResult { - // 从 self 上获取属性并抽取为 Rust 类型 - let name: String = self_obj.getattr("fname")?.extract()?; - let url: String = self_obj.getattr("url")?.extract()?; - let raw_stream_url: String = self_obj.getattr("raw_stream_url")?.extract()?; - let title: String = self_obj.getattr("room_title")?.extract()?; - let live_cover_path: Option = self_obj.getattr("live_cover_path")?.extract()?; - let is_download: bool = self_obj.getattr("is_download")?.extract()?; - let platform: String = self_obj.getattr("platform")?.extract()?; - let stream_headers: HashMap = - self_obj.getattr("stream_headers")?.extract()?; - self_obj.call_method1("update_headers", (&stream_headers,))?; - - // date 直接使用传入的 start_time(保留为 Python 对象) - let date = OffsetDateTime::now_utc(); - // end_time: 若传入 None 或“假值”,则使用 time.localtime() - // let end_time_obj: PyObject = match end_time { - // Some(et) if et.is_true()? => et.to_object(py), - // _ => { - // let time_mod = py.import("time")?; - // let lt = time_mod.getattr("localtime")?.call0()?; - // lt.to_object(py) - // } - // };self.update_headers(self.stream_headers) - - Ok(Self { - name, - url, - suffix: media_ext_from_url(&raw_stream_url).unwrap(), - raw_stream_url, - title, - date, - live_cover_path, - platform, - stream_headers, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum StreamStatus { - Live { stream_info: StreamInfo }, - Offline, - Unknown, -} - -#[async_trait] -pub trait DownloadBase: Send + Sync { - async fn check_stream(&self) -> Result>; - async fn get_stream_info(&self) -> Result>; - async fn download(&self, output_path: impl AsRef) -> Result<(), Report>; - fn should_record(&self, room_title: &str) -> bool; - fn get_platform_name(&self) -> &'static str; -} - -#[async_trait] -pub trait DownloadPlugin { - fn matches(&self, url: &str) -> bool; - async fn check_status(&self, url: &str) -> Result>; - async fn create_downloader( - &self, - stream_info: &StreamInfo, - worker: &Worker, - ) -> AppResult>; - - fn name(&self) -> &str; -} - -#[derive(Debug)] -pub struct PyPlugin { - plugin: Arc>, - pattern: Regex, - name: String, -} - -impl PyPlugin { - pub fn from_pytype(class: &Bound) -> PyResult { - let pattern = class.getattr("VALID_URL_BASE")?.to_string(); - // info!("{pattern}"); - let re = Regex::new(&pattern).unwrap(); - let plugin = class.clone(); - let name: String = class.getattr("__name__")?.extract()?; - // info!("发现插件类: {name}"); - Ok(Self { - plugin: Arc::new(plugin.unbind()), - pattern: re, - name, - }) - } -} - -#[async_trait] -impl DownloadPlugin for PyPlugin { - fn matches(&self, url: &str) -> bool { - if self.pattern.is_match(url).unwrap() { - // 找到匹配的部分 - if let Some(mat) = self.pattern.find(url).unwrap() { - debug!(" 匹配内容: {}", mat.as_str()); - return true; - } - } - false - } - - async fn check_status(&self, url: &str) -> Result> { - info!("Checking status"); - match call_via_threads(self.plugin.clone(), url) - .await - .change_context(AppError::Unknown)? - { - Some(info) => Ok(StreamStatus::Live { stream_info: info }), - None => Ok(StreamStatus::Offline), - } - } - - async fn create_downloader( - &self, - stream_info: &StreamInfo, - worker: &Worker, - ) -> AppResult> { - let config = worker.get_config().await?; - let streamer = worker.get_streamer().await?; - // info!(stream_info=?stream_info, "Create downloader"); - let raw_stream_url = &stream_info.raw_stream_url; - let suffix = streamer - .format - .unwrap_or_else(|| stream_info.suffix.clone()); - println!("suffix: {suffix}"); - let recorder = Recorder::new( - streamer.filename_prefix, - &streamer.remark, - &stream_info.title, - &suffix, - ); - - match config.downloader { - Some(DownloaderType::Ffmpeg) => { - let config = DownloadConfig { - format: suffix, - segment_time: config.segment_time, - file_size: Some(config.file_size), // 2GB - headers: stream_info.stream_headers.clone(), - extra_args: vec![], - downloader_type: DownloaderType::FfmpegInternal, - filename_prefix: recorder.generate_filename(), - }; - - Ok(Box::new(FfmpegDownloader::new( - raw_stream_url, - config, - PathBuf::from("./downloads"), - ))) - } - // Some(DownloaderType::StreamGears) => { - // - // }, - _ => Ok(Box::new(StreamGears::new( - raw_stream_url, - construct_headers(&stream_info.stream_headers), - recorder.filename_template(), - Segmentable::new( - config.segment_time.as_deref().map(parse_time), - Some(config.file_size), - ), - None, - ))), - } - } - - fn name(&self) -> &str { - &self.name - } -} - -async fn call_via_threads(obj: Arc>, url: &str) -> PyResult> { - let url = url.to_string(); - // obj. - tokio::task::spawn_blocking(move || { - Python::attach(|py| -> PyResult> { - // 从 biliup.util 获取 loop(按你项目里真实的名字来取) - let util = PyModule::import(py, "biliup.common.util")?; - // 下面两行二选一(取决于 biliup.util 的 API): - // let loop_obj: Py = util.getattr("loop")?.into_py(py); - // 或: - // let loop_obj: Py = util.call_method0("get_loop")?.into_py(py); - - // 这里假设是直接暴露了 util.loop - let loop_obj = util.getattr("loop")?; - - let asyncio = PyModule::import(py, "asyncio")?; - - // 生成协程 self.acheck_stream() - let instance = obj.bind(py).call1(("fname", url))?; - let coro = instance.call_method0("acheck_stream")?; - - // 调度到指定 loop - let fut = asyncio - .getattr("run_coroutine_threadsafe")? - .call1((coro, loop_obj))?; - - let res = fut.call_method0("result")?; - let is_live = res.unbind().extract(py)?; - let info = if is_live { - Some(StreamInfo::from_py(py, &instance)?) - } else { - None - }; - Ok(info) - }) - }) - .await - .expect("spawn_blocking panicked") -} - -pub fn from_py(actor_handle: Arc) -> PyResult> { - let classes: Vec = Python::attach(|py| -> PyResult> { - let plugins = py.import("biliup.plugins")?; - let decorators = py.import("biliup.engine.decorators")?; - // 获取 Plugin 类 - let plugin_class = decorators.getattr("Plugin")?; - - let instance = plugin_class.call1((plugins,))?; - - // 如果要获取类属性(而不是实例属性) - let bound = plugin_class.getattr("download_plugins")?; - let plugin_list: &Bound = bound.downcast()?; - - plugin_list - .iter() - .map(|x| { - let download = x.downcast::()?; - let py_plugin = PyPlugin::from_pytype(download)?; - Ok(DownloadManager::new(py_plugin, actor_handle.clone())) - }) - .collect::>() - // 基类 DownloadBase - // py.import("biliup.engine.download")?.getattr("DownloadBase") - })?; - // println!("类属性值: {:?}", class_attr); - - // let download_base: &Bound = download_base.downcast()?; - - Ok(classes) -} diff --git a/crates/stream-gears/src/server/errors.rs b/crates/stream-gears/src/server/errors.rs deleted file mode 100644 index dd2d34c..0000000 --- a/crates/stream-gears/src/server/errors.rs +++ /dev/null @@ -1,56 +0,0 @@ -use axum::Json; -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use error_stack::Report; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use thiserror::Error; -use tracing::log::error; - -#[derive(Error, Debug)] -pub enum AppError { - #[error("Unknown Error")] - Unknown, - - #[error("{0}")] - Custom(String), -} - -pub fn report_to_response(report: impl Into>) -> Response { - let report = report.into(); - let (status, error_message) = match report.downcast_ref::() { - Some(AppError::Unknown) => (StatusCode::INTERNAL_SERVER_ERROR, report.to_string()), - Some(AppError::Custom(msg)) => (StatusCode::INTERNAL_SERVER_ERROR, msg.into()), - _ => (StatusCode::INTERNAL_SERVER_ERROR, report.to_string()), - }; - tracing::error!(error = ?report); - // I'm not a fan of the error specification, so for the sake of consistency, - // serialize singular errors as a map of vectors similar to the 422 validation responses - let body = Json(ApiError::new(error_message)); - - (status, body).into_response() -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct ApiError { - pub errors: Vec>, - pub message: String, -} - -impl ApiError { - pub fn new(message: String) -> Self { - let errors: Vec> = Vec::new(); - // errors.push(HashMap::from([ - // (String::from("resource"), "Issue".to_string()), - // (String::from("field"), "title".to_string()), - // (String::from("code"), "missing_field".to_string()), - // (String::from("code"), "unprocessable".to_string()), - // (String::from("code"), "already_exists".to_string()), - // (String::from("code"), "invalid".to_string()), - // (String::from("code"), "missing".to_string()), - // ])); - Self { errors, message } - } -} - -pub(crate) type AppResult = Result>; diff --git a/crates/stream-gears/src/server/infrastructure/connection_pool.rs b/crates/stream-gears/src/server/infrastructure/connection_pool.rs deleted file mode 100644 index a191015..0000000 --- a/crates/stream-gears/src/server/infrastructure/connection_pool.rs +++ /dev/null @@ -1,39 +0,0 @@ -use anyhow::{Context, Result}; -use sqlx::sqlite::SqlitePoolOptions; -use sqlx::{Pool, Sqlite}; -use std::path::Path; -use tracing::info; - -pub type ConnectionPool = Pool; - -pub struct ConnectionManager; - -impl ConnectionManager { - pub async fn new_pool(path: &str) -> Result { - // 创建所有父级目录(如果不存在) - if let Some(parent) = Path::new(path).parent() { - std::fs::create_dir_all(parent)?; // 创建 data/ 目录 - } - let db_url = format!("sqlite://{path}"); - /// Start by making a database connection. - std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(path)?; - let pool = SqlitePoolOptions::new() - .max_connections(2) - .connect(&db_url) - .await - .context("error while initializing the database connection pool")?; - - /// Query builder syntax closely follows SQL syntax, translated into chained function calls. - - info!("migrations enabled, running..."); - sqlx::migrate!() - .run(&pool) - .await - .context("error while running database migrations")?; - - Ok(pool) - } -} diff --git a/crates/stream-gears/src/server/infrastructure/repositories.rs b/crates/stream-gears/src/server/infrastructure/repositories.rs deleted file mode 100644 index 832c9f9..0000000 --- a/crates/stream-gears/src/server/infrastructure/repositories.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::server::config::Config; -use crate::server::errors::{AppError, AppResult}; -use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::models::{ - Configuration, InsertConfiguration, LiveStreamer, UploadStreamer, -}; -use error_stack::ResultExt; -use ormlite::{Insert, Model}; - -pub async fn get_streamer(pool: &ConnectionPool, id: i64) -> AppResult { - LiveStreamer::select() - .where_("id = ?") - .bind(id) - .fetch_one(pool) - .await - .change_context(AppError::Unknown) -} - -pub async fn del_streamer(pool: &ConnectionPool, id: i64) -> AppResult { - let streamer = get_streamer(pool, id).await?; - streamer - .clone() - .delete(pool) - .await - .change_context(AppError::Unknown)?; - Ok(streamer) -} -pub async fn get_all_streamer(pool: &ConnectionPool) -> AppResult> { - LiveStreamer::select() - .fetch_all(pool) - .await - .change_context(AppError::Unknown) -} - -pub async fn get_config(pool: &ConnectionPool) -> AppResult { - let configuration = Configuration::select() - .where_("key = 'config'") - .fetch_optional(pool) - .await - .change_context(AppError::Unknown)?; - if let Some(configuration) = configuration { - let json: Config = - serde_json::from_str(&configuration.value).change_context(AppError::Unknown)?; - Ok(json) - } else { - let config = Config::builder().streamers(Default::default()).build(); - Ok(config) - } -} - -pub async fn insert_config(pool: &ConnectionPool, config: &Config) -> AppResult { - let configuration = InsertConfiguration { - key: "config".to_string(), - value: serde_json::to_string(config).unwrap(), - } - .insert(pool) - .await - .change_context(AppError::Unknown)?; - Ok(configuration) -} - -pub async fn get_all_uploader(pool: &ConnectionPool) -> AppResult> { - UploadStreamer::select() - .fetch_all(pool) - .await - .change_context(AppError::Unknown) -} diff --git a/crates/stream-gears/src/server/infrastructure/service_register.rs b/crates/stream-gears/src/server/infrastructure/service_register.rs deleted file mode 100644 index 51e6f63..0000000 --- a/crates/stream-gears/src/server/infrastructure/service_register.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::server::config; -use crate::server::config::Config; -use crate::server::core::download_manager::{ActorHandle, DownloadManager}; -use crate::server::core::monitor::Monitor; -use crate::server::core::plugin; -use crate::server::errors::{AppError, AppResult}; -use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::context::{Context, Worker}; -use crate::server::infrastructure::repositories; -use axum::extract::FromRef; -use biliup::client::StatelessClient; -use biliup::credential::Credential; -use error_stack::bail; -use ormlite::Model; -use std::sync::{Arc, RwLock}; -use tracing::info; - -#[derive(FromRef, Clone)] -pub struct ServiceRegister { - pub pool: ConnectionPool, - pub workers: Arc>>>, - pub managers: Arc>, - pub config: Arc>, - pub actor_handle: Arc, - pub client: StatelessClient, -} - -/// A simple service container responsible for managing the various services our API endpoints will pull from through axum extensions. -impl ServiceRegister { - pub fn new(pool: ConnectionPool, config: Arc>) -> Self { - info!("initializing utility services..."); - let client = StatelessClient::default(); - - info!(config=?config); - let actor_handle = Arc::new(ActorHandle::new( - config.read().unwrap().pool1_size, - config.read().unwrap().pool2_size, - )); - - info!("utility services initialized, building feature services..."); - - let vec = plugin::from_py(actor_handle.clone()).unwrap(); - - info!("feature services successfully initialized!"); - - ServiceRegister { - pool, - workers: Arc::new(Default::default()), - managers: Arc::new(vec), - config: config.clone(), - actor_handle, - client, - } - } - - pub fn get_manager(&self, url: &str) -> Option<&DownloadManager> { - self.managers - .iter() - .find(|&manager| manager.matches(url)) - .map(|v| v as _) - } - - pub async fn add_room( - &self, - id: i64, - url: &str, - monitor: Arc, - ) -> AppResult> { - let worker = Arc::new(Worker::new( - id, - url, - Context::new(self.pool.clone(), self.client.clone()), - )); - monitor.rooms_handle.add(worker.clone()).await; - self.workers.write().unwrap().push(worker); - info!("add {url} success"); - Ok(Some(())) - } - - pub async fn del_room(&self, id: i64) -> AppResult<()> { - let Some(i) = self.workers.read().unwrap().iter().position(|x| x.id == id) else { - return Err(error_stack::Report::new(AppError::Unknown)); - }; - - let removed = self.workers.write().unwrap().swap_remove(i); - let url = &removed.url; - let Some(manager) = self.get_manager(url) else { - info!("not found url: {url}"); - bail!(AppError::Unknown) - }; - let monitor = manager.ensure_monitor(); - let len = monitor.rooms_handle.del(id).await; - info!("{id} removed, remained len {len}"); - if len == 0 { - *manager.monitor.lock().unwrap() = None; - } - - Ok(()) - } -} - -// impl FromRef for ConnectionPool { -// fn from_ref(app_state: &ServiceRegister) -> ConnectionPool { -// app_state.pool.clone() -// } -// } - -// pub(crate) async fn add_room(&self, id: i64, pool: ConnectionPool) -> Arc { -// let arc = self.ensure_monitor().rooms_handle.clone(); -// let worker = Arc::new(Worker::new(id, pool, arc.clone())); -// arc.add(worker.clone()).await; -// worker -// } -// -// pub async fn del_room(&self, id: i64) { -// let len = self.ensure_monitor().rooms_handle.del(id).await; -// info!("{id} removed, remained len {len}"); -// if len == 0 { -// *self.monitor.lock().unwrap() = None; -// } -// } diff --git a/crates/stream-gears/src/uploader.rs b/crates/stream-gears/src/uploader.rs index ce869cf..d14acbe 100644 --- a/crates/stream-gears/src/uploader.rs +++ b/crates/stream-gears/src/uploader.rs @@ -1,4 +1,3 @@ -use anyhow::{Context, Result}; use biliup::client::StatelessClient; use biliup::error::Kind; use biliup::uploader::bilibili::{Credit, ResponseData, Studio}; @@ -10,12 +9,14 @@ use futures::StreamExt; use pyo3::prelude::*; use pyo3::pyclass; +use error_stack::ResultExt; use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use std::time::Instant; use tracing::info; +use biliup_cli::server::errors::{AppError, AppResult}; use typed_builder::TypedBuilder; #[pyclass] @@ -82,7 +83,7 @@ pub async fn upload( studio_pre: StudioPre, submit: Option<&str>, proxy: Option<&str>, -) -> Result { +) -> AppResult { // let file = std::fs::File::options() // .read(true) // .write(true) @@ -114,10 +115,13 @@ pub async fn upload( let bilibili = login_by_cookies(&cookie_file, proxy).await; let bilibili = match bilibili { - Err(Kind::IO(_)) => bilibili.with_context(|| { - String::from("open cookies file: ") + &cookie_file.to_string_lossy() + Err(Kind::IO(_)) => bilibili.change_context_lazy(|| { + AppError::Custom(format!( + "open cookies file: {}", + &cookie_file.to_string_lossy() + )) })?, - _ => bilibili?, + _ => bilibili.change_context_lazy(|| AppError::Unknown)?, }; let client = StatelessClient::default(); @@ -141,12 +145,21 @@ pub async fn upload( _ => Probe::probe(&client.client).await.unwrap_or_default(), }; for video_path in video_path { - println!("{:?}", video_path.canonicalize()?.to_str()); + println!( + "{:?}", + video_path + .canonicalize() + .change_context_lazy(|| AppError::Unknown)? + .to_str() + ); info!("{line:?}"); - let video_file = VideoFile::new(&video_path)?; + let video_file = VideoFile::new(&video_path).change_context_lazy(|| AppError::Unknown)?; let total_size = video_file.total_size; let file_name = video_file.file_name.clone(); - let uploader = line.pre_upload(&bilibili, video_file).await?; + let uploader = line + .pre_upload(&bilibili, video_file) + .await + .change_context_lazy(|| AppError::Unknown)?; let instant = Instant::now(); @@ -158,7 +171,8 @@ pub async fn upload( Ok((chunk, len)) }) }) - .await?; + .await + .change_context_lazy(|| AppError::Unknown)?; let t = instant.elapsed().as_millis(); info!( "Upload completed: {file_name} => cost {:.2}s, {:.2} MB/s.", @@ -203,9 +217,11 @@ pub async fn upload( let url = bilibili .cover_up( &std::fs::read(&studio.cover) - .with_context(|| format!("cover: {}", studio.cover))?, + .change_context_lazy(|| AppError::Unknown) + .attach_with(|| format!("cover: {}", studio.cover))?, ) - .await?; + .await + .change_context_lazy(|| AppError::Unknown)?; println!("{url}"); studio.cover = url; } @@ -216,8 +232,14 @@ pub async fn upload( }; let submit_result = match submit { - SubmitOption::BCutAndroid => bilibili.submit_by_bcut_android(&studio, proxy).await?, - _ => bilibili.submit_by_app(&studio, proxy).await?, + SubmitOption::BCutAndroid => bilibili + .submit_by_bcut_android(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, + _ => bilibili + .submit_by_app(&studio, proxy) + .await + .change_context_lazy(|| AppError::Unknown)?, }; Ok(submit_result) }