From ea17305b0a0e184531b29cee80ced70b771effec Mon Sep 17 00:00:00 2001 From: ForgQi <34411314+ForgQi@users.noreply.github.com> Date: Wed, 1 Oct 2025 00:26:07 +0800 Subject: [PATCH] Refactor actor system and add upload status tracking Refactored download and upload actor implementations into separate modules for better maintainability. Added upload_status field to LiveStreamerEntity and API responses, and updated the frontend to display upload status. Also added Huya protocol config option and improved Huya stream header handling in stream-gears. Minor bug fixes and code cleanups included. --- Cargo.lock | 4 +- Cargo.toml | 2 +- app/(app)/streamers/page.tsx | 14 +- app/lib/api-streamer.ts | 1 + crates/biliup-cli/src/server/api/endpoints.rs | 27 +-- .../biliup-cli/src/server/common/download.rs | 59 ++++++- crates/biliup-cli/src/server/common/upload.rs | 78 ++++++++- crates/biliup-cli/src/server/config.rs | 3 + .../src/server/core/download_manager.rs | 159 +----------------- crates/biliup-cli/src/server/core/monitor.rs | 7 +- .../src/server/infrastructure/dto.rs | 2 + .../infrastructure/models/live_streamer.rs | 2 +- .../server/infrastructure/service_register.rs | 1 - crates/stream-gears/src/server.rs | 11 +- 14 files changed, 188 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acc4c83..c7e4895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,7 +395,7 @@ dependencies = [ [[package]] name = "biliup-cli" -version = "1.1.13" +version = "1.1.14" dependencies = [ "async-channel", "async-trait", @@ -3641,7 +3641,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stream-gears" -version = "1.1.13" +version = "1.1.14" dependencies = [ "async-channel", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0fc36b9..0d6f288 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "3" [workspace.package] -version = "1.1.13" +version = "1.1.14" [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/app/(app)/streamers/page.tsx b/app/(app)/streamers/page.tsx index 54bda31..eacca58 100644 --- a/app/(app)/streamers/page.tsx +++ b/app/(app)/streamers/page.tsx @@ -7,7 +7,7 @@ import { Typography, Popconfirm, Notification, - Card, Dropdown, + Card, Dropdown, Badge, } from '@douyinfe/semi-ui' import { IconHelpCircle, @@ -15,7 +15,7 @@ import { IconVideoListStroked, IconEdit2Stroked, IconDeleteStroked, - IconWrench, IconTreeTriangleDown, IconPause, IconPlay, + IconWrench, IconTreeTriangleDown, IconPause, IconPlay, IconLock, IconUpload, } from '@douyinfe/semi-icons' import { List, ButtonGroup } from '@douyinfe/semi-ui' import React, { useState } from 'react' @@ -101,6 +101,7 @@ export default function Home() { console.log(values); delete values.status delete values.statusTag + delete values.upload_status if (values?.postprocessor) { values.postprocessor = values.postprocessor.map( ({ cmd, value }: { cmd: string; value: string }) => (cmd === 'rm' ? 'rm' : { [cmd]: value }) @@ -221,12 +222,15 @@ export default function Home() { } >
{item.statusTag}
+
+ {item.upload_status === "Pending" ? }> : null} +

{item.remark}

+
+ {item.url} +
+ )} /> diff --git a/app/lib/api-streamer.ts b/app/lib/api-streamer.ts index 7961926..b3880f8 100644 --- a/app/lib/api-streamer.ts +++ b/app/lib/api-streamer.ts @@ -101,6 +101,7 @@ export interface LiveStreamerEntity { split_size?: number; upload_id?: number; status?: string; + upload_status?: string; statusTag?: React.ReactNode; format?: string; time_range?: string | Date[]; diff --git a/crates/biliup-cli/src/server/api/endpoints.rs b/crates/biliup-cli/src/server/api/endpoints.rs index c0e49eb..426448d 100644 --- a/crates/biliup-cli/src/server/api/endpoints.rs +++ b/crates/biliup-cli/src/server/api/endpoints.rs @@ -49,20 +49,23 @@ pub async fn get_streamers_endpoint( Ok(Json( live_streamers .into_iter() - .map(|x| LiveStreamerResponse { - status: workers + .map(|x| { + let option = workers .read() .unwrap() - .iter() - .find_map(|worker| { - if worker.live_streamer.id == x.id { - Some(format!("{:?}", *worker.downloader_status.read().unwrap())) - } else { - None - } - }) - .unwrap_or_default(), - inner: x, + .clone() + .into_iter() + .find(|worker| worker.live_streamer.id == x.id); + LiveStreamerResponse { + status: option + .as_ref() + .map(|t| format!("{:?}", *t.downloader_status.read().unwrap())) + .unwrap_or_default(), + inner: x, + upload_status: option + .map(|t| format!("{:?}", *t.uploader_status.read().unwrap())) + .unwrap_or_default(), + } }) .collect(), )) diff --git a/crates/biliup-cli/src/server/common/download.rs b/crates/biliup-cli/src/server/common/download.rs index 15cb25c..0c49072 100644 --- a/crates/biliup-cli/src/server/common/download.rs +++ b/crates/biliup-cli/src/server/common/download.rs @@ -1,5 +1,5 @@ +use crate::server::common::upload::UploaderMessage; use crate::server::common::util::Recorder; -use crate::server::core::download_manager::{DownloaderMessage, UploaderMessage}; use crate::server::core::downloader::{DownloadStatus, Downloader, SegmentEvent, SegmentInfo}; use crate::server::core::monitor::RoomsHandle; use crate::server::core::plugin::{DownloadPlugin, StreamInfoExt}; @@ -112,7 +112,8 @@ impl FileValidator { if size < self.min_size { bail!(AppError::Custom(format!( - "File too small: {size} bytes, minimum: {} bytes", + "File {} too small: {size} bytes, minimum: {} bytes", + path.display(), self.min_size ))); } @@ -354,3 +355,57 @@ impl DownloadTask { Ok(()) } } + +/// 下载Actor +/// 负责处理下载相关的消息和任务 +pub struct DActor { + /// 下载消息接收器 + receiver: Receiver, + /// 上传消息发送器 + sender: Sender, +} + +impl DActor { + /// 创建新的下载Actor实例 + pub fn new(receiver: Receiver, sender: Sender) -> Self { + Self { receiver, sender } + } + + /// 运行Actor主循环,处理接收到的消息 + pub(crate) async fn run(&mut self) { + while let Ok(msg) = self.receiver.recv().await { + if let Err(e) = self.handle_message(msg).await { + error!("Error handling message: {}", e); + } + } + } + + /// 处理下载消息 + /// + /// # 参数 + /// * `msg` - 要处理的下载消息 + async fn handle_message(&mut self, msg: DownloaderMessage) -> AppResult<()> { + match msg { + DownloaderMessage::Start(plugin, ctx, rooms_handle) => { + // 创建下载任务 + let task = DownloadTask::new(plugin, ctx, rooms_handle); + + // 执行下载(使用 Result 链式处理) + task.execute(&self.sender).await?; + + Ok(()) + } + } + } +} + +/// 下载消息枚举 +/// 定义下载Actor可以处理的消息类型 +pub enum DownloaderMessage { + /// 开始下载消息,包含插件、流信息、上下文和房间句柄 + Start( + Arc, + Context, + Arc, + ), +} diff --git a/crates/biliup-cli/src/server/common/upload.rs b/crates/biliup-cli/src/server/common/upload.rs index 13a0241..c81b856 100644 --- a/crates/biliup-cli/src/server/common/upload.rs +++ b/crates/biliup-cli/src/server/common/upload.rs @@ -1,9 +1,8 @@ use crate::UploadLine; use crate::server::common::util::Recorder; -use crate::server::core::download_manager::UActor; use crate::server::core::downloader::{SegmentEvent, SegmentInfo}; use crate::server::errors::{AppError, AppResult}; -use crate::server::infrastructure::context::{Context, Worker}; +use crate::server::infrastructure::context::{Context, Stage, Worker, WorkerStatus}; use crate::server::infrastructure::models::InsertFileItem; use crate::server::infrastructure::models::hook_step::process_video; use crate::server::infrastructure::models::upload_streamer::UploadStreamer; @@ -359,3 +358,78 @@ pub async fn upload( Ok((bilibili, videos)) } + +/// 上传Actor +/// 负责处理上传相关的消息和任务 +pub struct UActor { + /// 上传消息接收器 + receiver: Receiver, +} + +impl UActor { + /// 创建新的上传Actor实例 + pub fn new(receiver: Receiver) -> Self { + Self { receiver } + } + + /// 运行Actor主循环,处理接收到的消息 + pub(crate) async fn run(&mut self) { + while let Ok(msg) = self.receiver.recv().await { + self.handle_message(msg).await; + } + } + + /// 处理上传消息 + /// + /// # 参数 + /// * `msg` - 要处理的上传消息 + async fn handle_message(&mut self, msg: UploaderMessage) { + match msg { + UploaderMessage::SegmentEvent(rx, ctx) => { + ctx.worker + .change_status(Stage::Upload, WorkerStatus::Pending); + let inspect = rx.inspect(|f| { + let pool = ctx.pool.clone(); + let streamer_info_id = ctx.stream_info.streamer_info.id; + let file = f.prev_file_path.display().to_string(); + tokio::spawn(async move { + let result = InsertFileItem { + file, + streamer_info_id, + } + .insert(&pool) + .await; + info!(result=?result, "Insert file"); + }); + }); + let result = match ctx.worker.get_upload_config() { + Some(config) => process_with_upload(inspect, &ctx, config).await, + None => { + let mut paths = Vec::new(); + pin!(inspect); + while let Some(event) = inspect.next().await { + paths.push(event.prev_file_path); + } + // 无上传配置时,直接执行后处理 + execute_postprocessor(paths, &ctx).await + } + }; + + if let Err(e) = &result { + error!("Process segment event failed: {}", e); + // 可以添加错误通知机制 + } + info!(url=ctx.stream_info.streamer_info.url, result=?result, "后处理执行完毕:Finished processing segment event"); + ctx.worker.change_status(Stage::Upload, WorkerStatus::Idle); + } + } + } +} + +/// 上传消息枚举 +/// 定义上传Actor可以处理的消息类型 +#[derive(Debug)] +pub enum UploaderMessage { + /// 分段事件消息,包含事件、接收器和工作器 + SegmentEvent(Receiver, Context), +} diff --git a/crates/biliup-cli/src/server/config.rs b/crates/biliup-cli/src/server/config.rs index 760af3d..0d8e40c 100644 --- a/crates/biliup-cli/src/server/config.rs +++ b/crates/biliup-cli/src/server/config.rs @@ -108,6 +108,9 @@ pub struct Config { /// 虎牙最大比率 #[serde(default)] pub huya_max_ratio: Option, + /// 虎牙 Flv or Hls + #[serde(default)] + pub huya_protocol: Option, // 抖音平台设置 /// 抖音弹幕录制 diff --git a/crates/biliup-cli/src/server/core/download_manager.rs b/crates/biliup-cli/src/server/core/download_manager.rs index 6bc969f..f6780f9 100644 --- a/crates/biliup-cli/src/server/core/download_manager.rs +++ b/crates/biliup-cli/src/server/core/download_manager.rs @@ -1,33 +1,14 @@ -use crate::server::common::download::DownloadTask; -use crate::server::common::upload::{execute_postprocessor, process_with_upload}; -use crate::server::common::util::Recorder; -use crate::server::core::downloader::{Downloader, SegmentEvent, SegmentInfo}; -use crate::server::core::monitor::{Monitor, RoomsHandle}; -use crate::server::core::plugin::{DownloadPlugin, StreamInfoExt}; -use crate::server::errors::{AppError, AppResult}; +use crate::server::common::download::{DActor, DownloaderMessage}; +use crate::server::common::upload::{UActor, UploaderMessage}; +use crate::server::core::downloader::Downloader; +use crate::server::core::monitor::Monitor; +use crate::server::core::plugin::DownloadPlugin; use crate::server::infrastructure::connection_pool::ConnectionPool; -use crate::server::infrastructure::context::{Context, Stage, Worker, WorkerStatus}; -use crate::server::infrastructure::models::InsertFileItem; -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; -use biliup::credential::login_by_cookies; -use biliup::uploader::line::{Line, Probe}; -use biliup::uploader::util::SubmitOption; -use biliup::uploader::{VideoFile, line}; +use async_channel::{Sender, bounded}; use core::fmt; use error_stack::ResultExt; -use futures::StreamExt; -use ormlite::Insert; -use std::path::Path; -use std::str::FromStr; use std::sync::{Arc, Mutex}; -use std::time::Instant; -use tokio::pin; use tokio::task::JoinHandle; -use tracing::{error, info, warn}; - /// 下载管理器 /// 负责管理特定平台的下载任务,包括监控器和插件 pub struct DownloadManager { @@ -92,115 +73,6 @@ impl fmt::Debug for DownloadManager { } } -/// 下载Actor -/// 负责处理下载相关的消息和任务 -pub struct DActor { - /// 下载消息接收器 - receiver: Receiver, - /// 上传消息发送器 - sender: Sender, -} - -impl DActor { - /// 创建新的下载Actor实例 - pub fn new(receiver: Receiver, sender: Sender) -> Self { - Self { receiver, sender } - } - - /// 运行Actor主循环,处理接收到的消息 - async fn run(&mut self) { - while let Ok(msg) = self.receiver.recv().await { - if let Err(e) = self.handle_message(msg).await { - error!("Error handling message: {}", e); - } - } - } - - /// 处理下载消息 - /// - /// # 参数 - /// * `msg` - 要处理的下载消息 - async fn handle_message(&mut self, msg: DownloaderMessage) -> AppResult<()> { - match msg { - DownloaderMessage::Start(plugin, ctx, rooms_handle) => { - // 创建下载任务 - let task = DownloadTask::new(plugin, ctx, rooms_handle); - - // 执行下载(使用 Result 链式处理) - task.execute(&self.sender).await?; - - Ok(()) - } - } - } -} -/// 上传Actor -/// 负责处理上传相关的消息和任务 -pub struct UActor { - /// 上传消息接收器 - receiver: Receiver, -} - -impl UActor { - /// 创建新的上传Actor实例 - pub fn new(receiver: Receiver) -> Self { - Self { receiver } - } - - /// 运行Actor主循环,处理接收到的消息 - async fn run(&mut self) { - while let Ok(msg) = self.receiver.recv().await { - self.handle_message(msg).await; - } - } - - /// 处理上传消息 - /// - /// # 参数 - /// * `msg` - 要处理的上传消息 - async fn handle_message(&mut self, msg: UploaderMessage) { - match msg { - UploaderMessage::SegmentEvent(rx, ctx) => { - ctx.worker - .change_status(Stage::Upload, WorkerStatus::Pending); - let inspect = rx.inspect(|f| { - let pool = ctx.pool.clone(); - let streamer_info_id = ctx.stream_info.streamer_info.id; - let file = f.prev_file_path.display().to_string(); - tokio::spawn(async move { - let result = InsertFileItem { - file, - streamer_info_id, - } - .insert(&pool) - .await; - info!(result=?result, "Insert file"); - }); - }); - let result = match ctx.worker.get_upload_config() { - Some(config) => process_with_upload(inspect, &ctx, config).await, - None => { - let mut paths = Vec::new(); - pin!(inspect); - while let Some(event) = inspect.next().await { - paths.push(event.prev_file_path); - } - // 无上传配置时,直接执行后处理 - execute_postprocessor(paths, &ctx).await - } - }; - - if let Err(e) = &result { - error!("Process segment event failed: {}", e); - // 可以添加错误通知机制 - } - info!(url=ctx.stream_info.streamer_info.url, result=?result, "后处理执行完毕:Finished processing segment event"); - ctx.worker.change_status(Stage::Upload, WorkerStatus::Idle); - } - } - } -} - /// Actor处理器 /// 管理下载和上传Actor的生命周期 pub struct ActorHandle { @@ -254,25 +126,6 @@ impl ActorHandle { } } -/// 上传消息枚举 -/// 定义上传Actor可以处理的消息类型 -#[derive(Debug)] -pub enum UploaderMessage { - /// 分段事件消息,包含事件、接收器和工作器 - SegmentEvent(Receiver, Context), -} - -/// 下载消息枚举 -/// 定义下载Actor可以处理的消息类型 -pub enum DownloaderMessage { - /// 开始下载消息,包含插件、流信息、上下文和房间句柄 - Start( - Arc, - Context, - Arc, - ), -} - impl Drop for ActorHandle { fn drop(&mut self) { // 发送端随 ActorHandle 一起被 drop,会关闭通道(如果没有其他 sender 克隆)。 diff --git a/crates/biliup-cli/src/server/core/monitor.rs b/crates/biliup-cli/src/server/core/monitor.rs index cbf0465..91361f1 100644 --- a/crates/biliup-cli/src/server/core/monitor.rs +++ b/crates/biliup-cli/src/server/core/monitor.rs @@ -1,4 +1,5 @@ -use crate::server::core::download_manager::{ActorHandle, DownloaderMessage}; +use crate::server::common::download::DownloaderMessage; +use crate::server::core::download_manager::ActorHandle; use crate::server::core::plugin::{DownloadPlugin, StreamStatus}; use crate::server::infrastructure::connection_pool::ConnectionPool; use crate::server::infrastructure::context::{Context, Stage, Worker, WorkerStatus}; @@ -74,8 +75,8 @@ async fn start_client( } } Ok(StreamStatus::Offline) => {} - Ok(StreamStatus::Unknown) => {}, - Err(e) => error!(e=?e, ctx=ctx.worker.live_streamer.url,"检查直播间出错") + Ok(StreamStatus::Unknown) => {} + Err(e) => error!(e=?e, ctx=ctx.worker.live_streamer.url,"检查直播间出错"), }; } // 等待下一次检查 diff --git a/crates/biliup-cli/src/server/infrastructure/dto.rs b/crates/biliup-cli/src/server/infrastructure/dto.rs index 79aae6e..d88b1fc 100644 --- a/crates/biliup-cli/src/server/infrastructure/dto.rs +++ b/crates/biliup-cli/src/server/infrastructure/dto.rs @@ -12,4 +12,6 @@ pub struct LiveStreamerResponse { /// 当前工作状态 pub status: String, + /// 上传状态 + pub upload_status: String, } diff --git a/crates/biliup-cli/src/server/infrastructure/models/live_streamer.rs b/crates/biliup-cli/src/server/infrastructure/models/live_streamer.rs index cb43be9..824d712 100644 --- a/crates/biliup-cli/src/server/infrastructure/models/live_streamer.rs +++ b/crates/biliup-cli/src/server/infrastructure/models/live_streamer.rs @@ -32,7 +32,7 @@ pub struct LiveStreamer { /// 注意:数据库空与json空有区别,所以这里不能用#[ormlite(json)] /// 只能使用 sqlx::types::Json #[ormlite(json)] - pub preprocessor: Option>, + pub preprocessor: Option>, /// 分段处理器列表(JSON格式) #[ormlite(json)] pub segment_processor: Option>, diff --git a/crates/biliup-cli/src/server/infrastructure/service_register.rs b/crates/biliup-cli/src/server/infrastructure/service_register.rs index bce7e4a..150c185 100644 --- a/crates/biliup-cli/src/server/infrastructure/service_register.rs +++ b/crates/biliup-cli/src/server/infrastructure/service_register.rs @@ -49,7 +49,6 @@ impl ServiceRegister { // 创建默认的HTTP客户端 let client = StatelessClient::default(); - info!("utility services initialized, building feature services..."); info!("feature services successfully initialized!"); diff --git a/crates/stream-gears/src/server.rs b/crates/stream-gears/src/server.rs index c686dd2..35c3290 100644 --- a/crates/stream-gears/src/server.rs +++ b/crates/stream-gears/src/server.rs @@ -228,8 +228,15 @@ pub fn stream_info_from_py( 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,))?; + + let stream_headers: HashMap = if platform == "Huya" { + let stream_headers = self_obj.getattr("stream_headers")?; + self_obj.call_method1("update_headers", (&stream_headers,))?; + stream_headers.extract()? + } else { + self_obj.getattr("stream_headers")?.extract()? + }; + let danmaku_init = self_obj.call_method0("danmaku_init")?; // let platform: Option = self_obj.getattr("danmaku")?.extract()?; // danmaku 可能在条件下没有设置(比如 bilibili_danmaku 为 False)