mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-08 08:52:05 +02:00
webui
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
);
|
||||
|
||||
@@ -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,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)))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user