Some checks failed
BotServer CI / build (push) Failing after 1m34s
Split 20+ files over 1000 lines into focused subdirectories for better maintainability and code organization. All changes maintain backward compatibility through re-export wrappers. Major splits: - attendance/llm_assist.rs (2074→7 modules) - basic/keywords/face_api.rs → face_api/ (7 modules) - basic/keywords/file_operations.rs → file_ops/ (8 modules) - basic/keywords/hear_talk.rs → hearing/ (6 modules) - channels/wechat.rs → wechat/ (10 modules) - channels/youtube.rs → youtube/ (5 modules) - contacts/mod.rs → contacts_api/ (6 modules) - core/bootstrap/mod.rs → bootstrap/ (5 modules) - core/shared/admin.rs → admin_*.rs (5 modules) - designer/canvas.rs → canvas_api/ (6 modules) - designer/mod.rs → designer_api/ (6 modules) - docs/handlers.rs → handlers_api/ (11 modules) - drive/mod.rs → drive_handlers.rs, drive_types.rs - learn/mod.rs → types.rs - main.rs → main_module/ (7 modules) - meet/webinar.rs → webinar_api/ (8 modules) - paper/mod.rs → (10 modules) - security/auth.rs → auth_api/ (7 modules) - security/passkey.rs → (4 modules) - sources/mod.rs → sources_api/ (5 modules) - tasks/mod.rs → task_api/ (5 modules) Stats: 38,040 deletions, 1,315 additions across 318 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
208 lines
5.8 KiB
Rust
208 lines
5.8 KiB
Rust
use axum::{
|
|
body::Body,
|
|
extract::{Path, Query, State},
|
|
http::{header, StatusCode},
|
|
response::{IntoResponse, Response},
|
|
routing::get,
|
|
Json, Router,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
|
|
use crate::core::shared::state::AppState;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MediaInfo {
|
|
pub path: String,
|
|
pub filename: String,
|
|
pub mime_type: String,
|
|
pub size: u64,
|
|
pub duration: Option<f64>,
|
|
pub width: Option<u32>,
|
|
pub height: Option<u32>,
|
|
pub format: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ThumbnailInfo {
|
|
pub path: String,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
pub format: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct StreamQuery {
|
|
pub quality: Option<String>,
|
|
pub start: Option<f64>,
|
|
pub end: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ThumbnailQuery {
|
|
pub width: Option<u32>,
|
|
pub height: Option<u32>,
|
|
pub time: Option<f64>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PlayerError {
|
|
pub error: String,
|
|
pub code: String,
|
|
}
|
|
|
|
impl IntoResponse for PlayerError {
|
|
fn into_response(self) -> Response {
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(serde_json::json!({"error": self.error, "code": self.code})),
|
|
)
|
|
.into_response()
|
|
}
|
|
}
|
|
|
|
fn get_mime_type(path: &str) -> &'static str {
|
|
let ext = path.rsplit('.').next().unwrap_or("").to_lowercase();
|
|
match ext.as_str() {
|
|
"mp4" => "video/mp4",
|
|
"webm" => "video/webm",
|
|
"ogv" => "video/ogg",
|
|
"mp3" => "audio/mpeg",
|
|
"wav" => "audio/wav",
|
|
"ogg" => "audio/ogg",
|
|
"m4a" => "audio/mp4",
|
|
"flac" => "audio/flac",
|
|
"pdf" => "application/pdf",
|
|
"png" => "image/png",
|
|
"jpg" | "jpeg" => "image/jpeg",
|
|
"gif" => "image/gif",
|
|
"svg" => "image/svg+xml",
|
|
"webp" => "image/webp",
|
|
_ => "application/octet-stream",
|
|
}
|
|
}
|
|
|
|
fn get_format(path: &str) -> String {
|
|
path.rsplit('.')
|
|
.next()
|
|
.unwrap_or("unknown")
|
|
.to_uppercase()
|
|
}
|
|
|
|
async fn get_file_info(
|
|
State(_state): State<Arc<AppState>>,
|
|
Path((bot_id, path)): Path<(String, String)>,
|
|
) -> Result<Json<MediaInfo>, PlayerError> {
|
|
let filename = path.rsplit('/').next().unwrap_or(&path).to_string();
|
|
let mime_type = get_mime_type(&path).to_string();
|
|
let format = get_format(&path);
|
|
|
|
let info = MediaInfo {
|
|
path: format!("{bot_id}/{path}"),
|
|
filename,
|
|
mime_type,
|
|
size: 0,
|
|
duration: None,
|
|
width: None,
|
|
height: None,
|
|
format,
|
|
};
|
|
|
|
Ok(Json(info))
|
|
}
|
|
|
|
async fn stream_file(
|
|
State(state): State<Arc<AppState>>,
|
|
Path((bot_id, path)): Path<(String, String)>,
|
|
Query(_query): Query<StreamQuery>,
|
|
) -> Result<Response<Body>, PlayerError> {
|
|
let mime_type = get_mime_type(&path);
|
|
let full_path = format!("{bot_id}.gbdrive/{path}");
|
|
|
|
let s3 = state.drive.as_ref().ok_or_else(|| PlayerError {
|
|
error: "Storage not configured".to_string(),
|
|
code: "STORAGE_NOT_CONFIGURED".to_string(),
|
|
})?;
|
|
|
|
let result = s3
|
|
.get_object()
|
|
.bucket(&format!("{bot_id}.gbai"))
|
|
.key(&full_path)
|
|
.send()
|
|
.await
|
|
.map_err(|e| PlayerError {
|
|
error: format!("Failed to get file: {e}"),
|
|
code: "FILE_NOT_FOUND".to_string(),
|
|
})?;
|
|
|
|
let body = result.body.collect().await.map_err(|e| PlayerError {
|
|
error: format!("Failed to read file: {e}"),
|
|
code: "READ_ERROR".to_string(),
|
|
})?;
|
|
|
|
let response = Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header(header::CONTENT_TYPE, mime_type)
|
|
.header(header::ACCEPT_RANGES, "bytes")
|
|
.body(Body::from(body.into_bytes()))
|
|
.map_err(|e| PlayerError {
|
|
error: format!("Failed to build response: {e}"),
|
|
code: "RESPONSE_ERROR".to_string(),
|
|
})?;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn get_thumbnail(
|
|
State(_state): State<Arc<AppState>>,
|
|
Path((bot_id, path)): Path<(String, String)>,
|
|
Query(query): Query<ThumbnailQuery>,
|
|
) -> Result<Response<Body>, PlayerError> {
|
|
let width = query.width.unwrap_or(320);
|
|
let height = query.height.unwrap_or(180);
|
|
|
|
let filename = path.rsplit('/').next().unwrap_or(&path);
|
|
let placeholder = format!(
|
|
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">
|
|
<rect width="100%" height="100%" fill="#374151"/>
|
|
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" fill="#9CA3AF" font-family="sans-serif" font-size="14">
|
|
{}
|
|
</text>
|
|
</svg>"##,
|
|
width, height, width, height, filename
|
|
);
|
|
|
|
let _ = bot_id;
|
|
|
|
let response = Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header(header::CONTENT_TYPE, "image/svg+xml")
|
|
.header(header::CACHE_CONTROL, "public, max-age=3600")
|
|
.body(Body::from(placeholder))
|
|
.map_err(|e| PlayerError {
|
|
error: format!("Failed to build response: {e}"),
|
|
code: "RESPONSE_ERROR".to_string(),
|
|
})?;
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
async fn get_supported_formats(
|
|
State(_state): State<Arc<AppState>>,
|
|
) -> Json<serde_json::Value> {
|
|
Json(serde_json::json!({
|
|
"video": ["mp4", "webm", "ogv"],
|
|
"audio": ["mp3", "wav", "ogg", "m4a", "flac"],
|
|
"document": ["pdf", "txt", "md", "html"],
|
|
"image": ["png", "jpg", "jpeg", "gif", "svg", "webp"],
|
|
"presentation": ["pptx", "odp"]
|
|
}))
|
|
}
|
|
|
|
pub fn configure_player_routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route("/api/player/formats", get(get_supported_formats))
|
|
.route("/api/player/:bot_id/info/*path", get(get_file_info))
|
|
.route("/api/player/:bot_id/stream/*path", get(stream_file))
|
|
.route("/api/player/:bot_id/thumbnail/*path", get(get_thumbnail))
|
|
}
|