botserver/src/player/mod.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
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>
2026-02-12 21:09:30 +00:00

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))
}