Network access restriction for api user (#728)

* Network access restriction for api user
* Fixed strm provider:// url bug
This commit is contained in:
euzu
2026-04-23 21:15:59 +02:00
committed by GitHub
parent 60cabc71fc
commit eb2a017fa3
50 changed files with 3054 additions and 935 deletions
+3 -1
View File
@@ -122,7 +122,9 @@ impl ApiProxyConfigDto {
let mut tokens = HashSet::new();
for target_user in &mut self.user {
for user in &mut target_user.credentials {
user.prepare();
if let Err(err) = user.prepare() {
errors.push(err.to_string());
}
if usernames.contains(&user.username) {
errors.push(format!("Non unique username found {}", &user.username));
} else {
+83 -2
View File
@@ -1,6 +1,6 @@
use crate::{
error::TuliproxError,
model::{ClusterFlags, ProxyType, ProxyUserStatus, XtreamCluster},
model::{ClusterFlags, NetworkAccessDto, ProxyType, ProxyUserStatus, XtreamCluster},
utils::{
default_as_true, default_user_priority, deserialize_timestamp, is_blank_optional_string, is_cluster_optional,
is_default_user_priority, is_true,
@@ -49,10 +49,21 @@ pub struct ProxyUserCredentialsDto {
pub soft_connections: u16,
#[serde(default = "default_user_priority", skip_serializing_if = "is_default_user_priority")]
pub soft_priority: i8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_access: Option<NetworkAccessDto>,
}
impl ProxyUserCredentialsDto {
pub fn prepare(&mut self) { self.trim(); }
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
self.trim();
if let Some(na) = &mut self.network_access {
na.prepare()?;
if na.is_empty() {
self.network_access = None;
}
}
Ok(())
}
fn trim(&mut self) {
self.username = self.username.trim().to_string();
@@ -125,6 +136,7 @@ impl Default for ProxyUserCredentialsDto {
priority: default_user_priority(),
soft_connections: 0,
soft_priority: default_user_priority(),
network_access: None,
}
}
}
@@ -176,4 +188,73 @@ mod tests {
assert_eq!(deserialized.output_clusters, Some(ClusterFlags::empty()));
}
#[test]
fn prepare_rejects_invalid_country_in_network_access() {
let mut user = ProxyUserCredentialsDto {
username: "alice".to_string(),
password: "secret".to_string(),
network_access: Some(NetworkAccessDto {
allowed_countries: Some(vec!["INVALID".to_string()]),
allowed_networks: None,
}),
..Default::default()
};
assert!(user.prepare().is_err());
}
#[test]
fn prepare_rejects_invalid_cidr_in_network_access() {
let mut user = ProxyUserCredentialsDto {
username: "alice".to_string(),
password: "secret".to_string(),
network_access: Some(NetworkAccessDto {
allowed_countries: None,
allowed_networks: Some(vec!["not-a-cidr".to_string()]),
}),
..Default::default()
};
assert!(user.prepare().is_err());
}
#[test]
fn prepare_normalizes_empty_network_access_to_none() {
let mut user = ProxyUserCredentialsDto {
username: "alice".to_string(),
password: "secret".to_string(),
network_access: Some(NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) }),
..Default::default()
};
user.prepare().unwrap();
assert_eq!(user.network_access, None);
}
#[test]
fn prepare_normalizes_and_deduplicates_network_access() {
let mut user = ProxyUserCredentialsDto {
username: "alice".to_string(),
password: "secret".to_string(),
network_access: Some(NetworkAccessDto {
allowed_countries: Some(vec!["de".to_string(), "DE".to_string(), "at".to_string()]),
allowed_networks: Some(vec![
"10.0.0.1/8".to_string(), // non-canonical, normalized to 10.0.0.0/8
"10.0.0.0/8".to_string(), // duplicate after normalization
"192.168.1.0/24".to_string(),
]),
}),
..Default::default()
};
user.prepare().unwrap();
let na = user.network_access.as_ref();
assert!(na.is_some());
if let Some(na) = na {
assert_eq!(na.allowed_countries, Some(vec!["DE".to_string(), "AT".to_string()]));
assert_eq!(na.allowed_networks.as_ref().map(|n| n.len()), Some(2));
assert!(na.allowed_networks.as_ref().is_some_and(|networks| networks.iter().any(|n| n == "10.0.0.0/8")));
assert!(na
.allowed_networks
.as_ref()
.is_some_and(|networks| networks.iter().any(|n| n == "192.168.1.0/24")));
}
}
}
+92 -1
View File
@@ -1,9 +1,45 @@
use crate::error::TuliproxError;
use enum_iterator::Sequence;
use std::fmt;
pub fn default_geoip_url() -> String {
String::from(
"https://raw.githubusercontent.com/sapics/ip-location-db/refs/heads/main/asn-country/asn-country-ipv4.csv",
)
}
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default, Sequence)]
#[serde(rename_all = "lowercase")]
pub enum GeoIpUnavailablePolicy {
#[default]
Deny,
Allow,
}
impl fmt::Display for GeoIpUnavailablePolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GeoIpUnavailablePolicy::Deny => write!(f, "deny"),
GeoIpUnavailablePolicy::Allow => write!(f, "allow"),
}
}
}
impl std::str::FromStr for GeoIpUnavailablePolicy {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"deny" => Ok(GeoIpUnavailablePolicy::Deny),
"allow" => Ok(GeoIpUnavailablePolicy::Allow),
_ => Err(TuliproxError::Config(format!("Unknown GeoIpUnavailablePolicy {s}"))),
}
}
}
pub const fn is_default_unavailable_policy(policy: &GeoIpUnavailablePolicy) -> bool {
matches!(policy, GeoIpUnavailablePolicy::Deny)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct GeoIpConfigDto {
@@ -11,8 +47,63 @@ pub struct GeoIpConfigDto {
pub enabled: bool,
#[serde(default = "default_geoip_url")]
pub url: String,
#[serde(default, skip_serializing_if = "is_default_unavailable_policy")]
pub unavailable_policy: GeoIpUnavailablePolicy,
}
impl GeoIpConfigDto {
pub fn is_empty(&self) -> bool { !self.enabled && self.url.trim().is_empty() }
pub fn is_empty(&self) -> bool {
!self.enabled && self.url.trim().is_empty() && self.unavailable_policy == GeoIpUnavailablePolicy::Deny
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn geoip_unavailable_policy_defaults_to_deny() {
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": false}"#).unwrap();
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
}
#[test]
fn geoip_unavailable_policy_accepts_allow() {
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "allow"}"#).unwrap();
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Allow);
}
#[test]
fn geoip_unavailable_policy_accepts_deny() {
let dto: GeoIpConfigDto = serde_json::from_str(r#"{"enabled": true, "unavailable_policy": "deny"}"#).unwrap();
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
}
#[test]
fn geoip_unavailable_policy_unknown_fails() {
let result: Result<GeoIpConfigDto, _> =
serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "invalid"}"#);
assert!(result.is_err());
}
#[test]
fn geoip_config_default_has_unavailable_policy_deny() {
let dto = GeoIpConfigDto::default();
assert_eq!(dto.unavailable_policy, GeoIpUnavailablePolicy::Deny);
}
#[test]
fn geoip_config_with_allow_is_not_empty() {
let dto: GeoIpConfigDto =
serde_json::from_str(r#"{"enabled": false, "unavailable_policy": "allow", "url": ""}"#).unwrap();
assert!(!dto.is_empty(), "is_empty() should return false for explicit allow policy");
}
#[test]
fn geoip_config_is_empty_respects_policy() {
// URL default means we need an explicit empty string to test is_empty with deny policy
let dto: GeoIpConfigDto =
serde_json::from_str(r#"{"enabled": false, "url": "", "unavailable_policy": "deny"}"#).unwrap();
assert!(dto.is_empty(), "disabled config with deny policy and empty url should be empty");
}
}
+3 -1
View File
@@ -7,6 +7,7 @@ mod cache;
mod config_type;
mod epg;
mod epg_smart_match;
mod geoip;
mod hdhomerun;
mod input;
mod ipcheck;
@@ -36,8 +37,8 @@ mod web_auth;
mod web_ui;
mod favourites;
mod geoip;
mod library;
mod network_access;
mod proxy_user_status;
mod qos_aggregation;
@@ -60,6 +61,7 @@ pub use library::*;
pub use log::*;
pub use messaging::*;
pub use metadata_update::*;
pub use network_access::*;
pub use panel_api::*;
pub use paths::*;
pub use pattern_template::*;
+257
View File
@@ -0,0 +1,257 @@
use crate::error::TuliproxError;
fn is_empty_vec<T>(v: &Option<Vec<T>>) -> bool { v.as_ref().is_none_or(|v| v.is_empty()) }
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct NetworkAccessDto {
#[serde(default, skip_serializing_if = "is_empty_vec")]
pub allowed_countries: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "is_empty_vec")]
pub allowed_networks: Option<Vec<String>>,
}
impl NetworkAccessDto {
pub fn is_empty(&self) -> bool { is_empty_vec(&self.allowed_countries) && is_empty_vec(&self.allowed_networks) }
/// Prepares the DTO for storage/comparison: trim whitespace, uppercase country codes,
/// deduplicate country codes, validate CIDRs, normalize empty lists to None.
pub fn prepare(&mut self) -> Result<(), TuliproxError> {
if let Some(countries) = &mut self.allowed_countries {
let mut seen = std::collections::HashSet::new();
let mut deduped = Vec::new();
for country in countries.drain(..) {
let upper = country.trim().to_uppercase();
if upper.is_empty() {
continue;
}
if upper.len() != 2 || !upper.chars().all(|ch| ch.is_ascii_alphabetic()) {
return Err(TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_countries entry '{upper}', expected 2-letter ISO code"
)));
}
if seen.insert(upper.clone()) {
deduped.push(upper);
}
}
*countries = deduped;
}
if let Some(networks) = &mut self.allowed_networks {
let mut deduped = Vec::new();
let mut seen = std::collections::HashSet::new();
for network in networks.drain(..) {
let trimmed = network.trim();
if trimmed.is_empty() {
continue;
}
let network_str = trimmed.to_string();
let ip: std::net::IpAddr = match network_str.split('/').next() {
Some(addr) => addr.trim().parse().map_err(|_| {
TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
))
})?,
None => {
return Err(TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
)));
}
};
let prefix: u8 = match network_str.split('/').nth(1) {
Some(p) => p.trim().parse().map_err(|_| {
TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
))
})?,
None => {
return Err(TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
)));
}
};
let prefix_valid = match ip {
std::net::IpAddr::V4(_) => prefix <= 32,
std::net::IpAddr::V6(_) => prefix <= 128,
};
if !prefix_valid {
return Err(TuliproxError::ProxyUser(format!(
"Invalid network_access.allowed_networks entry '{network_str}', expected CIDR"
)));
}
// Normalize to canonical network representation (network address / prefix)
let canonical_str = canonicalize_cidr(ip, prefix);
if seen.insert(canonical_str.clone()) {
deduped.push(canonical_str);
}
}
*networks = deduped;
}
if self.allowed_countries.as_ref().is_some_and(|c| c.is_empty()) {
self.allowed_countries = None;
}
if self.allowed_networks.as_ref().is_some_and(|n| n.is_empty()) {
self.allowed_networks = None;
}
Ok(())
}
}
fn canonicalize_cidr(ip: std::net::IpAddr, prefix: u8) -> String {
match ip {
std::net::IpAddr::V4(ipv4) => {
let mask = if prefix == 0 { 0 } else { u32::MAX << (32 - prefix) };
let network_addr = u32::from(ipv4) & mask;
let network_ip = std::net::Ipv4Addr::from(network_addr);
format!("{network_ip}/{prefix}")
}
std::net::IpAddr::V6(ipv6) => {
let mask = if prefix == 0 { 0 } else { u128::MAX << (128 - prefix) };
let network_addr = u128::from(ipv6) & mask;
let network_ip = std::net::Ipv6Addr::from(network_addr);
format!("{network_ip}/{prefix}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_empty() {
let dto = NetworkAccessDto::default();
assert_eq!(dto.allowed_countries, None);
assert_eq!(dto.allowed_networks, None);
assert!(dto.is_empty());
}
#[test]
fn empty_vecs_is_empty() {
let dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
assert!(dto.is_empty());
}
#[test]
fn non_empty_is_not_empty() {
let dto = NetworkAccessDto { allowed_countries: Some(vec!["DE".to_string()]), allowed_networks: None };
assert!(!dto.is_empty());
}
#[test]
fn prepare_uppercases_and_deduplicates_countries() {
let mut dto = NetworkAccessDto {
allowed_countries: Some(vec!["de".to_string(), "DE".to_string(), "At".to_string()]),
allowed_networks: None,
};
dto.prepare().unwrap();
let countries = dto.allowed_countries.unwrap();
assert_eq!(countries, vec!["DE", "AT"]);
}
#[test]
fn prepare_normalizes_empty_to_none() {
let mut dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
dto.prepare().unwrap();
assert_eq!(dto.allowed_countries, None);
assert_eq!(dto.allowed_networks, None);
assert!(dto.is_empty());
}
#[test]
fn deserialize_from_yaml_fragment() {
let value = serde_json::json!({
"allowed_countries": ["DE", "AT", "CH"],
"allowed_networks": ["192.168.1.0/24", "10.0.0.0/8"]
});
let dto: NetworkAccessDto = serde_json::from_value(value).unwrap();
assert_eq!(dto.allowed_countries, Some(vec!["DE".to_string(), "AT".to_string(), "CH".to_string()]));
assert_eq!(dto.allowed_networks, Some(vec!["192.168.1.0/24".to_string(), "10.0.0.0/8".to_string()]));
}
#[test]
fn serialize_omits_none_fields() {
let dto = NetworkAccessDto::default();
let serialized = serde_json::to_value(&dto).unwrap();
assert!(!serialized.as_object().unwrap().contains_key("allowed_countries"));
assert!(!serialized.as_object().unwrap().contains_key("allowed_networks"));
}
#[test]
fn serialize_omits_empty_vec_fields() {
let dto = NetworkAccessDto { allowed_countries: Some(vec![]), allowed_networks: Some(vec![]) };
let serialized = serde_json::to_value(&dto).unwrap();
assert!(!serialized.as_object().unwrap().contains_key("allowed_countries"));
assert!(!serialized.as_object().unwrap().contains_key("allowed_networks"));
}
#[test]
fn roundtrip_preserves_content() {
let dto = NetworkAccessDto {
allowed_countries: Some(vec!["DE".to_string()]),
allowed_networks: Some(vec!["10.0.0.0/8".to_string()]),
};
let serialized = serde_json::to_value(&dto).unwrap();
let deserialized: NetworkAccessDto = serde_json::from_value(serialized).unwrap();
assert_eq!(dto, deserialized);
}
#[test]
fn prepare_trims_whitespace() {
let mut dto = NetworkAccessDto {
allowed_countries: Some(vec![" de ".to_string()]),
allowed_networks: Some(vec![" 10.0.0.0/8 ".to_string()]),
};
dto.prepare().unwrap();
assert_eq!(dto.allowed_countries, Some(vec!["DE".to_string()]));
assert_eq!(dto.allowed_networks, Some(vec!["10.0.0.0/8".to_string()]));
}
#[test]
fn prepare_rejects_invalid_country_code() {
let mut dto = NetworkAccessDto { allowed_countries: Some(vec!["DEU".to_string()]), allowed_networks: None };
assert!(dto.prepare().is_err());
}
#[test]
fn prepare_rejects_invalid_cidr() {
let mut dto =
NetworkAccessDto { allowed_countries: None, allowed_networks: Some(vec!["not-a-cidr".to_string()]) };
assert!(dto.prepare().is_err());
}
#[test]
fn prepare_normalizes_and_deduplicates_cidrs() {
// "10.0.0.1/8" should be normalized to "10.0.0.0/8" (canonical network form)
// and deduplicated against an explicit "10.0.0.0/8"
let mut dto = NetworkAccessDto {
allowed_networks: Some(vec![
"10.0.0.1/8".to_string(), // non-canonical, equivalent to 10.0.0.0/8
"10.0.0.0/8".to_string(), // canonical, duplicate after normalization
"192.168.1.42/24".to_string(), // canonical
" 192.168.1.0/24 ".to_string(), // needs trimming
]),
..Default::default()
};
dto.prepare().unwrap();
let networks = dto.allowed_networks.unwrap();
assert_eq!(networks.len(), 2, "expected 2 unique networks, got {networks:?}");
assert!(networks.iter().any(|n| n == "10.0.0.0/8"), "missing 10.0.0.0/8 in {networks:?}");
assert!(networks.iter().any(|n| n == "192.168.1.0/24"), "missing 192.168.1.0/24 in {networks:?}");
}
#[test]
fn prepare_rejects_invalid_prefix() {
let mut dto =
NetworkAccessDto { allowed_networks: Some(vec!["192.168.1.0/33".to_string()]), ..Default::default() };
assert!(dto.prepare().is_err());
}
#[test]
fn prepare_accepts_and_canonicalizes_zero_prefix_networks() {
let mut dto = NetworkAccessDto {
allowed_networks: Some(vec!["10.1.2.3/0".to_string(), "2001:db8::1/0".to_string()]),
..Default::default()
};
assert!(dto.prepare().is_ok());
assert_eq!(dto.allowed_networks, Some(vec!["0.0.0.0/0".to_string(), "::/0".to_string()]));
}
}
+2 -2
View File
@@ -111,14 +111,14 @@ impl fmt::Display for PanelApiProvisioningMethod {
}
impl FromStr for PanelApiProvisioningMethod {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_uppercase().as_str() {
"HEAD" => Ok(Self::Head),
"GET" => Ok(Self::Get),
"POST" => Ok(Self::Post),
_ => Err("Unknown provisioning method: {s}".to_string()),
_ => Err(TuliproxError::Config(format!("Unknown provisioning method: {s}"))),
}
}
}
+37
View File
@@ -8,6 +8,10 @@ use crate::{
is_true, parse_to_kbps,
},
};
use std::{
fmt::{Display, Formatter},
str::FromStr,
};
const STREAM_QUEUE_SIZE: usize = 1024; // mpsc channel holding messages. with 8192byte chunks and 2Mbit/s approx 8MB
const MIN_SHARED_BURST_BUFFER_MB: u64 = 1;
@@ -34,6 +38,39 @@ impl AdmissionStrategy {
pub fn is_grace_hold(&self) -> bool { matches!(self, Self::GraceHoldStream) }
}
impl Display for AdmissionStrategy {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
AdmissionStrategy::EvictUserSameIpOldest => "evict_user_same_ip_oldest",
AdmissionStrategy::EvictUserSameIpLatest => "evict_user_same_ip_latest",
AdmissionStrategy::EvictUserOldest => "evict_user_oldest",
AdmissionStrategy::EvictUserLatest => "evict_user_latest",
AdmissionStrategy::GraceInstantStream => "grace_instant_stream",
AdmissionStrategy::GraceHoldStream => "grace_hold_stream",
}
)
}
}
impl FromStr for AdmissionStrategy {
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"evict_user_same_ip_oldest" => Ok(AdmissionStrategy::EvictUserSameIpOldest),
"evict_user_same_ip_latest" => Ok(AdmissionStrategy::EvictUserSameIpLatest),
"evict_user_oldest" => Ok(AdmissionStrategy::EvictUserOldest),
"evict_user_latest" => Ok(AdmissionStrategy::EvictUserLatest),
"grace_instant_stream" => Ok(AdmissionStrategy::GraceInstantStream),
"grace_hold_stream" => Ok(AdmissionStrategy::GraceHoldStream),
_ => Err(TuliproxError::Config(format!("Unknown admission strategy: {s}"))),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct StreamBufferConfigDto {
+8 -5
View File
@@ -1,6 +1,9 @@
use crate::utils::{
default_as_true, default_trakt_fuzzy_threshold, is_true, DEFAULT_USER_AGENT, TRAKT_API_KEY, TRAKT_API_URL,
TRAKT_API_VERSION,
use crate::{
error::TuliproxError,
utils::{
default_as_true, default_trakt_fuzzy_threshold, is_true, DEFAULT_USER_AGENT, TRAKT_API_KEY, TRAKT_API_URL,
TRAKT_API_VERSION,
},
};
use serde::{Deserialize, Serialize};
use std::{fmt, str::FromStr};
@@ -29,14 +32,14 @@ impl fmt::Display for TraktContentType {
}
impl FromStr for TraktContentType {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"vod" => Ok(TraktContentType::Vod),
"series" => Ok(TraktContentType::Series),
"both" => Ok(TraktContentType::Both),
_ => Err(format!("Invalid TraktContentType: {}", s)),
_ => Err(TuliproxError::Config(format!("Invalid TraktContentType: {}", s))),
}
}
}
+5 -4
View File
@@ -1,5 +1,6 @@
use crate::{
create_bitset,
error::TuliproxError,
model::{
xtream_const, ClusterFlags, CommonPlaylistItem, ConfigTargetOptions, EpisodeStreamProperties,
SeriesStreamProperties, StreamProperties, UUIDType, VideoStreamProperties, XtreamInfoDocument,
@@ -48,14 +49,14 @@ impl XtreamCluster {
}
impl FromStr for XtreamCluster {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"live" => Ok(XtreamCluster::Live),
"video" | "vod" | "movie" => Ok(XtreamCluster::Video),
"series" => Ok(XtreamCluster::Series),
_ => Err(format!("Invalid XtreamCluster: {s}")),
_ => Err(TuliproxError::Config(format!("Invalid XtreamCluster: {s}"))),
}
}
}
@@ -120,7 +121,7 @@ impl From<XtreamCluster> for PlaylistItemType {
}
impl FromStr for PlaylistItemType {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
@@ -135,7 +136,7 @@ impl FromStr for PlaylistItemType {
"LiveUnknown" => Ok(PlaylistItemType::LiveUnknown),
"LiveHls" => Ok(PlaylistItemType::LiveHls),
"LiveDash" => Ok(PlaylistItemType::LiveDash),
_ => Err(format!("Invalid PlaylistItemType: {s}")),
_ => Err(TuliproxError::Config(format!("Invalid PlaylistItemType: {s}"))),
}
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::error::TuliproxError;
use enum_iterator::Sequence;
use std::{fmt::Display, str::FromStr};
@@ -45,7 +46,7 @@ impl Display for ProcessingOrder {
}
impl FromStr for ProcessingOrder {
type Err = &'static str;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.trim().to_lowercase();
@@ -67,7 +68,7 @@ impl FromStr for ProcessingOrder {
x if x == Self::MFR => Ok(Self::Mfr),
x if x == Self::MRF => Ok(Self::Mrf),
_ => Err("invalid processing order"),
_ => Err(TuliproxError::Config(format!("Invalid processing order {s}"))),
}
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::error::TuliproxError;
use enum_iterator::Sequence;
use std::{fmt::Display, str::FromStr};
@@ -37,7 +38,7 @@ impl Display for StrmExportStyle {
}
impl FromStr for StrmExportStyle {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
@@ -45,7 +46,7 @@ impl FromStr for StrmExportStyle {
Self::PLEX => Ok(Self::Plex),
Self::EMBY => Ok(Self::Emby),
Self::JELLYFIN => Ok(Self::Jellyfin),
_ => Err(format!("Unknown StrmExportStyle: {}", s)),
_ => Err(TuliproxError::Config(format!("Unknown StrmExportStyle: {s}"))),
}
}
}
+3 -2
View File
@@ -1,3 +1,4 @@
use crate::error::TuliproxError;
use enum_iterator::Sequence;
use std::{fmt::Display, str::FromStr};
@@ -36,7 +37,7 @@ impl Display for TargetType {
}
impl FromStr for TargetType {
type Err = String;
type Err = TuliproxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
@@ -44,7 +45,7 @@ impl FromStr for TargetType {
Self::XTREAM => Ok(Self::Xtream),
Self::STRM => Ok(Self::Strm),
Self::HDHOMERUN => Ok(Self::HdHomeRun),
_ => Err(format!("Unknown TargetType: {}", s)),
_ => Err(TuliproxError::Config(format!("Unknown TargetType: {s}"))),
}
}
}