>(&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