diff --git a/CHANGELOG.md b/CHANGELOG.md index d2bff3329..c688ce7f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,7 +145,6 @@ ipcheck: url_ipv4: https://ipinfo.io/ip ``` - # 2.2.5 (2025-03-27) - fixed web ui playlist regexp search - added `web_ui_path` to `config.yml` diff --git a/Cargo.lock b/Cargo.lock index 0f12841a0..404d684b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,6 +1594,7 @@ dependencies = [ "filetime", "flate2", "futures", + "indexmap", "jsonwebtoken", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index 964cf86a2..73a66612f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ rayon = "1.10" base64 = "0" paste = "1" bitflags = "2" +indexmap = "2" #[cfg(target_os = "macos")] libc = "0" #[cfg(target_os = "windows")] diff --git a/frontend/public/assets/i18n/en_common.json b/frontend/public/assets/i18n/en_common.json index 78c05ec08..94e4fccc8 100644 --- a/frontend/public/assets/i18n/en_common.json +++ b/frontend/public/assets/i18n/en_common.json @@ -102,9 +102,17 @@ "BUILD_TIME": "Build time", "MEMORY": "Memory", "ACTIVE_USERS": "Active users", - "ACTIVE_USER_CONNECTIONS": "Active user conn.", - "ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn." -}, + "ACTIVE_USER_CONNECTIONS": "Active user conn...", + "ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...", + "IP_CHECK": "IP Check", + "URL_IPV4": "Url IPv4", + "URL_IPV6": "Url IPv6", + "PATTERN_IPV4": "Pattern IPv4", + "PATTERN_IPV6": "Pattern IPv6", + "IPv4": "IPv4", + "IPv6": "IPv6", + "IP": "IP" + }, "TITLE": { "USER_BOUQUET_EDITOR": "User group editor" }, diff --git a/frontend/src/api/server-status-api-service.ts b/frontend/src/api/server-status-api-service.ts index 8a0be99a8..bd6f799a8 100644 --- a/frontend/src/api/server-status-api-service.ts +++ b/frontend/src/api/server-status-api-service.ts @@ -1,11 +1,14 @@ import {Observable} from "rxjs"; import ApiService, {DefaultApiService} from "./api-service"; -import {ServerStatus} from "../model/server-status"; +import {ServerIpCheck, ServerStatus} from "../model/server-status"; const STATUS_PATH = "status"; +const IPCHECK_PATH = "ipinfo"; export default interface ServerStatusApiService extends ApiService { getServerStatus(): Observable; + + getServerIpCheck(): Observable; } export class DefaultServerStatusApiService extends DefaultApiService implements ServerStatusApiService { @@ -13,4 +16,8 @@ export class DefaultServerStatusApiService extends DefaultApiService implements getServerStatus(): Observable { return this.get(STATUS_PATH); } + + getServerIpCheck(): Observable { + return this.get(IPCHECK_PATH); + } } diff --git a/frontend/src/component/main-config-view/main-config-view.tsx b/frontend/src/component/main-config-view/main-config-view.tsx index 014d96491..d37c727cd 100644 --- a/frontend/src/component/main-config-view/main-config-view.tsx +++ b/frontend/src/component/main-config-view/main-config-view.tsx @@ -1,9 +1,9 @@ import React, {useCallback, useMemo, useState} from "react"; import './main-config-view.scss'; import ServerConfig, { - CacheConfig, + CacheConfig, IpCheckConfig, LogConfig, - MessagingConfig, + MessagingConfig, ProxyConfig, PushoverConfig, RateLimitConfig, RestConfig, @@ -45,6 +45,9 @@ const getObject = (value: any): any => { if (Object.keys(value).length === 0) { return undefined; } + if (Object.values(value).filter(Boolean).length === 0) { + return undefined; + } return value; } @@ -166,12 +169,28 @@ const TABS = [ {label: 'LABEL.API', key: 'api'}, {label: 'LABEL.MAIN', key: 'main'}, {label: 'LABEL.WEB_UI', key: 'web_ui'}, + {label: 'LABEL.PROXY', key: 'proxy'}, {label: 'LABEL.SCHEDULES', key: 'schedules'}, {label: 'LABEL.REVERSE_PROXY', key: 'reverseProxy'}, {label: 'LABEL.MESSAGING', key: 'messaging'}, {label: 'LABEL.VIDEO', key: 'video'} ]; +const CONFIG_PROXY_FIELDS = [ + {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, + {name: 'username', label: 'LABEL.USERNAME', fieldType: FormFieldType.TEXT}, + {name: 'password', label: 'LABEL.PASSWORD', fieldType: FormFieldType.TEXT}, +]; + +const CONFIG_IP_CHECK_FIELDS = [ + {name: 'url', label: 'LABEL.URL', fieldType: FormFieldType.TEXT}, + {name: 'url_ipv4', label: 'LABEL.URL_IPV4', fieldType: FormFieldType.TEXT}, + {name: 'pattern_ipv4', label: 'LABEL.PATTERN_IPV4', fieldType: FormFieldType.TEXT}, + {name: 'url_ipv6', label: 'LABEL.URL_IPV6', fieldType: FormFieldType.TEXT}, + {name: 'pattern_ipv6', label: 'LABEL.PATTERN_IPV6', fieldType: FormFieldType.TEXT}, +]; + + // const translateLabels = (config: any, translateFunc: any) => config.map((c: any) => ({ // ...c, // label: translateFunc(c.label) @@ -205,6 +224,8 @@ export default function MainConfigView(props: MainConfigViewProps) { const reverseProxyCacheConfig = useMemo(() => config?.reverse_proxy?.cache || {} as any, [config]); const reverseProxyRateLimitConfig = useMemo(() => config?.reverse_proxy?.rate_limit || {} as any, [config]); const logConfig = useMemo(() => config?.log || {} as any, [config]); + const proxyConfig = useMemo(() => config?.proxy || {} as any, [config]); + const ipCheckConfig = useMemo(() => config?.ipcheck || {} as any, [config]); const configs = useMemo(() => { CONFIG_WEB_UI_FIELDS.forEach(def => { const serverOptions = config?.api_proxy?.server?.map(serverInfo => ({ @@ -233,6 +254,8 @@ export default function MainConfigView(props: MainConfigViewProps) { pushover_fields: translateLabels(CONFIG_PUSHOVER_FIELDS, translate), video_fields: translateLabels(CONFIG_VIDEO_FIELDS, translate), video_download_fields: translateLabels(CONFIG_VIDEO_DOWNLOAD_FIELDS, translate), + proxy_config_fields: translateLabels(CONFIG_PROXY_FIELDS, translate), + ipcheck_config_fields: translateLabels(CONFIG_IP_CHECK_FIELDS, translate), tabs: translateLabels(TABS, translate), } return translations; @@ -286,6 +309,20 @@ export default function MainConfigView(props: MainConfigViewProps) { auth: webAuthConfig, } + const cfgProxyConfig: ProxyConfig = { + url: proxyConfig.url, + username: proxyConfig.username, + password: proxyConfig.password, + } + + const cfgIpCheckConfig: IpCheckConfig = { + url: ipCheckConfig.url, + url_ipv4: ipCheckConfig.url_ipv4, + url_ipv6: ipCheckConfig.url_ipv6, + pattern_ipv4: ipCheckConfig.pattern_ipv4, + pattern_ipv6: ipCheckConfig.pattern_ipv6, + } + const cfgVideo = getObject(videoConfig) && getObject(videoDownloadConfig) ? { ...videoConfig, download: getObject(videoDownloadConfig) @@ -307,6 +344,8 @@ export default function MainConfigView(props: MainConfigViewProps) { log: getObject(cfgLog), update_on_boot: mainConfig.update_on_boot, web_ui: getObject(cfgWebUiConfig), + proxy: getObject(cfgProxyConfig), + ipcheck: getObject(cfgIpCheckConfig), }; services.config().saveMainConfig(cfgMain).subscribe({ @@ -317,7 +356,7 @@ export default function MainConfigView(props: MainConfigViewProps) { }, [mainConfig, apiConfig, videoConfig, messagingConfig, telegramConfig, restConfig, pushoverConfig, videoDownloadConfig, reverseProxyConfig, enqueueSnackbar, services, logConfig, reverseProxyRateLimitConfig, reverseProxyCacheConfig, reverseProxyStreamConfig, reverseProxyStreamBufferConfig, - webUiConfig, webAuthConfig, + webUiConfig, webAuthConfig, proxyConfig, ipCheckConfig, translate]); const handleTabChange = useCallback((tab: string) => { @@ -350,6 +389,12 @@ export default function MainConfigView(props: MainConfigViewProps) { +
+ + + + +
diff --git a/frontend/src/component/status-view/status-view.scss b/frontend/src/component/status-view/status-view.scss index d17feb243..5505da73c 100644 --- a/frontend/src/component/status-view/status-view.scss +++ b/frontend/src/component/status-view/status-view.scss @@ -1,12 +1,22 @@ .status-view { display: flex; flex-flow: column; - gap: 4px; + gap: 12px; + &__content { display: grid; grid-template-columns: auto 1fr; gap: 10px; } + + &__section-title { + color: var(--title-label-color); + font-weight: bold; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin: 8px 0; + } + &__col-label { color: var(--label-color); } diff --git a/frontend/src/component/status-view/status-view.tsx b/frontend/src/component/status-view/status-view.tsx index f6f26a161..fe17b6f09 100644 --- a/frontend/src/component/status-view/status-view.tsx +++ b/frontend/src/component/status-view/status-view.tsx @@ -1,11 +1,10 @@ import {useServices} from "../../provider/service-provider"; import useTranslator from "../../hook/use-translator"; -import {useEffect, useState} from "react"; -import {ServerStatus} from "../../model/server-status"; +import React, {useCallback, useEffect, useState} from "react"; +import {ServerIpCheck, ServerStatus} from "../../model/server-status"; import {interval} from "rxjs"; import {first} from "rxjs/operators"; import './status-view.scss'; -import React from "react"; const REQUEST_INTERVAL = 5000; const STATUS_COLUMNS = [ @@ -23,19 +22,39 @@ export default function StatusView() { const services = useServices(); const translate = useTranslator(); const [status, setStatus] = useState(); + const [ipCheck, setIpCheck] = useState(); + + const checkIp = useCallback(() => { + services.status().getIpCheck().pipe(first()).subscribe({ + next: (data: ServerIpCheck) => setIpCheck(data), + error: (err) => { + setIpCheck(undefined); + }, + }); + }, [services]) useEffect(() => { const sub = interval(REQUEST_INTERVAL).subscribe(() => services.status().getServerStatus().pipe(first()).subscribe((data: any) => setStatus(data))); + checkIp(); return () => sub.unsubscribe(); - }, [services]); - + }, [services, checkIp]); return
+ {ipCheck && <> +
{translate("LABEL.IP")}
+
+
{translate("LABEL.IPv4")}
+
{ipCheck?.ipv4 ?? '?'}
+
{translate("LABEL.IPv6")}
+
{ipCheck?.ipv6 ?? '?'}
+
+ } +
{translate("LABEL.STATUS")}
- {status && STATUS_COLUMNS.map(col => + {status && STATUS_COLUMNS.map(col =>
{translate('LABEL.' + col.toUpperCase())}
{ - (col === "active_provider_connections") ? JSON.stringify((status as any)?.[col]) : (status as any)?.[col] + (col === "active_provider_connections") ? JSON.stringify((status as any)?.[col]) : (status as any)?.[col] }
)} diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index 192d6659c..9529518db 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -236,6 +236,20 @@ export interface WebUiConfig { player_server?: string, } +export interface ProxyConfig { + url: string, + username?: string, + password?: string, +} + +export interface IpCheckConfig { + url?: string, + url_ipv4?: string, + url_ipv6?: string, + pattern_ipv4?: string, + pattern_ipv6?: string, +} + export interface ServerMainConfig { threads: number; api: ServerApiConfig; @@ -248,6 +262,8 @@ export interface ServerMainConfig { update_on_boot: boolean, web_ui?: WebUiConfig, reverse_proxy?: ReverseProxyConfig, + proxy?: ProxyConfig, + ipcheck?: IpCheckConfig, } export default interface ServerConfig extends ServerMainConfig { diff --git a/frontend/src/model/server-status.tsx b/frontend/src/model/server-status.tsx index 49fbdc45c..664ae1e3a 100644 --- a/frontend/src/model/server-status.tsx +++ b/frontend/src/model/server-status.tsx @@ -9,3 +9,8 @@ export interface ServerStatus { active_user_connections: number, active_provider_connections: Record, } + +export interface ServerIpCheck { + ipv4?: string, + ipv6?: string, +} \ No newline at end of file diff --git a/frontend/src/service/server-status-service.ts b/frontend/src/service/server-status-service.ts index e545efd08..cbbfd1e4f 100644 --- a/frontend/src/service/server-status-service.ts +++ b/frontend/src/service/server-status-service.ts @@ -1,5 +1,5 @@ import {Observable} from "rxjs"; -import {ServerStatus} from "../model/server-status"; +import {ServerIpCheck, ServerStatus} from "../model/server-status"; import ServerStatusApiService, {DefaultServerStatusApiService} from "../api/server-status-api-service"; export default class ServerStatusService { @@ -10,4 +10,7 @@ export default class ServerStatusService { getServerStatus(): Observable { return this.serverStatusApiService.getServerStatus(); } + getIpCheck(): Observable { + return this.serverStatusApiService.getServerIpCheck(); + } } diff --git a/src/api/endpoints/api_playlist_utils.rs b/src/api/endpoints/api_playlist_utils.rs index 9f6710733..f25714367 100644 --- a/src/api/endpoints/api_playlist_utils.rs +++ b/src/api/endpoints/api_playlist_utils.rs @@ -6,8 +6,8 @@ use crate::utils::{m3u, xtream}; use axum::response::IntoResponse; use serde::Serialize; use serde_json::{json, Value}; -use std::collections::HashMap; use std::sync::Arc; +use indexmap::IndexMap; #[derive(serde::Serialize, serde::Deserialize)] struct PlaylistResponseGroup { @@ -32,7 +32,7 @@ fn group_playlist_items( where T: Serialize, { - let mut groups: HashMap> = HashMap::new(); + let mut groups: IndexMap> = IndexMap::new(); for item in iter { let group_key = get_group(&item); diff --git a/src/api/endpoints/download_api.rs b/src/api/endpoints/download_api.rs index 4feff0ef2..c2af79e1b 100644 --- a/src/api/endpoints/download_api.rs +++ b/src/api/endpoints/download_api.rs @@ -1,6 +1,6 @@ use crate::api::model::app_state::AppState; use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest}; -use crate::model::{ConfigProxy, VideoDownloadConfig}; +use crate::model::{ProxyConfig, VideoDownloadConfig}; use crate::utils::request; use tokio::sync::RwLock; use futures::stream::TryStreamExt; @@ -62,7 +62,7 @@ async fn download_file(active: Arc>>, client: &reqwe } } -async fn run_download_queue(proxy_config: Option<&ConfigProxy>, download_cfg: &VideoDownloadConfig, download_queue: &Arc) -> Result<(), String> { +async fn run_download_queue(proxy_config: Option<&ProxyConfig>, download_cfg: &VideoDownloadConfig, download_queue: &Arc) -> Result<(), String> { let next_download = download_queue.as_ref().queue.lock().await.pop_front(); if next_download.is_some() { { *download_queue.as_ref().active.write().await = next_download; } diff --git a/src/api/endpoints/v1_api.rs b/src/api/endpoints/v1_api.rs index 50c9dadeb..e3b2ec88b 100644 --- a/src/api/endpoints/v1_api.rs +++ b/src/api/endpoints/v1_api.rs @@ -278,6 +278,8 @@ async fn config( messaging: config.messaging.clone(), video: config.video.clone(), sources: config.sources.iter().map(map_source).collect(), + proxy: config.proxy.clone(), + ipcheck: config.ipcheck.clone(), api_proxy: config_reader::read_api_proxy(config, app_state.config.t_api_proxy_file_path.as_str(), false), }; diff --git a/src/api/model/config.rs b/src/api/model/config.rs index 3798033bd..bbb243701 100644 --- a/src/api/model/config.rs +++ b/src/api/model/config.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::model::{ApiProxyConfig, ConfigApi, InputType, LogConfig, MessagingConfig, ProcessingOrder, ReverseProxyConfig, ScheduleConfig, VideoConfig, ConfigRename, ConfigSort, ConfigTargetOptions, TargetOutput, WebUiConfig}; +use crate::model::{ApiProxyConfig, ConfigApi, InputType, LogConfig, MessagingConfig, ProcessingOrder, ReverseProxyConfig, ScheduleConfig, VideoConfig, ConfigRename, ConfigSort, ConfigTargetOptions, TargetOutput, WebUiConfig, ProxyConfig, IpCheckConfig}; #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ServerInputConfig { @@ -52,5 +52,7 @@ pub struct ServerConfig { pub log: Option, pub update_on_boot: bool, pub web_ui: Option, + pub proxy: Option, + pub ipcheck: Option, } diff --git a/src/model/config.rs b/src/model/config.rs index 3705492ab..a8afae851 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -19,7 +19,7 @@ use crate::utils::file_utils; use crate::utils::{parse_size_base_2, parse_to_kbps}; use crate::utils::exit; use crate::m3u_filter_error::{ create_m3u_filter_error_result}; -use crate::model::{ConfigInput, ConfigInputOptions, ConfigIpCheck, ConfigProxy, ConfigSource, ConfigTarget, HdHomeRunConfig, LogConfig, MessagingConfig, ProcessTargets, TargetOutput, VideoConfig, WebUiConfig}; +use crate::model::{ConfigInput, ConfigInputOptions, IpCheckConfig, ProxyConfig, ConfigSource, ConfigTarget, HdHomeRunConfig, LogConfig, MessagingConfig, ProcessTargets, TargetOutput, VideoConfig, WebUiConfig}; const STREAM_QUEUE_SIZE: usize = 1024; // mpsc channel holding messages. with 8192byte chunks and 2Mbit/s approx 8MB @@ -132,6 +132,10 @@ pub struct ConfigDto { pub web_ui: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub reverse_proxy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub proxy: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub ipcheck: Option, } impl ConfigDto { @@ -366,9 +370,9 @@ pub struct Config { #[serde(default, skip_serializing_if = "Option::is_none")] pub hdhomerun: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub proxy: Option, + pub proxy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub ipcheck: Option, + pub ipcheck: Option, #[serde(skip)] pub t_api_proxy: Arc>>, #[serde(skip)] diff --git a/src/model/config_ip_check.rs b/src/model/config_ip_check.rs index ffdeb5ed4..37420afa7 100644 --- a/src/model/config_ip_check.rs +++ b/src/model/config_ip_check.rs @@ -3,7 +3,7 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] -pub struct ConfigIpCheck { +pub struct IpCheckConfig { /// URL that may return both IPv4 and IPv6 in one response #[serde(default, skip_serializing_if = "Option::is_none")] pub url: Option, @@ -30,7 +30,7 @@ pub struct ConfigIpCheck { pub t_pattern_ipv6: Option, } -impl ConfigIpCheck { +impl IpCheckConfig { pub fn prepare(&mut self) -> Result<(), M3uFilterError> { if let Some(url) = &self.url { let url = url.trim(); diff --git a/src/model/config_proxy.rs b/src/model/config_proxy.rs index 36ea24491..c089dbfd6 100644 --- a/src/model/config_proxy.rs +++ b/src/model/config_proxy.rs @@ -2,13 +2,13 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] #[serde(deny_unknown_fields)] -pub struct ConfigProxy { +pub struct ProxyConfig { pub url: String, pub username: Option, pub password: Option, } -impl ConfigProxy { +impl ProxyConfig { pub(crate) fn prepare(&mut self) -> Result<(), M3uFilterError> { if self.username.is_some() || self.password.is_some() { if let (Some(username), Some(password)) = (self.username.as_ref(), self.password.as_ref()) { diff --git a/src/processing/processor/sort.rs b/src/processing/processor/sort.rs index deb5ba01b..3ae43ad84 100644 --- a/src/processing/processor/sort.rs +++ b/src/processing/processor/sort.rs @@ -1,8 +1,8 @@ -use std::cmp::Ordering; -use deunicode::deunicode; use crate::foundation::filter::get_field_value; use crate::model::{ConfigSortChannel, ConfigSortGroup, ConfigTarget, SortOrder}; use crate::model::{PlaylistGroup, PlaylistItem}; +use deunicode::deunicode; +use std::cmp::Ordering; fn playlist_comparator( sequence: Option<&Vec>, @@ -137,11 +137,10 @@ pub(in crate::processing::processor) fn sort_playlist(target: &ConfigTarget, new #[cfg(test)] mod tests { - use crate::model::{ConfigSortChannel, ItemField, SortOrder}; use crate::model::{PlaylistItem, PlaylistItemHeader}; - use regex::Regex; use crate::processing::processor::sort::playlistitem_comparator; + use regex::Regex; #[test] fn test_sort() { @@ -168,4 +167,57 @@ mod tests { let sorted = channels.into_iter().map(|pli| pli.header.title.clone()).collect::>(); assert_eq!(expected, sorted); } + + #[test] + fn test_sort2() { + let mut channels: Vec = vec![ + "US| EAST [FHD] abc", + "US| EAST [FHD] def", + "US| EAST [FHD] ghi", + "US| EAST [HD] jkl", + "US| EAST [HD] mno", + "US| EAST [HD] pqrs", + "US| EAST [HD] tuv", + "US| EAST [HD] wxy", + "US| EAST [HD] z", + "US| EAST [SD] a", + "US| EAST [FHD] bc", + "US| EAST [FHD] de", + "US| EAST [HD] f", + "US| EAST [HD] h", + "US| EAST [SD] ijk", + "US| EAST [SD] l", + "US| EAST [UHD] m", + "US| WEST [FHD] no", + "US| WEST [HD] qrst", + "US| WEST [HD] uvw", + "US| (West) xv", + "US| East d", + "US| West e", + "US| West f", + ].into_iter().map(|name| PlaylistItem { header: PlaylistItemHeader { title: name.to_string(), ..Default::default() } }).collect::>().into(); + + let channel_sort = ConfigSortChannel { + field: ItemField::Caption, + group_pattern: ".*US.*".to_string(), + order: SortOrder::Asc, + sequence: None, + t_sequence: Some(vec![ + Regex::new(r"^US\| EAST.*?\[\bUHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| EAST.*?\[\bFHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| EAST.*?\[\bHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| EAST.*?\[\bSD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| WEST.*?\[\bUHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| WEST.*?\[\bFHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| WEST.*?\[\bHD\b\](?P.*)").unwrap(), + Regex::new(r"^US\| WEST.*?\[\bSD\b\](?P.*)").unwrap(), + ]), + t_re_group_pattern: Some(Regex::new(".*").unwrap()), + }; + + channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, &channel_sort, true)); + let sorted = channels.into_iter().map(|pli| pli.header.title.clone()).collect::>(); + let expected = vec!["US| EAST [UHD] m", "US| EAST [FHD] abc", "US| EAST [FHD] bc", "US| EAST [FHD] de", "US| EAST [FHD] def", "US| EAST [FHD] ghi", "US| EAST [HD] f", "US| EAST [HD] h", "US| EAST [HD] jkl", "US| EAST [HD] mno", "US| EAST [HD] pqrs", "US| EAST [HD] tuv", "US| EAST [HD] wxy", "US| EAST [HD] z", "US| EAST [SD] a", "US| EAST [SD] ijk", "US| EAST [SD] l", "US| WEST [FHD] no", "US| WEST [HD] qrst", "US| WEST [HD] uvw", "US| (West) xv", "US| East d", "US| West e", "US| West f"]; + assert_eq!(expected, sorted); + } } \ No newline at end of file diff --git a/src/utils/network/ip_checker.rs b/src/utils/network/ip_checker.rs index 70f9059c5..b03276324 100644 --- a/src/utils/network/ip_checker.rs +++ b/src/utils/network/ip_checker.rs @@ -1,5 +1,5 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; -use crate::model::ConfigIpCheck; +use crate::model::IpCheckConfig; use regex::Regex; use reqwest::Client; use std::sync::Arc; @@ -27,7 +27,7 @@ async fn fetch_ip(client: &Arc, url: &str, regex: Option<&Regex>) -> Res } /// Fetch both IPs from a shared URL (if both regex patterns are available) -async fn fetch_combined_ips(client: &Arc, config: &ConfigIpCheck, url: &str) -> (Option, Option) { +async fn fetch_combined_ips(client: &Arc, config: &IpCheckConfig, url: &str) -> (Option, Option) { let response = client.get(url).send().await.ok(); let text = match response { Some(r) => r.text().await.ok(), @@ -55,7 +55,7 @@ async fn fetch_combined_ips(client: &Arc, config: &ConfigIpCheck, url: & /// Fetch both IPv4 and IPv6 addresses, using separate or combined URL(s) -pub async fn get_ips(client: &Arc, config: &ConfigIpCheck) -> Result<(Option, Option), M3uFilterError> { +pub async fn get_ips(client: &Arc, config: &IpCheckConfig) -> Result<(Option, Option), M3uFilterError> { match (&config.url_ipv4, &config.url_ipv6, &config.url) { // Both dedicated URLs provided (Some(url_v4), Some(url_v6), _) => { diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index 2cbf99d23..9689d2587 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -17,7 +17,7 @@ use url::Url; use crate::m3u_filter_error::create_m3u_filter_error_result; use crate::m3u_filter_error::{str_to_io_error, M3uFilterError, M3uFilterErrorKind}; use crate::model::format_elapsed_time; -use crate::model::{ConfigInput, ConfigProxy, InputFetchMethod}; +use crate::model::{ConfigInput, ProxyConfig, InputFetchMethod}; use crate::repository::storage::{get_input_storage_path, short_hash}; use crate::repository::storage_const; use crate::utils::compression::compression_utils::{is_deflate, is_gzip}; @@ -494,7 +494,7 @@ pub fn get_base_url_from_str(url: &str) -> Option { } } -pub fn create_client(proxy_config: Option<&ConfigProxy>) -> reqwest::ClientBuilder { +pub fn create_client(proxy_config: Option<&ProxyConfig>) -> reqwest::ClientBuilder { let client = reqwest::Client::builder(); if let Some(proxy_cfg) = proxy_config { let proxy = match reqwest::Proxy::all(&proxy_cfg.url) {