Refactor server code and migrate modules to biliup-cli
Moved and refactored server-related modules from stream-gears to biliup-cli, including API endpoints, configuration, core logic, and infrastructure. Replaced usage of anyhow with error-stack and custom AppError/AppResult types throughout the codebase. Updated dependencies in Cargo.toml and Cargo.lock, and adjusted code to use new error handling and module paths. Removed obsolete modules and files from stream-gears, and updated README and workspace version.
This commit is contained in:
1 parent
0bdcf14a00
commit
fb4dc2b50a
77 files changed
+1387
-4539
No files matched your search
Generated
+12
-4
@@ -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",
|
||||
|
||||
+1
-2
@@ -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"
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
|
||||
## 💴捐赠
|
||||
<img src=".github/resource/Image.jpg" width="200" />
|
||||
|
||||
[爱发电 »](https://afdian.com/a/biliup)
|
||||
|
||||
## ⭐Stars
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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<u64>,
|
||||
split_time: Option<humantime::Duration>,
|
||||
) -> 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<String> = 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}");
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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.)
|
||||
|
||||
@@ -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;
|
||||
+1
-1
@@ -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<Credentials>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -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<HashMap<String, String>>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
) -> Result<Json<serde_json::Value>, 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<HashMap<String, String>>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
println!("{}", ¶ms["user"]);
|
||||
let bili = login_by_cookies(¶ms["user"], None).await?;
|
||||
Ok(Json(bili.my_info().await?))
|
||||
) -> Result<Json<serde_json::Value>, 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<StatelessClient>,
|
||||
State(client): State<StatelessClient>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> AppResult<Bytes> {
|
||||
println!("{}", ¶ms["url"]);
|
||||
) -> Result<Bytes, Response> {
|
||||
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)
|
||||
}
|
||||
@@ -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<ConnectionPool>,
|
||||
) -> AppResult<Json<Vec<LiveStreamer>>> {
|
||||
let live_streamers = LiveStreamer::select().fetch_all(&pool).await?;
|
||||
State(workers): State<Arc<RwLock<Vec<Arc<Worker>>>>>,
|
||||
) -> Result<Json<Vec<LiveStreamerResponse>>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(payload): Json<InsertLiveStreamer>,
|
||||
) -> Result<Json<LiveStreamer>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(payload): Json<LiveStreamer>,
|
||||
) -> Result<Json<LiveStreamer>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
State(workers): State<Arc<RwLock<Vec<Arc<Worker>>>>>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<LiveStreamer>, 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<ConnectionPool>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let configurations = Configuration::select()
|
||||
.where_("key = 'config'")
|
||||
.fetch_one(&pool)
|
||||
.await?;
|
||||
State(config): State<Arc<RwLock<Config>>>,
|
||||
) -> Result<Json<Config>, 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<Arc<RwLock<Config>>>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(json_data): Json<Config>,
|
||||
) -> Result<Json<Config>, 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<i64> =
|
||||
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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> AppResult<Json<Vec<StreamerInfo>>> {
|
||||
let streamer_infos = StreamerInfo::select().fetch_all(&pool).await?;
|
||||
let file_items = FileItem::select().fetch_all(&pool).await?;
|
||||
) -> Result<Json<Vec<StreamerInfo>>, 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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> AppResult<Json<Vec<UploadStreamer>>> {
|
||||
let uploader_streamers = UploadStreamer::select().fetch_all(&pool).await?;
|
||||
) -> Result<Json<Vec<UploadStreamer>>, 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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(upload_streamer): Json<InsertUploadStreamer>,
|
||||
) -> Result<Json<serde_json::Value>, 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<ConnectionPool>,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<Json<UploadStreamer>> {
|
||||
) -> Result<Json<UploadStreamer>, 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<DynLiveStreamersService>,
|
||||
Extension(download_actor_handle): Extension<DownloadActorHandle>,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<Json<LiveStreamerDto>> {
|
||||
Ok(Json(streamers_service.get_streamer_by_id(id).await?))
|
||||
}
|
||||
|
||||
pub async fn add_streamer_endpoint(
|
||||
Extension(streamers_service): Extension<DynLiveStreamersService>,
|
||||
Extension(download_actor_handle): Extension<DownloadActorHandle>,
|
||||
Json(request): Json<AddLiveStreamerDto>,
|
||||
) -> AppResult<Json<LiveStreamerDto>> {
|
||||
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<DownloadActorHandle>,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<Json<()>> {
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn update_streamer_endpoint(
|
||||
Extension(download_actor_handle): Extension<DownloadActorHandle>,
|
||||
Json(request): Json<LiveStreamerEntity>,
|
||||
) -> AppResult<Json<LiveStreamerDto>> {
|
||||
download_actor_handle.update_streamer(&request.url);
|
||||
Err(AppError::InvalidLoginAttempt)
|
||||
}
|
||||
|
||||
pub async fn add_upload_streamer_endpoint(
|
||||
Extension(streamers_service): Extension<DynUploadStreamersRepository>,
|
||||
Json(request): Json<StudioEntity>,
|
||||
) -> AppResult<Json<StudioEntity>> {
|
||||
Ok(Json(streamers_service.create_streamer(request).await?))
|
||||
}
|
||||
|
||||
pub async fn delete_template_endpoint(
|
||||
Extension(streamers_service): Extension<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<Json<()>> {
|
||||
Ok(Json(streamers_service.delete_streamer(id).await?))
|
||||
}
|
||||
|
||||
pub async fn update_template_endpoint(
|
||||
Extension(streamers_service): Extension<DynUploadStreamersRepository>,
|
||||
Json(request): Json<StudioEntity>,
|
||||
) -> AppResult<Json<StudioEntity>> {
|
||||
Ok(Json(streamers_service.update_streamer(request).await?))
|
||||
) -> Result<Json<()>, 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<ConnectionPool>,
|
||||
) -> AppResult<Json<Vec<serde_json::Value>>> {
|
||||
) -> Result<Json<Vec<serde_json::Value>>, 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<User>) -> AppResult<Json<User>> {
|
||||
Err(AppError::InternalServerError)
|
||||
pub async fn add_user_endpoint(
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(user): Json<InsertConfiguration>,
|
||||
) -> Result<Json<Configuration>, 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<i64>) -> AppResult<Json<()>> {
|
||||
pub async fn delete_user_endpoint(
|
||||
Path(id): Path<i64>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> Result<Json<()>, 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<Json<serde_json::Value>, 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<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, Response> {
|
||||
let info = tokio::time::timeout(
|
||||
Duration::from_secs(300),
|
||||
Credential::new(None).login_by_qrcode(value),
|
||||
// std::future::pending::<AppResult<LoginInfo>>(),
|
||||
)
|
||||
.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<Json<Vec<serde_json::Value>>, 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))
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod auth;
|
||||
pub mod bilibili_endpoints;
|
||||
pub mod endpoints;
|
||||
pub mod spa;
|
||||
@@ -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::<HeaderValue>().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(())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -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<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
|
||||
bail!("load_or_create: {:?}", path.as_ref().display())
|
||||
pub fn load_or_create<P: AsRef<Path>>(path: P) -> AppResult<Self> {
|
||||
bail!(AppError::Custom(format!(
|
||||
"load_or_create: {:?}",
|
||||
path.as_ref().display()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// 进程级全局单例(安全):OnceLock + Arc + RwLock
|
||||
pub(crate) static CONFIG: LazyLock<Arc<RwLock<Config>>> = LazyLock::new(|| {
|
||||
Arc::new(RwLock::new(
|
||||
Config::builder().streamers(Default::default()).build(),
|
||||
))
|
||||
});
|
||||
|
||||
fn cfg_arc() -> &'static Arc<RwLock<Config>> {
|
||||
&*CONFIG
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
struct ConfigState {
|
||||
// 用 PyObject 存,方便保持任意 Python 对象
|
||||
map: Arc<RwLock<Config>>,
|
||||
}
|
||||
|
||||
#[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<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let guard = self.map.read().unwrap();
|
||||
// serde_json::to_value(guard.deref())
|
||||
if let Some(bound) = pythonize(py, guard.deref())?
|
||||
.extract::<Bound<PyDict>>()?
|
||||
.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<ConfigState> {
|
||||
let state = ConfigState {
|
||||
map: cfg_arc().clone(),
|
||||
};
|
||||
// pythonize(py, &config)
|
||||
Ok(state)
|
||||
}
|
||||
@@ -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<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename_prefix: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time_range: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub excluded_keywords: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub uploader: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tid: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub copyright: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cover_path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub preprocessor: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub downloaded_processor: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub postprocessor: Option<Vec<PostprocessorConfig>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub segment_processor: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub opt_args: Option<Vec<String>>,
|
||||
|
||||
// 其他字段作为额外的键值对存储
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// 后处理器配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PostprocessorConfig {
|
||||
Simple(String),
|
||||
Complex {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
rm: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
mv: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
run: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
webhook: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 用户配置结构,对应Python版本的user配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cookies: Option<HashMap<String, String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub access_token: Option<String>,
|
||||
}
|
||||
|
||||
/// 主配置结构,对应Python版本的config
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BiliUpConfig {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub streamers: Option<HashMap<String, StreamerConfig>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<UserConfig>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub threads: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lines: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pool1_size: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub pool2_size: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event_loop_interval: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub check_sourcecode: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delay: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_upload_limit: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filtering_threshold: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub downloader: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub segment_time: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_size: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub filename_prefix: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_live_cover: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub segment_processor_parallel: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dolby: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hires: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub charging_pay: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub no_reprint: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub submit_api: Option<String>,
|
||||
|
||||
// 日志配置
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logging: Option<HashMap<String, serde_json::Value>>,
|
||||
|
||||
// 其他字段作为额外的键值对存储
|
||||
#[serde(flatten)]
|
||||
pub extra_fields: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
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<PathBuf>,
|
||||
format: Option<ConfigFormat>,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config: BiliUpConfig::default(),
|
||||
config_path: None,
|
||||
format: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从文件加载配置,对应Python的load方法
|
||||
pub async fn load<P: AsRef<Path>>(&mut self, path: Option<P>) -> 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<P: AsRef<Path>>(&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<PathBuf> {
|
||||
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<P: AsRef<Path>>(&self, path: P) -> Result<ConfigFormat> {
|
||||
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<StreamerConfig> {
|
||||
self.config.streamers.as_mut()?.remove(name)
|
||||
}
|
||||
|
||||
/// 获取所有流名称
|
||||
pub fn get_streamer_names(&self) -> Vec<String> {
|
||||
self.config
|
||||
.streamers
|
||||
.as_ref()
|
||||
.map(|streamers| streamers.keys().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 获取配置值(通用方法)
|
||||
pub fn get<T>(&self, key: &str) -> Option<T>
|
||||
where
|
||||
T: for<'de> Deserialize<'de> + Clone,
|
||||
{
|
||||
// 这是一个简化版本,实际实现需要处理嵌套的键路径
|
||||
// 例如:get("streamers.streamer1.url")
|
||||
None
|
||||
}
|
||||
|
||||
/// 从cookies文件加载,对应Python的load_cookies
|
||||
pub async fn load_cookies<P: AsRef<Path>>(&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));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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<StreamStatus>,
|
||||
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<dyn Fn(&str) + Send> {
|
||||
// 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<dyn Error + Send + Sync>>(())
|
||||
// });
|
||||
// }
|
||||
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<LiveStreamerDto>,
|
||||
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<StreamStatus>, 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<RwLock<AnyMap<(Cycle<StreamStatus>, JoinHandle<()>)>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DownloadActorHandle {
|
||||
platform_map: StreamActorMap,
|
||||
// client: StatelessClient,
|
||||
actor: DownloadActor,
|
||||
}
|
||||
|
||||
impl DownloadActorHandle {
|
||||
pub fn new(list: Vec<LiveStreamerDto>, 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<String, StreamStatus> {
|
||||
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)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
+13
-9
@@ -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<Option<Arc<Monitor>>>,
|
||||
@@ -39,7 +39,7 @@ impl DownloadManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_monitor(&self) -> Arc<Monitor> {
|
||||
pub fn ensure_monitor(&self) -> Arc<Monitor> {
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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<dyn LiveStreamersRepository + Send + Sync>;
|
||||
pub type DynDownloadRecordsRepository = Arc<dyn DownloadRecordsRepository + Send + Sync>;
|
||||
pub type DynVideosRepository = Arc<dyn VideosRepository + Send + Sync>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LiveStreamersRepository {
|
||||
async fn create_streamer(
|
||||
&self,
|
||||
entity: AddLiveStreamerDto,
|
||||
) -> anyhow::Result<LiveStreamerEntity>;
|
||||
async fn delete_streamer(&self, id: i64) -> anyhow::Result<()>;
|
||||
async fn update_streamer(
|
||||
&self,
|
||||
entity: LiveStreamerEntity,
|
||||
) -> anyhow::Result<LiveStreamerEntity>;
|
||||
async fn get_streamers(&self) -> anyhow::Result<Vec<LiveStreamerEntity>>;
|
||||
async fn get_streamer_by_url(&self, url: &str) -> anyhow::Result<LiveStreamerEntity>;
|
||||
async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result<LiveStreamerEntity>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait DownloadRecordsRepository {
|
||||
async fn create(&self, entity: DownloadRecords) -> anyhow::Result<DownloadRecords>;
|
||||
// async fn delete(&self, id: i64) -> anyhow::Result<()>;
|
||||
// async fn update(&self, entity: DownloadRecords) -> anyhow::Result<DownloadRecords>;
|
||||
async fn get_all(&self) -> anyhow::Result<Vec<DownloadRecords>>;
|
||||
// async fn get_by_id(&self, id: i64) -> anyhow::Result<DownloadRecords>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait VideosRepository {
|
||||
async fn create(&self, entity: Videos) -> anyhow::Result<Videos>;
|
||||
// async fn delete(&self, id: i64) -> anyhow::Result<()>;
|
||||
async fn update(&self, entity: Videos) -> anyhow::Result<Videos>;
|
||||
// async fn get_all(&self) -> anyhow::Result<Vec<Videos>>;
|
||||
async fn get_by_id(&self, id: i64) -> anyhow::Result<Videos>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromRow)]
|
||||
pub struct LiveStreamerEntity {
|
||||
pub id: i64,
|
||||
pub url: String,
|
||||
pub remark: String,
|
||||
pub filename: String,
|
||||
pub split_time: Option<i64>,
|
||||
pub split_size: Option<i64>,
|
||||
pub upload_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct DownloadRecords {
|
||||
pub id: i64,
|
||||
pub title: String,
|
||||
pub live_id: i64,
|
||||
pub upload_id: Option<i64>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct Videos {
|
||||
pub id: i64,
|
||||
pub start_time: chrono::DateTime<Utc>,
|
||||
pub end_time: chrono::DateTime<Utc>,
|
||||
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<dyn LiveStreamersService + Send + Sync>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait LiveStreamersService {
|
||||
async fn add_streamer(&self, request: AddLiveStreamerDto) -> anyhow::Result<LiveStreamerDto>;
|
||||
async fn get_streamer_by_url(&self, url: &str) -> anyhow::Result<LiveStreamerDto>;
|
||||
async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result<LiveStreamerDto>;
|
||||
async fn get_streamers(&self) -> anyhow::Result<Vec<LiveStreamerDto>>;
|
||||
async fn get_studio_by_url(&self, url: &str) -> anyhow::Result<Option<Studio>>;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
pub struct AddLiveStreamerDto {
|
||||
pub url: String,
|
||||
pub remark: String,
|
||||
pub filename: String,
|
||||
pub split_time: Option<u64>,
|
||||
pub split_size: Option<u64>,
|
||||
pub upload_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug)]
|
||||
pub struct LiveStreamerDto {
|
||||
pub id: i64,
|
||||
pub url: String,
|
||||
pub remark: String,
|
||||
pub filename: String,
|
||||
pub split_time: Option<u64>,
|
||||
pub split_size: Option<u64>,
|
||||
pub status: StreamStatus,
|
||||
}
|
||||
@@ -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<ToMain>,
|
||||
}
|
||||
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<StreamStatus>),
|
||||
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<ToMain>) -> 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<StreamStatus>,
|
||||
// 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<dyn Fn(&str) + Send + Sync> {
|
||||
// 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<dyn Error + Send + Sync>>(())
|
||||
// }
|
||||
// });
|
||||
// task.change(url, StreamStatus::Working);
|
||||
// }
|
||||
File renamed without changes.
File renamed without changes.
@@ -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<String>,
|
||||
pub platform: String,
|
||||
pub stream_headers: HashMap<String, String>,
|
||||
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<bool, Report<AppError>>;
|
||||
async fn get_stream_info(&self) -> Result<StreamInfo, Report<AppError>>;
|
||||
async fn download(&self, output_path: impl AsRef<Path>) -> Result<(), Report<AppError>>;
|
||||
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<StreamStatus, Report<AppError>>;
|
||||
async fn create_downloader(
|
||||
&self,
|
||||
stream_info: &StreamInfo,
|
||||
worker: &Worker,
|
||||
) -> AppResult<Box<dyn Downloader>>;
|
||||
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
@@ -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<ActorMessage>,
|
||||
client: StatelessClient,
|
||||
studio: Studio,
|
||||
vid: Option<Vid>,
|
||||
}
|
||||
enum ActorMessage {
|
||||
Upload { path: PathBuf },
|
||||
}
|
||||
|
||||
impl UploadActor {
|
||||
fn new(
|
||||
studio: Studio,
|
||||
client: StatelessClient,
|
||||
receiver: mpsc::UnboundedReceiver<ActorMessage>,
|
||||
) -> Self {
|
||||
UploadActor {
|
||||
receiver,
|
||||
client,
|
||||
studio,
|
||||
vid: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn upload(
|
||||
&self,
|
||||
video_paths: &[&Path],
|
||||
bili: &BiliBili,
|
||||
line: Line,
|
||||
limit: usize,
|
||||
) -> biliup::error::Result<Vec<Video>> {
|
||||
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<ActorMessage>,
|
||||
}
|
||||
|
||||
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<T: AsRef<Path>>(&self, path: T) {
|
||||
let msg = ActorMessage::Upload {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
};
|
||||
let _ = self.sender.send(msg);
|
||||
}
|
||||
}
|
||||
@@ -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<dyn UploadStreamersRepository + Send + Sync>;
|
||||
pub type DynUploadRecordsRepository = Arc<dyn UploadRecordsRepository + Send + Sync>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait UploadStreamersRepository {
|
||||
async fn create_streamer(&self, studio: StudioEntity) -> anyhow::Result<StudioEntity>;
|
||||
async fn delete_streamer(&self, id: i64) -> anyhow::Result<()>;
|
||||
async fn update_streamer(&self, studio: StudioEntity) -> anyhow::Result<StudioEntity>;
|
||||
async fn get_streamers(&self) -> anyhow::Result<Vec<StudioEntity>>;
|
||||
async fn get_streamer_by_id(&self, id: i64) -> anyhow::Result<StudioEntity>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait UploadRecordsRepository {
|
||||
async fn create(&self, entity: UploadRecords) -> anyhow::Result<UploadRecords>;
|
||||
async fn delete(&self, id: i64) -> anyhow::Result<()>;
|
||||
// async fn update(&self, entity: UploadRecords) -> anyhow::Result<UploadRecords>;
|
||||
async fn get_all(&self) -> anyhow::Result<Vec<UploadRecords>>;
|
||||
async fn get_by_id(&self, id: i64) -> anyhow::Result<UploadRecords>;
|
||||
}
|
||||
|
||||
// #[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<u32>,
|
||||
pub interactive: u8,
|
||||
pub mission_id: Option<u32>,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type DynUsersRepository = Arc<dyn UsersRepository + Send + Sync>;
|
||||
|
||||
#[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<User>;
|
||||
async fn get_users(&self) -> anyhow::Result<Vec<User>>;
|
||||
async fn delete_user(&self, id: i64) -> anyhow::Result<()>;
|
||||
async fn update_user(&self, user: User) -> anyhow::Result<User>;
|
||||
async fn get_user_by_id(&self, id: i64) -> anyhow::Result<User>;
|
||||
}
|
||||
@@ -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<T> = HashMap<TypeId, T, BuildHasherDefault<IdHasher>>;
|
||||
|
||||
// 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<T> {
|
||||
map: Arc<RwLock<IndexMap<String, T>>>,
|
||||
}
|
||||
|
||||
impl<T: Copy> Cycle<T> {
|
||||
pub fn new(map: IndexMap<String, T>) -> 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<String, T> {
|
||||
let read_guard = self.map.read().unwrap();
|
||||
read_guard.clone()
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, map: IndexMap<String, T>) {
|
||||
if map.is_empty() {
|
||||
unreachable!("list must not be empty")
|
||||
}
|
||||
*self.map.write().unwrap() = map;
|
||||
}
|
||||
|
||||
pub fn write(&self) -> RwLockWriteGuard<'_, IndexMap<String, T>> {
|
||||
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<T: Debug> Debug for Cycle<T> {
|
||||
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<T, O: 'static>(future: T) -> JoinHandle<T::Output>
|
||||
where
|
||||
T: Future<Output = Result<O, Box<dyn Error + Send + Sync>>> + Send + 'static,
|
||||
T::Output: Send + 'static,
|
||||
{
|
||||
tokio::spawn(async move {
|
||||
future.await.map_err(|e| {
|
||||
error!("spawn error: {}", e);
|
||||
e
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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<T> = Result<T, AppError>;
|
||||
|
||||
pub type ConduitErrorMap = HashMap<Cow<'static, str>, Vec<Cow<'static, str>>>;
|
||||
|
||||
#[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::<Box<dyn sqlx::error::DatabaseError>>()
|
||||
{
|
||||
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<Report<AppError>>) -> Response {
|
||||
let report = report.into();
|
||||
let (status, error_message) = match report.downcast_ref::<AppError>() {
|
||||
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<T> = Result<T, Report<AppError>>;
|
||||
@@ -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<FileInfo> },
|
||||
}
|
||||
|
||||
/// 流信息结构,对应Python版本的stream_info
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamInfo {
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
pub title: Option<String>,
|
||||
pub date: chrono::DateTime<chrono::Utc>,
|
||||
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub live_cover_path: Option<String>,
|
||||
pub is_download: bool,
|
||||
pub platform: String,
|
||||
pub database_row_id: Option<i64>,
|
||||
}
|
||||
|
||||
/// 文件信息结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileInfo {
|
||||
pub video: String,
|
||||
pub danmaku: Option<String>,
|
||||
}
|
||||
|
||||
/// 事件处理器trait
|
||||
#[async_trait]
|
||||
pub trait EventHandler: Send + Sync {
|
||||
async fn handle(&self, event: BiliUpEvent) -> Result<Option<BiliUpEvent>>;
|
||||
fn event_type(&self) -> EventType;
|
||||
}
|
||||
|
||||
/// 事件管理器上下文,对应Python版本的context
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EventContext {
|
||||
pub url_upload_count: HashMap<String, i32>,
|
||||
pub upload_filename: Vec<String>,
|
||||
pub file_upload_count: HashMap<String, i32>,
|
||||
pub url_status: HashMap<String, i32>, // 0: idle, 1: downloading, 2: stopped
|
||||
}
|
||||
|
||||
/// 事件管理器,对应Python版本的EventManager
|
||||
pub struct EventManager {
|
||||
context: Arc<RwLock<EventContext>>,
|
||||
handlers: Arc<RwLock<HashMap<EventType, Vec<Box<dyn EventHandler>>>>>,
|
||||
event_tx: mpsc::UnboundedSender<BiliUpEvent>,
|
||||
event_rx: Arc<RwLock<Option<mpsc::UnboundedReceiver<BiliUpEvent>>>>,
|
||||
}
|
||||
|
||||
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<dyn EventHandler>) {
|
||||
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<EventContext> {
|
||||
self.context.read().await
|
||||
}
|
||||
|
||||
/// 获取上下文的写访问
|
||||
pub async fn get_context_mut(&self) -> tokio::sync::RwLockWriteGuard<EventContext> {
|
||||
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<Option<BiliUpEvent>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<Option<BiliUpEvent>> {
|
||||
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<StreamInfo> {
|
||||
// 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<Option<BiliUpEvent>> {
|
||||
if let BiliUpEvent::Download { name, url } = event {
|
||||
// 设置下载状态
|
||||
let result = async {
|
||||
let stream_info = self.perform_download(&name, &url).await?;
|
||||
Ok::<StreamInfo, anyhow::Error>(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<FileInfo> {
|
||||
// TODO: 实现文件列表获取,对应Python的UploadBase.file_list
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for DownloadedHandler {
|
||||
async fn handle(&self, event: BiliUpEvent) -> Result<Option<BiliUpEvent>> {
|
||||
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<FileInfo> {
|
||||
// TODO: 实现文件列表获取
|
||||
vec![]
|
||||
}
|
||||
|
||||
async fn perform_upload(&self, stream_info: &StreamInfo) -> Result<Vec<FileInfo>> {
|
||||
// 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<Option<BiliUpEvent>> {
|
||||
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<Option<BiliUpEvent>> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod connection_pool;
|
||||
pub mod repositories;
|
||||
pub mod service_register;
|
||||
@@ -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<Sqlite>;
|
||||
@@ -9,18 +10,27 @@ pub type ConnectionPool = Pool<Sqlite>;
|
||||
pub struct ConnectionManager;
|
||||
|
||||
impl ConnectionManager {
|
||||
pub async fn new_pool(path: &str) -> Result<ConnectionPool> {
|
||||
pub async fn new_pool(path: &str) -> AppResult<ConnectionPool> {
|
||||
// 创建所有父级目录(如果不存在)
|
||||
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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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<WorkerStatus>,
|
||||
File renamed without changes.
+1
-1
@@ -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;
|
||||
File renamed without changes.
+4
-1
@@ -105,7 +105,10 @@ impl HookStep {
|
||||
}
|
||||
|
||||
/// 处理所有后处理器
|
||||
pub async fn process_video(video_path: &Path, processors: &[HookStep]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub async fn process_video(
|
||||
video_path: &Path,
|
||||
processors: &[HookStep],
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
for processor in processors {
|
||||
processor.execute(video_path).await?;
|
||||
}
|
||||
@@ -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> {
|
||||
LiveStreamer::select()
|
||||
.where_("id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
|
||||
pub async fn del_streamer(pool: &ConnectionPool, id: i64) -> AppResult<LiveStreamer> {
|
||||
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<Vec<LiveStreamer>> {
|
||||
LiveStreamer::select()
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
|
||||
pub async fn get_config(pool: &ConnectionPool) -> AppResult<Config> {
|
||||
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<Configuration> {
|
||||
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<Vec<UploadStreamer>> {
|
||||
UploadStreamer::select()
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
-25
@@ -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<DownloadRecords> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_all(&self) -> anyhow::Result<Vec<DownloadRecords>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
pub tid: Option<u16>,
|
||||
pub copyright: Option<u8>,
|
||||
pub copyright_source: Option<String>,
|
||||
pub cover_path: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub dynamic: Option<String>,
|
||||
pub dtime: Option<u32>,
|
||||
pub dolby: Option<u8>,
|
||||
pub hires: Option<u8>,
|
||||
pub charging_pay: Option<u8>,
|
||||
pub no_reprint: Option<u8>,
|
||||
pub uploader: Option<String>,
|
||||
pub user_cookie: Option<String>,
|
||||
pub tags: Value, // not null
|
||||
pub credits: Option<Value>,
|
||||
pub up_selection_reply: Option<bool>,
|
||||
pub up_close_reply: Option<bool>,
|
||||
pub up_close_danmu: Option<bool>,
|
||||
pub extra_fields: Option<String>,
|
||||
pub is_only_self: Option<i64>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub time_range: Option<String>,
|
||||
pub upload_streamers_id: Option<i64>, // FK,可空
|
||||
pub format: Option<String>,
|
||||
#[ormlite(column = "override")]
|
||||
pub override_: Option<Value>, // "override" 为关键字,字段名避让
|
||||
pub preprocessor: Option<Value>,
|
||||
pub segment_processor: Option<Value>,
|
||||
pub downloaded_processor: Option<Value>,
|
||||
pub postprocessor: Option<Value>,
|
||||
pub opt_args: Option<Value>,
|
||||
pub excluded_keywords: Option<Value>,
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
@@ -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<UploadRecords> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn delete(&self, _id: i64) -> anyhow::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_all(&self) -> anyhow::Result<Vec<UploadRecords>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_by_id(&self, _id: i64) -> anyhow::Result<UploadRecords> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
-12
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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<Videos> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn update(&self, entity: Videos) -> anyhow::Result<Videos> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_by_id(&self, id: i64) -> anyhow::Result<Videos> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -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<RwLock<Vec<Arc<Worker>>>>,
|
||||
pub managers: Arc<Vec<DownloadManager>>,
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
pub actor_handle: Arc<ActorHandle>,
|
||||
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<RwLock<Config>>,
|
||||
actor_handle: Arc<ActorHandle>,
|
||||
download_manager: Vec<DownloadManager>,
|
||||
) -> 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<Monitor>,
|
||||
) -> AppResult<Option<()>> {
|
||||
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<ServiceRegister> for ConnectionPool {
|
||||
fn from_ref(app_state: &ServiceRegister) -> ConnectionPool {
|
||||
app_state.pool.clone()
|
||||
}
|
||||
}
|
||||
// impl FromRef<ServiceRegister> 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<Worker> {
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
+1
-1
@@ -72,7 +72,7 @@ impl Backend {
|
||||
Ok(user.is_some())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_user(&mut self, creds: Credentials) -> AppResult<User> {
|
||||
pub async fn create_user(&mut self, creds: Credentials) -> AppResult<User> {
|
||||
// create the new user account...
|
||||
// 验证输入
|
||||
if creds.username.is_empty() || creds.password.is_empty() {
|
||||
@@ -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<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
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<bool> {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<bool>;
|
||||
|
||||
/// 获取流信息
|
||||
async fn get_stream_info(&self, url: &str) -> Result<Option<StreamInfo>>;
|
||||
|
||||
/// 开始下载,对应Python的start方法
|
||||
async fn start_download(
|
||||
&self,
|
||||
name: &str,
|
||||
url: &str,
|
||||
kwargs: HashMap<String, String>,
|
||||
) -> Result<StreamInfo>;
|
||||
|
||||
/// 检查是否应该录制,对应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<String>, stream_info: &StreamInfo) -> Result<Vec<String>>;
|
||||
|
||||
/// 获取上传配置
|
||||
fn get_upload_config(&self) -> HashMap<String, String> {
|
||||
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<bool> {
|
||||
// TODO: 使用PyO3调用Python插件的acheck_stream方法
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn get_stream_info(&self, url: &str) -> Result<Option<StreamInfo>> {
|
||||
// TODO: 从Python插件获取流信息
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn start_download(
|
||||
&self,
|
||||
name: &str,
|
||||
url: &str,
|
||||
kwargs: HashMap<String, String>,
|
||||
) -> Result<StreamInfo> {
|
||||
// TODO: 调用Python插件的start方法
|
||||
Err(anyhow::anyhow!("Not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件注册表
|
||||
pub struct PluginRegistry {
|
||||
downloaders: Arc<RwLock<HashMap<String, Box<dyn DownloaderPlugin>>>>,
|
||||
uploaders: Arc<RwLock<HashMap<String, Box<dyn UploaderPlugin>>>>,
|
||||
url_patterns: Arc<RwLock<Vec<(regex::Regex, String)>>>, // 正则表达式和插件名的映射
|
||||
}
|
||||
|
||||
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<dyn DownloaderPlugin>) -> 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<dyn UploaderPlugin>) -> 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<String> {
|
||||
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<tokio::sync::RwLockReadGuard<HashMap<String, Box<dyn DownloaderPlugin>>>> {
|
||||
let downloaders = self.downloaders.read().await;
|
||||
if downloaders.contains_key(name) {
|
||||
Some(downloaders)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取上传器插件
|
||||
pub async fn get_uploader(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Option<tokio::sync::RwLockReadGuard<HashMap<String, Box<dyn UploaderPlugin>>>> {
|
||||
let uploaders = self.uploaders.read().await;
|
||||
if uploaders.contains_key(name) {
|
||||
Some(uploaders)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出所有插件
|
||||
pub async fn list_plugins(&self) -> (Vec<String>, Vec<String>) {
|
||||
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<Vec<String>> {
|
||||
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<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn get_stream_info(&self, _url: &str) -> Result<Option<StreamInfo>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn start_download(
|
||||
&self,
|
||||
_name: &str,
|
||||
_url: &str,
|
||||
_kwargs: HashMap<String, String>,
|
||||
) -> Result<StreamInfo> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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<SubmitOption>,
|
||||
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<u32>,
|
||||
) -> 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<BiliBili> {
|
||||
async fn login_by_cookies(user_cookie: PathBuf, proxy: Option<&str>) -> AppResult<BiliBili> {
|
||||
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<B
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn cover_up(studio: &mut Studio, bili: &BiliBili) -> 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<UploadLine>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Video>> {
|
||||
) -> AppResult<Vec<Video>> {
|
||||
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<LoginInfo> {
|
||||
pub async fn login_by_password(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<LoginInfo> {
|
||||
pub async fn login_by_sms(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<LoginInfo> {
|
||||
|
||||
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<LoginInfo> {
|
||||
let value = credential.get_qrcode().await?;
|
||||
pub async fn login_by_qrcode(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<LoginInfo> {
|
||||
);
|
||||
// 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<LoginInfo> {
|
||||
let value = credential.get_qrcode().await?;
|
||||
pub async fn login_by_browser(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<LoginInfo> {
|
||||
pub async fn login_by_web_cookies(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<LoginInfo> {
|
||||
pub async fn login_by_webqr_cookies(credential: Credential) -> AppResult<LoginInfo> {
|
||||
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<Progressbar> for Body {
|
||||
@@ -400,13 +476,15 @@ impl From<Progressbar> for Body {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fopen_rw<P: AsRef<Path>>(path: P) -> Result<std::fs::File> {
|
||||
pub fn fopen_rw<P: AsRef<Path>>(path: P) -> AppResult<std::fs::File> {
|
||||
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<Option<Bytes>> {
|
||||
pub fn progress(&mut self) -> AppResult<Option<Bytes>> {
|
||||
let pb = &self.pb;
|
||||
|
||||
let content_bytes = &mut self.bytes;
|
||||
@@ -442,7 +520,7 @@ impl Progressbar {
|
||||
}
|
||||
|
||||
impl Stream for Progressbar {
|
||||
type Item = Result<Bytes>;
|
||||
type Item = AppResult<Bytes>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
// download(
|
||||
// "test.ts")?;
|
||||
Ok(())
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
// download(
|
||||
// "test.flv")?;
|
||||
Ok(())
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
// 测试时间分割
|
||||
let mut seg = Segmentable::new(Some(Duration::from_secs(10)), None);
|
||||
assert!(!seg.needed());
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<String>) -> PyResult<bool> {
|
||||
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<String>) -> PyResult<String> {
|
||||
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::<UploadLine>()?;
|
||||
m.add_class::<PySegment>()?;
|
||||
Ok(())
|
||||
|
||||
@@ -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<BiliBili> {
|
||||
let login_info = biliup::uploader::credential::login_by_cookies(file, proxy).await?;
|
||||
pub async fn login_by_cookies(file: &str, proxy: Option<&str>) -> AppResult<BiliBili> {
|
||||
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<serde_json::Value> {
|
||||
let ret = Credential::new(proxy).send_sms(phone, country_code).await?;
|
||||
) -> AppResult<serde_json::Value> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<serde_json::Value> {
|
||||
let qrcode = Credential::new(proxy).get_qrcode().await?;
|
||||
pub async fn get_qrcode(proxy: Option<&str>) -> AppResult<serde_json::Value> {
|
||||
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<bool> {
|
||||
) -> AppResult<bool> {
|
||||
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<bool> {
|
||||
) -> AppResult<bool> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Py<PyType>>,
|
||||
pattern: Regex,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl PyPlugin {
|
||||
pub fn from_pytype(class: &Bound<PyType>) -> PyResult<Self> {
|
||||
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<StreamStatus, Report<AppError>> {
|
||||
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<Box<dyn Downloader>> {
|
||||
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<Py<PyType>>, url: &str) -> PyResult<Option<StreamInfo>> {
|
||||
let url = url.to_string();
|
||||
// obj.
|
||||
tokio::task::spawn_blocking(move || {
|
||||
Python::attach(|py| -> PyResult<Option<StreamInfo>> {
|
||||
// 从 biliup.util 获取 loop(按你项目里真实的名字来取)
|
||||
let util = PyModule::import(py, "biliup.common.util")?;
|
||||
// 下面两行二选一(取决于 biliup.util 的 API):
|
||||
// let loop_obj: Py<PyAny> = util.getattr("loop")?.into_py(py);
|
||||
// 或:
|
||||
// let loop_obj: Py<PyAny> = 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<ActorHandle>) -> PyResult<Vec<DownloadManager>> {
|
||||
let classes: Vec<DownloadManager> = Python::attach(|py| -> PyResult<Vec<DownloadManager>> {
|
||||
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<PyList> = bound.downcast()?;
|
||||
|
||||
plugin_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let download = x.downcast::<PyType>()?;
|
||||
let py_plugin = PyPlugin::from_pytype(download)?;
|
||||
Ok(DownloadManager::new(py_plugin, actor_handle.clone()))
|
||||
})
|
||||
.collect::<PyResult<_>>()
|
||||
// 基类 DownloadBase
|
||||
// py.import("biliup.engine.download")?.getattr("DownloadBase")
|
||||
})?;
|
||||
// println!("类属性值: {:?}", class_attr);
|
||||
|
||||
// let download_base: &Bound<PyType> = download_base.downcast()?;
|
||||
|
||||
Ok(classes)
|
||||
}
|
||||
|
||||
/// 从 Python 的 `self`(Bound<PyAny>)与 start_time / end_time 构造 StreamInfo
|
||||
/// end_time 的语义与 Python 中一致:若未提供或为“假值”,则使用 time.localtime()
|
||||
pub fn stream_info_from_py(py: Python<'_>, self_obj: &Bound<'_, PyAny>) -> PyResult<StreamInfo> {
|
||||
// 从 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<String> = 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<String, String> = 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<RwLock<Config>>,
|
||||
}
|
||||
|
||||
#[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<Bound<'py, PyAny>>,
|
||||
) -> PyResult<Bound<'py, PyAny>> {
|
||||
let guard = self.map.read().unwrap();
|
||||
// serde_json::to_value(guard.deref())
|
||||
if let Some(bound) = pythonize(py, guard.deref())?
|
||||
.extract::<Bound<PyDict>>()?
|
||||
.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<ConfigState> {
|
||||
let state = ConfigState {
|
||||
map: cfg_arc().clone(),
|
||||
};
|
||||
// pythonize(py, &config)
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
// 进程级全局单例(安全):OnceLock + Arc + RwLock
|
||||
pub static CONFIG: LazyLock<Arc<RwLock<Config>>> = LazyLock::new(|| {
|
||||
Arc::new(RwLock::new(
|
||||
Config::builder().streamers(Default::default()).build(),
|
||||
))
|
||||
});
|
||||
|
||||
fn cfg_arc() -> &'static Arc<RwLock<Config>> {
|
||||
&*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())));
|
||||
|
||||
@@ -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<HashMap<String, String>>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> Result<Json<serde_json::Value>, 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<HashMap<String, String>>,
|
||||
) -> Result<Json<serde_json::Value>, 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<StatelessClient>,
|
||||
Query(params): Query<HashMap<String, String>>,
|
||||
) -> Result<Bytes, Response> {
|
||||
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)
|
||||
}
|
||||
@@ -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<ConnectionPool>,
|
||||
State(workers): State<Arc<RwLock<Vec<Arc<Worker>>>>>,
|
||||
) -> Result<Json<Vec<LiveStreamerResponse>>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(payload): Json<InsertLiveStreamer>,
|
||||
) -> Result<Json<LiveStreamer>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(payload): Json<LiveStreamer>,
|
||||
) -> Result<Json<LiveStreamer>, 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<ServiceRegister>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
State(workers): State<Arc<RwLock<Vec<Arc<Worker>>>>>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<LiveStreamer>, 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<Arc<RwLock<Config>>>,
|
||||
) -> Result<Json<Config>, Response> {
|
||||
Ok(Json(config.read().unwrap().clone()))
|
||||
}
|
||||
|
||||
// #[axum_macros::debug_handler(state = ServiceRegister)]
|
||||
pub async fn put_configuration(
|
||||
State(config): State<Arc<RwLock<Config>>>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(json_data): Json<Config>,
|
||||
) -> Result<Json<Config>, 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<i64> =
|
||||
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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> Result<Json<Vec<StreamerInfo>>, 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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> Result<Json<Vec<UploadStreamer>>, 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<DynUploadStreamersRepository>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
Json(upload_streamer): Json<InsertUploadStreamer>,
|
||||
) -> Result<Json<serde_json::Value>, 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<ConnectionPool>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<UploadStreamer>, 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<ConnectionPool>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Json<()>, 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<ConnectionPool>,
|
||||
) -> Result<Json<Vec<serde_json::Value>>, 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<ConnectionPool>,
|
||||
Json(user): Json<InsertConfiguration>,
|
||||
) -> Result<Json<Configuration>, 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<i64>,
|
||||
State(pool): State<ConnectionPool>,
|
||||
) -> Result<Json<()>, 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<Json<serde_json::Value>, 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<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, Response> {
|
||||
let info = tokio::time::timeout(
|
||||
Duration::from_secs(300),
|
||||
Credential::new(None).login_by_qrcode(value),
|
||||
// std::future::pending::<AppResult<LoginInfo>>(),
|
||||
)
|
||||
.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<Json<Vec<serde_json::Value>>, 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))
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod auth;
|
||||
pub(crate) mod bilibili_endpoints;
|
||||
pub mod endpoints;
|
||||
pub(crate) mod spa;
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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<String>,
|
||||
pub platform: String,
|
||||
pub stream_headers: HashMap<String, String>,
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
impl StreamInfo {
|
||||
/// 从 Python 的 `self`(Bound<PyAny>)与 start_time / end_time 构造 StreamInfo
|
||||
/// end_time 的语义与 Python 中一致:若未提供或为“假值”,则使用 time.localtime()
|
||||
pub fn from_py(py: Python<'_>, self_obj: &Bound<'_, PyAny>) -> PyResult<Self> {
|
||||
// 从 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<String> = 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<String, String> =
|
||||
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<bool, Report<AppError>>;
|
||||
async fn get_stream_info(&self) -> Result<StreamInfo, Report<AppError>>;
|
||||
async fn download(&self, output_path: impl AsRef<Path>) -> Result<(), Report<AppError>>;
|
||||
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<StreamStatus, Report<AppError>>;
|
||||
async fn create_downloader(
|
||||
&self,
|
||||
stream_info: &StreamInfo,
|
||||
worker: &Worker,
|
||||
) -> AppResult<Box<dyn Downloader>>;
|
||||
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PyPlugin {
|
||||
plugin: Arc<Py<PyType>>,
|
||||
pattern: Regex,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl PyPlugin {
|
||||
pub fn from_pytype(class: &Bound<PyType>) -> PyResult<Self> {
|
||||
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<StreamStatus, Report<AppError>> {
|
||||
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<Box<dyn Downloader>> {
|
||||
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<Py<PyType>>, url: &str) -> PyResult<Option<StreamInfo>> {
|
||||
let url = url.to_string();
|
||||
// obj.
|
||||
tokio::task::spawn_blocking(move || {
|
||||
Python::attach(|py| -> PyResult<Option<StreamInfo>> {
|
||||
// 从 biliup.util 获取 loop(按你项目里真实的名字来取)
|
||||
let util = PyModule::import(py, "biliup.common.util")?;
|
||||
// 下面两行二选一(取决于 biliup.util 的 API):
|
||||
// let loop_obj: Py<PyAny> = util.getattr("loop")?.into_py(py);
|
||||
// 或:
|
||||
// let loop_obj: Py<PyAny> = 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<ActorHandle>) -> PyResult<Vec<DownloadManager>> {
|
||||
let classes: Vec<DownloadManager> = Python::attach(|py| -> PyResult<Vec<DownloadManager>> {
|
||||
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<PyList> = bound.downcast()?;
|
||||
|
||||
plugin_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let download = x.downcast::<PyType>()?;
|
||||
let py_plugin = PyPlugin::from_pytype(download)?;
|
||||
Ok(DownloadManager::new(py_plugin, actor_handle.clone()))
|
||||
})
|
||||
.collect::<PyResult<_>>()
|
||||
// 基类 DownloadBase
|
||||
// py.import("biliup.engine.download")?.getattr("DownloadBase")
|
||||
})?;
|
||||
// println!("类属性值: {:?}", class_attr);
|
||||
|
||||
// let download_base: &Bound<PyType> = download_base.downcast()?;
|
||||
|
||||
Ok(classes)
|
||||
}
|
||||
@@ -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<Report<AppError>>) -> Response {
|
||||
let report = report.into();
|
||||
let (status, error_message) = match report.downcast_ref::<AppError>() {
|
||||
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<HashMap<String, String>>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn new(message: String) -> Self {
|
||||
let errors: Vec<HashMap<String, String>> = 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<T> = Result<T, Report<AppError>>;
|
||||
@@ -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<Sqlite>;
|
||||
|
||||
pub struct ConnectionManager;
|
||||
|
||||
impl ConnectionManager {
|
||||
pub async fn new_pool(path: &str) -> Result<ConnectionPool> {
|
||||
// 创建所有父级目录(如果不存在)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
LiveStreamer::select()
|
||||
.where_("id = ?")
|
||||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
|
||||
pub async fn del_streamer(pool: &ConnectionPool, id: i64) -> AppResult<LiveStreamer> {
|
||||
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<Vec<LiveStreamer>> {
|
||||
LiveStreamer::select()
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
|
||||
pub async fn get_config(pool: &ConnectionPool) -> AppResult<Config> {
|
||||
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<Configuration> {
|
||||
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<Vec<UploadStreamer>> {
|
||||
UploadStreamer::select()
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.change_context(AppError::Unknown)
|
||||
}
|
||||
@@ -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<RwLock<Vec<Arc<Worker>>>>,
|
||||
pub managers: Arc<Vec<DownloadManager>>,
|
||||
pub config: Arc<RwLock<Config>>,
|
||||
pub actor_handle: Arc<ActorHandle>,
|
||||
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<RwLock<Config>>) -> 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<Monitor>,
|
||||
) -> AppResult<Option<()>> {
|
||||
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<ServiceRegister> 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<Worker> {
|
||||
// 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;
|
||||
// }
|
||||
// }
|
||||
@@ -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<ResponseData> {
|
||||
) -> AppResult<ResponseData> {
|
||||
// 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)
|
||||
}
|
||||
Reference in new issue
Block a user