Add QR code login and user management endpoints

Introduces endpoints for QR code-based login and user management in the stream-gears server. Adds support for creating, deleting, and listing users, as well as endpoints for QR code generation and login via QR code. Refactors configuration management to use a global LazyLock, updates model serialization for streamer hooks, and improves context and service registration to support stateless clients. Also updates the frontend to handle new user and QR code flows, and adds an image resource for donation display in the README.
This commit is contained in:
ForgQi committed 2025-09-20 21:43:51 +08:00
1 parent fe163af02d
commit a5faefb533
31 files changed
+265 -161

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Generated
+1 -1
View File
@@ -3617,7 +3617,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stream-gears"
version = "1.1.1"
version = "1.1.2"
dependencies = [
"anyhow",
"async-channel",
+1 -1
View File
@@ -7,7 +7,7 @@ members = [
resolver = "3"
[workspace.package]
version = "1.1.1"
version = "1.1.2"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
+1 -1
View File
@@ -74,8 +74,8 @@
## 💴捐赠
![支付宝](.github/resource/Image.jpg)
[爱发电 »](https://afdian.com/a/biliup)
## ⭐Stars
[![Star History Chart](https://api.star-history.com/svg?repos=biliup/biliup&type=Date)](https://star-history.com/#biliup/biliup&Date)
+1
View File
@@ -23,6 +23,7 @@ export function useBiliUsers() {
const [list, setList] = useState<any[]>([]);
useEffect(() => {
if (!data || data.length === 0) {
setList([])
return;
}
const updateList = async (item: User) => {
+5 -6
View File
@@ -29,16 +29,11 @@ const Qrcode: React.FC<QrcodeProps> = ({ onSuccess }) => {
body: JSON.stringify(qrData),
signal: signal,
})
if (!(res.status >= 200 && res.status < 300)) {
throw new Error(await res.text())
}
const data = await res.json()
if (!res.ok) {
throw new Error(data.message)
}
onSuccess(data['filename'])
})().catch(e => {
console.log(e)
setUrl(e.message)
Notification.error({
title: 'QRcode',
content: <Typography.Paragraph style={{ maxWidth: 450 }}>{e.message}</Typography.Paragraph>,
@@ -53,6 +48,10 @@ const Qrcode: React.FC<QrcodeProps> = ({ onSuccess }) => {
if (url === '') {
return <Spin />
}
if (!url.startsWith("http")) {
return <> {url} </>
}
return (
<div
style={{
+7 -5
View File
@@ -37,7 +37,7 @@ type UserListProps = {
children?: React.ReactNode
}
const UserList: React.FC<UserListProps> = ({ onCancel, visible }) => {
const { trigger } = useSWRMutation('/v1/users', sendRequest<User>)
const { trigger } = useSWRMutation('/v1/users', sendRequest)
const { trigger: deleteUser } = useSWRMutation('/v1/users', requestDelete)
const { biliUsers: list } = useBiliUsers()
const [modalVisible, setVisible] = useState(false)
@@ -55,10 +55,12 @@ const UserList: React.FC<UserListProps> = ({ onCancel, visible }) => {
throw new Error(ret.message)
}
await trigger({
id: 0,
name: value,
value: value,
platform: 'bilibili-cookies',
// id: 0,
// name: value,
// value: value,
// platform: 'bilibili-cookies',
key: 'bilibili-cookies',
value: value
})
setVisible(false)
Toast.success('创建成功')
+1 -1
View File
@@ -137,7 +137,7 @@ pub fn generate_json(mut file_name: PathBuf) -> Result<()> {
script_tag_count += 1;
let (_, tag_data) = script_data(i).unwrap();
FlvTag {
header: tag_header,
data: TagDataHeader::Script(tag_data),
@@ -1,7 +1,6 @@
use crate::server::core::download_actor::DownloadActorHandle;
use crate::server::core::live_streamers::{
AddLiveStreamerDto, DynLiveStreamersService, LiveStreamerDto,
LiveStreamerEntity,
AddLiveStreamerDto, DynLiveStreamersService, LiveStreamerDto, LiveStreamerEntity,
};
use crate::server::core::upload_streamers::{DynUploadStreamersRepository, StudioEntity};
use crate::server::core::users::User;
+24 -22
View File
@@ -349,33 +349,35 @@ impl ConfigManager {
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);
&& 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());
}
&& let Some(ref mut user) = self.config.user
{
user.access_token = Some(access_token.to_string());
}
Ok(())
}
@@ -6,7 +6,6 @@ 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 {
@@ -1,6 +1,5 @@
use crate::server::infrastructure::connection_pool::ConnectionPool;
#[derive(Clone)]
pub struct SqliteUploadStreamersRepository {
pool: ConnectionPool,
@@ -1,7 +1,4 @@
use crate::server::core::live_streamers::{
DynDownloadRecordsRepository,
DynVideosRepository,
};
use crate::server::core::live_streamers::{DynDownloadRecordsRepository, DynVideosRepository};
use crate::server::core::upload_streamers::DynUploadRecordsRepository;
use crate::server::infrastructure::connection_pool::ConnectionPool;
use crate::server::infrastructure::repositories::download_records_repository::SqliteDownloadRecordsRepository;
-3
View File
@@ -1,6 +1,3 @@
pub mod error;
pub mod extractor;
pub mod flv_parser;
+14 -14
View File
@@ -76,20 +76,20 @@ impl SiteDefinition for DouyuLive {
&& let Some(key) = regex::Regex::new(r"(\d{1,8}[0-9a-zA-Z]+)_?\d{0,4}(/playlist|.m3u8)")
.unwrap()
.captures(&result["data"]["rtmp_live"].to_string())
{
return Ok(Site {
name: "douyu",
title: room_info
.get("room")
.and_then(|room| room.get("room_name"))
.and_then(|name| name.as_str())
.unwrap_or_default()
.to_string(),
direct_url: format!("https://hw-tct.douyucdn.cn/live/{}.flv?uuid=", &key[1]),
extension: Extension::Flv,
client,
});
}
{
return Ok(Site {
name: "douyu",
title: room_info
.get("room")
.and_then(|room| room.get("room_name"))
.and_then(|name| name.as_str())
.unwrap_or_default()
.to_string(),
direct_url: format!("https://hw-tct.douyucdn.cn/live/{}.flv?uuid=", &key[1]),
extension: Extension::Flv,
client,
});
}
Err(Error::Custom(result.to_string()))
}
-1
View File
@@ -129,7 +129,6 @@ pub(crate) async fn parse_flv(
}
on_meta_data = Some((tag_header, bytes.clone(), previous_tag_size.clone()));
FlvTag {
header: tag_header,
data: TagDataHeader::Script(tag_data),
+1 -1
View File
@@ -113,7 +113,7 @@ pub struct LoginInfo {
pub struct TokenInfo {
pub access_token: String,
expires_in: u32,
mid: u64,
pub mid: u64,
refresh_token: String,
}
+2 -3
View File
@@ -56,9 +56,8 @@ async fn _main() -> AppResult<()> {
let conn_pool = ConnectionManager::new_pool("data/data.sqlite3")
.await
.expect("could not initialize the database connection pool");
let configs = repositories::get_config(&conn_pool).await?;
let service_register = ServiceRegister::new(conn_pool, configs);
*config::CONFIG.write().unwrap() = repositories::get_config(&conn_pool).await?;
let service_register = ServiceRegister::new(conn_pool, config::CONFIG.clone());
let all_streamer = repositories::get_all_streamer(&service_register.pool).await?;
let all_uploader = repositories::get_all_uploader(&service_register.pool).await?;
+1 -1
View File
@@ -92,7 +92,7 @@ mod post {
mod get {
use super::*;
use tracing::error;
// pub async fn login(
@@ -53,7 +53,7 @@ pub async fn get_myinfo_endpoint(
}
pub async fn get_proxy_endpoint(
Extension(client): Extension<StatelessClient>,
State(client): State<StatelessClient>,
Query(params): Query<HashMap<String, String>>,
) -> Result<Bytes, Response> {
info!(params = &params["url"]);
+100 -8
View File
@@ -1,23 +1,23 @@
use crate::server::errors::{AppError, report_to_response};
use std::sync::{Arc, RwLock};
use crate::server::config::Config;
use crate::server::errors::{AppError, report_to_response, AppResult};
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, InsertLiveStreamer, InsertUploadStreamer, LiveStreamer, StreamerInfo,
UploadStreamer,
};
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::Json;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use biliup::credential::{Credential, LoginInfo};
use error_stack::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(
@@ -244,6 +244,98 @@ pub async fn get_users_endpoint(
Ok(Json(res))
}
pub async fn delete_user_endpoint(Path(id): Path<i64>) -> Result<Json<()>, Response> {
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))
}
+2 -3
View File
@@ -58,10 +58,9 @@ impl ApplicationController {
// let app = protected::router()
let client = StatelessClient::default();
// let vec = service_register.streamers_service.get_streamers().await?;
// build our application with a route
let enable_login_guard = true;
let enable_login_guard = false;
let mut app = server::router::router(service_register);
if enable_login_guard {
app = app
@@ -82,7 +81,7 @@ impl ApplicationController {
.allow_origin("http://localhost:3000".parse::<HeaderValue>().unwrap())
.allow_methods(AllowMethods::any()),
)
.layer(Extension(client.clone()))
// .layer(Extension(client.clone()))
.fallback(static_handler);
// run our app with hyper
// `axum::Server` is a re-export of `hyper::Server`
+8 -3
View File
@@ -7,8 +7,9 @@ use pyo3::types::{PyAnyMethods, PyDict};
use pyo3::{Bound, FromPyObject, PyAny, PyResult, Python, pyclass, pyfunction, pymethods};
use pythonize::pythonize;
use serde::{Deserialize, Serialize};
use std::cell::LazyCell;
use std::ops::Deref;
use std::sync::{Arc, OnceLock, RwLock};
use std::sync::{Arc, LazyLock, OnceLock, RwLock};
use std::{collections::HashMap, path::Path, path::PathBuf};
#[derive(bon::Builder, Debug, Clone, Serialize, Deserialize)]
@@ -329,10 +330,14 @@ impl Config {
}
// 进程级全局单例(安全):OnceLock + Arc + RwLock
pub(crate) static CONFIG: OnceLock<Arc<RwLock<Config>>> = OnceLock::new();
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.get().expect("Config not initialized")
&*CONFIG
}
#[pyclass]
@@ -126,7 +126,7 @@ impl UActor {
return;
};
let config = worker.get_config().await.unwrap();
let client = StatelessClient::default();
let client = worker.context.client.clone();
let line = config.lines;
let line = match line.as_str() {
"bda2" => line::bda2(),
@@ -199,9 +199,10 @@ impl UActor {
if !studio.cover.is_empty()
&& let Ok(c) = &std::fs::read(&studio.cover).map_err(|e| error!(e=?e))
&& let Ok(url) = bilibili.cover_up(c).await.map_err(|e| error!(e=?e)) {
studio.cover = url;
};
&& let Ok(url) = bilibili.cover_up(c).await.map_err(|e| error!(e=?e))
{
studio.cover = url;
};
let submit = match config.submit_api {
Some(submit) => SubmitOption::from_str(&submit).unwrap_or(SubmitOption::App),
@@ -1,7 +1,7 @@
use std::path::Path;
use anyhow::{Context, Result};
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::{Pool, Sqlite};
use std::path::Path;
use tracing::info;
pub type ConnectionPool = Pool<Sqlite>;
@@ -3,29 +3,26 @@ use crate::server::errors::{AppError, AppResult};
use crate::server::infrastructure::connection_pool::ConnectionPool;
use crate::server::infrastructure::models::{Configuration, LiveStreamer, UploadStreamer};
use crate::server::infrastructure::repositories::{get_config, get_streamer};
use biliup::client::StatelessClient;
use error_stack::ResultExt;
use ormlite::Model;
use serde::{Deserialize, Serialize};
use std::sync::RwLock;
use tracing::info;
#[derive(Debug)]
pub struct Context {
uploads: Vec<UploadStreamer>,
configuration: Configuration,
streamers: Vec<LiveStreamer>,
pool: ConnectionPool,
pub client: StatelessClient,
}
impl Context {
pub fn new(
uploads: Vec<UploadStreamer>,
configuration: Configuration,
streamers: Vec<LiveStreamer>,
) -> Self {
Self {
uploads,
configuration,
streamers,
}
pub fn new(pool: ConnectionPool, client: StatelessClient) -> Self {
Self { pool, client }
}
pub async fn get_config(&self) -> AppResult<Config> {
get_config(&self.pool).await
}
}
@@ -33,16 +30,16 @@ impl Context {
pub struct Worker {
pub(crate) id: i64,
pub url: String,
pool: ConnectionPool,
pub context: Context,
pub downloader_status: RwLock<WorkerStatus>,
pub uploader_status: RwLock<WorkerStatus>,
}
impl Worker {
pub fn new(id: i64, url: &str, pool: ConnectionPool) -> Self {
pub fn new(id: i64, url: &str, context: Context) -> Self {
Self {
id,
pool,
context,
downloader_status: RwLock::new(Default::default()),
uploader_status: Default::default(),
url: url.to_string(),
@@ -50,11 +47,11 @@ impl Worker {
}
pub async fn get_streamer(&self) -> AppResult<LiveStreamer> {
get_streamer(&self.pool, self.id).await
get_streamer(&self.context.pool, self.id).await
}
pub async fn get_config(&self) -> AppResult<Config> {
get_config(&self.pool).await
self.context.get_config().await
}
pub async fn get_upload_config(&self) -> AppResult<Option<UploadStreamer>> {
@@ -65,7 +62,7 @@ impl Worker {
UploadStreamer::select()
.where_("id = ?")
.bind(id)
.fetch_optional(&self.pool)
.fetch_optional(&self.context.pool)
.await
.change_context(AppError::Unknown)
}
@@ -61,12 +61,14 @@ pub struct LiveStreamer {
// #[ormlite(json)] 数据库空与json空有区别所以这里不能用
// pub preprocessor: Option<Vec<String>>,
// 只能使用 sqlx::types::Json
pub preprocessor: sqlx::types::Json<Option<Vec<String>>>,
pub segment_processor: sqlx::types::Json<Option<Vec<HookStep>>>,
pub downloaded_processor: sqlx::types::Json<Option<Vec<HookStep>>>,
pub postprocessor: sqlx::types::Json<Option<Vec<HookStep>>>,
#[ormlite(json)]
pub preprocessor: Option<Vec<String>>,
#[ormlite(json)]
pub segment_processor: Option<Vec<HookStep>>,
#[ormlite(json)]
pub downloaded_processor: Option<Vec<HookStep>>,
#[ormlite(json)]
pub postprocessor: Option<Vec<HookStep>>,
pub opt_args: Option<Value>,
pub excluded_keywords: Option<Value>,
}
@@ -80,7 +82,7 @@ pub struct FileItem {
}
#[derive(Model, Debug, Clone, Serialize, Deserialize)]
#[ormlite(table = "configuration", insert = "InsertConfiguration")]
#[ormlite(table = "configuration")]
pub struct Configuration {
pub id: i64,
pub key: String,
@@ -110,12 +112,14 @@ pub struct InsertLiveStreamer {
// #[ormlite(json)] 数据库空与json空有区别所以这里不能用
// pub preprocessor: Option<Vec<String>>,
// 只能使用 sqlx::types::Json
pub preprocessor: sqlx::types::Json<Option<Vec<String>>>,
pub segment_processor: sqlx::types::Json<Option<Vec<HookStep>>>,
pub downloaded_processor: sqlx::types::Json<Option<Vec<HookStep>>>,
pub postprocessor: sqlx::types::Json<Option<Vec<HookStep>>>,
#[ormlite(json)]
pub preprocessor: Option<Vec<String>>,
#[ormlite(json)]
pub segment_processor: Option<Vec<HookStep>>,
#[ormlite(json)]
pub downloaded_processor: Option<Vec<HookStep>>,
#[ormlite(json)]
pub postprocessor: Option<Vec<HookStep>>,
pub opt_args: Option<Value>,
pub excluded_keywords: Option<Value>,
}
@@ -148,3 +152,10 @@ pub struct InsertUploadStreamer {
pub extra_fields: Option<String>,
pub is_only_self: Option<i64>,
}
#[derive(Insert, Debug, Clone, Serialize, Deserialize)]
#[ormlite(returns = "Configuration")]
pub struct InsertConfiguration {
pub key: String,
pub value: String, // TEXT
}
@@ -1,7 +1,9 @@
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 crate::server::infrastructure::models::{
Configuration, InsertConfiguration, LiveStreamer, UploadStreamer,
};
use error_stack::ResultExt;
use ormlite::{Insert, Model};
@@ -41,18 +43,20 @@ pub async fn get_config(pool: &ConnectionPool) -> AppResult<Config> {
serde_json::from_str(&configuration.value).change_context(AppError::Unknown)?;
Ok(json)
} else {
let config = Config::builder()
.streamers(Default::default())
.build();
let configuration = InsertConfiguration {
key: "config".to_string(),
value: serde_json::to_string(&config).unwrap(),
}.insert(pool).await.change_context(AppError::Unknown)?;
let json: Config =
serde_json::from_str(&configuration.value).change_context(AppError::Unknown)?;
Ok(json)
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>> {
@@ -5,8 +5,11 @@ 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::Worker;
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};
@@ -19,27 +22,19 @@ pub struct ServiceRegister {
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: Config) -> Self {
pub fn new(pool: ConnectionPool, config: Arc<RwLock<Config>>) -> Self {
info!("initializing utility services...");
let client = StatelessClient::default();
let cf = config::CONFIG.get_or_init(|| {
Arc::new(RwLock::new(config))
// Arc::new(RwLock::new(Config::builder()
// // .user(UserConfig::default())
// // .douyu_cdn("hw-h5".to_string())
// // .douyu_rate(0)
// .streamers(HashMap::<String, StreamerConfig>::new())
// .build()))
});
info!(config=?cf);
info!(config=?config);
let actor_handle = Arc::new(ActorHandle::new(
cf.read().unwrap().pool1_size,
cf.read().unwrap().pool2_size,
config.read().unwrap().pool1_size,
config.read().unwrap().pool2_size,
));
info!("utility services initialized, building feature services...");
@@ -52,13 +47,17 @@ impl ServiceRegister {
pool,
workers: Arc::new(Default::default()),
managers: Arc::new(vec),
config: cf.clone(),
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 _)
self.managers
.iter()
.find(|&manager| manager.matches(url))
.map(|v| v as _)
}
pub async fn add_room(
@@ -67,7 +66,11 @@ impl ServiceRegister {
url: &str,
monitor: Arc<Monitor>,
) -> AppResult<Option<()>> {
let worker = Arc::new(Worker::new(id, url, self.pool.clone()));
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");
+7 -9
View File
@@ -1,15 +1,10 @@
use crate::server::api::bilibili_endpoints::{
archive_pre_endpoint, get_myinfo_endpoint, get_proxy_endpoint,
};
use crate::server::api::endpoints::{
add_upload_streamer_endpoint, delete_streamers_endpoint, delete_template_endpoint,
delete_user_endpoint, get_configuration, get_streamer_info, get_streamers_endpoint,
get_upload_streamer_endpoint, get_upload_streamers_endpoint, get_users_endpoint,
post_streamers_endpoint, put_streamers_endpoint,
};
use crate::server::api::endpoints::{add_upload_streamer_endpoint, add_user_endpoint, delete_streamers_endpoint, delete_template_endpoint, delete_user_endpoint, get_configuration, get_qrcode, get_streamer_info, get_streamers_endpoint, get_upload_streamer_endpoint, get_upload_streamers_endpoint, get_users_endpoint, get_videos, login_by_qrcode, post_streamers_endpoint, put_streamers_endpoint};
use crate::server::infrastructure::service_register::ServiceRegister;
use axum::routing::{delete, get, post};
use axum::Router;
use axum::routing::{delete, get, post};
pub fn router(service_register: ServiceRegister) -> Router<()> {
Router::new()
@@ -42,13 +37,16 @@ pub fn router(service_register: ServiceRegister) -> Router<()> {
.route("/v1/upload/streamers", post(add_upload_streamer_endpoint))
.route(
"/v1/users",
get(get_users_endpoint), // .post(add_user_endpoint)
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))
// .route("/", get(root))
.route("/v1/get_qrcode", get(get_qrcode))
.route("/v1/login_by_qrcode", post(login_by_qrcode))
.route("/v1/videos", get(get_videos))
// .layer(Extension(client.clone()))
.with_state(service_register)
}
+4 -3
View File
@@ -104,9 +104,10 @@ pub fn media_ext_from_url(input: &str) -> Option<String> {
// a) 从最后一个 path segment 取扩展名
if let Some(seg) = url.path_segments().and_then(|mut s| s.next_back())
&& let Some((_, ext)) = seg.rsplit_once('.')
&& let Some(ext) = clean_ext(ext) {
return Some(ext);
}
&& let Some(ext) = clean_ext(ext)
{
return Some(ext);
}
// b) 常见 query 参数中找一次(不重复多轮扫描),忽略大小写
let keys = ["format", "type", "ext", "filetype", "fmt"];