This commit is contained in:
euzu
2025-07-08 18:23:03 +02:00
parent 2f26468499
commit b65eb87417
30 changed files with 399 additions and 121 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use log::{debug, error, log_enabled, trace};
use reqwest::StatusCode;
use shared::model::{InputFetchMethod, PlaylistEntry, PlaylistItemType, TargetType, UserConnectionPermission, XtreamCluster};
use shared::utils::{default_grace_period_millis, human_readable_byte_size};
use shared::utils::{default_grace_period_millis, human_readable_byte_size, trim_slash};
use shared::utils::{extract_extension_from_url, replace_url_extension, sanitize_sensitive_info, DASH_EXT, HLS_EXT};
use std::borrow::Cow;
use std::collections::HashMap;
@@ -439,7 +439,7 @@ where
if self.action_path.is_empty() {
format!("{provider_id}{extension}")
} else {
format!("{}/{provider_id}{extension}", self.action_path)
format!("{}/{provider_id}{extension}", trim_slash(self.action_path))
}
}
}
+11 -9
View File
@@ -20,7 +20,7 @@ use shared::error::info_err;
use shared::error::{str_to_io_error, TuliproxError, TuliproxErrorKind};
use crate::utils::trace_if_enabled;
use crate::utils::xtream::create_vod_info_from_item;
use shared::utils::{extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, sanitize_sensitive_info, HLS_EXT};
use shared::utils::{extract_extension_from_url, generate_playlist_uuid, get_u32_from_serde_value, hex_encode, sanitize_sensitive_info, trim_slash, HLS_EXT};
use crate::utils::{request, xtream};
use crate::auth::Fingerprint;
use axum::http::{HeaderMap, StatusCode};
@@ -150,14 +150,16 @@ pub(in crate::api) fn get_xtream_player_api_stream_url(
| ApiStreamContext::Series
| ApiStreamContext::Timeshift => context.to_string()
};
let ctx_path = if ctx.is_empty() { String::new() } else { format!("{ctx}/") };
Some(format!("{}/{}{}/{}/{}",
&input_user_info.base_url,
ctx_path,
&input_user_info.username,
&input_user_info.password,
action_path
))
let mut parts = vec![
trim_slash(&input_user_info.base_url),
trim_slash(&ctx),
trim_slash(&input_user_info.username),
trim_slash(&input_user_info.password),
trim_slash(action_path),
];
parts.retain(|s| !s.is_empty());
Some(parts.join("/"))
} else if !fallback_url.is_empty() {
Some(String::from(fallback_url))
} else {
+3 -2
View File
@@ -2,7 +2,7 @@ use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKi
use crate::model::ConfigInput;
use shared::model::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
use crate::model::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream};
use shared::utils::generate_playlist_uuid;
use shared::utils::{generate_playlist_uuid, trim_last_slash};
use crate::utils::xtream::{get_xtream_stream_url_base};
use serde_json::Value;
use std::collections::HashMap;
@@ -80,6 +80,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str,
username: &str, password: &str,
stream_id: u32, container_extension: Option<&String>,
live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String {
let url = trim_last_slash(url);
let stream_base_url = match xtream_cluster {
XtreamCluster::Live => {
let ctx_path = if live_stream_use_prefix { "live/" } else { "" };
@@ -91,7 +92,7 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str,
format!("{url}/movie/{username}/{password}/{stream_id}.{ext}")
}
XtreamCluster::Series =>
format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url, username, password), crate::model::XC_ACTION_GET_SERIES_INFO)
format!("{}&action={}&series_id={stream_id}", get_xtream_stream_url_base(url.as_ref(), username, password), crate::model::XC_ACTION_GET_SERIES_INFO)
};
stream_base_url
}
+2 -2
View File
@@ -9,7 +9,7 @@ use std::io::{BufRead, Cursor, Error};
use std::path::PathBuf;
use url::Url;
use shared::model::InputType;
use shared::utils::get_credentials_from_url;
use shared::utils::{get_credentials_from_url, trim_last_slash};
const CSV_SEPARATOR: char = ';';
const HEADER_PREFIX: char = '#';
@@ -33,7 +33,7 @@ fn csv_assign_mandatory_fields(alias: &mut ConfigInputAlias, input_type: InputTy
alias.url = url.origin().ascii_serialization().to_string();
} else if input_type == InputType::M3uBatch && alias.username.is_some() && alias.password.is_some() {
alias.url = format!("{}/get.php?username={}&password={}&type=m3u_plus",
url.origin().ascii_serialization(),
trim_last_slash(&url.origin().ascii_serialization()),
alias.username.as_deref().unwrap_or(""),
alias.password.as_deref().unwrap_or("")
);
+2 -1
View File
@@ -4,6 +4,7 @@ use reqwest::header::{HeaderMap, HeaderValue};
use std::sync::Arc;
use log::{debug, info};
use shared::error::{info_err, TuliproxErrorKind};
use shared::utils::trim_last_slash;
use super::errors::{handle_trakt_api_error};
pub struct TraktClient {
@@ -34,7 +35,7 @@ impl TraktClient {
}
fn build_list_url(&self, user: &str, list_slug: &str) -> String {
format!("{}/users/{user}/lists/{list_slug}/items", self.api_config.url)
format!("{}/users/{user}/lists/{list_slug}/items", trim_last_slash(&self.api_config.url))
}
pub async fn get_list_items(&self, list_config: &TraktListConfig) -> Result<Vec<TraktListItem>, TuliproxError> {
+5 -2
View File
@@ -5,7 +5,7 @@ use enum_iterator::Sequence;
use crate::{check_input_credentials, create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err};
use crate::error::{TuliproxError, TuliproxErrorKind};
use crate::model::{EpgConfigDto};
use crate::utils::{default_as_true, get_base_url_from_str, get_credentials_from_url_str, get_trimmed_string, sanitize_sensitive_info};
use crate::utils::{default_as_true, get_base_url_from_str, get_credentials_from_url_str, get_trimmed_string, sanitize_sensitive_info, trim_last_slash};
use log::debug;
@@ -209,7 +209,10 @@ impl ConfigInputDto {
} else {
let base_url = get_base_url_from_str(&self.url);
if base_url.is_some() {
let provider_epg_url = format!("{}/xmltv.php?username={}&password={}", base_url.unwrap_or_default(), username.unwrap_or_default(), password.unwrap_or_default());
let provider_epg_url = format!("{}/xmltv.php?username={}&password={}",
trim_last_slash(&base_url.unwrap_or_default()),
username.unwrap_or_default(),
password.unwrap_or_default());
Ok(provider_epg_url)
} else {
Err(format!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, sanitize_sensitive_info(&self.url)))
+11
View File
@@ -1,3 +1,4 @@
use std::fmt::Display;
use crate::error::{TuliproxError, TuliproxErrorKind};
use crate::foundation::filter::{apply_templates_to_pattern, apply_templates_to_pattern_single};
use crate::model::{ItemField, PatternTemplate, TemplateValue};
@@ -24,6 +25,16 @@ pub enum SortOrder {
Desc,
}
impl Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
SortOrder::Asc => "asc".to_string(),
SortOrder::Desc => "desc".to_string(),
};
write!(f, "{}", str)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfigSortGroupDto {
+19
View File
@@ -1,3 +1,5 @@
use std::borrow::Cow;
pub trait Capitalize {
fn capitalize(&self) -> String;
}
@@ -63,6 +65,23 @@ pub fn get_non_empty_str<'a>(first: &'a str, second: &'a str, third: &'a str) ->
}
}
pub fn trim_slash(s: &str) -> Cow<str> {
let trimmed = s.trim_matches('/');
if trimmed.len() == s.len() {
Cow::Borrowed(s) // Keine Änderung → kein Clone
} else {
Cow::Owned(trimmed.to_string()) // Änderung → neue String
}
}
pub fn trim_last_slash(s: &str) -> Cow<str> {
if s.ends_with('/') {
Cow::Owned(s[..s.len() - 1].to_string())
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
+6 -1
View File
@@ -158,7 +158,12 @@
"CLEANUP": "Cleanup",
"PROPERTIES": "Properties",
"DEVICE": "Device",
"USE_OUTPUT": "Use output"
"USE_OUTPUT": "Use output",
"MATCH_AS_ASCII": "Match as ascii",
"SORT_SETTINGS": "Sort Settings",
"ORDER": "Order",
"SEQUENCE": "Sequence",
"GROUPS": "Groups"
},
"TABLE": {
"EMPTY": "",
+5
View File
@@ -65,6 +65,11 @@ option {
fill: var(--attention-color) !important;
}
.tp__update_action {
color: var(--active-color) !important;
fill: var(--active-color) !important;
}
.svg-icon {
pointer-events: none;
width: 1rem;
+3
View File
@@ -19,6 +19,7 @@
@forward "components/playlist/playlist_view";
@forward "components/playlist/assistant/playlist_assistant";
@forward "components/reveal_content";
@forward "components/playlist/target/target_common";
@forward "components/playlist/target/target_watch";
@forward "components/playlist/target/target_options";
@forward "components/playlist/target/target_output";
@@ -27,6 +28,8 @@
@forward "components/playlist/target/strm_output";
@forward "components/playlist/target/hdhomerun_output";
@forward "components/playlist/target/target_table";
@forward "components/playlist/target/target_sort";
@forward "components/playlist/target/target_rename";
@forward "components/playlist/playlist_mappings";
@forward "components/playlist/playlist_processing";
@forward "components/tag_list";
@@ -9,4 +9,9 @@
box-shadow: var(--popup-shadow);
z-index: 9999;
hr {
border: none;
height: 2px;
background-color: var(--border-color);
}
}
@@ -0,0 +1,41 @@
.tp__target-common {
display: flex;
justify-content: center;
flex-flow: column;
gap: var(--gap-small);
box-sizing: border-box;
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding-micro) var(--padding-mini);
background-color: var(--table-card-background-color);
overflow: hidden;
.tp__tag_list {
flex-flow: row nowrap;
}
li {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__row {
flex-flow: row;
}
&__label {
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
@@ -11,48 +11,6 @@
}
}
.tp__target-output__output {
display: flex;
justify-content: center;
flex-flow: column;
gap: var(--gap-small);
box-sizing: border-box;
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding-micro) var(--padding-mini);
background-color: var(--table-card-background-color);
overflow: hidden;
.tp__tag_list {
flex-flow: row nowrap;
}
li {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__row {
flex-flow: row;
}
&__label {
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
.tp__target-output__xtream.tp__target-output__has_options .tp__collapse-panel__header {
color: var(--output-xtream-color);
}
@@ -0,0 +1,41 @@
.tp__target-rename {
display: flex;
justify-content: center;
flex-flow: column;
gap: var(--gap-small);
box-sizing: border-box;
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--padding-micro) var(--padding-mini);
background-color: var(--table-card-background-color);
overflow: hidden;
.tp__tag_list {
flex-flow: row nowrap;
}
li {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__row {
flex-flow: row;
}
&__label {
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
@@ -0,0 +1,49 @@
.tp__target-sort {
display: flex;
justify-content: center;
flex-flow: column;
gap: var(--gap-small);
box-sizing: border-box;
h2 {
margin-top: 0;
}
.tp__target-sort__sequence {
max-height: 300px;
overflow: auto;
}
&__section {
display: flex;
flex-flow: column;
gap: var(--gap-small);
padding: var(--padding-micro) var(--padding-mini);
overflow: hidden;
//border: 1px solid var(--border-color);
//border-radius: var(--border-radius);
.tp__tag_list {
flex-flow: row nowrap;
}
li {
overflow: hidden;
text-overflow: ellipsis;
}
}
&__row {
flex-flow: row;
}
&__label {
font-weight: bold;
white-space: nowrap;
vertical-align: middle;
color: var(--modest-text-color);
&:after {
content: ":";
}
}
}
@@ -11,17 +11,17 @@ pub struct HdHomeRunOutputProps {
pub fn HdHomeRunOutput(props: &HdHomeRunOutputProps) -> Html {
let translator = use_translation();
html! {
<div class="tp__hdhomerun-output tp__target-output__output">
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.DEVICE")}</span>
<div class="tp__hdhomerun-output tp__target-common">
<div class="tp__target-common__section tp__target-common__row">
<span class="tp__target-common__label">{translator.t("LABEL.DEVICE")}</span>
<span>{ props.output.device.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USERNAME")}</span>
<div class="tp__target-common__section tp__target-common__row">
<span class="tp__target-common__label">{translator.t("LABEL.USERNAME")}</span>
<span>{ props.output.username.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USE_OUTPUT")}</span>
<div class="tp__target-common__section tp__target-common__row">
<span class="tp__target-common__label">{translator.t("LABEL.USE_OUTPUT")}</span>
<span>{ props.output.use_output.map_or_else(String::new, |o| o.to_string()) }</span>
</div>
</div>
@@ -27,12 +27,12 @@ pub fn M3uOutput(props: &M3uOutputProps) -> Html {
html! {
<div class="tp__m3u-output tp__target-output__output">
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.FILENAME")}</span>
<div class="tp__m3u-output tp__target-common">
<div class="tp__target-common__section">
<span class="tp__target-common__label">{translator.t("LABEL.FILENAME")}</span>
{ props.output.filename.as_ref().map(|f| html! {<span>{ f }</span>}) }
</div>
<div class="tp__target-output__output__section">
<div class="tp__target-common__section">
<TagList tags={(*tags).clone()} />
</div>
</div>
@@ -6,6 +6,7 @@ mod target_output;
mod target_options;
mod target_watch;
mod target_sort;
mod target_rename;
pub use self::xtream_output::*;
pub use self::m3u_output::*;
@@ -15,3 +16,5 @@ pub use self::target_output::*;
pub use self::target_options::*;
pub use self::target_watch::*;
pub use self::target_sort::*;
pub use self::target_rename::*;
@@ -26,24 +26,24 @@ pub fn StrmOutput(props: &StrmOutputProps) -> Html {
};
html! {
<div class="tp__strm-output tp__target-output__output">
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.DIRECTORY")}</span>
<div class="tp__strm-output tp__target-common">
<div class="tp__target-common__section tp__target-common__row">
<span class="tp__target-common__label">{translator.t("LABEL.DIRECTORY")}</span>
<span>{ props.output.directory.clone() }</span>
</div>
<div class="tp__target-output__output__section tp__target-output__output__row">
<span class="tp__target-output__output__label">{translator.t("LABEL.USERNAME")}</span>
<div class="tp__target-common__section tp__target-common__row">
<span class="tp__target-common__label">{translator.t("LABEL.USERNAME")}</span>
{ props.output.username.as_ref().map(|f| html! {<span>{ f }</span>}) }
</div>
<div class="tp__target-output__output__section">
<div class="tp__target-common__section">
<TagList tags={(*tags).clone()} />
</div>
{
match props.output.strm_props.as_ref() {
None => html! {},
Some(props) => html! {
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.PROPERTIES")}</span>
<div class="tp__target-common__section">
<span class="tp__target-common__label">{translator.t("LABEL.PROPERTIES")}</span>
<ul>
{ props.iter().map(|p| html! { <li>{p}</li> }).collect::<Html>() }
</ul>
@@ -0,0 +1,22 @@
use shared::model::{ConfigTargetDto};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct TargetRenameProps {
pub target: Rc<ConfigTargetDto>,
}
#[function_component]
pub fn TargetRename(props: &TargetRenameProps) -> Html {
let translate = use_translation();
match props.target.sort.as_ref() {
None => html! {},
Some(sort) => html! {
<div class="tp__target-sort">
{"Sort"}
</div>
}
}
}
@@ -1,4 +1,5 @@
use shared::model::{ConfigTargetDto};
use crate::app::components::{convert_bool_to_chip_style, Card, Chip};
use shared::model::{ConfigTargetDto, ItemField, SortOrder};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
@@ -10,13 +11,85 @@ pub struct TargetSortProps {
#[function_component]
pub fn TargetSort(props: &TargetSortProps) -> Html {
let translate = use_translation();
match props.target.sort.as_ref() {
let translator = use_translation();
let sort = match props.target.sort.as_ref() {
Some(s) => s,
None => return html! {},
};
let groups_html = match sort.groups.as_ref() {
Some(groups) => {
match groups.sequence.as_ref() {
Some(seq) => html! {
<Card>
<h2>{translator.t("LABEL.GROUPS")}</h2>
<div class="tp__target-sort__section tp__target-sort__row">
<span class="tp__target-sort__label">{translator.t("LABEL.ORDER")}</span>
<span>{ groups.order.to_string() }</span>
</div>
<div class="tp__target-sort__section tp__target-sort__row">
<span class="tp__target-sort__label">{translator.t("LABEL.SEQUENCE")}</span>
<span class="tp__target-sort__sequence">
<ul>
{ for seq.iter().map(|p| html! { <li>{p}</li> }) }
</ul>
</span>
</div>
</Card>
},
None => html! {},
}
},
None => html! {},
Some(watch) => html! {
<div class="tp__target-sort">
{"Sort"}
};
html! {
<div class="tp__target-sort">
<h2>{translator.t("LABEL.SORT_SETTINGS")}</h2>
<div class="tp__target-sort__section tp__target-sort__row">
<Chip class={ convert_bool_to_chip_style(sort.match_as_ascii) }
label={translator.t("LABEL.MATCH_AS_ASCII")} />
</div>
}
{ groups_html }
</div>
}
}
}
// </div>
// <div class="tp__target-output__output__section tp__target-output__output__row">
// <span class="tp__target-output__output__label">{translator.t("LABEL.USERNAME")}</span>
// <span>{ props.output.username.clone() }</span>
// </div>
// <div class="tp__target-output__output__section tp__target-output__output__row">
// <span class="tp__target-output__output__label">{translator.t("LABEL.USE_OUTPUT")}</span>
// <span>{ props.output.use_output.map_or_else(String::new, |o| o.to_string()) }</span>
// </div>
// </div>
// pub match_as_ascii: bool,
// pub groups: Option<ConfigSortGroupDto>,
// pub channels: Option<Vec<ConfigSortChannelDto>>,
//
// #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
// #[serde(deny_unknown_fields)]
// pub struct ConfigSortGroupDto {
// pub order: SortOrder,
// #[serde(default, skip_serializing_if = "Option::is_none")]
// pub sequence: Option<Vec<String>>,
// #[serde(skip)]
// pub t_sequence: Option<Vec<Regex>>,
// }
// pub struct ConfigSortChannelDto {
// // channel field
// pub field: ItemField,
// // match against group title
// pub group_pattern: String,
// pub order: SortOrder,
// #[serde(default, skip_serializing_if = "Option::is_none")]
// pub sequence: Option<Vec<String>>,
// #[serde(skip)]
// pub t_sequence: Option<Vec<Regex>>,
// }
@@ -1,7 +1,6 @@
use shared::model::{ConfigTargetDto};
use std::rc::Rc;
use yew::prelude::*;
use yew_i18n::use_translation;
#[derive(Properties, Clone, PartialEq, Debug)]
pub struct TargetWatchProps {
@@ -10,7 +9,6 @@ pub struct TargetWatchProps {
#[function_component]
pub fn TargetWatch(props: &TargetWatchProps) -> Html {
let translate = use_translation();
match props.target.watch.as_ref() {
None => html! {},
Some(watch) => html! {
@@ -40,26 +40,15 @@ pub fn XtreamOutput(props: &XtreamOutputProps) -> Html {
};
html! {
<div class="tp__xtream-output tp__target-output__output">
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
<div class="tp__xtream-output tp__target-common">
<div class="tp__target-common__section">
<span class="tp__target-common__label">{translator.t("LABEL.SKIP_DIRECT_SOURCE")}</span>
<TagList tags={(*tags_skip_direct_source).clone()} />
</div>
<div class="tp__target-output__output__section">
<span class="tp__target-output__output__label">{translator.t("LABEL.RESOLVE")}</span>
<div class="tp__target-common__section">
<span class="tp__target-common__label">{translator.t("LABEL.RESOLVE")}</span>
<TagList tags={(*tags_resolve).clone()} />
</div>
</div>
}
}
/*
<div class="tp__tp__target-output_row"><label>{translate.t("LABEL.RESOVE")}</label></div>
#[serde(default = "default_as_true")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trakt: Option<TraktConfigDto>,
<span>{"xc"}</span>
},
*/
@@ -1,12 +1,13 @@
use std::fmt::Display;
use crate::app::components::popup_menu::PopupMenu;
use crate::app::components::reveal_content::RevealContent;
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, PlaylistMappings, PlaylistProcessing, Table, TableDefinition, TargetOptions, TargetOutput, TargetSort, TargetWatch};
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, PlaylistMappings, PlaylistProcessing, Table, TableDefinition, TargetOptions, TargetOutput, TargetRename, TargetSort, TargetWatch};
use crate::hooks::use_service_context;
use shared::model::{ConfigTargetDto};
use std::future;
use std::rc::Rc;
use std::str::FromStr;
use log::info;
use yew::platform::spawn_local;
use yew::prelude::*;
use yew::suspense::use_future;
@@ -97,9 +98,9 @@ pub fn TargetTable() -> Html {
2 => html! { &dto.name.to_string() },
3 => html! { <TargetOutput target={Rc::clone(&dto)} /> },
4 => html! { <TargetOptions target={Rc::clone(&dto)} /> },
5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { <RevealContent>{format!("{s:?}")}</RevealContent> }),
5 => dto.sort.as_ref().map_or_else(|| html! {}, |s| html! { <RevealContent><TargetSort target={Rc::clone(&dto)} /></RevealContent> }),
6 => html! { &dto.filter.clone() },
7 => html! { <RevealContent><TargetSort target={Rc::clone(&dto)} /></RevealContent> },
7 => dto.rename.as_ref().map_or_else(|| html! {}, |r| html! { <RevealContent><TargetRename target={Rc::clone(&dto)} /></RevealContent> }),
8 => html! { <PlaylistMappings mappings={dto.mapping.clone()} /> },
9 => html! { <PlaylistProcessing order={dto.processing_order} /> },
10 => html! { <TargetWatch target={Rc::clone(&dto)} /> },
@@ -146,14 +147,27 @@ pub fn TargetTable() -> Html {
});
}
let handle_menu_edit = {
let handle_menu_click = {
let popup_is_open_state = popup_is_open.clone();
let confirm = dialog.clone();
let translate = translate.clone();
let services_ctx = services.clone();
let selected_dto = selected_dto.clone();
Callback::from(move |name:String| {
if let Ok(action) = TableAction::from_str(&name) {
match action {
TableAction::Edit => {}
TableAction::Refresh => {
let services_ctx = services_ctx.clone();
let dto_name = selected_dto.as_ref().map_or_else(String::new, |d| d.name.to_string());
spawn_local(async move {
let targets = vec![dto_name.as_str()];
match services_ctx.playlist.update_targets(&targets).await {
true => { info!("Ok"); }
false => { info!("not ok"); }
}
});
}
TableAction::Delete => {
let confirm = confirm.clone();
let translator = translate.clone();
@@ -178,8 +192,10 @@ pub fn TargetTable() -> Html {
<>
<Table::<ConfigTargetDto> definition={(*table_definition).as_ref().unwrap().clone()} />
<PopupMenu is_open={*popup_is_open} anchor_ref={(*popup_anchor_ref).clone()} on_close={handle_popup_close}>
<MenuItem icon="Edit" name={TableAction::Edit.to_string()} label={translate.t("LABEL.EDIT")} onclick={&handle_menu_edit}></MenuItem>
<MenuItem icon="Delete" name={TableAction::Delete.to_string()} label={translate.t("LABEL.DELETE")} onclick={&handle_menu_edit} style="tp__delete_action"></MenuItem>
<MenuItem icon="Edit" name={TableAction::Edit.to_string()} label={translate.t("LABEL.EDIT")} onclick={&handle_menu_click}></MenuItem>
<MenuItem icon="Refresh" name={TableAction::Refresh.to_string()} label={translate.t("LABEL.REFRESH")} onclick={&handle_menu_click} style="tp__update_action"></MenuItem>
<hr/>
<MenuItem icon="Delete" name={TableAction::Delete.to_string()} label={translate.t("LABEL.DELETE")} onclick={&handle_menu_click} style="tp__delete_action"></MenuItem>
</PopupMenu>
</>
}
@@ -197,6 +213,7 @@ pub fn TargetTable() -> Html {
#[derive(Debug, Clone, Eq, PartialEq)]
enum TableAction {
Edit,
Refresh,
Delete,
}
@@ -204,6 +221,7 @@ 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",
})
}
@@ -215,6 +233,8 @@ impl FromStr for TableAction {
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 {
+1 -1
View File
@@ -28,7 +28,7 @@ pub fn RevealContent(props: &RevealContentProps) -> Html {
let actions = actions.clone();
let dlg = dialog.clone();
spawn_local(async move {
let result = dlg.content(content, actions).await;
let _result = dlg.content(content, actions).await;
});
})
};
+5 -2
View File
@@ -1,12 +1,13 @@
use std::rc::Rc;
use yew::prelude::*;
use crate::model::WebConfig;
use crate::services::{AuthService, ConfigService, StatusService};
use crate::services::{AuthService, ConfigService, PlaylistService, StatusService};
pub struct Services {
pub auth: Rc<AuthService>,
pub config: Rc<ConfigService>,
pub status: Rc<StatusService>,
pub playlist: Rc<PlaylistService>,
}
impl Services {
@@ -14,10 +15,12 @@ impl Services {
let auth = Rc::new(AuthService::new());
let config = Rc::new(ConfigService::new(config));
let status = Rc::new(StatusService::new());
let playlist = Rc::new(PlaylistService::new());
Self {
auth,
config,
status
status,
playlist
}
}
}
+2
View File
@@ -3,9 +3,11 @@ mod config_service;
mod requests;
mod status_service;
mod dialog_service;
mod playlist_service;
pub use self::auth_service::*;
pub use self::config_service::*;
pub use self::requests::*;
pub use self::status_service::*;
pub use self::dialog_service::*;
pub use self::playlist_service::*;
+22
View File
@@ -0,0 +1,22 @@
use crate::services::{request_post};
use std::rc::Rc;
use log::error;
const TARGET_UPDATE_API_PATH: &str = "/api/v1/playlist/update";
pub struct PlaylistService {
}
impl PlaylistService {
pub fn new() -> Self {
Self {
}
}
pub async fn update_targets(&self, targets: &[&str]) -> bool {
request_post::<&[&str], ()>(TARGET_UPDATE_API_PATH, targets).await.map_or_else(|err| {
error!("{err}");
false
}, |_| true)
}
}
+8 -6
View File
@@ -44,16 +44,18 @@ where
request = request.header("Authorization", format!("Bearer {token}").as_str());
}
match request.send().await {
Ok(response) => {
match response.status() {
200 => {
let data: Result<T, _> = response.json::<T>().await;
if let Ok(data) = data {
// debug!("Response: {:?}", data);
Ok(data)
if std::any::TypeId::of::<T>() == std::any::TypeId::of::<()>() {
Ok(serde_json::from_str("null").unwrap()) // `T = ()` valid
} else {
Err(Error::DeserializeError)
let data: Result<T, _> = response.json::<T>().await;
if let Ok(data) = data {
Ok(data)
} else {
Err(Error::DeserializeError)
}
}
},
401 => Err(Error::Unauthorized),