diff --git a/Cargo.lock b/Cargo.lock index ad1ec1fc1..a638d4dcf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,19 +241,13 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -398,17 +392,17 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -520,9 +514,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "jobserver", "libc", @@ -645,9 +639,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -853,7 +847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -1015,9 +1009,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "h2" @@ -1200,9 +1194,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", @@ -1278,9 +1272,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is_terminal_polyfill" @@ -1465,6 +1459,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha1", "time", "unidecode", "url", @@ -1499,15 +1494,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1662,9 +1648,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -1731,9 +1717,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.11" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +checksum = "9c73c26c01b8c87956cea613c907c9d6ecffd8d18a2a5908e5de0adfaa185cea" dependencies = [ "memchr", "thiserror", @@ -1742,9 +1728,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.11" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +checksum = "664d22978e2815783adbdd2c588b455b1bd625299ce36b2a99881ac9627e6d8d" dependencies = [ "pest", "pest_generator", @@ -1752,9 +1738,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.11" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +checksum = "a2d5487022d5d33f4c30d91c22afa240ce2a644e87fe08caad974d4eab6badbe" dependencies = [ "pest", "pest_meta", @@ -1765,9 +1751,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.7.11" +version = "2.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +checksum = "0091754bbd0ea592c4deb3a122ce8ecbb0753b738aa82bc055fcc2eccc8d8174" dependencies = [ "once_cell", "pest", @@ -2180,9 +2166,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring", "rustls-pki-types", @@ -2197,11 +2183,11 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2241,18 +2227,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.209" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", @@ -2307,6 +2293,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -2730,6 +2722,7 @@ checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "rand", + "sha1_smol", "uuid-macro-internal", ] diff --git a/Cargo.toml b/Cargo.toml index bac795c9d..90bcdbf57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "m3u-filter" version = "2.0.3" -edition = "2018" +edition = "2021" [profile.release] opt-level = 'z' # Optimize for size. @@ -35,7 +35,7 @@ futures = "0.3" path-absolutize = "3.1" pest = "2.7" pest_derive = "2.7" -enum-iterator = "2.0" +enum-iterator = "2" unidecode = "0" petgraph = "0" openssl = { version = "*", features = ["vendored"] } #https://docs.rs/openssl/0.10.34/openssl/#vendored @@ -44,8 +44,9 @@ log = "0.4" env_logger = "0.11" rustelebot = "0.3" bincode = "1.3" -uuid = { version = "1.8", features = ["v4", "fast-rng", "macro-diagnostics"] } +uuid = { version = "1", features = ["v4", "v5", "fast-rng", "macro-diagnostics"] } rand = "0.8" rpassword = "7.3" flate2 = "1" time = "0.3" +sha1 = "0.10.6" diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index a0955e9e1..972701d69 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -85,7 +85,7 @@ fn get_xtream_player_api_action_url(input: &ConfigInput, action: &str) -> Option } } -fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: &XtreamCluster, stream_id: u32) -> Option { +fn get_xtream_player_api_info_url(input: &ConfigInput, cluster: XtreamCluster, stream_id: u32) -> Option { let (action, stream_id_field) = match cluster { XtreamCluster::Live => ("get_live_info", "live_id"), XtreamCluster::Video => ("get_vod_info", "vod_id"), @@ -323,8 +323,8 @@ async fn xtream_get_stream_info_content(info_url: &str, input: &ConfigInput) -> } async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &ConfigTarget, - pli: &XtreamPlaylistItem, info_url: &str, cluster: &XtreamCluster) -> Result { - if cluster == &XtreamCluster::Series { + pli: &XtreamPlaylistItem, info_url: &str, cluster: XtreamCluster) -> Result { + if cluster == XtreamCluster::Series { if let Ok(content) = xtream_repository::xtream_load_series_info(config, target.name.replace(' ', "_").as_str(), pli.stream_id) { return Ok(content); } @@ -348,7 +348,7 @@ async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &C async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, - cluster: &XtreamCluster) -> HttpResponse { + cluster: XtreamCluster) -> HttpResponse { let req_stream_id: u32 = match FromStr::from_str(stream_id) { Ok(id) => id, Err(_) => return HttpResponse::BadRequest().finish() @@ -359,7 +359,7 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC if let Some(input) = app_state.config.get_input_by_id(input_id) { if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, pli.provider_id) { // Redirect is only possible for live streams, vod and series info needs to be modified - if user.proxy == ProxyType::Redirect && cluster == &XtreamCluster::Live { + if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live { return HttpResponse::Found().insert_header(("Location", info_url)).finish(); } else if let Ok(content) = xtream_get_stream_info(&app_state.config, input, target, &pli, info_url.as_str(), cluster).await { return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content); @@ -436,7 +436,7 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget Err(_) => return HttpResponse::BadRequest().finish() }; - if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(req_stream_id, &app_state.config, target, Some(&XtreamCluster::Live)) { + if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(req_stream_id, &app_state.config, target, Some(XtreamCluster::Live)) { let input_id = pli.input_id; if let Some(input) = app_state.config.get_input_by_id(input_id) { if let Some(info_url) = get_xtream_player_api_action_url(input, "get_simple_data_table") @@ -504,12 +504,12 @@ async fn xtream_player_api( "get_series_info" => { xtream_get_stream_info_response(app_state, &user, target, api_req.series_id.trim(), - &XtreamCluster::Series).await + XtreamCluster::Series).await } "get_vod_info" => { xtream_get_stream_info_response(app_state, &user, target, api_req.vod_id.trim(), - &XtreamCluster::Video).await + XtreamCluster::Video).await } "get_epg" | "get_short_epg" => { @@ -530,9 +530,9 @@ async fn xtream_player_api( } else { let cat_id = if category_id.is_empty() { 0 } else { category_id.parse::().unwrap_or(0) }; match match action { - "get_live_streams" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Live, &app_state.config, target, cat_id), - "get_vod_streams" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Video, &app_state.config, target, cat_id), - "get_series" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Series, &app_state.config, target, cat_id), + "get_live_streams" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.config, target, cat_id), + "get_vod_streams" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.config, target, cat_id), + "get_series" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.config, target, cat_id), _ => Err(Error::new(ErrorKind::Unsupported, format!("Cant find action: {action} for target: {target_name}"))), } { Ok(payload) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(payload), diff --git a/src/model/config.rs b/src/model/config.rs index ecb3e74a3..80df7d618 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, RwLock}; use log::{debug, error, warn}; use path_absolutize::Absolutize; +use url::{Url}; use crate::auth::user::UserCredential; use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider}; @@ -79,18 +80,18 @@ macro_rules! handle_m3u_filter_error_result { pub(crate) enum TargetType { #[serde(rename = "m3u")] M3u, - #[serde(rename = "strm")] - Strm, #[serde(rename = "xtream")] Xtream, + #[serde(rename = "strm")] + Strm, } impl Display for TargetType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { Self::M3u => write!(f, "M3u"), - Self::Strm => write!(f, "Strm"), Self::Xtream => write!(f, "Xtream"), + Self::Strm => write!(f, "Strm"), } } } @@ -545,6 +546,7 @@ impl ConfigInput { self.persist = None; } } + Ok(()) } @@ -557,7 +559,7 @@ impl ConfigInput { password: self.password.as_ref().unwrap().to_owned(), }); } - } else if let Ok(url) = url::Url::parse(&self.url) { + } else if let Ok(url) = Url::parse(&self.url) { let base_url = url.origin().ascii_serialization(); let mut username = None; let mut password = None; diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 132332715..a030a0569 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -2,7 +2,8 @@ use std::cell::RefCell; use std::cmp::PartialEq; use std::fmt::{Display, Formatter}; use std::rc::Rc; - +use uuid::{Uuid}; +use sha1::{Sha1, Digest}; use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; @@ -11,20 +12,21 @@ use crate::model::config::{ConfigInput, ConfigTarget}; use crate::model::xmltv::TVGuide; use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions}; use crate::utils::default_utils::{default_as_false, default_as_zero_u16, default_as_zero_u32, default_playlist_item_type, default_stream_cluster}; +use crate::utils::request_utils::get_base_url; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html #[derive(Debug, Clone)] -pub(crate) struct FetchedPlaylist<'a> { +pub(crate) struct FetchedPlaylist<'a> { // Contains playlist for one input pub input: &'a ConfigInput, - pub playlist: Vec, + pub playlistgroups: Vec, pub epg: Option, } impl FetchedPlaylist<'_> { pub(crate) fn update_playlist(&mut self, plg: &PlaylistGroup) { - for grp in &mut self.playlist { + for grp in &mut self.playlistgroups { if grp.id == plg.id { plg.channels.iter().for_each(|item| grp.channels.push(item.clone())); return; @@ -33,7 +35,8 @@ impl FetchedPlaylist<'_> { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[repr(u8)] pub(crate) enum XtreamCluster { Live = 1, Video = 2, @@ -87,9 +90,9 @@ pub(crate) trait FieldAccessor { #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct PlaylistItemHeader { - // stream_id is a custom field for processing - pub stream_id: Rc, - pub id: Rc, + pub uuid: Rc, // calculated + pub stream_id: Rc, // virtual id + pub id: Rc, // provider id pub name: Rc, pub chno: Rc, pub logo: Rc, @@ -115,6 +118,27 @@ pub(crate) struct PlaylistItemHeader { pub input_id: u16, } +impl PlaylistItemHeader { + + pub(crate) fn gen_uuid(&mut self) { + let cluster = self.xtream_cluster as u8; + let mut hasher = Sha1::new(); + hasher.update(get_base_url(&self.url).unwrap_or(self.url.to_string())); + hasher.update(self.stream_id.as_str()); + hasher.update(self.title.as_str()); + hasher.update(self.name.as_str()); + hasher.update(self.group.as_str()); + hasher.update(vec![cluster].as_slice()); + let hashed_bytes = hasher.finalize(); + let namespace = Uuid::NAMESPACE_OID; + let uuid = Uuid::new_v5(&namespace, &hashed_bytes).to_string(); + self.uuid = Rc::new(uuid); + } + pub(crate) fn get_uuid(&self) -> &str { + self.uuid.as_ref() + } +} + macro_rules! to_m3u_non_empty_fields { ($header:expr, $line:expr, $(($prop:ident, $field:expr)),*;) => { $( @@ -159,7 +183,6 @@ macro_rules! generate_field_accessor_impl_for_playlist_item_header { } } -// !! should be in sync with MAPPER_ATTRIBUTES except stream_id !! generate_field_accessor_impl_for_playlist_item_header!(id, stream_id, name, chno, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, url;); #[derive(Debug, Clone, Serialize, Deserialize)] @@ -274,7 +297,7 @@ impl PlaylistItem { rec: Rc::clone(&header.rec), url: Rc::clone(&header.url), epg_channel_id: header.epg_channel_id.clone(), - xtream_cluster: header.xtream_cluster.clone(), + xtream_cluster: header.xtream_cluster, additional_properties: match &header.additional_properties { None => None, Some(props) => match serde_json::to_string(props) { @@ -304,3 +327,10 @@ pub(crate) struct PlaylistGroup { pub xtream_cluster: XtreamCluster, } +impl PlaylistGroup { + + pub(crate) fn on_load(&mut self) { + self.channels.iter().for_each(|pl| pl.header.borrow_mut().gen_uuid()); + } +} + diff --git a/src/processing/affix_processor.rs b/src/processing/affix_processor.rs index 5dd40e46e..3504602b2 100644 --- a/src/processing/affix_processor.rs +++ b/src/processing/affix_processor.rs @@ -54,7 +54,7 @@ fn get_affix_processor(input: &ConfigInput) -> Option { pub fn apply_affixes(fetched_playlists: &mut [FetchedPlaylist]) { for fetched_playlist in fetched_playlists.iter_mut() { - let FetchedPlaylist { input, playlist, epg: _ } = fetched_playlist; + let FetchedPlaylist { input, playlistgroups: playlist, epg: _ } = fetched_playlist; if let Some(affix_processor) = get_affix_processor(input) { for group in playlist.iter_mut() { group.channels.iter_mut().for_each(|channel| { diff --git a/src/processing/m3u_parser.rs b/src/processing/m3u_parser.rs index d95c4732e..70e8b1334 100644 --- a/src/processing/m3u_parser.rs +++ b/src/processing/m3u_parser.rs @@ -55,6 +55,7 @@ fn skip_digit(it: &mut std::str::Chars) -> Option { fn create_empty_playlistitem_header(input_id: u16, url: &str) -> PlaylistItemHeader { PlaylistItemHeader { + uuid: default_as_empty_rc_str(), id: default_as_empty_rc_str(), stream_id: default_as_empty_rc_str(), name: default_as_empty_rc_str(), @@ -125,6 +126,7 @@ fn process_header(input: &ConfigInput, video_suffixes: &Vec<&str>, content: &str plih.id = Rc::new(chanid); } } + plih.stream_id = Rc::clone(&plih.id); plih.epg_channel_id = Some(Rc::clone(&plih.id)); } @@ -211,12 +213,14 @@ pub(crate) fn parse_m3u(cfg: &Config, input: &ConfigInput, lines: &[String]) -> let mut result: Vec = vec![]; for (grp_id, (key, channels)) in (1_u32..).zip(groups.into_iter()) { - let cluster = channels.first().map(|pli| pli.header.borrow().xtream_cluster.clone()); - result.push(PlaylistGroup { id: grp_id, xtream_cluster: cluster.unwrap(), title: Rc::clone(&key), channels }); + let cluster = channels.first().map(|pli| pli.header.borrow().xtream_cluster); + result.push(PlaylistGroup {id: grp_id, xtream_cluster: cluster.unwrap(), title: Rc::clone(&key), channels }); } + // apply the sort order from the provider + let mut sort_iterator = sort_order.iter(); result.sort_by(|f, s| { - let i1 = sort_order.iter().position(|r| **r == *f.title).unwrap(); - let i2 = sort_order.iter().position(|r| **r == *s.title).unwrap(); + let i1 = sort_iterator.position(|r| **r == *f.title).unwrap(); + let i2 = sort_iterator.position(|r| **r == *s.title).unwrap(); i1.cmp(&i2) }); result diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index f39c6c9c4..f0572eea1 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -18,7 +18,7 @@ use crate::messaging::{MsgKind, send_message}; use crate::model::config::{ConfigSortChannel, ConfigSortGroup, ConfigTarget, InputType, ItemField, ProcessingOrder, ProcessTargets, SortOrder::{Asc, Desc}}; use crate::model::mapping::{Mapping, MappingValueProcessor}; -use crate::model::playlist::{FetchedPlaylist, PlaylistGroup, PlaylistItem}; +use crate::model::playlist::{FetchedPlaylist, PlaylistGroup, PlaylistItem, XtreamCluster}; use crate::model::stats::{InputStats, PlaylistStats}; use crate::processing::affix_processor::apply_affixes; use crate::processing::playlist_watch::process_group_watch; @@ -46,7 +46,7 @@ fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt id: pg.id, title: pg.title.clone(), channels, - xtream_cluster: pg.xtream_cluster.clone(), + xtream_cluster: pg.xtream_cluster, }); } } @@ -180,7 +180,7 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option let mut grp = playlist_group.clone(); let mappings = target.t_mapping.as_ref().unwrap(); mappings.iter().filter(|&mapping| !mapping.mapper.is_empty()).for_each(|mapping| - grp.channels = grp.channels.drain(..).map(|chan| map_channel(chan, mapping)).collect()); + grp.channels = grp.channels.drain(..).map(|chan| map_channel(chan, mapping)).collect()); grp }).collect(); @@ -200,7 +200,7 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option id: grp_id, title: Rc::clone(title), channels: vec![channel.clone()], - xtream_cluster: cluster.clone(), + xtream_cluster: *cluster, }); } } @@ -227,14 +227,14 @@ fn is_target_enabled(target: &ConfigTarget, user_targets: &ProcessTargets) -> bo async fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc) -> (Vec, Vec) { let source = cfg.sources.get(source_idx).unwrap(); - let mut all_playlist = Vec::new(); + let mut source_playlists = Vec::new(); let enabled_inputs = source.inputs.iter().filter(|item| item.enabled).count(); let mut errors = vec![]; let mut stats = HashMap::::new(); for input in &source.inputs { let input_id = input.id; if is_input_enabled(enabled_inputs, input.enabled, input_id, &user_targets) { - let (playlist, mut error_list) = match input.input_type { + let (mut playlistgroups, mut error_list) = match input.input_type { InputType::M3u => download::get_m3u_playlist(&cfg, input, &cfg.working_dir).await, InputType::Xtream => download::get_xtream_playlist(input, &cfg.working_dir).await, }; @@ -249,18 +249,19 @@ async fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc

input.url.as_str(), Some(name_val) => name_val.as_str() }; - let group_count = playlist.len(); - let channel_count = playlist.iter() + let group_count = playlistgroups.len(); + let channel_count = playlistgroups.iter() .map(|group| group.channels.len()) .sum(); - if playlist.is_empty() { + if playlistgroups.is_empty() { info!("source is empty {}", input.url); errors.push(M3uFilterError::new(M3uFilterErrorKind::Notify, format!("source is empty {input_name}"))); } else { - all_playlist.push( + playlistgroups.iter_mut().for_each(PlaylistGroup::on_load); + source_playlists.push( FetchedPlaylist { input, - playlist, + playlistgroups, epg: tvguide, } ); @@ -280,18 +281,18 @@ async fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc

()); } for target in &source.targets { if is_target_enabled(target, &user_targets) { - match process_playlist(&mut all_playlist, target, &cfg, &mut stats, &mut errors).await { + match process_playlist(&mut source_playlists, target, &cfg, &mut stats, &mut errors).await { Ok(()) => {} Err(mut err) => err.drain(..).for_each(|e| errors.push(e)) } @@ -357,6 +358,51 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe { } } + +fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &mut FetchedPlaylist<'a>) -> FetchedPlaylist<'a> { + let mut new_fpl = FetchedPlaylist { + input: fpl.input, + playlistgroups: fpl.playlistgroups.clone(), // we need to clone, because of multiple target definitions, we cant change the initial playlist. + epg: fpl.epg.clone(), + }; + for f in pipe { + if let Some(groups) = f(&mut new_fpl.playlistgroups, target) { + new_fpl.playlistgroups = groups; + } + } + new_fpl +} + +// This method is needed, because of duplicate group names in different inputs. +// We merge the same group names considering cluster together. +fn flatten_groups(mut playlistgroups: Vec) -> Vec { + let mut group_map: HashMap<(Rc, XtreamCluster), PlaylistGroup> = HashMap::new(); + let mut sort_order = vec![]; + playlistgroups.drain(..).for_each(|group| { + let key = (Rc::clone(&group.title), group.xtream_cluster); + match group_map.entry(key) { + std::collections::hash_map::Entry::Occupied(o) => { + // we loose the group id (category id) at this point, which should be available in + // the playlist item header. + o.into_mut().channels.extend(group.channels); + } + std::collections::hash_map::Entry::Vacant(v) => { + sort_order.push(Rc::clone(&group.title)); + v.insert(group); + }, + }; + }); + let mut flat_groups: Vec = group_map.into_values().collect(); + // apply the initial sort order + let mut sort_iterator = sort_order.iter(); + flat_groups.sort_by(|f, s| { + let i1 = sort_iterator.position(|r| **r == *f.title).unwrap(); + let i2 = sort_iterator.position(|r| **r == *s.title).unwrap(); + i1.cmp(&i2) + }); + flat_groups +} + async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], target: &ConfigTarget, cfg: &Config, stats: &mut HashMap, @@ -368,28 +414,16 @@ async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], let mut new_fetched_playlists: Vec = vec![]; for fpl in playlists.iter_mut() { - let mut new_fpl = FetchedPlaylist { - input: fpl.input, - playlist: fpl.playlist.clone(), // we need to clone, because of multiple target definitions, we cant change the initial playlist. - epg: fpl.epg.clone(), - }; - for f in &pipe { - let playlist = &mut new_fpl.playlist; - let r = f(playlist, target); - if let Some(v) = r { - new_fpl.playlist = v; - } - } + let mut new_fpl = execute_pipe(target, &pipe, fpl); playlist_resolve_series(target, errors, &pipe, fpl, &mut new_fpl).await; // stats let input_stats = stats.get_mut(&new_fpl.input.id); if let Some(stat) = input_stats { - stat.processed_stats.group_count = new_fpl.playlist.len(); - stat.processed_stats.channel_count = new_fpl.playlist.iter() + stat.processed_stats.group_count = new_fpl.playlistgroups.len(); + stat.processed_stats.channel_count = new_fpl.playlistgroups.iter() .map(|group| group.channels.len()) .sum(); } - new_fetched_playlists.push(new_fpl); } @@ -399,9 +433,12 @@ async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], let mut new_epg = vec![]; new_fetched_playlists.drain(..).for_each(|mut fp| { - let epg_channel_ids: HashSet<_> = fp.playlist.iter().flat_map(|g| &g.channels) + let epg_channel_ids: HashSet<_> = fp.playlistgroups.iter().flat_map(|g| &g.channels) .filter_map(|c| c.header.borrow().epg_channel_id.clone()).collect(); - fp.playlist.drain(..).for_each(|group| new_playlist.push(group)); + + fp.playlistgroups.drain(..).for_each(|group| { + new_playlist.push(group); + }); if !epg_channel_ids.is_empty() { if let Some(tv_guide) = fp.epg { debug!("found epg information for {}", &target.name); @@ -419,21 +456,24 @@ async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], Ok(()) } else { sort_playlist(target, &mut new_playlist); + process_watch(target, cfg, &new_playlist); + let mut flat_new_playlist = flatten_groups(new_playlist); + persist_playlist(&mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, cfg) + } +} - if target.t_watch_re.is_some() { - if default_as_default().eq_ignore_ascii_case(&target.name) { - error!("cant watch a target with no unique name"); - } else { - let watch_re = target.t_watch_re.as_ref().unwrap(); - for pl in &new_playlist { - if watch_re.iter().any(|r| r.is_match(&pl.title)) { - process_group_watch(cfg, &target.name, pl); - } +fn process_watch(target: &ConfigTarget, cfg: &Config, new_playlist: &Vec) { + if target.t_watch_re.is_some() { + if default_as_default().eq_ignore_ascii_case(&target.name) { + error!("cant watch a target with no unique name"); + } else { + let watch_re = target.t_watch_re.as_ref().unwrap(); + for pl in new_playlist { + if watch_re.iter().any(|r| r.is_match(&pl.title)) { + process_group_watch(cfg, &target.name, pl); } } } - - persist_playlist(&mut new_playlist, flatten_tvguide(&new_epg).as_ref(), target, cfg) } } diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index e14f11b82..80dabba4a 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -20,7 +20,7 @@ fn map_to_xtream_category(category: &Value) -> Result, M3uFi } } -fn map_to_xtream_streams(xtream_cluster: &XtreamCluster, streams: &Value) -> Result, M3uFilterError> { +fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: &Value) -> Result, M3uFilterError> { match serde_json::from_value::>(streams.to_owned()) { Ok(stream_list) => Ok(stream_list), Err(err) => { @@ -39,6 +39,7 @@ pub(crate) fn parse_xtream_series_info(info: &Value, group_title: &str, input: & let result: Vec = series_info.episodes.values().flatten().map(|episode| PlaylistItem { header: RefCell::new(PlaylistItemHeader { + uuid: default_as_empty_rc_str(), id: Rc::new(episode.id.clone()), stream_id: Rc::new(episode.id.clone()), name: Rc::new(episode.title.clone()), @@ -76,7 +77,7 @@ pub(crate) fn parse_xtream_series_info(info: &Value, group_title: &str, input: & } pub(crate) fn parse_xtream(input: &ConfigInput, - xtream_cluster: &XtreamCluster, + xtream_cluster: XtreamCluster, category: &Value, streams: &Value) -> Result>, M3uFilterError> { match map_to_xtream_category(category) { @@ -96,16 +97,17 @@ pub(crate) fn parse_xtream(input: &ConfigInput, for stream in streams { if let Some(group) = group_map.get(&stream.category_id) { let mut grp = group.borrow_mut(); - let title = &grp.category_name; + let category_name = &grp.category_name; let item = PlaylistItem { header: RefCell::new(PlaylistItemHeader { + uuid: default_as_empty_rc_str(), id: Rc::new(stream.get_stream_id()), stream_id: Rc::new(stream.get_stream_id()), name: Rc::clone(&stream.name), chno: default_as_empty_rc_str(), logo: Rc::clone(&stream.stream_icon), logo_small: default_as_empty_rc_str(), - group: Rc::clone(title), + group: Rc::clone(category_name), title: Rc::clone(&stream.name), parent_code: default_as_empty_rc_str(), audio_track: default_as_empty_rc_str(), @@ -132,7 +134,7 @@ pub(crate) fn parse_xtream(input: &ConfigInput, XtreamCluster::Video => PlaylistItemType::Movie, XtreamCluster::Series => PlaylistItemType::SeriesInfo, }, - xtream_cluster: xtream_cluster.clone(), + xtream_cluster, additional_properties: stream.get_additional_properties(), series_fetched: false, category_id: 0, @@ -147,7 +149,7 @@ pub(crate) fn parse_xtream(input: &ConfigInput, let cat = category.borrow(); PlaylistGroup { id: cat.category_id.parse::().unwrap_or(0), - xtream_cluster: xtream_cluster.clone(), + xtream_cluster, title: Rc::clone(&cat.category_name), channels: cat.channels.clone(), } diff --git a/src/repository/index_record.rs b/src/repository/index_record.rs index 1f01e07f3..1bb0e7808 100644 --- a/src/repository/index_record.rs +++ b/src/repository/index_record.rs @@ -12,13 +12,13 @@ of the encoded file, and size is the size of the encoded struct. We also use it for different purposes, like storing id -> to id mapping. */ -pub(crate) struct IndexRecord { +pub(in crate::repository) struct IndexRecord { pub left: u32, pub right: u32, } impl IndexRecord { - pub(crate) fn from_file(file: &mut File, offset: u32) -> Result { + pub fn from_file(file: &mut File, offset: u32) -> Result { file.seek(SeekFrom::Start(u64::from(offset)))?; let mut left_bytes = [0u8; 4]; let mut right_bytes = [0u8; 4]; @@ -29,7 +29,7 @@ impl IndexRecord { Ok(IndexRecord { left, right }) } - pub(crate) fn to_file(path: &Path, left: u32, right: u32, append: bool) -> Result<(), Error> { + pub fn to_file(path: &Path, left: u32, right: u32, append: bool) -> Result<(), Error> { match file_utils::open_file_append(path, append) { Ok(mut file) => { let bytes = IndexRecord::to_bytes(left, right); @@ -56,7 +56,7 @@ impl IndexRecord { // IndexRecord::to_bytes(self.index, self.size) // } - pub(crate) fn to_bytes(left: u32, right: u32) -> [u8; 8] { + pub fn to_bytes(left: u32, right: u32) -> [u8; 8] { let left_bytes: [u8; 4] = left.to_le_bytes(); let right_bytes: [u8; 4] = right.to_le_bytes(); let mut combined_bytes: [u8; 8] = [0; 8]; @@ -65,6 +65,6 @@ impl IndexRecord { combined_bytes } - pub(crate) fn get_record_size() -> u32 { 8 } - pub(crate) fn get_index_offset(index: u32) -> u32 { index * 8 } + pub fn get_record_size() -> u32 { 8 } + pub fn get_index_offset(index: u32) -> u32 { index * 8 } } \ No newline at end of file diff --git a/src/repository/indexed_document_reader.rs b/src/repository/indexed_document_reader.rs index df4f09537..ee727f5d7 100644 --- a/src/repository/indexed_document_reader.rs +++ b/src/repository/indexed_document_reader.rs @@ -6,7 +6,7 @@ use std::path::Path; use crate::repository::index_record::IndexRecord; -pub(crate) struct IndexedDocumentReader { +pub(in crate::repository) struct IndexedDocumentReader { main_file: File, index_file: File, cursor: u32, @@ -103,7 +103,7 @@ impl Iterator for IndexedDocumentReader } } -pub(crate) fn read_indexed_item(main_path: &Path, index_path: &Path, offset: u32) -> Result +pub(in crate::repository) fn read_indexed_item(main_path: &Path, index_path: &Path, offset: u32) -> Result where T: ?Sized + serde::de::DeserializeOwned { if main_path.exists() && index_path.exists() { diff --git a/src/repository/indexed_document_writer.rs b/src/repository/indexed_document_writer.rs index 377e9af6f..289bdca7f 100644 --- a/src/repository/indexed_document_writer.rs +++ b/src/repository/indexed_document_writer.rs @@ -7,7 +7,7 @@ use crate::repository::index_record::IndexRecord; use crate::utils::file_utils; use crate::utils::file_utils::create_file_tuple; -pub(crate) struct IndexedDocumentWriter { +pub(in crate::repository) struct IndexedDocumentWriter { main_path: PathBuf, index_path: PathBuf, main_file: File, diff --git a/src/repository/playlist_repository.rs b/src/repository/playlist_repository.rs index 3813dcb6a..3a820f793 100644 --- a/src/repository/playlist_repository.rs +++ b/src/repository/playlist_repository.rs @@ -13,8 +13,8 @@ pub(crate) fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg> for output in &target.output { match match output.target { TargetType::M3u => m3u_write_playlist(target, cfg, playlist), + TargetType::Xtream => xtream_write_playlist(target, cfg, playlist), TargetType::Strm => kodi_write_strm_playlist(target, cfg, playlist, &output.filename), - TargetType::Xtream => xtream_write_playlist(target, cfg, playlist) } { Ok(()) => { if !playlist.is_empty() { diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index cee2c6099..fcba80f58 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -56,7 +56,6 @@ fn xtream_get_catchup_id_mapping_file_path(storage_path: &Path) -> PathBuf { storage_path.join("mapping_catchup.db") } - fn ensure_xtream_storage_path(cfg: &Config, target_name: &str) -> Result { if let Some(path) = xtream_get_storage_path(cfg, target_name) { if std::fs::create_dir_all(&path).is_err() { @@ -70,8 +69,8 @@ fn ensure_xtream_storage_path(cfg: &Config, target_name: &str) -> Result Option<(PathBuf, PathBuf)> { - if cluster == &XtreamCluster::Series { +fn xtream_get_info_file_paths(storage_path: &Path, cluster: XtreamCluster) -> Option<(PathBuf, PathBuf)> { + if cluster == XtreamCluster::Series { let xtream_path = storage_path.join("series_info.db"); let extension = xtream_path.extension().map(|ext| format!("{}_", ext.to_str().unwrap_or(""))); let index_path = xtream_path.with_extension(format!("{}idx", &extension.unwrap_or_default())); @@ -81,7 +80,7 @@ fn xtream_get_info_file_paths(storage_path: &Path, cluster: &XtreamCluster) -> O } fn xtream_clear_series_info(storage_path: &Path) { - if let Some((info_path, idx_path)) = xtream_get_info_file_paths(storage_path, &XtreamCluster::Series) { + if let Some((info_path, idx_path)) = xtream_get_info_file_paths(storage_path, XtreamCluster::Series) { let _ = std::fs::remove_file(info_path); let _ = std::fs::remove_file(idx_path); let _ = std::fs::remove_file(xtream_get_series_episode_id_mapping_file_path(storage_path)); @@ -93,7 +92,7 @@ fn xtream_clear_catchup(storage_path: &Path) { let _ = std::fs::remove_file(xtream_get_catchup_id_mapping_file_path(storage_path)); } -fn write_playlist_to_file(storage_path: &Path, stream_id: &mut u32, cluster: &XtreamCluster, playlist: &mut [PlaylistItem]) -> Result<(), M3uFilterError> { +fn write_playlist_to_file(storage_path: &Path, stream_id: &mut u32, cluster: XtreamCluster, playlist: &mut [PlaylistItem]) -> Result<(), M3uFilterError> { let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster); match IndexedDocumentWriter::new(xtream_path.clone(), idx_path) { Ok(mut writer) => { @@ -106,9 +105,9 @@ fn write_playlist_to_file(storage_path: &Path, stream_id: &mut u32, cluster: &Xt } } } - if cluster == &XtreamCluster::Live { + if cluster == XtreamCluster::Live { xtream_clear_catchup(storage_path); - } else if cluster == &XtreamCluster::Series { + } else if cluster == XtreamCluster::Series { xtream_clear_series_info(storage_path); } Ok(()) @@ -144,8 +143,8 @@ fn write_playlists_to_file(storage_path: &Path, collections: Vec<(XtreamCluster, let mut stream_id: u32 = 1; for (cluster, playlist) in collections { let start = stream_id; - write_playlist_to_file(storage_path, &mut stream_id, &cluster, playlist)?; - id_list.push((cluster.clone(), start, stream_id)); + write_playlist_to_file(storage_path, &mut stream_id, cluster, playlist)?; + id_list.push((cluster, start, stream_id)); } match save_stream_id_cluster_mapping(storage_path, &mut id_list) { Ok(()) => Ok(()), @@ -199,7 +198,7 @@ pub(crate) fn xtream_get_epg_file_path(path: &Path) -> PathBuf { path.join("epg.xml") } -pub(crate) fn xtream_get_file_paths(storage_path: &Path, cluster: &XtreamCluster) -> (PathBuf, PathBuf) { +pub(crate) fn xtream_get_file_paths(storage_path: &Path, cluster: XtreamCluster) -> (PathBuf, PathBuf) { let xtream_path = storage_path.join(format!("{}.db", match cluster { XtreamCluster::Live => COL_LIVE, XtreamCluster::Video => COL_VOD, @@ -308,12 +307,12 @@ pub(crate) fn xtream_get_collection_path(cfg: &Config, target_name: &str, collec Err(Error::new(ErrorKind::Other, format!("Cant find collection: {target_name}/{collection_name}"))) } -fn _xtream_get_item_for_stream_id(stream_id: u32, storage_path: &Path, xtream_cluster: Option<&XtreamCluster>, mapping: &[(XtreamCluster, u32, u32)]) -> Result { +fn _xtream_get_item_for_stream_id(stream_id: u32, storage_path: &Path, xtream_cluster: Option, mapping: &[(XtreamCluster, u32, u32)]) -> Result { if let Some((cluster, cluster_start, _end)) = match xtream_cluster { - Some(clus) => mapping.iter().find(|(c, _, _)| c == clus), + Some(clus) => mapping.iter().find(|(c, _, _)| *c == clus), None => mapping.iter().find(|(_cluster, start, _end)| stream_id >= *start), } { - let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster); + let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, *cluster); if stream_id >= *cluster_start { return read_indexed_item::(&xtream_path, &idx_path, IndexRecord::get_index_offset(stream_id - cluster_start)); } @@ -321,7 +320,7 @@ fn _xtream_get_item_for_stream_id(stream_id: u32, storage_path: &Path, xtream_cl Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}"))) } -pub(crate) fn xtream_get_item_for_stream_id(stream_id: u32, config: &Config, target: &ConfigTarget, xtream_cluster: Option<&XtreamCluster>) -> Result { +pub(crate) fn xtream_get_item_for_stream_id(stream_id: u32, config: &Config, target: &ConfigTarget, xtream_cluster: Option) -> Result { if let Some(storage_path) = xtream_get_storage_path(config, target.name.replace(' ', "_").as_str()) { if let Some(mapping) = load_stream_id_cluster_mapping(&storage_path) { if let Some(max) = mapping.iter().map(|(_cluster, _start, end)| end).max().copied() { @@ -344,7 +343,7 @@ pub(crate) fn xtream_get_item_for_stream_id(stream_id: u32, config: &Config, tar Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}"))) } -pub(crate) fn xtream_load_rewrite_playlist(cluster: &XtreamCluster, config: &Config, target: &ConfigTarget, category_id: u32) -> Result { +pub(crate) fn xtream_load_rewrite_playlist(cluster: XtreamCluster, config: &Config, target: &ConfigTarget, category_id: u32) -> Result { if let Some(storage_path) = xtream_get_storage_path(config, target.name.replace(' ', "_").as_str()) { let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, cluster); match IndexedDocumentReader::::new(&xtream_path, &idx_path) { @@ -440,7 +439,7 @@ pub(crate) fn xtream_write_series_info(config: &Config, target_name: &str, series_id: u32, episode_id_mapping: &[(u32, u32)], content: &str) -> Result<(), Error> { if let Some(storage_path) = xtream_get_storage_path(config, target_name) { - if let Some((info_path, idx_path)) = xtream_get_info_file_paths(&storage_path, &XtreamCluster::Series) { + if let Some((info_path, idx_path)) = xtream_get_info_file_paths(&storage_path, XtreamCluster::Series) { return match IndexedDocumentWriter::new_append(info_path.clone(), idx_path) { Ok(mut writer) => { match writer.write_doc(content) { @@ -464,7 +463,7 @@ pub(crate) fn xtream_load_series_info(config: &Config, target_name: &str, series let series_id_index_mapping_path = xtream_get_series_id_series_info_mapping_file_path(&storage_path); if series_id_index_mapping_path.exists() { if let Some(storage_path) = xtream_get_storage_path(config, target_name) { - if let Some((info_path, idx_path)) = xtream_get_info_file_paths(&storage_path, &XtreamCluster::Series) { + if let Some((info_path, idx_path)) = xtream_get_info_file_paths(&storage_path, XtreamCluster::Series) { if info_path.exists() && idx_path.exists() { let mut file = File::open(series_id_index_mapping_path)?; let mut buffer = [0u8; 8]; diff --git a/src/utils/download.rs b/src/utils/download.rs index b0f6a5480..9a0bf2358 100644 --- a/src/utils/download.rs +++ b/src/utils/download.rs @@ -40,7 +40,7 @@ pub(crate) async fn get_m3u_playlist(cfg: &Config, input: &ConfigInput, working_ pub(crate) async fn get_xtream_playlist_series<'a>(fpl: &mut FetchedPlaylist<'a>, errors: &mut Vec, resolve_delay: u16) -> Vec { let input = fpl.input; let mut result: Vec = vec![]; - for plg in &mut fpl.playlist { + for plg in &mut fpl.playlistgroups { let mut group_series: Vec = vec![]; for pli in &plg.channels { let (fetch_series, series_info_url) = { @@ -128,7 +128,7 @@ pub(crate) async fn get_xtream_playlist(input: &ConfigInput, working_dir: &Strin match request_utils::get_input_json_content(input, stream_url.as_str(), stream_file_path).await { Ok(stream_content) => { match xtream_parser::parse_xtream(input, - xtream_cluster, + *xtream_cluster, &category_content, &stream_content) { Ok(sub_playlist_opt) => { @@ -147,10 +147,9 @@ pub(crate) async fn get_xtream_playlist(input: &ConfigInput, working_dir: &Strin } } playlist.sort_by(|a, b| a.title.partial_cmp(&b.title).unwrap_or(Ordering::Greater)); - let mut counter = 1; - for plg in &mut playlist { - plg.id = counter; - counter += 1; + + for (grp_id, plg) in (1_u32..).zip(playlist.iter_mut()) { + plg.id = grp_id; } (playlist, errors) } diff --git a/src/utils/request_utils.rs b/src/utils/request_utils.rs index f3eb674a3..c70a7f312 100644 --- a/src/utils/request_utils.rs +++ b/src/utils/request_utils.rs @@ -10,6 +10,7 @@ use crate::model::config::{ConfigInput}; use crate::utils::file_utils::{get_file_path, open_file, persist_file}; use reqwest::header::CONTENT_ENCODING; use flate2::read::{GzDecoder, ZlibDecoder}; +use url::Url; fn is_gzip(bytes: &[u8]) -> bool { // Gzip files start with the bytes 0x1F 0x8B @@ -209,3 +210,12 @@ pub(crate) async fn get_input_json_content(input: &ConfigInput, url: &str, persi Err(e) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "cant download input url: {url} => {}", e) } } + + + +pub(crate) fn get_base_url(url: &str) -> Option { + match Url::parse(url) { + Ok(url) => Some(url.origin().ascii_serialization()), + Err(_) => None + } +} \ No newline at end of file