diff --git a/Cargo.lock b/Cargo.lock index 10140a538..1c2f50b8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -997,6 +997,7 @@ dependencies = [ "implicit-clone", "js-sys", "log", + "paste", "prost", "regex", "reqwasm", diff --git a/backend/src/api/api_utils.rs b/backend/src/api/api_utils.rs index 94f0f283b..0314002db 100644 --- a/backend/src/api/api_utils.rs +++ b/backend/src/api/api_utils.rs @@ -390,10 +390,10 @@ async fn resolve_streaming_strategy( // force_stream_provider means we keep the url and the provider. // If force_stream_provider or the input is the same as the config we don't need to get new url let (provider, url) = if force_provider.is_some() || provider.id == input.id { - (input.name.to_string(), stream_url.to_string()) + (input.name.clone(), stream_url.to_string()) } else { ( - provider.name.to_string(), + provider.name.clone(), get_stream_alternative_url(stream_url, input, provider), ) }; diff --git a/backend/src/api/endpoints/api_playlist_utils.rs b/backend/src/api/endpoints/api_playlist_utils.rs index 77b869fb8..a6cb51c5d 100644 --- a/backend/src/api/endpoints/api_playlist_utils.rs +++ b/backend/src/api/endpoints/api_playlist_utils.rs @@ -34,7 +34,7 @@ where .map(|(index, (key, value))| PlaylistResponseGroup { #[allow(clippy::cast_possible_truncation)] id: index as u32, - title: key.to_string(), + title: key.clone(), channels: value.into_iter().map(Into::into).collect(), xtream_cluster: cluster, }) diff --git a/backend/src/api/endpoints/hdhomerun_api.rs b/backend/src/api/endpoints/hdhomerun_api.rs index 95d5694dd..c7787d99a 100644 --- a/backend/src/api/endpoints/hdhomerun_api.rs +++ b/backend/src/api/endpoints/hdhomerun_api.rs @@ -98,13 +98,13 @@ where .map_or((true, false), |o| (o.xtream_live_stream_use_prefix, o.xtream_live_stream_without_extension)); let container_extension = item.get_additional_property("container_extension").map(|v| get_string_from_serde_value(&v).unwrap_or_default()); let stream_url = match &base_url { - None => item.url.to_string(), + None => item.url.clone(), Some(url) => get_xtream_url(cluster, url, &credentials.username, &credentials.password, item.virtual_id, container_extension.as_ref(), live_stream_use_prefix, live_stream_without_extension) }; let lineup = Lineup { - guide_number: item.epg_channel_id.unwrap_or(item.name).to_string(), - guide_name: item.title.to_string(), + guide_number: item.epg_channel_id.unwrap_or(item.name).clone(), + guide_name: item.title.clone(), url: stream_url, }; match serde_json::to_string(&lineup) { @@ -134,9 +134,9 @@ where Some(chans) => { let mapped = chans.map(move |(item, has_next)| { let lineup = Lineup { - guide_number: item.epg_channel_id.unwrap_or(item.name).to_string(), - guide_name: item.title.to_string(), - url: (if item.t_stream_url.is_empty() { &item.url } else { &item.t_stream_url }).to_string(), + guide_number: item.epg_channel_id.unwrap_or(item.name).clone(), + guide_name: item.title.clone(), + url: (if item.t_stream_url.is_empty() { &item.url } else { &item.t_stream_url }).clone(), }; match serde_json::to_string(&lineup) { Ok(content) => { @@ -163,16 +163,16 @@ fn create_device(app_state: &Arc) -> Option { let device = &app_state.device; let device_url = format!("{}://{}:{}", server_info.protocol, server_info.host, device.port); Some(Device { - friendly_name: device.friendly_name.to_string(), - manufacturer: device.manufacturer.to_string(), + friendly_name: device.friendly_name.clone(), + manufacturer: device.manufacturer.clone(), //manufacturer_url: "https://github.com/euzu/tuliprox".to_string(), - model_number: device.model_number.to_string(), - model_name: device.model_name.to_string(), - firmware_name: device.firmware_name.to_string(), + model_number: device.model_number.clone(), + model_name: device.model_name.clone(), + firmware_name: device.firmware_name.clone(), tuner_count: device.tuner_count, - firmware_version: device.firmware_version.to_string(), + firmware_version: device.firmware_version.clone(), auth: String::new(), - id: device.device_udn.to_string(), + id: device.device_udn.clone(), lineup_url: concat_path(&device_url, "lineup.json"), discover_url: concat_path(&device_url, "discover.json"), base_url: device_url, diff --git a/backend/src/api/endpoints/hls_api.rs b/backend/src/api/endpoints/hls_api.rs index 130c890b5..3b4da92f8 100644 --- a/backend/src/api/endpoints/hls_api.rs +++ b/backend/src/api/endpoints/hls_api.rs @@ -61,7 +61,7 @@ pub(in crate::api) async fn handle_hls_stream_request( { Some(provider_cfg) => { let stream_url = get_stream_alternative_url(&url, input, &provider_cfg); - (stream_url, Some(session.token.to_string())) + (stream_url, Some(session.token.clone())) } None => (url, None), } diff --git a/backend/src/api/endpoints/m3u_api.rs b/backend/src/api/endpoints/m3u_api.rs index b03005b09..519958462 100644 --- a/backend/src/api/endpoints/m3u_api.rs +++ b/backend/src/api/endpoints/m3u_api.rs @@ -31,7 +31,7 @@ async fn m3u_api(api_req: &UserApiRequest, app_state: &AppState) -> impl IntoRes // Convert the iterator into a stream of `Bytes` let content_stream = stream::iter(m3u_iter.map(|line| { Ok::(Bytes::from( - [line.to_string().as_bytes(), b"\n"].concat(), + [line.clone().as_bytes(), b"\n"].concat(), )) })); diff --git a/backend/src/api/endpoints/user_api.rs b/backend/src/api/endpoints/user_api.rs index c2f746a53..f4b11195b 100644 --- a/backend/src/api/endpoints/user_api.rs +++ b/backend/src/api/endpoints/user_api.rs @@ -20,7 +20,7 @@ fn get_categories_from_xtream(categories: Option>) - let mut groups: Vec = Vec::new(); if let Some(cats) = categories { for category in cats { - groups.push(category.name.to_string()); + groups.push(category.name.clone()); } } groups @@ -33,8 +33,8 @@ async fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &AppCon let mut unique_groups = HashSet::new(); for (item, _has_next) in iter { if !unique_groups.contains(item.group.as_str()) { - unique_groups.insert(item.group.to_string()); - groups.push(item.group.to_string()); + unique_groups.insert(item.group.clone()); + groups.push(item.group.clone()); } } } diff --git a/backend/src/api/endpoints/v1_api.rs b/backend/src/api/endpoints/v1_api.rs index 411259048..6a9845117 100644 --- a/backend/src/api/endpoints/v1_api.rs +++ b/backend/src/api/endpoints/v1_api.rs @@ -4,7 +4,7 @@ use crate::api::endpoints::user_api::user_api_register; use crate::api::model::AppState; use crate::auth::create_access_token; use crate::auth::validator_admin; -use crate::model::{InputSource, TargetUser}; +use crate::model::{InputSource, ProxyUserCredentials}; use crate::model::{ConfigInput, ConfigInputOptions}; use crate::processing::processor::playlist; use crate::repository::user_repository::store_api_user; @@ -15,9 +15,9 @@ use axum::response::IntoResponse; use log::error; use serde_json::json; use shared::error::TuliproxError; -use shared::model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigDto, InputType, IpCheckDto, PlaylistRequest, PlaylistRequestType, StatusCheck, TargetUserDto, WebplayerUrlRequest}; +use shared::model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigDto, InputType, IpCheckDto, PlaylistRequest, PlaylistRequestType, ProxyUserCredentialsDto, StatusCheck, WebplayerUrlRequest}; use shared::utils::{concat_path_leading_slash, sanitize_sensitive_info}; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap}; use std::sync::Arc; use crate::utils::{prepare_sources_batch, prepare_users}; use crate::utils::request::download_text_content; @@ -45,33 +45,64 @@ fn intern_save_config_main(file_path: &str, backup_dir: &str, cfg: &ConfigDto) - } async fn save_config_api_proxy_user( + method: axum::http::Method, axum::extract::State(app_state): axum::extract::State>, - axum::extract::Json(mut users): axum::extract::Json>, + axum::extract::Path(target_name): axum::extract::Path, + axum::extract::Json(mut credential): axum::extract::Json, ) -> impl axum::response::IntoResponse + Send { - let mut usernames = HashSet::new(); - let mut tokens = HashSet::new(); - for target_user in &mut users { - for credential in &mut target_user.credentials { - credential.prepare(); - if let Err(err) = credential.validate() { - return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response(); - } - if usernames.contains(&credential.username) { - return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))).into_response(); - } - usernames.insert(&credential.username); - if let Some(token) = &credential.token { - if tokens.contains(token) { - return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate token {token}")}))).into_response(); - } - tokens.insert(token); - } - } + credential.prepare(); + if let Err(err) = credential.validate() { + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response(); } + let is_update = method == axum::http::Method::PUT; + if let Some(old_api_proxy) = app_state.app_config.api_proxy.load().clone() { let mut api_proxy = (*old_api_proxy).clone(); - api_proxy.user = users.iter().map(TargetUser::from).collect(); + let mut target_found = false; + for target_user in &api_proxy.user { + if target_user.target == target_name { + target_found = true; + } + for user in &target_user.credentials { + if !is_update && user.username == credential.username { + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))).into_response(); + } + if let (Some(u), Some(c)) = (&user.token, &credential.token) { + if u == c && user.username != credential.username { + return ( + axum::http::StatusCode::BAD_REQUEST, + axum::Json(json!({"error": format!("Duplicate token {c}")})) + ).into_response(); + } + } + } + } + + if is_update && !target_found { + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Target not found {target_name}")}))).into_response(); + } + + for target in &mut api_proxy.user { + if target.target == target_name { + if is_update { + let mut updated = false; + for user in &mut target.credentials { + if user.username == credential.username { + *user = ProxyUserCredentials::from(&credential); + updated = true; + break; + } + } + if !updated { + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("User {} not found in target {target_name}", credential.username)}))).into_response(); + } + } else { + target.credentials.push(ProxyUserCredentials::from(&credential)); + } + } + } + let new_api_proxy = Arc::new(api_proxy); app_state.app_config.api_proxy.store(Some(Arc::clone(&new_api_proxy))); @@ -91,6 +122,45 @@ async fn save_config_api_proxy_user( axum::http::StatusCode::OK.into_response() } +async fn delete_config_api_proxy_user( + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Path((target_name, username)): axum::extract::Path<(String, String)>, +) -> impl axum::response::IntoResponse + Send { + if let Some(old_api_proxy) = app_state.app_config.api_proxy.load().clone() { + let mut api_proxy = (*old_api_proxy).clone(); + let mut modified = false; + + for target_user in &mut api_proxy.user { + if target_user.target == target_name { + let count = target_user.credentials.len(); + target_user.credentials.retain(|user| user.username != username); + modified = count != target_user.credentials.len(); + break; + } + } + if modified { + let new_api_proxy = Arc::new(api_proxy); + app_state.app_config.api_proxy.store(Some(Arc::clone(&new_api_proxy))); + + if new_api_proxy.use_user_db { + if let Err(err) = store_api_user(&app_state.app_config, &new_api_proxy.user) { + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); + } + } else { + let config = app_state.app_config.config.load(); + let backup_dir = config.get_backup_dir(); + let paths = app_state.app_config.paths.load(); + if let Some(err) = intern_save_config_api_proxy(backup_dir.as_ref(), &ApiProxyConfigDto::from(&*new_api_proxy), paths.api_proxy_file_path.as_str()) { + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); + } + } + } else { + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("User not found {username} in target {target_name}")}))).into_response(); + } + } + axum::http::StatusCode::OK.into_response() +} + async fn save_config_main( axum::extract::State(app_state): axum::extract::State>, axum::extract::Json(cfg): axum::extract::Json, @@ -361,7 +431,9 @@ pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc, web_ui_ .route("/config", axum::routing::get(config)) .route("/config/batchContent/{input_id}", axum::routing::get(config_batch_content)) .route("/config/main", axum::routing::post(save_config_main)) - .route("/config/user", axum::routing::post(save_config_api_proxy_user)) + .route("/user/{target}", axum::routing::post(save_config_api_proxy_user)) + .route("/user/{target}", axum::routing::put(save_config_api_proxy_user)) + .route("/user/{target}/{username}", axum::routing::delete(delete_config_api_proxy_user)) .route("/config/apiproxy", axum::routing::post(save_config_api_proxy_config)) .route("/playlist/webplayer", axum::routing::post(playlist_webplayer)) .route("/playlist/update", axum::routing::post(playlist_update)) diff --git a/backend/src/api/endpoints/web_index.rs b/backend/src/api/endpoints/web_index.rs index a1bdabc6f..a86a6ba5e 100644 --- a/backend/src/api/endpoints/web_index.rs +++ b/backend/src/api/endpoints/web_index.rs @@ -48,7 +48,7 @@ async fn token( return axum::Json( TokenResponse { token, - username: req.username.to_string(), + username: req.username.clone(), }).into_response(); } } @@ -60,7 +60,7 @@ async fn token( return axum::Json( TokenResponse { token, - username: req.username.to_string(), + username: req.username.clone(), }).into_response(); } } @@ -97,7 +97,7 @@ async fn token_refresh( return axum::Json( TokenResponse { token, - username: username.to_string(), + username: username.clone(), }).into_response(); } } diff --git a/backend/src/api/endpoints/xtream_api.rs b/backend/src/api/endpoints/xtream_api.rs index ae910b32b..aea7450dc 100644 --- a/backend/src/api/endpoints/xtream_api.rs +++ b/backend/src/api/endpoints/xtream_api.rs @@ -889,9 +889,9 @@ async fn xtream_player_api_timeshift_stream( ); let action_path = format!("{duration}/{start}"); - api_req.username = username.to_string(); - api_req.password = password.to_string(); - api_req.stream_id = stream_id.to_string(); + api_req.username.clone_from(&username); + api_req.password.clone_from(&password); + api_req.stream_id.clone_from(&stream_id); xtream_player_api_stream( &fingerprint, @@ -1503,7 +1503,7 @@ fn xtream_create_content_stream( Ok::(Bytes::from(if has_next { format!("{line},") } else { - line.to_string() + line.clone() })) })) .chain(stream::once(async { diff --git a/backend/src/api/main_api.rs b/backend/src/api/main_api.rs index 67b4e5dc3..135e1a4f4 100644 --- a/backend/src/api/main_api.rs +++ b/backend/src/api/main_api.rs @@ -149,7 +149,7 @@ pub(in crate::api) fn start_hdhomerun( cancel_token: &CancellationToken, ) { let config = app_config.config.load(); - let host = config.api.host.to_string(); + let host = config.api.host.clone(); let guard = app_config.hdhomerun.load(); if let Some(hdhomerun) = &*guard { if hdhomerun.enabled { @@ -213,7 +213,7 @@ pub async fn start_server( ) -> futures::io::Result<()> { let mut infos = Vec::new(); let cfg = app_config.config.load(); - let host = cfg.api.host.to_string(); + let host = cfg.api.host.clone(); let port = cfg.api.port; let web_ui_enabled = cfg.web_ui.as_ref().is_some_and(|c| c.enabled); let web_dir_path = match get_web_dir_path(web_ui_enabled, cfg.api.web_root.as_str()) { diff --git a/backend/src/api/model/streams/provider_stream.rs b/backend/src/api/model/streams/provider_stream.rs index 447d8020f..a1d506aaa 100644 --- a/backend/src/api/model/streams/provider_stream.rs +++ b/backend/src/api/model/streams/provider_stream.rs @@ -22,7 +22,7 @@ fn create_video_stream(video_buffer: Option<&TransportStreamBuffer>, headers: &[ trace!("{log_message}"); let mut response_headers: Vec<(String, String)> = headers.iter() .filter(|(key, _)| !(key.eq("content-type") || key.eq("content-length") || key.contains("range"))) - .map(|(key, value)| (key.to_string(), value.to_string())).collect(); + .map(|(key, value)| (key.clone(), value.clone())).collect(); response_headers.push(("content-type".to_string(), "video/mp2t".to_string())); (Some(Box::pin(CustomVideoStream::new(video.clone()))), Some((response_headers, StatusCode::OK, None))) } else { diff --git a/backend/src/api/model/xtream.rs b/backend/src/api/model/xtream.rs index e9f18ef92..553e13768 100644 --- a/backend/src/api/model/xtream.rs +++ b/backend/src/api/model/xtream.rs @@ -87,9 +87,9 @@ impl XtreamAuthorizationResponse { exp_date, is_trial, max_connections, - message: server_info.message.to_string(), - password: user.password.to_string(), - username: user.username.to_string(), + message: server_info.message.clone(), + password: user.password.clone(), + username: user.username.clone(), status: user_status.to_string(), }, server_info: XtreamServerInfoResponse { @@ -98,7 +98,7 @@ impl XtreamAuthorizationResponse { https_port: if server_info.protocol == "https" { server_info.port.as_ref().map_or("443", |v| v.as_str()).to_string() } else { String::from("443") }, server_protocol: server_info.protocol.clone(), rtmp_port: String::new(), - timezone: server_info.timezone.to_string(), + timezone: server_info.timezone.clone(), timestamp_now: now.timestamp(), time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(), }, diff --git a/backend/src/api/scheduler.rs b/backend/src/api/scheduler.rs index 508de9c5b..ba5afa8a1 100644 --- a/backend/src/api/scheduler.rs +++ b/backend/src/api/scheduler.rs @@ -36,7 +36,7 @@ pub fn exec_scheduler(client: &Arc, app_state: &Arc, vec![] }; for schedule in schedules { - let expression = schedule.schedule.to_string(); + let expression = schedule.schedule.clone(); let exec_targets = get_process_targets(cfg, targets, schedule.targets.as_ref()); let app_state_clone = Arc::clone(app_state); let http_client = Arc::clone(client); diff --git a/backend/src/messaging.rs b/backend/src/messaging.rs index a2a961f36..e0a528079 100644 --- a/backend/src/messaging.rs +++ b/backend/src/messaging.rs @@ -49,7 +49,7 @@ fn send_pushover_message(client: &Arc, msg: &str, messaging: &M .append_pair("message", msg) .finish(); let the_client = Arc::clone(client); - let pushover_url = pushover.url.to_string(); + let pushover_url = pushover.url.clone(); tokio::spawn(async move { match the_client .post(pushover_url) diff --git a/backend/src/model/config/api.rs b/backend/src/model/config/api.rs index d41f555b0..c837417bc 100644 --- a/backend/src/model/config/api.rs +++ b/backend/src/model/config/api.rs @@ -12,9 +12,9 @@ macros::from_impl!(ConfigApi); impl From<&ConfigApiDto> for ConfigApi { fn from(dto: &ConfigApiDto) -> Self { Self { - host:dto.host.to_string(), + host:dto.host.clone(), port: dto.port, - web_root: dto.web_root.to_string(), + web_root: dto.web_root.clone(), } } } @@ -22,9 +22,9 @@ impl From<&ConfigApiDto> for ConfigApi { impl From<&ConfigApi> for ConfigApiDto { fn from(instance: &ConfigApi) -> Self { Self { - host: instance.host.to_string(), + host: instance.host.clone(), port: instance.port, - web_root: instance.web_root.to_string(), + web_root: instance.web_root.clone(), } } } \ No newline at end of file diff --git a/backend/src/model/config/api_proxy.rs b/backend/src/model/config/api_proxy.rs index ed2489de9..89e0849e5 100644 --- a/backend/src/model/config/api_proxy.rs +++ b/backend/src/model/config/api_proxy.rs @@ -28,12 +28,12 @@ macros::from_impl!(ApiProxyServerInfo); impl From<&ApiProxyServerInfoDto> for ApiProxyServerInfo { fn from(dto: &ApiProxyServerInfoDto) -> Self { Self { - name: dto.name.to_string(), - protocol: dto.protocol.to_string(), - host: dto.host.to_string(), + name: dto.name.clone(), + protocol: dto.protocol.clone(), + host: dto.host.clone(), port: dto.port.clone(), - timezone: dto.timezone.to_string(), - message: dto.message.to_string(), + timezone: dto.timezone.clone(), + message: dto.message.clone(), path: dto.path.clone(), } } @@ -42,12 +42,12 @@ impl From<&ApiProxyServerInfoDto> for ApiProxyServerInfo { impl From<&ApiProxyServerInfo> for ApiProxyServerInfoDto { fn from(instance: &ApiProxyServerInfo) -> Self { Self { - name: instance.name.to_string(), - protocol: instance.protocol.to_string(), - host: instance.host.to_string(), + name: instance.name.clone(), + protocol: instance.protocol.clone(), + host: instance.host.clone(), port: instance.port.clone(), - timezone: instance.timezone.to_string(), - message: instance.message.to_string(), + timezone: instance.timezone.clone(), + message: instance.message.clone(), path: instance.path.clone(), } } diff --git a/backend/src/model/config/api_user.rs b/backend/src/model/config/api_user.rs index 372b25a11..dba5bb920 100644 --- a/backend/src/model/config/api_user.rs +++ b/backend/src/model/config/api_user.rs @@ -27,8 +27,8 @@ macros::from_impl!(ProxyUserCredentials); impl From<&ProxyUserCredentialsDto> for ProxyUserCredentials { fn from(dto: &ProxyUserCredentialsDto) -> Self { Self { - username: dto.username.to_string(), - password: dto.password.to_string(), + username: dto.username.clone(), + password: dto.password.clone(), token: dto.token.clone(), proxy: dto.proxy, server: dto.server.clone(), @@ -46,8 +46,8 @@ impl From<&ProxyUserCredentialsDto> for ProxyUserCredentials { impl From<&ProxyUserCredentials> for ProxyUserCredentialsDto { fn from(instance: &ProxyUserCredentials) -> Self { Self { - username: instance.username.to_string(), - password: instance.password.to_string(), + username: instance.username.clone(), + password: instance.password.clone(), token: instance.token.clone(), proxy: instance.proxy, server: instance.server.clone(), @@ -122,7 +122,7 @@ macros::from_impl!(TargetUser); impl From<&TargetUserDto> for TargetUser { fn from(dto: &TargetUserDto) -> Self { Self { - target: dto.target.to_string(), + target: dto.target.clone(), credentials: dto.credentials.iter().map(Into::into).collect(), } } @@ -131,7 +131,7 @@ impl From<&TargetUserDto> for TargetUser { impl From<&TargetUser> for TargetUserDto { fn from(instance: &TargetUser) -> Self { Self { - target: instance.target.to_string(), + target: instance.target.clone(), credentials: instance.credentials.iter().map(Into::into).collect(), } } diff --git a/backend/src/model/config/base.rs b/backend/src/model/config/base.rs index c2c472715..53e49a31e 100644 --- a/backend/src/model/config/base.rs +++ b/backend/src/model/config/base.rs @@ -17,8 +17,8 @@ fn create_directories(cfg: &Config, temp_path: &Path) { Some(cfg.working_dir.clone()), cfg.backup_dir.clone(), cfg.user_config_dir.clone(), - cfg.video.as_ref().and_then(|v| v.download.as_ref()).map(|d| d.directory.to_string()), - cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref().and_then(|c| if c.enabled { Some(c.dir.to_string()) } else { None })) + cfg.video.as_ref().and_then(|v| v.download.as_ref()).map(|d| d.directory.clone()), + cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref().and_then(|c| if c.enabled { Some(c.dir.clone()) } else { None })) ]; let mut paths: Vec = paths_strings.iter() @@ -112,7 +112,7 @@ impl Config { self.hdhomerun.as_ref().map(|hdhr| HdHomeRunDeviceOverview { enabled: hdhr.enabled, - devices: hdhr.devices.iter().map(|d| d.name.to_string()).collect::>(), + devices: hdhr.devices.iter().map(|d| d.name.clone()).collect::>(), }) } } @@ -124,7 +124,7 @@ impl From<&ConfigDto> for Config { Config { threads: dto.threads, api: ConfigApi::from(&dto.api), - working_dir: dto.working_dir.to_string(), + working_dir: dto.working_dir.clone(), backup_dir: dto.backup_dir.clone(), user_config_dir: dto.user_config_dir.clone(), mapping_path: dto.mapping_path.clone(), diff --git a/backend/src/model/config/cache.rs b/backend/src/model/config/cache.rs index d0fa721e7..5690ce4b0 100644 --- a/backend/src/model/config/cache.rs +++ b/backend/src/model/config/cache.rs @@ -28,7 +28,7 @@ impl From<&CacheConfig> for CacheConfigDto { Self { enabled: instance.enabled, // Dto prepare should have set the right path - dir: Some(instance.dir.to_string()), + dir: Some(instance.dir.clone()), size: instance.size_str.clone(), } } diff --git a/backend/src/model/config/epg.rs b/backend/src/model/config/epg.rs index 0ab5527ec..bf31cdf11 100644 --- a/backend/src/model/config/epg.rs +++ b/backend/src/model/config/epg.rs @@ -12,7 +12,7 @@ macros::from_impl!(EpgSource); impl From<&EpgSourceDto> for EpgSource { fn from(dto: &EpgSourceDto) -> Self { Self { - url: dto.url.to_string(), + url: dto.url.clone(), priority: dto.priority, logo_override: dto.logo_override, } diff --git a/backend/src/model/config/hdhomerun.rs b/backend/src/model/config/hdhomerun.rs index 7ea3a0509..03d061978 100644 --- a/backend/src/model/config/hdhomerun.rs +++ b/backend/src/model/config/hdhomerun.rs @@ -23,15 +23,15 @@ macros::from_impl!(HdHomeRunDeviceConfig); impl From<&HdHomeRunDeviceConfigDto> for HdHomeRunDeviceConfig { fn from(dto: &HdHomeRunDeviceConfigDto) -> Self { Self { - friendly_name: dto.friendly_name.to_string(), - manufacturer: dto.manufacturer.to_string(), - model_name: dto.model_name.to_string(), - model_number: dto.model_number.to_string(), - firmware_name: dto.firmware_name.to_string(), - firmware_version: dto.firmware_version.to_string(), - device_type: dto.device_type.to_string(), - device_udn: dto.device_udn.to_string(), - name: dto.name.to_string(), + friendly_name: dto.friendly_name.clone(), + manufacturer: dto.manufacturer.clone(), + model_name: dto.model_name.clone(), + model_number: dto.model_number.clone(), + firmware_name: dto.firmware_name.clone(), + firmware_version: dto.firmware_version.clone(), + device_type: dto.device_type.clone(), + device_udn: dto.device_udn.clone(), + name: dto.name.clone(), port: dto.port, tuner_count: dto.tuner_count, t_username: String::new(), diff --git a/backend/src/model/config/input.rs b/backend/src/model/config/input.rs index bf06096a0..5bf36e346 100644 --- a/backend/src/model/config/input.rs +++ b/backend/src/model/config/input.rs @@ -106,8 +106,8 @@ impl From<&ConfigInputAliasDto> for ConfigInputAlias { fn from(dto: &ConfigInputAliasDto) -> Self { Self { id: dto.id, - name: dto.name.to_string(), - url: get_base_url_from_str(&dto.url).map_or_else(|| dto.url.to_string(), |base_url| base_url), + name: dto.name.clone(), + url: get_base_url_from_str(&dto.url).map_or_else(|| dto.url.clone(), |base_url| base_url), username: dto.username.clone(), password: dto.password.clone(), priority: dto.priority, @@ -191,7 +191,7 @@ impl ConfigInput { self.max_connections = first.max_connections; self.priority = first.priority; if self.name.is_empty() { - self.name = first.name.to_string(); + self.name.clone_from(&first.name); } } } @@ -209,7 +209,7 @@ impl ConfigInput { name: alias.name.clone(), input_type: self.input_type, headers: self.headers.clone(), - url: alias.url.to_string(), + url: alias.url.clone(), epg: self.epg.clone(), username: alias.username.clone(), password: alias.password.clone(), @@ -231,7 +231,7 @@ impl From<&ConfigInputDto> for ConfigInput { fn from(dto: &ConfigInputDto) -> Self { Self { id: dto.id, - name: dto.name.to_string(), + name: dto.name.clone(), input_type: dto.input_type, headers: dto.headers.clone(), url: dto.url.clone(), //get_base_url_from_str(&dto.url).map_or_else(|| dto.url.to_string(), |base_url| base_url), diff --git a/backend/src/model/config/messaging.rs b/backend/src/model/config/messaging.rs index 9dbff561b..f6812495e 100644 --- a/backend/src/model/config/messaging.rs +++ b/backend/src/model/config/messaging.rs @@ -11,7 +11,7 @@ macros::from_impl!(TelegramMessagingConfig); impl From<&TelegramMessagingConfigDto> for TelegramMessagingConfig { fn from(dto: &TelegramMessagingConfigDto) -> Self { Self { - bot_token: dto.bot_token.to_string(), + bot_token: dto.bot_token.clone(), chat_ids: dto.chat_ids.clone(), } } @@ -20,7 +20,7 @@ impl From<&TelegramMessagingConfigDto> for TelegramMessagingConfig { impl From<&TelegramMessagingConfig> for TelegramMessagingConfigDto { fn from(instance: &TelegramMessagingConfig) -> Self { Self { - bot_token: instance.bot_token.to_string(), + bot_token: instance.bot_token.clone(), chat_ids: instance.chat_ids.clone(), } } @@ -35,7 +35,7 @@ macros::from_impl!(RestMessagingConfig); impl From<&RestMessagingConfigDto> for RestMessagingConfig { fn from(dto: &RestMessagingConfigDto) -> Self { Self { - url: dto.url.to_string(), + url: dto.url.clone(), } } } @@ -43,7 +43,7 @@ impl From<&RestMessagingConfigDto> for RestMessagingConfig { impl From<&RestMessagingConfig> for RestMessagingConfigDto { fn from(instance: &RestMessagingConfig) -> Self { Self { - url: instance.url.to_string(), + url: instance.url.clone(), } } } @@ -60,8 +60,8 @@ impl From<&PushoverMessagingConfigDto> for PushoverMessagingConfig { fn from(dto: &PushoverMessagingConfigDto) -> Self { Self { url: dto.url.as_ref().map_or_else(|| String::from("https://api.pushover.net/1/messages.json"), ToString::to_string), - token: dto.token.to_string(), - user: dto.user.to_string(), + token: dto.token.clone(), + user: dto.user.clone(), } } } @@ -69,9 +69,9 @@ impl From<&PushoverMessagingConfigDto> for PushoverMessagingConfig { impl From<&PushoverMessagingConfig> for PushoverMessagingConfigDto { fn from(instance: &PushoverMessagingConfig) -> Self { Self { - url: Some(instance.url.to_string()), - token: instance.token.to_string(), - user: instance.user.to_string(), + url: Some(instance.url.clone()), + token: instance.token.clone(), + user: instance.user.clone(), } } } diff --git a/backend/src/model/config/proxy.rs b/backend/src/model/config/proxy.rs index 9a24f6d88..432db99f9 100644 --- a/backend/src/model/config/proxy.rs +++ b/backend/src/model/config/proxy.rs @@ -12,7 +12,7 @@ macros::from_impl!(ProxyConfig); impl From<&ProxyConfigDto> for ProxyConfig { fn from(dto: &ProxyConfigDto) -> Self { Self { - url: dto.url.to_string(), + url: dto.url.clone(), username: dto.username.clone(), password: dto.password.clone(), } @@ -22,7 +22,7 @@ impl From<&ProxyConfigDto> for ProxyConfig { impl From<&ProxyConfig> for ProxyConfigDto { fn from(dto: &ProxyConfig) -> Self { Self { - url: dto.url.to_string(), + url: dto.url.clone(), username: dto.username.clone(), password: dto.password.clone(), } diff --git a/backend/src/model/config/rename.rs b/backend/src/model/config/rename.rs index 32990eac7..2068eb08d 100644 --- a/backend/src/model/config/rename.rs +++ b/backend/src/model/config/rename.rs @@ -13,7 +13,7 @@ impl From<&ConfigRenameDto> for ConfigRename { fn from(dto: &ConfigRenameDto) -> Self { Self { field: dto.field, - new_name: dto.new_name.to_string(), + new_name: dto.new_name.clone(), pattern: regex::Regex::new(&dto.pattern).unwrap() } } @@ -23,7 +23,7 @@ impl From<&ConfigRename> for ConfigRenameDto { fn from(instance: &ConfigRename) -> Self { Self { field: instance.field, - new_name: instance.new_name.to_string(), + new_name: instance.new_name.clone(), pattern: instance.pattern.to_string() } } diff --git a/backend/src/model/config/schedule.rs b/backend/src/model/config/schedule.rs index 85a9d2a66..f3e6b0e34 100644 --- a/backend/src/model/config/schedule.rs +++ b/backend/src/model/config/schedule.rs @@ -11,7 +11,7 @@ macros::from_impl!(ScheduleConfig); impl From<&ScheduleConfigDto> for ScheduleConfig { fn from(dto: &ScheduleConfigDto) -> Self { Self { - schedule: dto.schedule.to_string(), + schedule: dto.schedule.clone(), targets: dto.targets.clone(), } } @@ -19,7 +19,7 @@ impl From<&ScheduleConfigDto> for ScheduleConfig { impl From<&ScheduleConfig> for ScheduleConfigDto { fn from(dto: &ScheduleConfig) -> Self { Self { - schedule: dto.schedule.to_string(), + schedule: dto.schedule.clone(), targets: dto.targets.clone(), } } diff --git a/backend/src/model/config/source.rs b/backend/src/model/config/source.rs index 7c2aba18f..40cdf39cf 100644 --- a/backend/src/model/config/source.rs +++ b/backend/src/model/config/source.rs @@ -107,7 +107,7 @@ impl SourcesConfig { let key = user_target.to_lowercase(); if target.name.eq_ignore_ascii_case(key.as_str()) { targets.push(target.id); - target_names.push(target.name.to_string()); + target_names.push(target.name.clone()); target_added = true; if let Some(value) = check_targets.get(key.as_str()) { check_targets.insert(key, value + 1); @@ -120,7 +120,7 @@ impl SourcesConfig { } } - let missing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v == 0).map(|(k, _)| k.to_string()).collect(); + let missing_targets: Vec = check_targets.iter().filter(|&(_, v)| *v == 0).map(|(k, _)| k.clone()).collect(); if !missing_targets.is_empty() { return create_tuliprox_error_result!(TuliproxErrorKind::Info, "No target found for {}", missing_targets.join(", ")); } diff --git a/backend/src/model/config/target.rs b/backend/src/model/config/target.rs index e277c5390..4e55890e5 100644 --- a/backend/src/model/config/target.rs +++ b/backend/src/model/config/target.rs @@ -116,7 +116,7 @@ macros::from_impl!(StrmTargetOutput); impl From<&StrmTargetOutputDto> for StrmTargetOutput { fn from(dto: &StrmTargetOutputDto) -> Self { Self { - directory: dto.directory.to_string(), + directory: dto.directory.clone(), username: dto.username.clone(), style: dto.style, flat: dto.flat, @@ -129,7 +129,7 @@ impl From<&StrmTargetOutputDto> for StrmTargetOutput { impl From<&StrmTargetOutput> for StrmTargetOutputDto { fn from(instance: &StrmTargetOutput) -> Self { Self { - directory: instance.directory.to_string(), + directory: instance.directory.clone(), username: instance.username.clone(), style: instance.style, flat: instance.flat, @@ -151,8 +151,8 @@ macros::from_impl!(HdHomeRunTargetOutput); impl From<&HdHomeRunTargetOutputDto> for HdHomeRunTargetOutput { fn from(dto: &HdHomeRunTargetOutputDto) -> Self { Self { - device: dto.device.to_string(), - username: dto.username.to_string(), + device: dto.device.clone(), + username: dto.username.clone(), use_output: dto.use_output, } } @@ -160,8 +160,8 @@ impl From<&HdHomeRunTargetOutputDto> for HdHomeRunTargetOutput { impl From<&HdHomeRunTargetOutput> for HdHomeRunTargetOutputDto { fn from(instance: &HdHomeRunTargetOutput) -> Self { Self { - device: instance.device.to_string(), - username: instance.username.to_string(), + device: instance.device.clone(), + username: instance.username.clone(), use_output: instance.use_output, } } @@ -279,7 +279,7 @@ impl From<&ConfigTargetDto> for ConfigTarget { Self { id: dto.id, enabled: dto.enabled, - name: dto.name.to_string(), + name: dto.name.clone(), options: dto.options.clone(), sort: dto.sort.as_ref().map(Into::into), filter: dto.t_filter.as_ref().unwrap().clone(), diff --git a/backend/src/model/config/trakt.rs b/backend/src/model/config/trakt.rs index 93951b988..289a09b34 100644 --- a/backend/src/model/config/trakt.rs +++ b/backend/src/model/config/trakt.rs @@ -13,9 +13,9 @@ macros::from_impl!(TraktApiConfig); impl From<&TraktApiConfigDto> for TraktApiConfig { fn from(dto: &TraktApiConfigDto) -> Self { Self { - key: dto.key.to_string(), - version: dto.version.to_string(), - url: dto.url.to_string(), + key: dto.key.clone(), + version: dto.version.clone(), + url: dto.url.clone(), } } } @@ -23,9 +23,9 @@ impl From<&TraktApiConfigDto> for TraktApiConfig { impl From<&TraktApiConfig> for TraktApiConfigDto { fn from(instance: &TraktApiConfig) -> Self { Self { - key: instance.key.to_string(), - version: instance.version.to_string(), - url: instance.url.to_string(), + key: instance.key.clone(), + version: instance.version.clone(), + url: instance.url.clone(), } } } @@ -43,9 +43,9 @@ macros::from_impl!(TraktListConfig); impl From<&TraktListConfigDto> for TraktListConfig { fn from(dto: &TraktListConfigDto) -> Self { Self { - user: dto.user.to_string(), - list_slug: dto.list_slug.to_string(), - category_name: dto.category_name.to_string(), + user: dto.user.clone(), + list_slug: dto.list_slug.clone(), + category_name: dto.category_name.clone(), content_type: dto.content_type, fuzzy_match_threshold: dto.fuzzy_match_threshold } @@ -55,9 +55,9 @@ impl From<&TraktListConfigDto> for TraktListConfig { impl From<&TraktListConfig> for TraktListConfigDto { fn from(instance: &TraktListConfig) -> Self { Self { - user: instance.user.to_string(), - list_slug: instance.list_slug.to_string(), - category_name: instance.category_name.to_string(), + user: instance.user.clone(), + list_slug: instance.list_slug.clone(), + category_name: instance.category_name.clone(), content_type: instance.content_type, fuzzy_match_threshold: instance.fuzzy_match_threshold } diff --git a/backend/src/model/mapping.rs b/backend/src/model/mapping.rs index 788ea4ea2..dd23f3af1 100644 --- a/backend/src/model/mapping.rs +++ b/backend/src/model/mapping.rs @@ -17,8 +17,8 @@ macros::from_impl!(Mapper); impl From<&MapperDto> for Mapper { fn from(dto: &MapperDto) -> Self { Self { - filter: dto.filter.to_string(), - script: dto.script.to_string(), + filter: dto.filter.clone(), + script: dto.script.clone(), t_filter: dto.t_filter.clone(), t_script: dto.t_script.clone(), } @@ -41,7 +41,7 @@ pub struct Mapping { impl From<&MappingDto> for Mapping { fn from(dto: &MappingDto) -> Self { Self { - id: dto.id.to_string(), + id: dto.id.clone(), match_as_ascii: dto.match_as_ascii, mapper: dto.mapper.as_ref().map(|l| l.iter().map(Mapper::from).collect()), counter: dto.counter.clone(), diff --git a/backend/src/model/xtream.rs b/backend/src/model/xtream.rs index 4fcb03a7e..d265dff1c 100644 --- a/backend/src/model/xtream.rs +++ b/backend/src/model/xtream.rs @@ -322,13 +322,13 @@ impl XtreamSeriesEpisode { Self { id: info_episode.get_id(), episode_num: info_episode.episode_num, - title: info_episode.title.to_string(), - container_extension: info_episode.container_extension.to_string(), - custom_sid: info_episode.custom_sid.to_string(), - added: info_episode.added.to_string(), + title: info_episode.title.clone(), + container_extension: info_episode.container_extension.clone(), + custom_sid: info_episode.custom_sid.clone(), + added: info_episode.added.clone(), season: info_episode.season, tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb_id).unwrap_or(0), - direct_source: info_episode.direct_source.to_string(), + direct_source: info_episode.direct_source.clone(), } } } @@ -428,7 +428,7 @@ fn append_prepared_series_properties(add_props: Option<&Map>, doc Some(value) => { document.insert("rating".to_string(), match value { Value::Number(val) => Value::String(format!("{:.0}", val.as_f64().unwrap_or(0f64))), - Value::String(val) => Value::String(val.to_string()), + Value::String(val) => Value::String(val.clone()), _ => Value::String("0".to_string()), }); } @@ -499,7 +499,7 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti if let Some(ref add_props) = props { for (field_name, field_value) in add_props { if !document.contains_key(field_name) { - document.insert(field_name.to_string(), field_value.to_owned()); + document.insert(field_name.clone(), field_value.to_owned()); } } } diff --git a/backend/src/processing/parser/m3u.rs b/backend/src/processing/parser/m3u.rs index d66790ba1..5f3bcf4a4 100644 --- a/backend/src/processing/parser/m3u.rs +++ b/backend/src/processing/parser/m3u.rs @@ -38,7 +38,7 @@ fn get_value(stack: &mut String, it: &mut std::str::Chars) -> String { stack.push(c); } - let result = (*stack).to_string(); + let result = (*stack).clone(); stack.clear(); result } @@ -67,7 +67,7 @@ fn token_till(stack: &mut String, it: &mut std::str::Chars, stop_char: char, sta if stack.is_empty() { None } else { - let result = (*stack).to_string(); + let result = (*stack).clone(); stack.clear(); Some(result) } @@ -161,7 +161,7 @@ fn process_header(input_name: &str, video_suffixes: &[&str], content: &str, url: plih.id = chanid; } } else { - plih.epg_channel_id = Some(plih.id.to_string()); + plih.epg_channel_id = Some(plih.id.clone()); if let Some(pid) = provider_id { plih.id = pid; } @@ -266,7 +266,7 @@ where if let Some((cluster, group_title)) = channel.map(|pli| (pli.header.xtream_cluster, &pli.header.group)) { grp_id += 1; - Some(PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: group_title.to_string(), channels }) + Some(PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: group_title.clone(), channels }) } else { None } diff --git a/backend/src/processing/parser/xmltv.rs b/backend/src/processing/parser/xmltv.rs index a99e9b124..3963512f5 100644 --- a/backend/src/processing/parser/xmltv.rs +++ b/backend/src/processing/parser/xmltv.rs @@ -134,7 +134,7 @@ impl TVGuide { EPG_TAG_ICON => { if let Some(src) = child.get_attribute_value("src") { if !src.is_empty() { - tag.icon = XmlTagIcon::Src(src.to_string()); + tag.icon = XmlTagIcon::Src(src.clone()); child.icon = XmlTagIcon::Exists; } } @@ -207,7 +207,7 @@ impl TVGuide { let score = min(100, (jw * 100.0).round() as u16); score >= best_match_threshold }) { - return (true, Some(good_enough.to_string())); + return (true, Some(good_enough.clone())); } } } @@ -436,7 +436,7 @@ fn collect_tag_attributes(e: &BytesStart, is_channel: bool, is_program: bool) -> if value.is_empty() { None } else if (is_channel && key == EPG_ATTRIB_ID) || (is_program && key == EPG_ATTRIB_CHANNEL) { - Some((key, value.to_lowercase().to_string())) + Some((key, value.to_lowercase().clone())) } else { Some((key, value.to_string())) } diff --git a/backend/src/processing/parser/xtream.rs b/backend/src/processing/parser/xtream.rs index bbd7d64c9..94876329e 100644 --- a/backend/src/processing/parser/xtream.rs +++ b/backend/src/processing/parser/xtream.rs @@ -49,10 +49,10 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s (episode.clone(), PlaylistItem { header: PlaylistItemHeader { - id: episode.id.to_string(), + id: episode.id.clone(), uuid: generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url), name: series_name.to_string(), - logo: episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string()), + logo: episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.clone()), group: group_title.to_string(), title: episode.title.clone(), url: episode_url.to_string(), @@ -60,7 +60,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s xtream_cluster: XtreamCluster::Series, additional_properties: episode.get_additional_properties(&series_info), category_id: 0, - input_name: input.name.to_string(), + input_name: input.name.clone(), ..Default::default() } }) @@ -104,7 +104,7 @@ pub fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &st stream.container_extension.as_ref().map(std::string::ToString::to_string).as_ref(), live_stream_use_prefix, live_stream_without_extension) } else { - stream.direct_source.to_string() + stream.direct_source.clone() } } @@ -114,7 +114,7 @@ pub fn parse_xtream(input: &ConfigInput, streams: &Value) -> Result>, TuliproxError> { match map_to_xtream_category(categories) { Ok(xtream_categories) => { - let input_name = Arc::new(input.name.to_string()); + let input_name = Arc::new(input.name.clone()); let url = input.url.as_str(); let username = input.username.as_ref().map_or("", |v| v); let password = input.password.as_ref().map_or("", |v| v); @@ -123,7 +123,7 @@ pub fn parse_xtream(input: &ConfigInput, Ok(mut xtream_streams) => { let mut group_map: HashMap = xtream_categories.into_iter().map(|category| - (category.category_id.to_string(), category) + (category.category_id.clone(), category) ).collect(); let mut unknown_grp = XtreamCategory { category_id: "0".to_string(), @@ -152,11 +152,11 @@ pub fn parse_xtream(input: &ConfigInput, header: PlaylistItemHeader { id: stream.get_stream_id().to_string(), uuid: generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url), - name: stream.name.to_string(), - logo: stream.stream_icon.to_string(), - group: category_name.to_string(), - title: stream.name.to_string(), - url: stream_url.to_string(), + name: stream.name.clone(), + logo: stream.stream_icon.clone(), + group: category_name.clone(), + title: stream.name.clone(), + url: stream_url.clone(), epg_channel_id: stream.epg_channel_id.clone(), item_type, xtream_cluster, @@ -178,7 +178,7 @@ pub fn parse_xtream(input: &ConfigInput, PlaylistGroup { id: category.category_id.parse::().unwrap_or(0), xtream_cluster, - title: category.category_name.to_string(), + title: category.category_name.clone(), channels: category.channels.clone(), } }).collect())) diff --git a/backend/src/processing/playlist_watch.rs b/backend/src/processing/playlist_watch.rs index 6ef0a6a90..babd64aa3 100644 --- a/backend/src/processing/playlist_watch.rs +++ b/backend/src/processing/playlist_watch.rs @@ -12,7 +12,7 @@ pub fn process_group_watch(client: &Arc, cfg: &Config, target_n let mut new_tree = BTreeSet::new(); pl.channels.iter().for_each(|chan| { let header = &chan.header; - let title = if header.title.is_empty() { header.title.to_string() } else { header.name.to_string() }; + let title = if header.title.is_empty() { header.title.clone() } else { header.name.clone() }; new_tree.insert(title); }); diff --git a/backend/src/processing/processor/epg.rs b/backend/src/processing/processor/epg.rs index a24ff21e1..468730d14 100644 --- a/backend/src/processing/processor/epg.rs +++ b/backend/src/processing/processor/epg.rs @@ -75,9 +75,9 @@ impl EpgIdCache<'_> { let normalized = self.normalize(key); let phonetic = self.phonetic(&normalized); - self.normalized.insert(normalized.to_string(), None); + self.normalized.insert(normalized.clone(), None); self.phonetics - .entry(phonetic.to_string()) + .entry(phonetic.clone()) .or_default() .insert(normalized); } @@ -196,13 +196,13 @@ fn assign_channel_epg(new_epg: &mut Vec, fp: &mut FetchedPlaylist, id_cache (epg_source.logo_override || chan.header.logo.is_empty() || chan.header.logo_small.is_empty()) { if let Some(icon_tag) = icon_tags.get(epg_channel_id) { if let XmlTagIcon::Src(icon) = &icon_tag.icon { - icon_assigned.insert(epg_channel_id.to_string()); + icon_assigned.insert(epg_channel_id.clone()); if epg_source.logo_override || chan.header.logo.is_empty() { trace!("Matched channel {} to epg icon {icon}", chan.header.name); - chan.header.logo = (*icon).to_string(); + chan.header.logo = (*icon).clone(); } if epg_source.logo_override || chan.header.logo_small.is_empty() { - chan.header.logo_small = (*icon).to_string(); + chan.header.logo_small = (*icon).clone(); } } } diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index 86c7b40ed..e733d8dc2 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -128,7 +128,7 @@ fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { if let Some(mapper) = &mapping.mapper { if !mapper.is_empty() { let header = &channel.header; - let channel_name = if mapping.match_as_ascii { deunicode(&header.name) } else { header.name.to_string() }; + let channel_name = if mapping.match_as_ascii { deunicode(&header.name) } else { header.name.clone() }; if mapping.match_as_ascii && log_enabled!(Level::Trace) { trace!("Decoded {} for matching to {}", &header.name, &channel_name); } let ref_chan = &mut channel; let templates = mapping.templates.as_ref(); @@ -172,7 +172,7 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option grp_id += 1; new_groups.push(PlaylistGroup { id: grp_id, - title: title.to_string(), + title: title.clone(), channels: vec![channel.clone()], xtream_cluster: *cluster, }); @@ -287,7 +287,7 @@ async fn process_source(client: Arc, cfg: Arc, sourc ); } let elapsed = start_time.elapsed().as_secs(); - input_stats.insert(input_name.to_string(), create_input_stat(group_count, channel_count, error_list.len(), + input_stats.insert(input_name.clone(), create_input_stat(group_count, channel_count, error_list.len(), input.input_type, input_name, elapsed)); } } @@ -443,7 +443,7 @@ fn flatten_groups(playlistgroups: Vec) -> Vec { let mut idx: usize = 0; let mut group_map: HashMap<(String, XtreamCluster), usize> = HashMap::new(); for group in playlistgroups { - let key = (group.title.to_string(), group.xtream_cluster); + let key = (group.title.clone(), group.xtream_cluster); match group_map.entry(key) { std::collections::hash_map::Entry::Vacant(v) => { v.insert(idx); diff --git a/backend/src/processing/processor/sort.rs b/backend/src/processing/processor/sort.rs index f6a9a26e1..723a52d2a 100644 --- a/backend/src/processing/processor/sort.rs +++ b/backend/src/processing/processor/sort.rs @@ -95,8 +95,8 @@ fn playlist_comparator( } fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &ConfigSortGroup, match_as_ascii: bool) -> Ordering { - let value_a = if match_as_ascii { deunicode(&a.title) } else { a.title.to_string() }; - let value_b = if match_as_ascii { deunicode(&b.title) } else { b.title.to_string() }; + let value_a = if match_as_ascii { deunicode(&a.title) } else { a.title.clone() }; + let value_b = if match_as_ascii { deunicode(&b.title) } else { b.title.clone() }; playlist_comparator(group_sort.sequence.as_ref(), group_sort.order, &value_a, &value_b) } @@ -125,7 +125,7 @@ pub(in crate::processing::processor) fn sort_playlist(target: &ConfigTarget, new for channel_sort in channel_sorts { let regexp = &channel_sort.group_pattern; for group in new_playlist.iter_mut() { - let group_title = if match_as_ascii { deunicode(&group.title) } else { group.title.to_string() }; + let group_title = if match_as_ascii { deunicode(&group.title) } else { group.title.clone() }; if regexp.is_match(group_title.as_str()) { group.channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, channel_sort, match_as_ascii)); } diff --git a/backend/src/repository/m3u_playlist_iterator.rs b/backend/src/repository/m3u_playlist_iterator.rs index 28c2dee8b..d527adeea 100644 --- a/backend/src/repository/m3u_playlist_iterator.rs +++ b/backend/src/repository/m3u_playlist_iterator.rs @@ -50,8 +50,8 @@ impl M3uPlaylistIterator { Ok(Self { reader, base_url: server_info.get_base_url(), - username: user.username.to_string(), - password: user.password.to_string(), + username: user.username.clone(), + password: user.password.clone(), target_options: target.options.clone(), include_type_in_url: m3u_output.include_type_in_url, mask_redirect_url: m3u_output.mask_redirect_url, @@ -98,14 +98,14 @@ impl M3uPlaylistIterator { fn get_next(&mut self) -> Option<(M3uPlaylistItem, bool)> { let entry = if let Some(set) = &self.filter { if let Some((current_item, _)) = self.lookup_item.take() { - let next_valid = self.reader.find(|(pli, _)| set.contains(&pli.group.to_string())); + let next_valid = self.reader.find(|(pli, _)| set.contains(&pli.group.clone())); self.lookup_item = next_valid; let has_next = self.lookup_item.is_some(); Some((current_item, has_next)) } else { - let current_item = self.reader.find(|(item, _)| set.contains(&item.group.to_string())); + let current_item = self.reader.find(|(item, _)| set.contains(&item.group.clone())); if let Some((item, _)) = current_item { - self.lookup_item = self.reader.find(|(item, _)| set.contains(&item.group.to_string())); + self.lookup_item = self.reader.find(|(item, _)| set.contains(&item.group.clone())); let has_next = self.lookup_item.is_some(); Some((item, has_next)) } else { @@ -125,12 +125,12 @@ impl M3uPlaylistIterator { } else { None }; - let url = m3u_pli.url.to_string(); + let url = m3u_pli.url.clone(); let (stream_url, resource_url) = rewrite_urls .map_or_else(|| (url, None), |(su, ru)| (su, ru.as_ref().map(String::to_string))); - m3u_pli.t_stream_url = stream_url.to_string(); - m3u_pli.t_resource_url = resource_url.map(|s| s.to_string()); + m3u_pli.t_stream_url.clone_from(&stream_url); + m3u_pli.t_resource_url.clone_from(&resource_url); (m3u_pli, has_next) }) } diff --git a/backend/src/repository/m3u_repository.rs b/backend/src/repository/m3u_repository.rs index e603083eb..78ca70d65 100644 --- a/backend/src/repository/m3u_repository.rs +++ b/backend/src/repository/m3u_repository.rs @@ -39,7 +39,7 @@ fn persist_m3u_playlist_as_text(cfg: &Config, target: &ConfigTarget, target_outp let mut buf_writer = utils::file_writer(&file); let _ = buf_writer.write(b"#EXTM3U\n"); for m3u in m3u_playlist { - let _ = buf_writer.write(m3u.to_m3u(target.options.as_ref(), false).to_string().as_bytes()); + let _ = buf_writer.write(m3u.to_m3u(target.options.as_ref(), false).clone().as_bytes()); let _ = buf_writer.write(b"\n"); } } diff --git a/backend/src/repository/strm_repository.rs b/backend/src/repository/strm_repository.rs index c1e2484ee..3a7001468 100644 --- a/backend/src/repository/strm_repository.rs +++ b/backend/src/repository/strm_repository.rs @@ -267,13 +267,13 @@ impl StrmItemInfo { fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { let header = &mut pli.header; - let group = header.group.to_string(); - let title = header.title.to_string(); + let group = header.group.clone(); + let title = header.title.clone(); let item_type = header.item_type; let provider_id = header.get_provider_id(); let virtual_id = header.virtual_id; - let input_name = header.input_name.to_string(); - let url = header.url.to_string(); + let input_name = header.input_name.clone(); + let url = header.url.clone(); let (series_name, release_date, added, season, episode) = match header.item_type { PlaylistItemType::Series => { let series_name = match header.get_field("name") { @@ -1006,11 +1006,11 @@ fn get_strm_url( user_and_server_info: Option<&(ProxyUserCredentials, ApiProxyServerInfo)>, str_item_info: &StrmItemInfo, ) -> String { - let Some((user, server_info)) = user_and_server_info else { return str_item_info.url.to_string(); }; + let Some((user, server_info)) = user_and_server_info else { return str_item_info.url.clone(); }; let redirect = user.proxy.is_redirect(str_item_info.item_type) || target_force_redirect.is_some_and(|f| f.has_cluster(str_item_info.item_type)); if redirect { - return str_item_info.url.to_string(); + return str_item_info.url.clone(); } if let Some(stream_type) = match str_item_info.item_type { @@ -1030,7 +1030,7 @@ fn get_strm_url( str_item_info.virtual_id ) } else { - str_item_info.url.to_string() + str_item_info.url.clone() } } diff --git a/backend/src/repository/xtream_repository.rs b/backend/src/repository/xtream_repository.rs index 3253fda35..c0213488c 100644 --- a/backend/src/repository/xtream_repository.rs +++ b/backend/src/repository/xtream_repository.rs @@ -563,7 +563,7 @@ fn rewrite_xtream_vod_info

( } else { movie_data.insert( crate::model::XC_TAG_DIRECT_SOURCE.to_string(), - Value::String(pli.get_provider_url().to_string()), + Value::String(pli.get_provider_url().clone()), ); } } diff --git a/backend/src/utils/file/csv_input_reader.rs b/backend/src/utils/file/csv_input_reader.rs index e56944a55..e50b49979 100644 --- a/backend/src/utils/file/csv_input_reader.rs +++ b/backend/src/utils/file/csv_input_reader.rs @@ -29,7 +29,7 @@ fn csv_assign_mandatory_fields(alias: &mut ConfigInputAliasDto, input_type: Inpu if username.is_none() || password.is_none() { // xtream url if input_type == InputType::XtreamBatch { - alias.url = url.origin().ascii_serialization().to_string(); + alias.url = url.origin().ascii_serialization(); } else if input_type == InputType::M3uBatch && alias.username.is_some() && alias.password.is_some() { alias.url = format!("{}/get.php?username={}&password={}&type=m3u_plus", trim_last_slash(&url.origin().ascii_serialization()), @@ -39,7 +39,7 @@ fn csv_assign_mandatory_fields(alias: &mut ConfigInputAliasDto, input_type: Inpu } } else { if input_type == InputType::XtreamBatch { - alias.url = url.origin().ascii_serialization().to_string(); + alias.url = url.origin().ascii_serialization(); } // m3u url alias.username = username; diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index a16749849..05911ca08 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -28,6 +28,7 @@ prost = "0" wasm-bindgen-futures = "0" bytes = "1" regex = "1.11.1" +paste = "1.0.15" [dependencies.web-sys] version = "0.3" diff --git a/frontend/public/assets/i18n/en.json b/frontend/public/assets/i18n/en.json index e2817dca1..cc23afb7d 100644 --- a/frontend/public/assets/i18n/en.json +++ b/frontend/public/assets/i18n/en.json @@ -283,6 +283,7 @@ "TARGET_NOT_EXISTS": "Target does not exist", "CONFIRM_DELETE": "You really want to delete?", "CONFIG_CHANGED": "Configuration on server changed.", + "USER_DELETED": "User successfully deleted", "DOWNLOAD": { "SUCCESS": "Successfully downloaded", "FAIL": "Failed to download!", @@ -310,7 +311,9 @@ "DUPLICATE_USERNAME": "Duplicate username!", "DUPLICATE_TOKEN": "Duplicate token!", "TARGET_NOT_FOUND": "Target not found!", - "MAX_CONNECTIONS_INVALID": "MaxConnections invalid" + "MAX_CONNECTIONS_INVALID": "MaxConnections invalid", + "NOTHING_TO_SAVE": "Nothing changed", + "TARGET_NOT_SELECTED": "Playlist not selected" } }, "PLAYLIST_UPDATE": { diff --git a/frontend/src/app/components/config/macros.rs b/frontend/src/app/components/config/macros.rs index 28be6f1d7..eafb6d350 100644 --- a/frontend/src/app/components/config/macros.rs +++ b/frontend/src/app/components/config/macros.rs @@ -81,6 +81,18 @@ macro_rules! config_field { }; } +#[macro_export] +macro_rules! config_field_custom { + ($label:expr, $value:expr) => { + html! { +

+ + {$value} +
+ } + }; +} + #[macro_export] macro_rules! config_field_child { ($label:expr, $body:block) => { @@ -105,15 +117,21 @@ macro_rules! config_field_empty { }; } +pub trait HasFormData { + type Data; + fn data(&self) -> &Self::Data; + //fn data_mut(&mut self) -> &mut Self::Data; +} + #[macro_export] macro_rules! edit_field_text_option { - ($instance:expr, $label:expr, $field:ident) => { - $crate::edit_field_text_option!(@inner $instance, $label, $field, false) + ($instance:expr, $label:expr, $field:ident, $action:path) => { + $crate::edit_field_text_option!(@inner $instance, $label, $field, $action, false) }; - ($instance:expr, $label:expr, $field:ident, $hidden:expr) => { - $crate::edit_field_text_option!(@inner $instance, $label, $field, $hidden) + ($instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => { + $crate::edit_field_text_option!(@inner $instance, $label, $field, $action, $hidden) }; - (@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{ + (@inner $instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => {{ let instance = $instance.clone(); html! {
@@ -122,13 +140,13 @@ macro_rules! edit_field_text_option { hidden={$hidden} name={stringify!($field)} autocomplete={true} - value={instance.borrow().$field.as_ref().map_or_else(String::new, |v|v.to_string())} + value={(*instance).data().$field.as_ref().map_or_else(String::new, |v|v.to_string())} on_change={Callback::from(move |value: String| { - instance.borrow_mut().$field = if value.is_empty() { + instance.dispatch($action(if value.is_empty() { None } else { Some(value) - }; + })); })} />
@@ -138,13 +156,13 @@ macro_rules! edit_field_text_option { #[macro_export] macro_rules! edit_field_text { - ($instance:expr, $label:expr, $field:ident) => { - $crate::edit_field_text!(@inner $instance, $label, $field, false) + ($instance:expr, $label:expr, $field:ident, $action:path) => { + $crate::edit_field_text!(@inner $instance, $label, $field, $action, false) }; - ($instance:expr, $label:expr, $field:ident, $hidden:expr) => { - $crate::edit_field_text!(@inner $instance, $label, $field, $hidden) + ($instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => { + $crate::edit_field_text!(@inner $instance, $label, $field, $action, $hidden) }; - (@inner $instance:expr, $label:expr, $field:ident, $hidden:expr) => {{ + (@inner $instance:expr, $label:expr, $field:ident, $action:path, $hidden:expr) => {{ let instance = $instance.clone(); html! {
@@ -153,9 +171,9 @@ macro_rules! edit_field_text { hidden={$hidden} name={stringify!($field)} autocomplete={true} - value={instance.borrow().$field.clone()} + value={(*instance).data().$field.clone()} on_change={Callback::from(move |value: String| { - instance.borrow_mut().$field = value; + instance.dispatch($action(value)); })} />
@@ -163,19 +181,17 @@ macro_rules! edit_field_text { }}; } -// - #[macro_export] macro_rules! edit_field_bool { - ($instance:expr, $label:expr, $field:ident) => {{ + ($instance:expr, $label:expr, $field:ident, $action:path) => {{ let instance = $instance.clone(); html! {
<$crate::app::components::ToggleSwitch - value={instance.borrow().$field} + value={(*instance).data().$field} readonly={false} - on_change={Callback::from(move |value| instance.borrow_mut().$field = value)} /> + on_change={Callback::from(move |value| instance.dispatch($action(value)))} />
} }}; @@ -183,18 +199,18 @@ macro_rules! edit_field_bool { #[macro_export] macro_rules! edit_field_number { - ($instance:expr, $label:expr, $field:ident) => {{ + ($instance:expr, $label:expr, $field:ident, $action:path) => {{ let instance = $instance.clone(); html! {
<$crate::app::components::number_input::NumberInput label={$label} name={stringify!($field)} - value={instance.borrow().$field.clone()} + value={(*instance).data().$field.clone()} on_change={Callback::from(move |value: Option| { match value { - Some(value) => instance.borrow_mut().$field = value, - None => instance.borrow_mut().$field = 0, + Some(value) => instance.dispatch($action(value)), + None => instance.dispatch($action(0)), } })} /> @@ -205,19 +221,84 @@ macro_rules! edit_field_number { #[macro_export] macro_rules! edit_field_date { - ($instance:expr, $label:expr, $field:ident) => {{ + ($instance:expr, $label:expr, $field:ident, $action:path) => {{ let instance = $instance.clone(); html! {
<$crate::app::components::date_input::DateInput label={$label} name={stringify!($field)} - value={instance.borrow().$field.clone()} + value={(*instance).data().$field.clone()} on_change={Callback::from(move |value: Option| { - instance.borrow_mut().$field = value; + instance.dispatch($action(value)); })} />
} }}; -} \ No newline at end of file +} + +#[macro_export] +macro_rules! generate_form_reducer { + ( + state: $state_name:ident { $data_field:ident: $data_type:ty }, + action_name: $action_name:ident, + fields { + $($set_name:ident => $field_name:ident : $field_type:ty),* $(,)? + } + ) => { + #[derive(Debug, Clone, PartialEq)] + pub struct $state_name { + pub $data_field: $data_type, + modified: bool, + } + + impl $state_name { + pub fn modified(&self) -> bool { + self.modified + } + } + + #[derive(Clone)] + pub enum $action_name { + $( + $set_name($field_type), + )* + SetAll($data_type), + } + + impl yew::prelude::Reducible for $state_name { + type Action = $action_name; + + fn reduce(self: std::rc::Rc, action: Self::Action) -> std::rc::Rc { + let mut new_data = self.$data_field.clone(); + let mut modified = self.modified; + match action { + $( + $action_name::$set_name(v) => { + new_data.$field_name = v; + if !modified { modified = true; } + }, + )* + $action_name::SetAll(v) => { + new_data = v; + modified = false; + }, + } + $state_name { $data_field: new_data, modified }.into() + } + } + + impl $crate::app::components::config::HasFormData for $state_name { + type Data = $data_type; + + fn data(&self) -> &Self::Data { + &self.$data_field + } + + // fn data_mut(&mut self) -> &mut Self::Data { + // &mut self.$data_field + // } + } + }; +} diff --git a/frontend/src/app/components/config/mod.rs b/frontend/src/app/components/config/mod.rs index 90dc69523..3a149c85e 100644 --- a/frontend/src/app/components/config/mod.rs +++ b/frontend/src/app/components/config/mod.rs @@ -22,4 +22,5 @@ pub use reverse_proxy_config_view::*; pub use hdhomerung_config_view::*; pub use proxy_config_view::*; pub use ipcheck_config_view::*; -pub use video_config_view::*; \ No newline at end of file +pub use video_config_view::*; +pub use macros::*; \ No newline at end of file diff --git a/frontend/src/app/components/date_input.rs b/frontend/src/app/components/date_input.rs index 630ff6ac2..499e4f033 100644 --- a/frontend/src/app/components/date_input.rs +++ b/frontend/src/app/components/date_input.rs @@ -26,10 +26,9 @@ pub fn DateInput(props: &DateInputProps) -> Html { if let Some(input) = local_ref.cast::() { if let Some(ts) = val { // Timestamp -> yyyy-mm-dd - let date = chrono::NaiveDateTime::from_timestamp_opt(*ts, 0) - .unwrap() - .date(); - input.set_value(&date.format("%Y-%m-%d").to_string()); + if let Some(date) = chrono::DateTime::from_timestamp(*ts, 0) { + input.set_value(&date.format("%Y-%m-%d").to_string()); + } } else { input.set_value(""); } @@ -42,13 +41,14 @@ pub fn DateInput(props: &DateInputProps) -> Html { let onchange_cb = props.on_change.clone(); Callback::from(move |event: yew::events::Event| { if let Some(input) = event.target_dyn_into::() { - let value = input.value(); // z.B. "2025-08-22" + let value = input.value(); // "2025-08-22" let ts = if value.is_empty() { None } else { chrono::NaiveDate::parse_from_str(&value, "%Y-%m-%d") .ok() - .map(|date| date.and_hms_opt(0, 0, 0).unwrap().timestamp()) + .and_then(|date| date.and_hms_opt(0, 0, 0)) + .map(|dt| dt.and_utc().timestamp()) }; if let Some(cb) = onchange_cb.as_ref() { cb.emit(ts); diff --git a/frontend/src/app/components/input.rs b/frontend/src/app/components/input.rs index f13071300..b4db7d79c 100644 --- a/frontend/src/app/components/input.rs +++ b/frontend/src/app/components/input.rs @@ -1,4 +1,4 @@ -use web_sys::{HtmlInputElement, KeyboardEvent}; +use web_sys::{HtmlInputElement, InputEvent, KeyboardEvent}; use yew::{function_component, html, use_effect_with, use_state, Callback, Html, NodeRef, Properties, TargetCast}; use crate::app::components::IconButton; use crate::html_if; @@ -46,23 +46,15 @@ pub fn Input(props: &InputProps) -> Html { }) }; - let handle_keydown = { - let onkeydown_clone = props.onkeydown.clone(); + let handle_oninput = { let ontext_clone = props.on_change.clone(); - Callback::from(move |event: KeyboardEvent| { - if event.key() == "Enter" { - event.prevent_default(); - } - + Callback::from(move |event: InputEvent| { if let Some(input) = event.target_dyn_into::() { let value = input.value(); if let Some(cb) = ontext_clone.as_ref() { - cb.emit(value); + cb.emit(value); } } - if let Some(cb) = onkeydown_clone.as_ref() { - cb.emit(event); - } }) }; @@ -80,7 +72,8 @@ pub fn Input(props: &InputProps) -> Html { type={if *hide_content { "password".to_string() } else { "text".to_string() }} name={props.name.clone()} autocomplete={if props.autocomplete { "on".to_string() } else { "off".to_string() }} - onkeydown={handle_keydown.clone()} + onkeydown={props.onkeydown.clone()} + oninput={handle_oninput} /> { html_if!(props.hidden, { diff --git a/frontend/src/app/components/userlist/edit.rs b/frontend/src/app/components/userlist/edit.rs index eebc2218f..e35368243 100644 --- a/frontend/src/app/components/userlist/edit.rs +++ b/frontend/src/app/components/userlist/edit.rs @@ -1,12 +1,17 @@ +use std::rc::Rc; +use yew::platform::spawn_local; use yew::prelude::*; use yew_i18n::use_translation; +use shared::model::ProxyUserCredentialsDto; use crate::app::components::{UserlistContext, UserlistPage, TextButton, Card}; use crate::app::components::userlist::proxy_user_credentials_form::ProxyUserCredentialsForm; -use crate::app::{ConfigContext, PlaylistContext}; +use crate::app::{ConfigContext, PlaylistContext, TargetUser}; +use crate::hooks::use_service_context; #[function_component] pub fn UserEdit() -> Html { let translate = use_translation(); + let services_ctx = use_service_context(); let userlist_ctx = use_context::().expect("Userlist context not found"); let playlist_ctx = use_context::().expect("Playlist context not found"); let config_ctx = use_context::().expect("Config context not found"); @@ -14,8 +19,7 @@ pub fn UserEdit() -> Html { let targets = use_memo(playlist_ctx.clone(), |playlist_ctx| { match playlist_ctx.sources.as_ref() { None => vec![], - Some(sources) => sources.iter().map(|(_, t)| t) - .flatten() + Some(sources) => sources.iter().flat_map(|(_, t)| t) .cloned() .collect() } @@ -27,7 +31,7 @@ pub fn UserEdit() -> Html { Some(app_config) => { match app_config.api_proxy.as_ref() { None => vec![], - Some(api_proxy) => api_proxy.server.iter().cloned().collect() + Some(api_proxy) => api_proxy.server.to_vec() } } } @@ -37,6 +41,54 @@ pub fn UserEdit() -> Html { let userlist_ctx = userlist_ctx.clone(); Callback::from(move |_| { userlist_ctx.active_page.set(UserlistPage::List); + userlist_ctx.selected_user.set(None); + }) + }; + + let handle_user_save = { + let userlist = userlist_ctx.clone(); + let handleback = handle_back.clone(); + let services = services_ctx.clone(); + let translate = translate.clone(); + Callback::from(move |(is_update, target, user):(bool, String, ProxyUserCredentialsDto)| { + let services = services.clone(); + let handleback = handleback.clone(); + let userlist = userlist.clone(); + let translate = translate.clone(); + spawn_local(async move { + match if is_update { services.user.update_user(target.clone(), user.clone()).await } else { services.user.create_user(target.clone(), user.clone()).await } { + Ok(()) => { + let new_user = Rc::new(TargetUser {target: target.clone(), credentials: Rc::new(user.clone()) }); + let new_user_list = if let Some(user_list) = userlist.users.as_ref() { + let mut new_list: Vec> = user_list.iter().map(|target_user| { + let mut new_user = target_user.as_ref().clone(); + if new_user.target == target { + if is_update { + if new_user.credentials.username == user.username { + new_user.credentials = Rc::new(user.clone()); + }; + } else { + new_user.credentials = Rc::new(user.clone()); + } + } + Rc::new(new_user) + }).collect(); + if !is_update { + new_list.push(new_user); + } + new_list + } else { + vec![new_user] + }; + userlist.users.set(Some(Rc::new(new_user_list))); + handleback.emit(String::new()); + services.toastr.success(translate.t("MESSAGES.SAVE.USER.SUCCESS")); + }, + Err(err) => { + services.toastr.error(err.to_string()); + } + } + }); }) }; @@ -51,7 +103,7 @@ pub fn UserEdit() -> Html {
- +
diff --git a/frontend/src/app/components/userlist/proxy_type_input.rs b/frontend/src/app/components/userlist/proxy_type_input.rs index 83b77aff7..f0a8c43b8 100644 --- a/frontend/src/app/components/userlist/proxy_type_input.rs +++ b/frontend/src/app/components/userlist/proxy_type_input.rs @@ -1,4 +1,3 @@ -use log::warn; use shared::model::{ClusterFlags, ProxyType}; use yew::prelude::*; use yew_i18n::use_translation; @@ -35,17 +34,7 @@ pub struct ProxyTypeInputProps { #[function_component] pub fn ProxyTypeInput(props: &ProxyTypeInputProps) -> Html { let translate = use_translation(); - let selections = use_state(|| get_flags(props.value)); - - { - let hash = props.value.to_string(); - let pt = props.value.clone(); - let set_selections = selections.clone(); - use_effect_with(hash, move |_| { - warn!("changed pt: {:?}", pt); - set_selections.set(get_flags(pt)); - }); - } + let selections = get_flags(props.value); let handle_change = { let onchange = props.on_change.clone(); @@ -74,71 +63,61 @@ pub fn ProxyTypeInput(props: &ProxyTypeInputProps) -> Html { }; let handle_redirect_click = { - let set_selections = selections.clone(); let emit_change = handle_change.clone(); Callback::from(move |_| { let new_flags = (true, false, false, false, false); - set_selections.set(new_flags); emit_change.emit(new_flags); }) }; let handle_reverse_click = { - let set_selections = selections.clone(); let emit_change = handle_change.clone(); Callback::from(move |_| { let new_flags = (false, true, true, true, true); - set_selections.set(new_flags); emit_change.emit(new_flags); }) }; let handle_reverse_live_click = { - let set_selections = selections.clone(); let emit_change = handle_change.clone(); + let flags = selections; Callback::from(move |_| { - let flags = *set_selections; let new_flags = if flags.0 { (false, true, true, true, true) } else { (false, true, !flags.2, flags.3, flags.4) }; let new_flags = check_flag_validity(new_flags); - set_selections.set(new_flags); emit_change.emit(new_flags); }) }; let handle_reverse_vod_click = { - let set_selections = selections.clone(); let emit_change = handle_change.clone(); + let flags = selections; Callback::from(move |_| { - let flags = *set_selections; let new_flags = if flags.0 { (false, true, true, true, true) } else { (false, true, flags.2, !flags.3, flags.4) }; let new_flags = check_flag_validity(new_flags); - set_selections.set(new_flags); emit_change.emit(new_flags); }) }; let handle_reverse_series_click = { - let set_selections = selections.clone(); let emit_change = handle_change.clone(); + let flags = selections; Callback::from(move |_| { - let flags = *set_selections; let new_flags = if flags.0 { (false, true, true, true, true) } else { (false, true, flags.2, flags.3, !flags.4) }; let new_flags = check_flag_validity(new_flags); - set_selections.set(new_flags); emit_change.emit(new_flags); }) }; - let (redirect, reverse, reverse_live, reverse_vod, reverse_series) = *selections; + let (redirect, reverse, reverse_live, reverse_vod, reverse_series) = selections; html! {
@@ -149,9 +128,9 @@ pub fn ProxyTypeInput(props: &ProxyTypeInputProps) -> Html { { translate.t("LABEL.REVERSE") } - { translate.t("LABEL.LIVE_SHORT") } - { translate.t("LABEL.VOD_SHORT") } - { translate.t("LABEL.SERIES_SHORT") } + { translate.t("LABEL.LIVE_SHORT") } + { translate.t("LABEL.VOD_SHORT") } + { translate.t("LABEL.SERIES_SHORT") }
diff --git a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs index b16288651..096ec80d7 100644 --- a/frontend/src/app/components/userlist/proxy_user_credentials_form.rs +++ b/frontend/src/app/components/userlist/proxy_user_credentials_form.rs @@ -1,68 +1,146 @@ -use std::cell::RefCell; use std::rc::Rc; -use log::warn; +use chrono::{Duration, Utc}; use yew::prelude::*; use yew_i18n::use_translation; use shared::model::{ApiProxyServerInfoDto, ConfigTargetDto, ProxyType, ProxyUserCredentialsDto, ProxyUserStatus}; use crate::app::TargetUser; -use crate::{config_field_child, edit_field_bool, edit_field_date, edit_field_number, edit_field_text, edit_field_text_option}; +use crate::{config_field_child, config_field_custom, edit_field_bool, edit_field_date, edit_field_number, edit_field_text, edit_field_text_option, generate_form_reducer}; use crate::app::components::select::Select; use crate::app::components::{DropDownOption, TextButton, UserStatus}; +use crate::app::components::config::HasFormData; use crate::app::components::userlist::proxy_type_input::ProxyTypeInput; +use crate::hooks::use_service_context; + +generate_form_reducer!( + state: UserFormState { form: ProxyUserCredentialsDto }, + action_name: UserFormAction, + fields { + Username => username: String, + Password => password: String, + Token => token: Option, + Proxy => proxy: ProxyType, + Server => server: Option, + Status => status: Option, + MaxConnections => max_connections: u32, + ExpDate => exp_date: Option, + UiEnabled => ui_enabled: bool, + EpgTimeshift => epg_timeshift: Option, + Comment => comment: Option, + } +); #[derive(Properties, PartialEq, Clone)] pub struct ProxyUserCredentialsFormProps { pub user: Option>, pub targets: Rc>>, pub server: Rc>, + pub on_save: Callback<(bool, String, ProxyUserCredentialsDto)>, } #[function_component] pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html { let translate = use_translation(); - let selected_target = use_state(|| props.user.as_ref().map(|u| u.target.clone())); - let form_state = use_memo(props.user.clone(), - |user| RefCell::new(user.as_ref() - .map_or_else(|| ProxyUserCredentialsDto::default(), - |usr| usr.credentials.as_ref().clone()))); + let service_ctx = use_service_context(); + let selected_target = use_state(|| None); + let update = use_state(|| false); - let targets = use_memo((props.targets.clone(), props.user.clone()), - |(targets, user)| - targets.iter().map(|t| Rc::new(DropDownOption { - id: t.name.to_string(), - label: html! { t.name.clone() }, - selected: user.as_ref().is_some_and(|user| user.target == t.name), - })).collect::>>(), - ); + let form_state: UseReducerHandle = use_reducer(|| { + UserFormState { form: ProxyUserCredentialsDto::default(), modified: false } + }); - let server = use_memo((props.server.clone(), props.user.clone()), - |(server, user)| - server.iter().map(|s| Rc::new(DropDownOption { - id: s.name.to_string(), - label: html! { s.name.clone() }, - selected: user.as_ref().is_some_and(|user| user.credentials.server.as_ref() == Some(&s.name)), - })).collect::>>(), - ); - - let proxy_user_status = use_memo(props.user.clone(), |user| - vec![ - ProxyUserStatus::Active, + let proxy_user_status = use_memo(form_state.clone(), |user| + [ProxyUserStatus::Active, ProxyUserStatus::Expired, ProxyUserStatus::Banned, ProxyUserStatus::Trial, ProxyUserStatus::Disabled, - ProxyUserStatus::Pending, - ].iter().map(|s| Rc::new(DropDownOption { + ProxyUserStatus::Pending].iter().map(|s| Rc::new(DropDownOption { id: s.to_string(), - label: html! { }, - selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)), + label: html! { }, + selected: user.data().status.as_ref() == Some(s), })).collect::>>(), ); + let targets = use_memo((props.targets.clone(), selected_target.clone()), + |(targets, user)| + targets.iter().map(|t| Rc::new(DropDownOption { + id: t.name.clone(), + label: html! { t.name.clone() }, + selected: (*user).as_ref().is_some_and(|ut: &String| ut == &t.name), + })).collect::>>(), + ); + + let server = use_memo((props.server.clone(), form_state.clone()), + |(server, user)| + server.iter().map(|s| Rc::new(DropDownOption { + id: s.name.to_string(), + label: html! { s.name.clone() }, + selected: user.data().server.as_ref() == Some(&s.name), + })).collect::>>(), + ); + + { + let form_state = form_state.clone(); + let set_selected_target = selected_target.clone(); + let set_update = update.clone(); + use_effect_with((props.user.clone(), props.server.clone()), move |(user, server)| { + if let Some(u) = user.clone() { + set_update.set(true); + set_selected_target.set(Some(u.target.clone())); + form_state.dispatch(UserFormAction::SetAll((*u.credentials).clone())); + } else { + set_update.set(false); + set_selected_target.set(None); + let mut user = ProxyUserCredentialsDto::default(); + if let Some(api_server) = (*server).first() { + user.server = Some(api_server.name.clone()); + } + user.max_connections = 1; + user.proxy = ProxyType::Redirect; + user.status = Some(ProxyUserStatus::Active); + user.ui_enabled = true; + let now = Utc::now(); + user.created_at = Some(now.timestamp()); + let in_one_year = now + Duration::days(365); + user.exp_date = Some(in_one_year.timestamp()); + + form_state.dispatch(UserFormAction::SetAll(user)); + } + || () + }, + ); + } + let handle_save_user = { let user = form_state.clone(); + let original = props.user.clone(); + let services = service_ctx.clone(); + let translate_clone = translate.clone(); + let target: Option = (*selected_target).clone(); + let onsave = props.on_save.clone(); + let is_update = update.clone(); Callback::from(move |_| { - warn!("{:?}", user.borrow()); + if let Some(target_name) = target.as_ref() { + if user.modified() { + let user = user.data(); + if let Err(err) = user.validate() { + services.toastr.error(err.to_string()); + } else { + match original.as_ref().map(|t| t.credentials.clone()) { + None => onsave.emit((*is_update, target_name.clone(), user.clone())), + Some(original_user) => { + if &(*original_user) != user { + onsave.emit((*is_update, target_name.clone(), user.clone())); + } + } + }; + } + } else { + services.toastr.warning(translate_clone.t("MESSAGES.SAVE.USER.NOTHING_TO_SAVE")); + } + } else { + services.toastr.error(translate_clone.t("MESSAGES.SAVE.USER.TARGET_NOT_SELECTED")); + } }) }; @@ -74,40 +152,48 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html { html! {
- { config_field_child!(translate.t("LABEL.PLAYLIST"), { + { if *update { + config_field_custom!(translate.t("LABEL.PLAYLIST"), (*set_selected_target).as_ref().map_or_else(String::new, |t| t.clone())) + } else { config_field_child!(translate.t("LABEL.PLAYLIST"), { html! { >)| { if let Some(status_option) = selections.first() { - if let Some(status) = status_option.id.parse::().ok() { - instance_status.borrow_mut().status = Some(status); + if let Ok(status) = status_option.id.parse::() { + instance_status.dispatch(UserFormAction::Status(Some(status))); } } })} options={(*proxy_user_status).clone()} /> }})} - { edit_field_text!(form_state, translate.t("LABEL.USERNAME"), username) } - { edit_field_text!(form_state, translate.t("LABEL.PASSWORD"), password, true) } - { edit_field_text_option!(form_state, translate.t("LABEL.TOKEN"), token, true) } + { if *update { + config_field_custom!(translate.t("LABEL.USERNAME"), form_state.data().username.clone()) + } else { + edit_field_text!(form_state, translate.t("LABEL.USERNAME"), username, UserFormAction::Username) + } + } + { edit_field_text!(form_state, translate.t("LABEL.PASSWORD"), password, UserFormAction::Password, true) } + { edit_field_text_option!(form_state, translate.t("LABEL.TOKEN"), token, UserFormAction::Token, true) } { config_field_child!(translate.t("LABEL.PROXY"), { html! { - }})} { config_field_child!(translate.t("LABEL.SERVER"), { @@ -116,19 +202,19 @@ pub fn ProxyUserCredentialsForm(props: &ProxyUserCredentialsFormProps) -> Html { multi_select={false} onselect={Callback::from(move |(_name, selections):(String, Vec>)| { if let Some(server_option) = selections.first().or((*server).first()) { - instance_server.borrow_mut().server = Some(server_option.id.clone()); + instance_server.dispatch(UserFormAction::Server(Some(server_option.id.clone()))); } else { - instance_server.borrow_mut().server = None; + instance_server.dispatch(UserFormAction::Server(None)); }; })} options={(*server_list).clone()} /> }})} - { edit_field_number!(form_state, translate.t("LABEL.MAX_CONNECTIONS"), max_connections) } - { edit_field_date!(form_state, translate.t("LABEL.EXP_DATE"), exp_date) } - { edit_field_text_option!(form_state, translate.t("LABEL.EPG_TIMESHIFT"), epg_timeshift) } - { edit_field_bool!(form_state, translate.t("LABEL.USER_UI_ENABLED"), ui_enabled) } - { edit_field_text_option!(form_state, translate.t("LABEL.COMMENT"), comment) } + { edit_field_number!(form_state, translate.t("LABEL.MAX_CONNECTIONS"), max_connections, UserFormAction::MaxConnections) } + { edit_field_date!(form_state, translate.t("LABEL.EXP_DATE"), exp_date, UserFormAction::ExpDate) } + { edit_field_text_option!(form_state, translate.t("LABEL.EPG_TIMESHIFT"), epg_timeshift, UserFormAction::EpgTimeshift) } + { edit_field_bool!(form_state, translate.t("LABEL.USER_UI_ENABLED"), ui_enabled, UserFormAction::UiEnabled) } + { edit_field_text_option!(form_state, translate.t("LABEL.COMMENT"), comment, UserFormAction::Comment) }
diff --git a/frontend/src/app/components/userlist/user_table.rs b/frontend/src/app/components/userlist/user_table.rs index 368512c0d..28bc5eda6 100644 --- a/frontend/src/app/components/userlist/user_table.rs +++ b/frontend/src/app/components/userlist/user_table.rs @@ -16,6 +16,8 @@ use std::str::FromStr; use yew::platform::spawn_local; use yew::prelude::*; use yew_i18n::use_translation; +use crate::app::TargetUserList; +use crate::hooks::use_service_context; const HEADERS: [&str; 15] = [ "LABEL.EMPTY", @@ -52,14 +54,48 @@ fn is_col_sortable(col: usize) -> bool { } +#[derive(Debug, Clone, Eq, PartialEq)] +enum TableAction { + Edit, + Refresh, + Delete, +} + +impl Display for TableAction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + Self::Edit => "edit", + Self::Refresh => "refresh", + Self::Delete => "delete", + }) + } +} + +impl FromStr for TableAction { + type Err = TuliproxError; + + fn from_str(s: &str) -> Result { + if s.eq("edit") { + Ok(Self::Edit) + } else if s.eq("refresh") { + Ok(Self::Refresh) + } else if s.eq("delete") { + Ok(Self::Delete) + } else { + create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown InputType: {}", s) + } + } +} + #[derive(Properties, PartialEq, Clone)] pub struct UserTableProps { - pub users: Option>>>, + pub users: TargetUserList, } #[function_component] pub fn UserTable(props: &UserTableProps) -> Html { let translate = use_translation(); + let service_ctx = use_service_context(); let dialog = use_context::().expect("Dialog service not found"); let userlist_context = use_context::().expect("Userlist context not found"); let popup_anchor_ref = use_state(|| None::); @@ -208,6 +244,7 @@ pub fn UserTable(props: &UserTableProps) -> Html { let popup_is_open_state = popup_is_open.clone(); let confirm = dialog.clone(); let translate = translate.clone(); + let services = service_ctx.clone(); let selected_dto = selected_dto.clone(); let ul_context = userlist_context.clone(); Callback::from(move |(name, _): (String, _)| { @@ -223,10 +260,28 @@ pub fn UserTable(props: &UserTableProps) -> Html { TableAction::Delete => { let confirm = confirm.clone(); let translator = translate.clone(); + let services = services.clone(); + let userlist = ul_context.clone(); + let selected_user = selected_dto.clone(); spawn_local(async move { let result = confirm.confirm(&translator.t("MESSAGES.CONFIRM_DELETE")).await; if result == DialogResult::Ok { - // TODO edit + if let Some(dto) = &*selected_user { + match services.user.delete_user(dto.target.clone(), dto.credentials.username.clone()).await { + Ok(()) => { + if let Some(user_list) = userlist.users.as_ref() { + let new_list: Vec> = user_list.iter().filter(|target_user| + !(target_user.target.eq(&dto.target) && target_user.credentials.username.eq(&dto.credentials.username)) + ).map(Rc::clone).collect(); + userlist.users.set(Some(Rc::new(new_list))); + services.toastr.success(translator.t("MESSAGES.USER_DELETED")); + } + }, + Err(err) => { + services.toastr.error(err.to_string()); + } + } + } } }); } @@ -253,37 +308,3 @@ pub fn UserTable(props: &UserTableProps) -> Html {
} } - - -#[derive(Debug, Clone, Eq, PartialEq)] -enum TableAction { - Edit, - Refresh, - Delete, -} - -impl Display for TableAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - Self::Edit => "edit", - Self::Refresh => "refresh", - Self::Delete => "delete", - }) - } -} - -impl FromStr for TableAction { - type Err = TuliproxError; - - fn from_str(s: &str) -> Result { - if s.eq("edit") { - Ok(Self::Edit) - } else if s.eq("refresh") { - Ok(Self::Refresh) - } else if s.eq("delete") { - Ok(Self::Delete) - } else { - create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown InputType: {}", s) - } - } -} \ No newline at end of file diff --git a/frontend/src/app/components/userlist/userlist_view.rs b/frontend/src/app/components/userlist/userlist_view.rs index afc6f48b2..8bd4cecd0 100644 --- a/frontend/src/app/components/userlist/userlist_view.rs +++ b/frontend/src/app/components/userlist/userlist_view.rs @@ -16,29 +16,34 @@ pub fn UserlistView() -> Html { let active_page = use_state(|| UserlistPage::List); let selected_user= use_state(|| None::>); let filtered_user= use_state(|| None::>>>); + let users = use_state(|| None::>>>); - let users = use_memo(config_ctx.config.as_ref().and_then(|c| c.api_proxy.clone()), - |api_cfg_opt| { - if let Some(api_cfg) = api_cfg_opt { - let mut users = Vec::new(); - for target in &api_cfg.user { - for creds in &target.credentials { - users.push(Rc::new(TargetUser { - target: target.target.to_string(), - credentials: Rc::new(creds.clone()), - })); - } - } - Some(Rc::new(users)) - } else { - None - } - }); + { + let users_state = users.clone(); + use_effect_with(config_ctx.config, move |api_cfg_opt| { + let new_users = api_cfg_opt.as_ref().and_then(|cfg| { + cfg.api_proxy.as_ref().map(|api_cfg| { + let mut users_vec = Vec::new(); + for target in &api_cfg.user { + for creds in &target.credentials { + users_vec.push(Rc::new(TargetUser { + target: target.target.to_string(), + credentials: Rc::new(creds.clone()), + })); + } + } + Rc::new(users_vec) + }) + }); + users_state.set(new_users); + || () + }); + } let userlist_context = UserlistContext { selected_user: selected_user.clone(), filtered_users: filtered_user.clone(), - users: (*users).clone(), + users: users.clone(), active_page: active_page.clone(), }; diff --git a/frontend/src/app/context.rs b/frontend/src/app/context.rs index eaaf49e65..5fff5ad96 100644 --- a/frontend/src/app/context.rs +++ b/frontend/src/app/context.rs @@ -30,13 +30,13 @@ pub struct TargetUser { pub credentials: Rc, } -type TargetUserList = Option>>>; +pub type TargetUserList = Option>>>; #[derive(Clone, PartialEq)] pub struct UserlistContext { pub selected_user: UseStateHandle>>, pub filtered_users: UseStateHandle, - pub users: TargetUserList, + pub users: UseStateHandle, pub active_page: UseStateHandle, } @@ -44,7 +44,7 @@ impl UserlistContext { pub fn get_users(&self) -> TargetUserList { match &*self.filtered_users { Some(filtered) => Some(Rc::clone(filtered)), - None => self.users.clone(), + None => (*self.users).clone(), } } diff --git a/frontend/src/error.rs b/frontend/src/error.rs index 8e85f48d4..37b243208 100644 --- a/frontend/src/error.rs +++ b/frontend/src/error.rs @@ -5,14 +5,23 @@ use thiserror::Error as ThisError; /// Conduit api error info for Unprocessable Entity error #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -pub struct ErrorInfo { +pub struct ErrorSetInfo { pub errors: HashMap>, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ErrorInfo { + pub error: String, +} /// Define all possible errors #[derive(ThisError, Clone, Debug, PartialEq, Eq)] pub enum Error { + /// 400 + #[error("{0}")] + BadRequest(String), + /// 401 #[error("Unauthorized")] Unauthorized, @@ -27,7 +36,7 @@ pub enum Error { /// 422 #[error("Unprocessable Entity: {0:?}")] - UnprocessableEntity(ErrorInfo), + UnprocessableEntity(ErrorSetInfo), /// 500 #[error("Internal Server Error")] diff --git a/frontend/src/hooks/use_service_context.rs b/frontend/src/hooks/use_service_context.rs index f7fd9fac2..45098b4a4 100644 --- a/frontend/src/hooks/use_service_context.rs +++ b/frontend/src/hooks/use_service_context.rs @@ -1,11 +1,12 @@ use std::rc::Rc; use yew::prelude::*; use crate::model::WebConfig; -use crate::services::{AuthService, ConfigService, EventService, PlaylistService, StatusService, ToastrService, WebSocketService}; +use crate::services::{AuthService, ConfigService, EventService, PlaylistService, StatusService, ToastrService, UserService, WebSocketService}; pub struct Services { pub auth: Rc, pub config: Rc, + pub user: Rc, pub status: Rc, pub event: Rc, pub playlist: Rc, @@ -21,6 +22,7 @@ impl Services { let event = Rc::new(EventService::new()); let playlist = Rc::new(PlaylistService::new()); let toastr = Rc::new(ToastrService::new()); + let user = Rc::new(UserService::new(Rc::clone(&event))); let websocket = Rc::new(WebSocketService::new(Rc::clone(&status), Rc::clone(&event))); Self { auth, @@ -28,6 +30,7 @@ impl Services { status, event, playlist, + user, toastr, websocket } diff --git a/frontend/src/services/event_service.rs b/frontend/src/services/event_service.rs index 51d725399..28a333514 100644 --- a/frontend/src/services/event_service.rs +++ b/frontend/src/services/event_service.rs @@ -2,13 +2,14 @@ use crate::model::EventMessage; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; type Subscriber = RefCell>>; pub struct EventService { subscriber_id: Rc, subscribers: Rc, + block_config_updated_message: AtomicBool, } impl Default for EventService { @@ -23,9 +24,18 @@ impl EventService { Self { subscriber_id: Rc::new(AtomicUsize::new(0)), subscribers: Rc::new(RefCell::new(HashMap::new())), + block_config_updated_message: AtomicBool::new(false), } } + pub fn is_config_change_message_blocked(&self) -> bool { + self.block_config_updated_message.load(Ordering::Relaxed) + } + + pub fn set_config_change_message_blocked(&self, value: bool) { + self.block_config_updated_message.store(value, Ordering::Relaxed); + } + pub fn subscribe(&self, callback: F) -> usize { let sub_id = self.subscriber_id.fetch_add(1, Ordering::SeqCst); self.subscribers.borrow_mut().insert(sub_id, Box::new(callback)); diff --git a/frontend/src/services/mod.rs b/frontend/src/services/mod.rs index a8b7d79b4..c0c3dfa25 100644 --- a/frontend/src/services/mod.rs +++ b/frontend/src/services/mod.rs @@ -7,6 +7,7 @@ mod playlist_service; mod websocket_service; mod toastr_service; mod event_service; +mod user_service; pub use self::auth_service::*; pub use self::config_service::*; @@ -16,4 +17,5 @@ pub use self::dialog_service::*; pub use self::playlist_service::*; pub use self::websocket_service::*; pub use self::toastr_service::*; -pub use self::event_service::*; \ No newline at end of file +pub use self::event_service::*; +pub use self::user_service::*; \ No newline at end of file diff --git a/frontend/src/services/requests.rs b/frontend/src/services/requests.rs index 2d3305bf5..3300fc6f6 100644 --- a/frontend/src/services/requests.rs +++ b/frontend/src/services/requests.rs @@ -1,4 +1,4 @@ -use crate::error::{Error, ErrorInfo}; +use crate::error::{Error, ErrorInfo, ErrorSetInfo}; use gloo_storage::{LocalStorage, Storage}; use log::error; use reqwasm::http::Request; @@ -76,12 +76,20 @@ where } } } + 400 => { + let data: Result = response.json::().await; + if let Ok(data) = data { + Err(Error::BadRequest(data.error)) + } else { + Err(Error::DeserializeError) + } + }, 401 => Err(Error::Unauthorized), 403 => Err(Error::Forbidden), 404 => Err(Error::NotFound), 500 => Err(Error::InternalServerError), 422 => { - let data: Result = response.json::().await; + let data: Result = response.json::().await; if let Ok(data) = data { Err(Error::UnprocessableEntity(data)) } else { diff --git a/frontend/src/services/user_service.rs b/frontend/src/services/user_service.rs new file mode 100644 index 000000000..5720e865e --- /dev/null +++ b/frontend/src/services/user_service.rs @@ -0,0 +1,64 @@ +use std::rc::Rc; +use log::error; +use shared::model::ProxyUserCredentialsDto; +use shared::utils::{concat_path, concat_path_leading_slash}; +use crate::error::Error; +use crate::services::{get_base_href, request_delete, request_post, request_put, EventService}; + +pub struct UserService { + user_path: String, + event_service: Rc, +} + +impl UserService { + pub fn new(event_service: Rc) -> Self { + let base_href = get_base_href(); + Self { + user_path: concat_path_leading_slash(&base_href, "api/v1/user"), + event_service, + } + } + + pub async fn create_user(&self, target: String, user: ProxyUserCredentialsDto) -> Result<(), Error> { + let path = concat_path(&self.user_path, &target); + match request_post::(&path, user, None, None).await { + Ok(()) => { Ok(()) }, + Err(err) => { + error!("{err}"); + Err(err) + } + } + } + + pub async fn update_user(&self, target: String, user: ProxyUserCredentialsDto) -> Result<(), Error> { + let path = concat_path(&self.user_path, &target); + self.event_service.set_config_change_message_blocked(true); + match request_put::(&path, user, None, None).await { + Ok(()) => { + self.event_service.set_config_change_message_blocked(false); + Ok(()) + }, + Err(err) => { + self.event_service.set_config_change_message_blocked(false); + error!("{err}"); + Err(err) + } + } + } + + pub async fn delete_user(&self, target: String, username: String) -> Result<(), Error> { + let path = concat_path(&concat_path(&self.user_path, &target), &username); + self.event_service.set_config_change_message_blocked(true); + match request_delete::<()>(&path, None, None).await { + Ok(()) => { + self.event_service.set_config_change_message_blocked(false); + Ok(()) + }, + Err(err) => { + self.event_service.set_config_change_message_blocked(false); + error!("{err}"); + Err(err) + } + } + } +} diff --git a/frontend/src/services/websocket_service.rs b/frontend/src/services/websocket_service.rs index 615e8b5c8..4e2c32d09 100644 --- a/frontend/src/services/websocket_service.rs +++ b/frontend/src/services/websocket_service.rs @@ -70,7 +70,9 @@ impl WebSocketService { event_service.broadcast(EventMessage::ServerStatus(data)); } ProtocolMessage::ConfigChangeResponse(config_type) => { - event_service.broadcast(EventMessage::ConfigChange(config_type)); + if !event_service.is_config_change_message_blocked() { + event_service.broadcast(EventMessage::ConfigChange(config_type)); + } } ProtocolMessage::ServerError(error) => { event_service.broadcast(EventMessage::ServerError(error));