use std::sync::Arc; use actix_web::{HttpResponse, web}; use actix_web::middleware::Condition; use actix_web_httpauth::middleware::HttpAuthentication; use log::error; use serde_json::json; use crate::api::api_model::{AppState, PlaylistRequest, ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig}; use crate::api::download_api; use crate::auth::authenticator::validator; use crate::m3u_filter_error::M3uFilterError; use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, ProxyUserCredentials, TargetUser}; use crate::model::config::{Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, validate_targets}; use crate::processing::playlist_processor; use crate::utils::{config_reader, download}; fn _save_config_api_proxy(backup_dir: &str, api_proxy: &mut ApiProxyConfig, file_path: &str) -> Option { match config_reader::save_api_proxy(file_path, backup_dir, api_proxy) { Ok(()) => {} Err(err) => { error!("Failed to save api_proxy.yml {}", err.to_string()); return Some(err); } } None } fn _save_config_main(file_path: &str, backup_dir: &str, cfg: &ConfigDto) -> Option { match config_reader::save_main_config(file_path, backup_dir, cfg) { Ok(()) => {} Err(err) => { error!("Failed to save config.yml {}", err.to_string()); return Some(err); } } None } async fn save_config_api_proxy_user( req: web::Json>, app_state: web::Data, ) -> HttpResponse { let mut users = req.0; users.iter_mut().flat_map(|t| &mut t.credentials).for_each(ProxyUserCredentials::trim); if let Some(api_proxy) = app_state.config.t_api_proxy.write().unwrap().as_mut() { let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); api_proxy.user = users; if let Some(err) = _save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) { return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); } api_proxy.user.iter_mut().flat_map(|t| &mut t.credentials).for_each(|c| c.prepare(true)); } HttpResponse::Ok().finish() } async fn save_config_main( req: web::Json, app_state: web::Data, ) -> HttpResponse { let cfg = req.0; if cfg.is_valid() { let file_path = app_state.config.t_config_file_path.as_str(); let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); if let Some(err) = _save_config_main(file_path, backup_dir, &cfg) { return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); } HttpResponse::Ok().finish() } else { HttpResponse::BadRequest().json(json!({"error": "Invalid content"})) } } async fn save_config_api_proxy_config( req: web::Json>, app_state: web::Data, ) -> HttpResponse { let mut req_api_proxy = req.0; for server_info in &mut req_api_proxy { if !server_info.is_valid() { return HttpResponse::BadRequest().json(json!({"error": "Invalid content"})); } } if let Some(api_proxy) = app_state.config.t_api_proxy.write().unwrap().as_mut() { api_proxy.server = req_api_proxy; let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); if let Some(err) = _save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) { return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); } } HttpResponse::Ok().finish() } async fn playlist_update( req: web::Json>, app_state: web::Data, ) -> HttpResponse { let targets = req.0; let user_targets = if targets.is_empty() { None } else { Some(targets) }; let process_targets = validate_targets(&user_targets, &app_state.config.sources); match process_targets { Ok(valid_targets) => { actix_rt::spawn(playlist_processor::exec_processing(Arc::clone(&app_state.config), Arc::new(valid_targets))); HttpResponse::Ok().finish() } Err(err) => { error!("Failed playlist update {}", err.to_string()); HttpResponse::BadRequest().json(json!({"error": err.to_string()})) } } } fn create_config_input_for_url(url: &str) -> ConfigInput { ConfigInput { id: 0, input_type: InputType::M3u, url: String::from(url), enabled: true, options: Some(ConfigInputOptions { xtream_skip_live: false, xtream_skip_vod: false, xtream_skip_series: false, }), ..Default::default() } } async fn get_playlist(cfg_input: Option<&ConfigInput>, cfg: &Config) -> HttpResponse { match cfg_input { Some(input) => { let (result, errors) = match input.input_type { InputType::M3u => download::get_m3u_playlist(cfg, input, &cfg.working_dir).await, InputType::Xtream => download::get_xtream_playlist(input, &cfg.working_dir).await, }; if result.is_empty() { let error_strings: Vec = errors.iter().map(std::string::ToString::to_string).collect(); HttpResponse::BadRequest().json(json!({"error": error_strings.join(", ")})) } else { HttpResponse::Ok().json(result) } } None => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})), } } async fn playlist( req: web::Json, app_state: web::Data, ) -> HttpResponse { return if let Some(input_id) = req.input_id { get_playlist(app_state.config.get_input_by_id(input_id), &app_state.config).await } else { let url = req.url.as_deref().unwrap_or(""); let input = create_config_input_for_url(url); get_playlist(Some(&input), &app_state.config).await } } async fn config( app_state: web::Data, ) -> HttpResponse { let map_input = |i: &ConfigInput| ServerInputConfig { id: i.id, input_type: i.input_type.clone(), url: i.url.clone(), username: i.username.clone(), password: i.password.clone(), persist: i.persist.clone(), name: i.name.clone(), enabled: i.enabled, }; let map_target = |t: &ConfigTarget| ServerTargetConfig { id: t.id, enabled: t.enabled, name: t.name.clone(), options: t.options.clone(), sort: t.sort.clone(), filter: t.filter.clone(), output: t.output.clone(), rename: t.rename.clone(), mapping: t.mapping.clone(), processing_order: t.processing_order.clone(), watch: t.watch.clone(), }; let map_source = |s: &ConfigSource| ServerSourceConfig { inputs: s.inputs.iter().map(map_input).collect(), targets: s.targets.iter().map(map_target).collect(), }; let map_config = |config: &Config| ServerConfig { api: config.api.clone(), threads: config.threads, working_dir: config.working_dir.clone(), backup_dir: config.backup_dir.clone(), schedule: config.schedule.clone(), messaging: config.messaging.clone(), video: config.video.clone(), sources: config.sources.iter().map(map_source).collect(), api_proxy: config_reader::read_api_proxy(app_state.config.t_api_proxy_file_path.as_str(), false), }; let mut result = match config_reader::read_config(app_state.config.t_config_path.as_str(), app_state.config.t_config_file_path.as_str(), app_state.config.t_sources_file_path.as_str()) { Ok(mut cfg) => { let _ = cfg.prepare(true); map_config(&cfg) } Err(_) => map_config(&app_state.config) }; // if we didn't read it from file then we should use it from app_state if result.api_proxy.is_none() { result.api_proxy.clone_from(&app_state.config.t_api_proxy.read().unwrap()); } HttpResponse::Ok().json(result) } pub(crate) fn v1_api_register(web_auth_enabled: bool) -> impl Fn(&mut web::ServiceConfig) { return move |cfg: &mut web::ServiceConfig| { cfg.service(web::scope("/api/v1") .wrap(Condition::new(web_auth_enabled, HttpAuthentication::with_fn(validator))) .route("/config", web::get().to(config)) .route("/config/main", web::post().to(save_config_main)) .route("/config/user", web::post().to(save_config_api_proxy_user)) .route("/config/apiproxy", web::post().to(save_config_api_proxy_config)) .route("/playlist", web::post().to(playlist)) .route("/playlist/update", web::post().to(playlist_update)) .route("/file/download", web::post().to(download_api::queue_download_file)) .route("/file/download/info", web::get().to(download_api::download_file_info))); }; }