Files
biliup/crates/biliup-cli/src/server/api/endpoints.rs
T
ForgQi ddc29f63b4 Update deps & fix server upload/download
Bump dependencies (Cargo.toml + Cargo.lock) to newer crate versions and adapt code to API changes. Refactor server-side upload/download logic, the upload_streamer model, and related API endpoints to improve streaming handling, buffer/error propagation and compatibility with updated crates (e.g. tokio-tungstenite/futures/etc.). These changes align the codebase with the upgraded dependency set and fix streaming/endpoint issues introduced by those upgrades.
2026-07-11 01:57:48 +08:00

608 lines
20 KiB
Rust

use crate::server::common::upload::{build_studio, submit_to_bilibili, upload};
use crate::server::common::util::Recorder;
use crate::server::config::Config;
use crate::server::core::download_manager::DownloadManager;
use crate::server::errors::{AppError, report_to_response};
use crate::server::infrastructure::connection_pool::ConnectionPool;
use crate::server::infrastructure::context::{Stage, WorkerStatus};
use crate::server::infrastructure::dto::LiveStreamerResponse;
use crate::server::infrastructure::models::live_streamer::{InsertLiveStreamer, LiveStreamer};
use crate::server::infrastructure::models::upload_streamer::{
InsertUploadStreamer, UploadStreamer,
};
use crate::server::infrastructure::models::{
Configuration, FileItem, InsertConfiguration, StreamerInfo,
};
use crate::server::infrastructure::repositories::{
del_streamer, get_all_streamer, get_upload_config,
};
use crate::server::infrastructure::service_register::ServiceRegister;
use crate::{LogHandle, UploadLine};
use axum::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use biliup::credential::Credential;
use chrono::Utc;
use clap::ValueEnum;
use error_stack::{Report, ResultExt};
use ormlite::{Insert, Model};
use serde::Deserialize;
use serde_json::json;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::{Duration, UNIX_EPOCH};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tracing::info;
use tracing_subscriber::EnvFilter;
pub async fn get_streamers_endpoint(
State(pool): State<ConnectionPool>,
State(managers): State<Arc<DownloadManager>>,
) -> Result<Json<Vec<LiveStreamerResponse>>, Response> {
let live_streamers = get_all_streamer(&pool).await.map_err(report_to_response)?;
let mut results = Vec::new();
let workers = managers.get_rooms().await;
for x in live_streamers {
let option = workers
.clone()
.into_iter()
.find(|worker| worker.live_streamer.id == x.id);
let status = match option.as_ref() {
Some(t) => format!("{:?}", *t.downloader_status.read().unwrap()),
None => String::new(),
};
results.push(LiveStreamerResponse {
status,
inner: x,
upload_status: option
.map(|t| format!("{:?}", *t.uploader_status.read().unwrap()))
.unwrap_or_default(),
});
}
Ok(Json(results))
}
pub async fn post_streamers_endpoint(
State(service_register): State<ServiceRegister>,
State(managers): State<Arc<DownloadManager>>,
State(pool): State<ConnectionPool>,
Json(payload): Json<InsertLiveStreamer>,
) -> Result<Json<LiveStreamer>, Response> {
let url = &payload.url.clone();
// You can insert the model directly.
let live_streamers = payload
.insert(&pool)
.await
.change_context(AppError::Unknown)
.map_err(report_to_response)?;
let upload_config = get_upload_config(&pool, live_streamers.id)
.await
.map_err(report_to_response)?;
let Some(_) = managers
.add_room(service_register.worker(live_streamers.clone(), upload_config))
.await
else {
info!("not supported url: {}", url);
return Err((StatusCode::BAD_REQUEST, "Not supported url").into_response());
};
info!(url = url, "successfully inserted new live streamers");
Ok(Json(live_streamers))
}
pub async fn put_streamers_endpoint(
State(service_register): State<ServiceRegister>,
State(managers): State<Arc<DownloadManager>>,
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 id = streamer.id;
managers.del_room(id).await;
let upload_config = get_upload_config(&pool, id)
.await
.map_err(report_to_response)?;
managers
.add_room(service_register.worker(streamer.clone(), upload_config))
.await
.ok_or(AppError::Unknown)
.map_err(report_to_response)?;
info!(id = id, "successfully update live streamers");
Ok(Json(streamer))
}
pub async fn delete_streamers_endpoint(
State(managers): State<Arc<DownloadManager>>,
State(pool): State<ConnectionPool>,
Path(id): Path<i64>,
) -> Result<Json<LiveStreamer>, Response> {
managers.del_room(id).await;
let live_streamers = del_streamer(&pool, id).await.map_err(report_to_response)?;
info!(workers=?live_streamers, "successfully inserted new live streamers");
Ok(Json(live_streamers))
}
// #[axum::debug_handler(state = ServiceRegister)]
pub async fn pause_streamers_endpoint(
State(managers): State<Arc<DownloadManager>>,
Path(id): Path<i64>,
) -> Result<Json<()>, Response> {
let worker = managers.get_room_by_id(id).await;
if let Some(w) = worker {
let worker_status = w.downloader_status.read().unwrap().clone();
match worker_status {
WorkerStatus::Working(_) => {
w.change_status(Stage::Download, WorkerStatus::Pause).await;
info!(url=?&w.live_streamer.url, "successfully pause live streamers");
managers.make_waker(id).await;
}
WorkerStatus::Pause => {
w.change_status(Stage::Download, WorkerStatus::Idle).await;
managers.wake_waker(id).await;
info!(url=?&w.live_streamer.url, "successfully start live streamers");
}
WorkerStatus::Pending => {
w.change_status(Stage::Download, WorkerStatus::Pause).await;
managers.make_waker(id).await;
info!(url=?&w.live_streamer.url, "successfully pause live streamers");
}
WorkerStatus::Idle => {
w.change_status(Stage::Download, WorkerStatus::Pause).await;
managers.make_waker(id).await;
info!(url=?&w.live_streamer.url, "successfully pause live streamers");
}
};
}
Ok(Json(()))
}
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>,
State(log_handle): State<LogHandle>,
Json(json_data): Json<Config>,
) -> Result<Json<Config>, Response> {
let mut json_data = json_data;
json_data.normalize_segment_limits();
json_data
.validate_segment_limits()
.map_err(report_to_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 mut saved_config: Config = serde_json::from_str(&saved.value)
.change_context(AppError::Unknown)
.map_err(report_to_response)?;
saved_config.normalize_segment_limits();
saved_config
.validate_segment_limits()
.map_err(report_to_response)?;
*config.write().unwrap() = saved_config;
let guard = config.read().unwrap();
if let Some(loggers_level) = &guard.loggers_level {
let new_filter = EnvFilter::try_new(loggers_level)
.change_context(AppError::Custom(String::from("Invalid log level format")))
.map_err(report_to_response)?;
log_handle
.modify(|filter| *filter = new_filter)
.change_context(AppError::Unknown)
.map_err(report_to_response)?;
}
Ok(Json(guard.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)?;
Ok(Json(streamer_infos))
}
pub async fn get_streamer_info_files(
// Extension(streamers_service): Extension<DynUploadStreamersRepository>,
State(pool): State<ConnectionPool>,
Path(id): Path<i64>,
) -> Result<Json<Vec<FileItem>>, Response> {
let file_items = FileItem::select()
.where_("streamer_info_id = ?")
.bind(id)
.fetch_all(&pool)
.await
.change_context(AppError::Unknown)
.map_err(report_to_response)?;
Ok(Json(file_items))
}
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())
&& media_extensions
.iter()
.any(|allowed| ext == allowed.trim_start_matches('.'))
&& 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))
}
// #[axum::debug_handler(state = ServiceRegister)]
pub async fn get_status(
State(_service_register): State<ServiceRegister>,
State(managers): State<Arc<DownloadManager>>,
State(config): State<Arc<RwLock<Config>>>,
) -> Result<Json<serde_json::Value>, Response> {
let workers = managers.get_rooms().await;
let mut sw = Vec::new();
for worker in &workers {
sw.push(serde_json::json!({
"downloader_status": format!("{:?}", worker.downloader_status.read()),
"uploader_status": format!("{:?}", worker.uploader_status.read().unwrap()),
"live_streamer": worker.live_streamer,
"upload_streamer": worker.upload_streamer,
}));
}
Ok(Json(serde_json::json!({
"version": env!("CARGO_PKG_VERSION"),
"rooms": sw,
"download_semaphore": managers.download_semaphore,
"update_semaphore": managers.u_kills.len(),
"config": config,
})))
}
#[derive(Deserialize)]
pub struct PostUploads {
files: Vec<PathBuf>,
params: UploadStreamer,
}
// #[debug_handler]
pub async fn post_uploads(
State(config): State<Arc<RwLock<Config>>>,
Json(json_data): Json<PostUploads>,
) -> Result<Json<serde_json::Value>, Response> {
let upload_config = json_data.params;
if upload_config.is_noop_uploader() {
info!(
uploader = ?upload_config.uploader,
"Skipping page upload because uploader is Noop"
);
return Ok(Json(json!({})));
}
let (line, limit, submit_api) = {
let config = config.read().unwrap();
let line = UploadLine::from_str(&config.lines, true).ok();
let limit = config.threads;
let submit_api = config.submit_api.clone();
(line, limit, submit_api)
};
info!("通过页面开始上传");
tokio::spawn(async move {
let (bilibili, videos) = upload(
upload_config
.user_cookie
.as_deref()
.unwrap_or("cookies.json"),
None,
line,
&json_data.files,
limit as usize,
)
.await?;
if !videos.is_empty() {
let recorder = Recorder::new(
upload_config.title.clone(),
StreamerInfo::new(
&upload_config.template_name,
"stream_title",
"",
Utc::now(),
"",
),
);
let studio = build_studio(&upload_config, &bilibili, videos, &recorder).await?;
let response_data =
submit_to_bilibili(&bilibili, &studio, submit_api.as_deref()).await?;
info!("通过页面上传成功 {:?}", response_data);
}
Ok::<_, Report<AppError>>(())
});
Ok(Json(serde_json::json!({})))
}