user editor form

This commit is contained in:
euzu
2025-08-26 23:34:04 +02:00
parent 1fca84e762
commit 30a88bb2ae
63 changed files with 791 additions and 398 deletions
Generated
+1
View File
@@ -997,6 +997,7 @@ dependencies = [
"implicit-clone",
"js-sys",
"log",
"paste",
"prost",
"regex",
"reqwasm",
+2 -2
View File
@@ -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),
)
};
@@ -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,
})
+13 -13
View File
@@ -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<HdHomerunAppState>) -> Option<Device> {
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,
+1 -1
View File
@@ -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),
}
+1 -1
View File
@@ -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, String>(Bytes::from(
[line.to_string().as_bytes(), b"\n"].concat(),
[line.clone().as_bytes(), b"\n"].concat(),
))
}));
+3 -3
View File
@@ -20,7 +20,7 @@ fn get_categories_from_xtream(categories: Option<Vec<PlaylistXtreamCategory>>) -
let mut groups: Vec<String> = 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());
}
}
}
+97 -25
View File
@@ -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<Arc<AppState>>,
axum::extract::Json(mut users): axum::extract::Json<Vec<TargetUserDto>>,
axum::extract::Path(target_name): axum::extract::Path<String>,
axum::extract::Json(mut credential): axum::extract::Json<ProxyUserCredentialsDto>,
) -> 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<Arc<AppState>>,
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<Arc<AppState>>,
axum::extract::Json(cfg): axum::extract::Json<ConfigDto>,
@@ -361,7 +431,9 @@ pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc<AppState>, 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))
+3 -3
View File
@@ -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();
}
}
+4 -4
View File
@@ -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, String>(Bytes::from(if has_next {
format!("{line},")
} else {
line.to_string()
line.clone()
}))
}))
.chain(stream::once(async {
+2 -2
View File
@@ -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()) {
@@ -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 {
+4 -4
View File
@@ -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(),
},
+1 -1
View File
@@ -36,7 +36,7 @@ pub fn exec_scheduler(client: &Arc<reqwest::Client>, app_state: &Arc<AppState>,
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);
+1 -1
View File
@@ -49,7 +49,7 @@ fn send_pushover_message(client: &Arc<reqwest::Client>, 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)
+4 -4
View File
@@ -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(),
}
}
}
+10 -10
View File
@@ -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(),
}
}
+6 -6
View File
@@ -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(),
}
}
+4 -4
View File
@@ -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<PathBuf> = 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::<Vec<String>>(),
devices: hdhr.devices.iter().map(|d| d.name.clone()).collect::<Vec<String>>(),
})
}
}
@@ -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(),
+1 -1
View File
@@ -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(),
}
}
+1 -1
View File
@@ -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,
}
+9 -9
View File
@@ -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(),
+5 -5
View File
@@ -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),
+9 -9
View File
@@ -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(),
}
}
}
+2 -2
View File
@@ -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(),
}
+2 -2
View File
@@ -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()
}
}
+2 -2
View File
@@ -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(),
}
}
+2 -2
View File
@@ -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<String> = check_targets.iter().filter(|&(_, v)| *v == 0).map(|(k, _)| k.to_string()).collect();
let missing_targets: Vec<String> = 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(", "));
}
+7 -7
View File
@@ -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(),
+12 -12
View File
@@ -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
}
+3 -3
View File
@@ -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(),
+7 -7
View File
@@ -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<String, Value>>, 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());
}
}
}
+4 -4
View File
@@ -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
}
+3 -3
View File
@@ -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()))
}
+12 -12
View File
@@ -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<Option<Vec<PlaylistGroup>>, 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<String, XtreamCategory> =
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::<u32>().unwrap_or(0),
xtream_cluster,
title: category.category_name.to_string(),
title: category.category_name.clone(),
channels: category.channels.clone(),
}
}).collect()))
+1 -1
View File
@@ -12,7 +12,7 @@ pub fn process_group_watch(client: &Arc<reqwest::Client>, 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);
});
+5 -5
View File
@@ -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<Epg>, 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();
}
}
}
+4 -4
View File
@@ -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<reqwest::Client>, cfg: Arc<AppConfig>, 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<PlaylistGroup>) -> Vec<PlaylistGroup> {
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);
+3 -3
View File
@@ -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));
}
@@ -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)
})
}
+1 -1
View File
@@ -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");
}
}
+7 -7
View File
@@ -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()
}
}
+1 -1
View File
@@ -563,7 +563,7 @@ fn rewrite_xtream_vod_info<P>(
} 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()),
);
}
}
+2 -2
View File
@@ -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;
+1
View File
@@ -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"
+4 -1
View File
@@ -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": {
+109 -28
View File
@@ -81,6 +81,18 @@ macro_rules! config_field {
};
}
#[macro_export]
macro_rules! config_field_custom {
($label:expr, $value:expr) => {
html! {
<div class="tp__form-field tp__form-field__text">
<label>{$label}</label>
<span class="tp__form-field__value">{$value}</span>
</div>
}
};
}
#[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! {
<div class="tp__form-field tp__form-field__text">
@@ -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)
};
}));
})}
/>
</div>
@@ -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! {
<div class="tp__form-field tp__form-field__text">
@@ -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));
})}
/>
</div>
@@ -163,19 +181,17 @@ macro_rules! edit_field_text {
}};
}
//<Input label={translate.t("LABEL.PASSWORD")} input_ref={password_ref} input_type="password" name="password" autocomplete={false} onkeydown={handle_key_down.clone()}/>
#[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! {
<div class="tp__form-field tp__form-field__bool">
<label>{$label}</label>
<$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)))} />
</div>
}
}};
@@ -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! {
<div class="tp__form-field tp__form-field__number">
<$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<u32>| {
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! {
<div class="tp__form-field tp__form-field__date">
<$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<i64>| {
instance.borrow_mut().$field = value;
instance.dispatch($action(value));
})}
/>
</div>
}
}};
}
}
#[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<Self>, action: Self::Action) -> std::rc::Rc<Self> {
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
// }
}
};
}
+2 -1
View File
@@ -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::*;
pub use video_config_view::*;
pub use macros::*;
+6 -6
View File
@@ -26,10 +26,9 @@ pub fn DateInput(props: &DateInputProps) -> Html {
if let Some(input) = local_ref.cast::<HtmlInputElement>() {
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::<HtmlInputElement>() {
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);
+6 -13
View File
@@ -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::<web_sys::HtmlInputElement>() {
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, {
<IconButton name="hide" icon="Visibility" onclick={handle_hide_content} />
+57 -5
View File
@@ -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::<UserlistContext>().expect("Userlist context not found");
let playlist_ctx = use_context::<PlaylistContext>().expect("Playlist context not found");
let config_ctx = use_context::<ConfigContext>().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<Rc<TargetUser>> = 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 {
</div>
<div class="tp__userlist-edit__body tp__list-create__body">
<Card>
<ProxyUserCredentialsForm server={server.clone()} targets={targets.clone()} user={(*userlist_ctx.selected_user).clone()}/>
<ProxyUserCredentialsForm server={server.clone()} targets={targets.clone()} user={(*userlist_ctx.selected_user).clone()} on_save={handle_user_save}/>
</Card>
</div>
</div>
@@ -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! {
<div class="tp__proxy-type-input">
@@ -149,9 +128,9 @@ pub fn ProxyTypeInput(props: &ProxyTypeInputProps) -> Html {
<span class={classes!("tp__chip", "tp__chip__group", "tp__proxy-type-input__reverse" , if reverse {"active"} else {""})}>
<span onclick={handle_reverse_click}>{ translate.t("LABEL.REVERSE") }</span>
<span class={"tp__chip__group__sub tp__proxy-type-input__mixed"}>
<span onclick={handle_reverse_live_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-live", if reverse_live {"active"} else { if reverse {"redirect-active"} else {""} })}>{ translate.t("LABEL.LIVE_SHORT") }</span>
<span onclick={handle_reverse_vod_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-vod", if reverse_vod {"active"} else {if reverse {"redirect-active"} else {""} })}>{ translate.t("LABEL.VOD_SHORT") }</span>
<span onclick={handle_reverse_series_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-series", if reverse_series {"active"} else {if reverse {"redirect-active"} else {""} })}>{ translate.t("LABEL.SERIES_SHORT") }</span>
<span onclick={handle_reverse_live_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-live", if reverse_live {"active"} else if reverse {"redirect-active"} else {""})}>{ translate.t("LABEL.LIVE_SHORT") }</span>
<span onclick={handle_reverse_vod_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-vod", if reverse_vod {"active"} else if reverse {"redirect-active"} else {""})}>{ translate.t("LABEL.VOD_SHORT") }</span>
<span onclick={handle_reverse_series_click} class={classes!("tp__chip", "tp__proxy-type-input__reverse-series", if reverse_series {"active"} else if reverse {"redirect-active"} else {""})}>{ translate.t("LABEL.SERIES_SHORT") }</span>
</span>
</span>
</div>
@@ -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<String>,
Proxy => proxy: ProxyType,
Server => server: Option<String>,
Status => status: Option<ProxyUserStatus>,
MaxConnections => max_connections: u32,
ExpDate => exp_date: Option<i64>,
UiEnabled => ui_enabled: bool,
EpgTimeshift => epg_timeshift: Option<String>,
Comment => comment: Option<String>,
}
);
#[derive(Properties, PartialEq, Clone)]
pub struct ProxyUserCredentialsFormProps {
pub user: Option<Rc<TargetUser>>,
pub targets: Rc<Vec<Rc<ConfigTargetDto>>>,
pub server: Rc<Vec<ApiProxyServerInfoDto>>,
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::<Vec<Rc<DropDownOption>>>(),
);
let form_state: UseReducerHandle<UserFormState> = 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::<Vec<Rc<DropDownOption>>>(),
);
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! { <UserStatus status={Some(s.clone())} /> },
selected: user.as_ref().is_some_and(|user| user.credentials.status.as_ref() == Some(s)),
label: html! { <UserStatus status={Some(*s)} /> },
selected: user.data().status.as_ref() == Some(s),
})).collect::<Vec<Rc<DropDownOption>>>(),
);
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::<Vec<Rc<DropDownOption>>>(),
);
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::<Vec<Rc<DropDownOption>>>(),
);
{
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<String> = (*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! {
<div class="tp__proxy-user-credentials-form tp__form-page">
<div class="tp__proxy-user-credentials-form__body tp__form-page__body">
{ 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! { <Select name="target"
multi_select={false}
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
if let Some(target_option) = selections.first() {
set_selected_target.set(Some(target_option.id.clone()));
set_selected_target.set(Some(target_option.id.clone()));
} else {
set_selected_target.set(None);
}
})}
options={(*targets).clone()}
/>
}})}
}})}}
{ config_field_child!(translate.t("LABEL.STATUS"), {
html! { <Select name="status"
multi_select={false}
onselect={Callback::from(move |(_name, selections):(String, Vec<Rc<DropDownOption>>)| {
if let Some(status_option) = selections.first() {
if let Some(status) = status_option.id.parse::<ProxyUserStatus>().ok() {
instance_status.borrow_mut().status = Some(status);
if let Ok(status) = status_option.id.parse::<ProxyUserStatus>() {
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! {
<ProxyTypeInput value={props.user.as_ref().map_or_else(|| ProxyType::Reverse(None), |u| u.credentials.proxy)}
on_change={Callback::from(move |proxy_type: ProxyType|
instance_proxy.borrow_mut().proxy = proxy_type
<ProxyTypeInput value={form_state.data().proxy}
on_change={Callback::from(move |proxy_type: ProxyType| {
instance_proxy.dispatch(UserFormAction::Proxy(proxy_type));
}
)}/>
}})}
{ 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<Rc<DropDownOption>>)| {
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) }
</div>
<div class="tp__proxy-user-credentials-form__toolbar tp__form-page__toolbar">
@@ -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<Self, TuliproxError> {
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<Rc<Vec<Rc<TargetUser>>>>,
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::<DialogService>().expect("Dialog service not found");
let userlist_context = use_context::<UserlistContext>().expect("Userlist context not found");
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
@@ -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<Rc<TargetUser>> = 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 {
</div>
}
}
#[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<Self, TuliproxError> {
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)
}
}
}
@@ -16,29 +16,34 @@ pub fn UserlistView() -> Html {
let active_page = use_state(|| UserlistPage::List);
let selected_user= use_state(|| None::<Rc<TargetUser>>);
let filtered_user= use_state(|| None::<Rc<Vec<Rc<TargetUser>>>>);
let users = use_state(|| None::<Rc<Vec<Rc<TargetUser>>>>);
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(),
};
+3 -3
View File
@@ -30,13 +30,13 @@ pub struct TargetUser {
pub credentials: Rc<ProxyUserCredentialsDto>,
}
type TargetUserList = Option<Rc<Vec<Rc<TargetUser>>>>;
pub type TargetUserList = Option<Rc<Vec<Rc<TargetUser>>>>;
#[derive(Clone, PartialEq)]
pub struct UserlistContext {
pub selected_user: UseStateHandle<Option<Rc<TargetUser>>>,
pub filtered_users: UseStateHandle<TargetUserList>,
pub users: TargetUserList,
pub users: UseStateHandle<TargetUserList>,
pub active_page: UseStateHandle<UserlistPage>,
}
@@ -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(),
}
}
+11 -2
View File
@@ -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<String, Vec<String>>,
}
#[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")]
+4 -1
View File
@@ -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<AuthService>,
pub config: Rc<ConfigService>,
pub user: Rc<UserService>,
pub status: Rc<StatusService>,
pub event: Rc<EventService>,
pub playlist: Rc<PlaylistService>,
@@ -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
}
+11 -1
View File
@@ -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<HashMap<usize, Box<dyn Fn(EventMessage)>>>;
pub struct EventService {
subscriber_id: Rc<AtomicUsize>,
subscribers: Rc<Subscriber>,
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<F: Fn(EventMessage) + 'static>(&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));
+3 -1
View File
@@ -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::*;
pub use self::event_service::*;
pub use self::user_service::*;
+10 -2
View File
@@ -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<ErrorInfo, _> = response.json::<ErrorInfo>().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<ErrorInfo, _> = response.json::<ErrorInfo>().await;
let data: Result<ErrorSetInfo, _> = response.json::<ErrorSetInfo>().await;
if let Ok(data) = data {
Err(Error::UnprocessableEntity(data))
} else {
+64
View File
@@ -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<EventService>,
}
impl UserService {
pub fn new(event_service: Rc<EventService>) -> 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::<ProxyUserCredentialsDto, ()>(&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::<ProxyUserCredentialsDto, ()>(&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)
}
}
}
}
+3 -1
View File
@@ -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));