mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-09 09:22:18 +02:00
user editor form
This commit is contained in:
Generated
+1
@@ -997,6 +997,7 @@ dependencies = [
|
||||
"implicit-clone",
|
||||
"js-sys",
|
||||
"log",
|
||||
"paste",
|
||||
"prost",
|
||||
"regex",
|
||||
"reqwasm",
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}));
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(", "));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user