Merge remote-tracking branch 'upstream/master'

This commit is contained in:
knylbyte
2025-08-14 13:31:13 +00:00
11 changed files with 195 additions and 19 deletions
+14 -1
View File
@@ -228,7 +228,20 @@
"FIRMWARE_VERSION": "Firmware Version",
"DEVICE_TYPE": "Device Type",
"DEVICE_UDN": "Device UDN",
"TUNER_COUNT": "Tuner Count"
"TUNER_COUNT": "Tuner Count",
"SOURCE": "Source",
"PRIORITY": "Priority",
"LOGO_OVERRIDE": "logo Override",
"SMART_EPG": "Smart Epg",
"DEFAULT_EPG": "Normal Epg",
"EPG_SMART_MATCH": "EPG Smart Match",
"FUZZY_MATCHING": "Fuzzy Matching",
"MATCH_THRESHOLD": "Match Threshold",
"BEST_MATCH_THRESHOLD": "Best Match Threshold",
"NORMALIZE_REGEX": "Normalize Regex",
"NAME_PREFIX": "Name Prefix",
"NAME_PREFIX_SEPARATOR":"Name Prefix Separator",
"STRIP": "Strip"
},
"TABLE": {
"EMPTY": "",
@@ -8,7 +8,10 @@
&:hover {
fill: var(--text-color);
cursor: pointer;
filter: brightness(1.2);
}
.svg-icon {
width: 1.2rem;
height: 1.2rem;
@@ -22,6 +25,7 @@
text-overflow: ellipsis;
max-height: 2rem;
max-width: var(--max-table-cell-width);
pointer-events: none;
}
}
+1 -1
View File
@@ -83,7 +83,7 @@ macro_rules! config_field {
#[macro_export]
macro_rules! config_field_child {
($config:expr, $label:expr, $body:block) => {
($label:expr, $body:block) => {
html! {
<div class="tp__config-field tp__config-field__text">
<label>{$label}</label>
@@ -16,7 +16,7 @@ pub fn MessagingConfigView() -> Html {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.TELEGRAM")}</h1>
{ config_field!(entry, translate.t("LABEL.BOT_TOKEN"), bot_token) }
{ config_field_child!(entry, translate.t("LABEL.CHAT_ID"), {
{ config_field_child!(translate.t("LABEL.CHAT_ID"), {
html! {
<div class="tp__config-view__tags">
{
@@ -102,7 +102,7 @@ pub fn MessagingConfigView() -> Html {
html! {
<>
<div class="tp__messaging-config-view__header tp__config-view-page__header">
{ config_field_child!(entry, translate.t("LABEL.NOTIFY_ON"), {
{ config_field_child!(translate.t("LABEL.NOTIFY_ON"), {
html! { <div class="tp__messaging-config-view__notify-on">
{ for messaging.notify_on.iter().map(|t| html! { <Chip label={t.to_string()} /> }) }
</div> }
@@ -20,7 +20,7 @@ pub fn SchedulesConfigView() -> Html {
{ for schedules.iter().map(|entry| html! {
<div class="tp__schedules-config-view__schedule">
{ config_field!(entry, translate.t("LABEL.SCHEDULE"), schedule) }
{ config_field_child!(entry, translate.t("LABEL.TARGETS"), {
{ config_field_child!(translate.t("LABEL.TARGETS"), {
html! { <div class="tp__config-view__tags">
{ match entry.targets.as_ref() {
None => html! { <Chip label={translate.t("LABEL.ALL")} /> },
@@ -12,7 +12,7 @@ pub fn VideoConfigView() -> Html {
let render_extensions = |extensions: &Vec<String>| html! {
<Card>
{ config_field_child!(entry, translate.t("LABEL.EXTENSIONS"), {
{ config_field_child!(translate.t("LABEL.EXTENSIONS"), {
html! {
<div class="tp__config-view__tags">
{
@@ -31,7 +31,7 @@ pub fn VideoConfigView() -> Html {
{ config_field_bool!(entry, translate.t("LABEL.ORGANIZE_INTO_DIRECTORIES"), organize_into_directories) }
{ config_field_optional!(entry, translate.t("LABEL.DIRECTORY"), directory) }
{ config_field_optional!(entry, translate.t("LABEL.EPISODE_PATTERN"), episode_pattern) }
{ config_field_child!(entry.headers, translate.t("LABEL.HEADERS"), {
{ config_field_child!(translate.t("LABEL.HEADERS"), {
html! {
<div class="tp__config-view__tags">
<ul>
@@ -0,0 +1,135 @@
use crate::app::components::{Card, Chip, NoContent};
use crate::{config_field, config_field_bool, config_field_child, config_field_optional};
use shared::model::{EpgConfigDto, EpgSmartMatchConfigDto, EpgSourceDto};
use yew::prelude::*;
use yew_i18n::use_translation;
#[derive(Properties, PartialEq, Clone)]
pub struct EpgConfigViewProps {
#[prop_or_default]
pub epg: Option<EpgConfigDto>
}
#[function_component]
pub fn EpgConfigView(props: &EpgConfigViewProps) -> Html {
let translate = use_translation();
let render_sources = |sources: Option<&Vec<EpgSourceDto>>| {
let render_empty_sources = || html! {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.SOURCES")}</h1>
<NoContent />
</Card>
};
if let Some(sources) = sources {
if sources.is_empty() {
render_empty_sources()
} else {
html!{
for sources.iter().map(|entry| html! {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.SOURCE")}</h1>
{ config_field!(entry, translate.t("LABEL.URL"), url) }
{ config_field!(entry, translate.t("LABEL.PRIORITY"), priority) }
{ config_field_bool!(entry, translate.t("LABEL.LOGO_OVERRIDE"), logo_override) }
</Card>
})}
}
} else {
render_empty_sources()
}
};
let render_smart_match = |epg_smart_match: Option<&EpgSmartMatchConfigDto>| {
let render_empty_smart_match = || html! {
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.EPG_SMART_MATCH")}</h1>
<NoContent />
</Card>
};
if let Some(entry) = epg_smart_match {
html!{
<Card class="tp__config-view__card">
<h1>{translate.t("LABEL.EPG_SMART_MATCH")}</h1>
{ config_field_bool!(entry, translate.t("LABEL.ENABLED"), enabled) }
{ config_field_bool!(entry, translate.t("LABEL.FUZZY_MATCHING"), fuzzy_matching) }
{ config_field!(entry, translate.t("LABEL.MATCH_THRESHOLD"), match_threshold) }
{ config_field!(entry, translate.t("LABEL.BEST_MATCH_THRESHOLD"), best_match_threshold) }
{ config_field_optional!(entry, translate.t("LABEL.NORMALIZE_REGEX"), normalize_regex) }
{ config_field!(entry, translate.t("LABEL.NAME_PREFIX"), name_prefix) }
{ config_field_child!(translate.t("LABEL.NAME_PREFIX_SEPARATOR"), {
html! {
<div class="tp__config-view__tags">
{
if let Some(name_prefixe_seperators) = entry.name_prefix_separator.as_ref() {
if name_prefixe_seperators.is_empty() {
html! {}
} else {
html! {
for name_prefixe_seperators.iter().map(|value| html! {
<Chip label={value.to_string()} />
})
}
}
} else {
html! {}
}
}
</div>
}
})}
{ config_field_child!(translate.t("LABEL.STRIP"), {
html! {
<div class="tp__config-view__tags">
{
if let Some(strip) = entry.strip.as_ref() {
if strip.is_empty() {
html! {}
} else {
html! {
for strip.iter().map(|value| html! {
<Chip label={value.clone()} />
})
}
}
} else {
html! {}
}
}
</div>
}
})}
</Card>
}
} else {
render_empty_smart_match()
}
};
let render_empty = || html! {
<div class="tp__messaging-config-view__body tp__config-view-page__body">
{render_smart_match(None)}
{render_sources(None)}
</div>
};
html! {
<div class="tp__epg-config-view tp__config-view-page">
{
if let Some(epg) = &props.epg {
html! {
<div class="tp__epg-config-view__body tp__config-view-page__body">
{render_smart_match(epg.smart_match.as_ref())}
{render_sources(epg.sources.as_ref())}
</div>
}
} else {
{ render_empty() }
}
}
</div>
}
}
@@ -1,6 +1,6 @@
use std::fmt::Display;
use crate::app::components::popup_menu::PopupMenu;
use crate::app::components::{convert_bool_to_chip_style, AppIcon, BatchInputContentView, Chip, HideContent, InputHeaders, InputOptions, InputTypeView, RevealContent, StagedInputView, Table, TableDefinition};
use crate::app::components::{convert_bool_to_chip_style, AppIcon, BatchInputContentView, Chip, EpgConfigView, HideContent, InputHeaders, InputOptions, InputTypeView, RevealContent, StagedInputView, Table, TableDefinition};
use std::rc::Rc;
use std::str::FromStr;
use yew::platform::spawn_local;
@@ -117,7 +117,14 @@ pub fn InputTable(props: &InputTableProps) -> Html {
9 => html! { dto.priority.to_string() },
10 => html! { dto.max_connections.to_string() },
11 => html! { dto.method.to_string() },
12 => html! { /* TODO */ },
12 => html_if!(dto.epg.is_some(),
{ <RevealContent preview={ html!{ dto.epg.as_ref().map_or_else(|| html!{}, |e| html! {
<Chip class={if e.smart_match.is_some() {"active"} else { "" }}
label={ if e.smart_match.is_some() {translator.t("LABEL.SMART_EPG")} else { translator.t("LABEL.DEFAULT_EPG")}}
/>
})}}>
<EpgConfigView epg={ dto.epg.clone() } />
</RevealContent> }),
13 => html! { <InputHeaders headers={dto.headers.clone()} /> },
14 => html_if!(dto.staged.is_some(),
{ <RevealContent preview={ html!{ dto.staged.as_ref().map_or_else(String::new, |s| s.url.clone())} }>
@@ -17,6 +17,7 @@ mod playlist_explorer;
mod playlist_update_view;
mod mapper_script_view;
mod mapper_counter_view;
mod epg_config_view;
use std::rc::Rc;
use yew_i18n::YewI18n;
@@ -40,6 +41,7 @@ pub use self::playlist_explorer_view::*;
pub use self::playlist_update_view::*;
pub use self::mapper_script_view::*;
pub use self::mapper_counter_view::*;
pub use self::epg_config_view::*;
pub fn make_tags(data: &[(bool, &str)], translate: &YewI18n) -> Vec<Rc<Tag>> {
data.iter()
@@ -1,3 +1,4 @@
use std::fmt::Display;
use log::warn;
use crate::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
@@ -10,6 +11,22 @@ pub enum EpgNamePrefix {
Prefix(String),
}
impl EpgNamePrefix {
const IGNORE: &'static str = "Ignore";
const SUFFIX: &'static str = "Suffix";
const PREFIX: &'static str = "Prefix";
}
impl Display for EpgNamePrefix {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Ignore => write!(f, "{}", Self::IGNORE),
Self::Suffix(s) => write!(f, "{}({s})", Self::SUFFIX),
Self::Prefix(s) => write!(f, "{}({s})", Self::PREFIX),
}
}
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct EpgSmartMatchConfigDto {
+8 -10
View File
@@ -1,6 +1,6 @@
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, trim_last_slash};
use crate::utils::{default_as_true, get_credentials_from_url_str, get_trimmed_string, sanitize_sensitive_info, trim_last_slash};
use crate::{check_input_credentials, check_input_connections, create_tuliprox_error_result, handle_tuliprox_error_result_list, info_err};
use enum_iterator::Sequence;
use std::collections::{HashMap, HashSet};
@@ -323,16 +323,14 @@ impl ConfigInputDto {
if username.is_none() || password.is_none() || base_url.is_none(){
Err(format!("auto_epg is enabled for input {}, but no credentials could be extracted", self.name))
} else if base_url.is_some() {
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 {
if base_url.is_some() {
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)))
}
Err(format!("auto_epg is enabled for input {}, but url could not be parsed {}", self.name, sanitize_sensitive_info(&self.url)))
}
};