diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5cf1c17..2b7327a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog # 2.0.11 (2025-01-xx) -- watch files are now moved inside the `target` folder. Move them manually from `watch__.bin` to `/watch_.bin` -- no error log for xtream api when content is skipped with options `xtream_skip_[live|vod|series]` +- Watch files are now moved inside the `target` folder. Move them manually from `watch__.bin` to `/watch_.bin` +- No error log for xtream api when content is skipped with options `xtream_skip_[live|vod|series]` - _experimental_: added live channel connection sharing in reverse proxy mode. To activate set `share_live_streams` in target options. -- added `info` and `tmdb-id` caching for vod and series with options `xtream_resolve_(series|vod)`. +- Added `info` and `tmdb-id` caching for vod and series with options `xtream_resolve_(series|vod)`. - The `kodi` format for movies can contain the `tmdb-id` (_optional_). To add the `tmdb-id` you can set now `kodi_style`, `xtream_resolve_vod`, `xtream_resolve_vod_delay`, `xtream_resolve_series` and `xtream_resolve_series_delay` to target options. - `kodi` output can now have `username` attribute to use reverse proxy mode when combined with `xtream` output. - Fixed webUI manual update for selected targets +- Added m3u logo url rewrite in `reverse proxy` mode or with `m3u_mask_redirect_url` option. - Breaking change: multi scheduler config with optional targets. ```yaml # sec min hour day of month month day of week year diff --git a/Cargo.lock b/Cargo.lock index 08a3b8184..842680399 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,13 +791,13 @@ dependencies = [ [[package]] name = "cron" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee8b2b4516038bc0f1d3c9934bcb4a13dd316e04abbc63c96757a6d75978532" +checksum = "dbe58904b68d95ad2c69a2e9607ffc70ce196c1910f83a340029c1448e06ed65" dependencies = [ "chrono", - "nom", "once_cell", + "winnow", ] [[package]] @@ -1824,12 +1824,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -1868,16 +1862,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -3427,6 +3411,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" +dependencies = [ + "memchr", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 296fe5ffc..2294bf46a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ clap = { version = "4", features = ["derive"] } url = "2.5" reqwest = { version = "0", features = ["blocking", "json", "stream", "rustls-tls"] } chrono = "0.4" -cron = "0.13" +cron = "0.14" actix-web = "4.9" actix-server = "2.5" actix-files = "0" diff --git a/config/config.yml b/config/config.yml index 35abc5d6a..1a64021e6 100644 --- a/config/config.yml +++ b/config/config.yml @@ -4,7 +4,8 @@ working_dir: /home/m3u-filter/data backup_dir: /home/m3u-filter/.backup update_on_boot: false # sec min hour day of month month day of week year -schedule: "0 0 8,12,16,20,22,1 * * * *" +schedules: + - schedule: "0 0 8,12,16,20,22,1 * * * *" web_ui_enabled: true web_auth: enabled: false diff --git a/config/source.yml b/config/source.yml index 30a8ac89c..527b8868c 100644 --- a/config/source.yml +++ b/config/source.yml @@ -2,14 +2,15 @@ templates: - name: DE_CHAN value: >- Group ~ "(?i)^.DE.*Serien.*" OR + - name: TR_CHAN + value: >- Group ~ "(?i)^.TR.*Filme.*" OR Group ~ "(?i)^.TR.*Live.*" - name: FR_CHAN value: Group ~ "^.FR..*" - name: ALL_CHAN value: >- - !DE_CHAN! OR - !FR_CHAN! + !DE_CHAN! OR !TR_CHAN! OR !FR_CHAN! sources: - inputs: - enabled: true @@ -32,7 +33,6 @@ sources: sort: groups: order: asc - mapping: - France TNT - France Premium diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 2bdb9a35a..7a3982e23 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -194,4 +194,32 @@ async fn shared_stream_response(app_state: &AppState, stream_url: &str) -> Optio pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarget) -> bool { item_type == PlaylistItemType::Live && target.options.as_ref().is_some_and(|opt| opt.share_live_streams) -} \ No newline at end of file +} + +pub async fn resource_response(_app_state: &AppState, resource_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse { + let req_headers: HashMap<&str, &[u8]> = req.headers().iter().map(|(k, v)| (k.as_str(), v.as_bytes())).collect(); + debug_if_enabled!("Try to open resource {}", mask_sensitive_info(resource_url)); + + if let Ok(url) = Url::parse(resource_url) { + let client = request_utils::get_client_request(input, &url, Some(&req_headers)); + match client.send().await { + Ok(response) => { + let status = response.status(); + if status.is_success() { + let mut response_builder = HttpResponse::Ok(); + response.headers().iter().for_each(|(k, v)| { + response_builder.insert_header((k.as_str(), v.as_ref())); + }); + return response_builder.body(actix_web::body::BodyStream::new(response.bytes_stream())); + } + debug_if_enabled!("Failed to open resource got status {} for {}", status, mask_sensitive_info(resource_url)); + } + Err(err) => { + error!("Received failure from server {}: {}", mask_sensitive_info(resource_url), err); + } + } + } else { + error!("Url is malformed {}", mask_sensitive_info(resource_url)); + } + HttpResponse::BadRequest().finish() +} diff --git a/src/api/m3u_api.rs b/src/api/m3u_api.rs index cd30ab261..38a4ae927 100644 --- a/src/api/m3u_api.rs +++ b/src/api/m3u_api.rs @@ -3,12 +3,13 @@ use bytes::Bytes; use futures::stream; use log::{debug, error}; -use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_stream_share_enabled, stream_response}; +use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, is_stream_share_enabled, resource_response, stream_response}; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; use crate::model::api_proxy::ProxyType; use crate::model::config::TargetType; -use crate::repository::m3u_playlist_iterator::M3U_STREAM_PATH; +use crate::model::playlist::FieldGetAccessor; +use crate::repository::m3u_playlist_iterator::{M3U_STREAM_PATH, M3U_RESOURCE_PATH}; use crate::repository::m3u_repository::{m3u_get_file_paths, m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; use crate::repository::storage::get_target_storage_path; use crate::utils::request_utils::mask_sensitive_info; @@ -22,7 +23,7 @@ async fn m3u_api( match m3u_load_rewrite_playlist(&app_state.config, target, &user).await { Ok(m3u_iter) => { // Convert the iterator into a stream of `Bytes` - let content_stream = stream::iter(m3u_iter.map(|line| Ok::(Bytes::from(format!("{line}\n"))))); + let content_stream = stream::iter(m3u_iter.map(|line| Ok::(Bytes::from([line.as_bytes(), b"\n"].concat())))); HttpResponse::Ok() .content_type(mime::TEXT_PLAIN_UTF_8) .streaming(content_stream) @@ -87,6 +88,48 @@ async fn m3u_api_stream( stream_response(&app_state, m3u_item.url.as_str(), &req, None, share_live_streams).await } +async fn m3u_api_resource( + req: HttpRequest, + api_req: web::Query, + path: web::Path<(String, String, String, String)>, + app_state: web::Data, +) -> HttpResponse { + let (username, password, stream_id, resource) = path.into_inner(); + let Ok(m3u_stream_id) = stream_id.parse::() else { return HttpResponse::BadRequest().finish() }; + let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state) else { return HttpResponse::BadRequest().finish() }; + + if !target.has_output(&TargetType::M3u) { + return HttpResponse::BadRequest().finish(); + } + + let Some(target_path) = get_target_storage_path(&app_state.config, target.name.as_str()) else { + error!("Failed to get target path for {}", target.name); + return HttpResponse::BadRequest().finish(); + }; + + let (m3u_path, idx_path) = m3u_get_file_paths(&target_path); + let m3u_item = match m3u_get_item_for_stream_id(&app_state.config, m3u_stream_id, &m3u_path, &idx_path).await { + Ok(item) => item, + Err(err) => { + error!("Failed to get m3u url: {}", mask_sensitive_info(err.to_string().as_str())); + return HttpResponse::BadRequest().finish(); + } + }; + + let stream_url = m3u_item.get_field(resource.as_str()); + match stream_url { + None => HttpResponse::NotFound().finish(), + Some(url) => { + if user.proxy == ProxyType::Redirect { + debug!("Redirecting stream request to {}", mask_sensitive_info(&url)); + HttpResponse::Found().insert_header(("Location", url.as_str())).finish() + } else { + resource_response(&app_state, url.as_str(), &req, None).await + } + } + } +} + macro_rules! register_m3u_stream_routes { ($cfg:expr, [$($path:expr),*]) => {{ $( @@ -106,5 +149,6 @@ macro_rules! register_m3u_api_routes { pub fn m3u_api_register(cfg: &mut web::ServiceConfig) { register_m3u_api_routes!(cfg, ["get.php", "apiget", "m3u"]); register_m3u_stream_routes!(cfg, ["live", "movie", "series"]); - cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}", "{username}/{password}/{stream_id}")).route(web::get().to(m3u_api_stream))); + cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}")).route(web::get().to(m3u_api_stream))); + cfg.service(web::resource(format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}")).route(web::get().to(m3u_api_resource))); } \ No newline at end of file diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 24282bf24..a10251559 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -13,7 +13,7 @@ use regex::Regex; use crate::filter::{apply_templates_to_pattern, get_filter, prepare_templates, Filter, PatternTemplate, RegexWithCaptures, ValueProcessor}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{ItemField, AFFIX_FIELDS, COUNTER_FIELDS, MAPPER_ATTRIBUTE_FIELDS}; -use crate::model::playlist::{FieldAccessor, PlaylistItem}; +use crate::model::playlist::{FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::utils::string_utils::Capitalize; use crate::{create_m3u_filter_error_result, handle_m3u_filter_error_result, info_err, valid_property}; diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 6a2d9cbdd..b35de9c5d 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -120,8 +120,10 @@ impl Display for PlaylistItemType { } } -pub trait FieldAccessor { +pub trait FieldGetAccessor { fn get_field(&self, field: &str) -> Option>; +} +pub trait FieldSetAccessor { fn set_field(&mut self, field: &str, value: &str) -> bool; } @@ -217,10 +219,19 @@ macro_rules! to_m3u_non_empty_fields { }; } +macro_rules! to_m3u_resource_non_empty_fields { + ($header:expr, $url:expr, $line:expr, $(($prop:ident, $field:expr)),*;) => { + $( + if !$header.$prop.is_empty() { + $line = format!("{} {}=\"{}/{}\"", $line, $field, $url, stringify!($prop)); + } + )* + }; +} macro_rules! generate_field_accessor_impl_for_playlist_item_header { ($($prop:ident),*;) => { - impl FieldAccessor for PlaylistItemHeader { + impl FieldGetAccessor for PlaylistItemHeader { fn get_field(&self, field: &str) -> Option> { match field { $( @@ -230,7 +241,8 @@ macro_rules! generate_field_accessor_impl_for_playlist_item_header { _ => None, } } - + } + impl FieldSetAccessor for PlaylistItemHeader { fn set_field(&mut self, field: &str, value: &str) -> bool { let val = String::from(value); match field { @@ -274,7 +286,10 @@ pub struct M3uPlaylistItem { } impl M3uPlaylistItem { - pub fn to_m3u(&self, target_options: Option<&ConfigTargetOptions>, url: Option<&str>) -> String { + pub fn to_m3u(&self, target_options: Option<&ConfigTargetOptions>, rewrite_urls: Option<&(String, String)>) -> String { + let (stream_url, resource_url) = rewrite_urls + .map_or_else(|| (self.url.as_str(), None), |(su, ru)| (su.as_str(), Some(ru.as_str()))); + let options = target_options.as_ref(); let ignore_logo = options.is_some_and(|o| o.ignore_logo); let mut line = format!("#EXTINF:-1 tvg-id=\"{}\" tvg-name=\"{}\" group-title=\"{}\"", @@ -282,7 +297,14 @@ impl M3uPlaylistItem { self.name, self.group); if !ignore_logo { - to_m3u_non_empty_fields!(self, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small");); + match resource_url { + None => { + to_m3u_non_empty_fields!(self, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small");); + }, + Some(res_url) => { + to_m3u_resource_non_empty_fields!(self, res_url, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small");); + } + } } to_m3u_non_empty_fields!(self, line, @@ -292,7 +314,7 @@ impl M3uPlaylistItem { (time_shift, "timeshift"), (rec, "tvg-rec");); - format!("{},{}\n{}", line, self.title, url.unwrap_or_else(|| self.url.as_str())) + format!("{},{}\n{}", line, self.title, stream_url) } } @@ -327,6 +349,24 @@ impl PlaylistEntry for M3uPlaylistItem { } } +macro_rules! generate_field_accessor_impl_for_m3u_playlist_item { + ($($prop:ident),*;) => { + impl FieldGetAccessor for M3uPlaylistItem { + fn get_field(&self, field: &str) -> Option> { + match field { + $( + stringify!($prop) => Some(self.$prop.clone()), + )* + "epg_channel_id" | "epg_id" => self.epg_channel_id.clone(), + _ => None, + } + } + } + } +} + +generate_field_accessor_impl_for_m3u_playlist_item!(provider_id, name, chno, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, url;); + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct XtreamPlaylistItem { pub virtual_id: u32, diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 8292243c8..6e608b71f 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -373,7 +373,7 @@ pub struct XtreamSeriesInfoEpisode { #[serde(default, deserialize_with = "string_default_on_null")] pub container_extension: String, #[serde(default)] - pub info: XtreamSeriesInfoEpisodeInfo, + pub info: Option, #[serde(default, deserialize_with = "string_default_on_null")] pub custom_sid: String, #[serde(default, deserialize_with = "string_default_on_null")] @@ -414,7 +414,7 @@ impl XtreamSeriesEpisode { custom_sid: info_episode.custom_sid.to_string(), added: info_episode.added.to_string(), season: info_episode.season, - tmdb_id: info_episode.info.tmdb_id.unwrap_or(0), + tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb_id).unwrap_or(0), direct_source: info_episode.direct_source.to_string(), } } @@ -425,7 +425,7 @@ pub struct XtreamSeriesInfo { #[serde(default)] pub seasons: Vec, #[serde(default)] - pub info: XtreamSeriesInfoInfo, + pub info: Option, #[serde(default)] pub episodes: HashMap>, } @@ -434,28 +434,29 @@ pub struct XtreamSeriesInfo { impl XtreamSeriesInfoEpisode { pub fn get_additional_properties(&self, series_info: &XtreamSeriesInfo) -> Option { let mut result = Map::new(); - let bdpath = &series_info.info.backdrop_path; + let info = series_info.info.as_ref(); + let bdpath = info.and_then(|i| i.backdrop_path.as_ref()); let bdpath_is_set = bdpath.as_ref().is_some_and(|bdpath| !bdpath.is_empty()); if bdpath_is_set { result.insert(String::from("backdrop_path"), Value::Array(Vec::from([Value::String(String::from(bdpath.as_ref().unwrap().first()?))]))); } - add_str_property_if_exists!(result, series_info.info.name.as_str(), "series_name"); - add_str_property_if_exists!(result, series_info.info.release_date, "series_release_date"); + add_str_property_if_exists!(result, info.map_or("", |i| i.name.as_str()), "series_name"); + add_str_property_if_exists!(result, info.map_or("", |i| i.release_date.as_str()), "series_release_date"); add_str_property_if_exists!(result, self.added.as_str(), "added"); - add_str_property_if_exists!(result, series_info.info.cast.as_str(), "cast"); + add_str_property_if_exists!(result, info.map_or("", |i| i.cast.as_str()), "cast"); add_str_property_if_exists!(result, self.container_extension.as_str(), "container_extension"); - add_str_property_if_exists!(result, self.info.movie_image, "cover"); - add_str_property_if_exists!(result, series_info.info.director, "director"); - add_str_property_if_exists!(result, series_info.info.episode_run_time, "episode_run_time"); - add_str_property_if_exists!(result, series_info.info.last_modified, "last_modified"); - add_str_property_if_exists!(result, self.info.plot, "plot"); - add_str_property_if_exists!(result, series_info.info.rating, "rating"); - add_f64_property_if_exists!(result, series_info.info.rating_5based, "rating_5based"); - add_str_property_if_exists!(result, self.info.releasedate, "release_date"); + add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| info.movie_image.as_str()), "cover"); + add_str_property_if_exists!(result, info.map_or("", |i| i.director.as_str()), "director"); + add_str_property_if_exists!(result, info.map_or("", |i| i.episode_run_time.as_str()), "episode_run_time"); + add_str_property_if_exists!(result, info.map_or("", |i| i.last_modified.as_str()), "last_modified"); + add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| info.plot.as_str()), "plot"); + add_f64_property_if_exists!(result, info.map_or(0_f64, |i| i.rating), "rating"); + add_f64_property_if_exists!(result, info.map_or(0_f64, |i| i.rating_5based), "rating_5based"); + add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| info.releasedate.as_str()), "release_date"); add_str_property_if_exists!(result, self.title, "title"); add_i64_property_if_exists!(result, self.season, "season"); add_i64_property_if_exists!(result, self.episode_num, "episode"); - add_opt_i64_property_if_exists!(result, self.info.tmdb_id, "tmdb_id"); + add_opt_i64_property_if_exists!(result, self.info.as_ref().and_then(|info| info.tmdb_id), "tmdb_id"); if result.is_empty() { None } else { Some(Value::Object(result)) } } } diff --git a/src/processing/affix_processor.rs b/src/processing/affix_processor.rs index cc9fe8740..707459f8c 100644 --- a/src/processing/affix_processor.rs +++ b/src/processing/affix_processor.rs @@ -1,5 +1,5 @@ use crate::model::config::{ConfigInput, InputAffix, AFFIX_FIELDS}; -use crate::model::playlist::{FetchedPlaylist, FieldAccessor, PlaylistItem}; +use crate::model::playlist::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistItem}; use crate::{debug_if_enabled, valid_property}; type AffixProcessor<'a> = Box; diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 30084c8f9..e9c6a8729 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -19,7 +19,7 @@ use crate::messaging::{send_message, MsgKind}; use crate::model::config::{ConfigSortChannel, ConfigSortGroup, ConfigTarget, InputType, ItemField, ProcessTargets, ProcessingOrder, SortOrder::{Asc, Desc}}; use crate::model::mapping::{CounterModifier, Mapping, MappingValueProcessor}; -use crate::model::playlist::{FetchedPlaylist, FieldAccessor, PlaylistGroup, PlaylistItem, XtreamCluster}; +use crate::model::playlist::{FetchedPlaylist, FieldGetAccessor, FieldSetAccessor, PlaylistGroup, PlaylistItem, XtreamCluster}; use crate::model::stats::{InputStats, PlaylistStats}; use crate::processing::affix_processor::apply_affixes; use crate::processing::playlist_watch::process_group_watch; diff --git a/src/processing/xtream_parser.rs b/src/processing/xtream_parser.rs index ce64390e7..ddea7d66d 100644 --- a/src/processing/xtream_parser.rs +++ b/src/processing/xtream_parser.rs @@ -10,6 +10,7 @@ use crate::model::config::ConfigInput; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; use crate::model::xtream::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream}; use crate::repository::storage::hash_string; +use crate::utils::request_utils::mask_sensitive_info; fn map_to_xtream_category(categories: &Value) -> Result, M3uFilterError> { match serde_json::from_value::>(categories.to_owned()) { @@ -54,7 +55,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s id: Rc::new(episode.id.to_string()), uuid: Rc::new(hash_string(&episode_url)), name: Rc::new(series_name.to_string()), - logo: Rc::new(episode.info.movie_image.clone()), + logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())), group: Rc::new(group_title.to_string()), title: Rc::new(episode.title.clone()), url: episode_url, @@ -69,7 +70,7 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s if result.is_empty() { Ok(None) } else { Ok(Some(result)) } } Err(err) => { - create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to process series info {}", &err) + create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to process series info {} {}", &err, mask_sensitive_info(url)) } } } diff --git a/src/repository/bplustree.rs b/src/repository/bplustree.rs index ecb54d657..b7e5eec9d 100644 --- a/src/repository/bplustree.rs +++ b/src/repository/bplustree.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use tempfile::NamedTempFile; use crate::utils::file_utils::{open_read_write_file, rename_or_copy}; +// TODO replace ZLib with faster compression like lz4 or zstd + const BINCODE_OVERHEAD: usize = 8; const BLOCK_SIZE: usize = 4096; const LEN_SIZE: usize = 4; diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index 36de1cf25..6465b4c2f 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -1,7 +1,7 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::api_proxy::{ApiProxyServerInfo, ProxyType, ProxyUserCredentials}; use crate::model::config::{Config, ConfigTarget, TargetOutput}; -use crate::model::playlist::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemType}; +use crate::model::playlist::{FieldGetAccessor, PlaylistGroup, PlaylistItem, PlaylistItemType}; use crate::model::xtream::XtreamSeriesEpisode; use crate::repository::bplustree::BPlusTree; use crate::repository::storage::get_input_storage_path; diff --git a/src/repository/m3u_playlist_iterator.rs b/src/repository/m3u_playlist_iterator.rs index 8dca5feb3..2361bb2e7 100644 --- a/src/repository/m3u_playlist_iterator.rs +++ b/src/repository/m3u_playlist_iterator.rs @@ -9,6 +9,7 @@ use crate::repository::storage::ensure_target_storage_path; use crate::utils::file_lock_manager::FileReadGuard; pub const M3U_STREAM_PATH: &str = "m3u-stream"; +pub const M3U_RESOURCE_PATH: &str = "m3u-resource"; pub struct M3uPlaylistIterator { reader: IndexedDocumentIterator, @@ -40,7 +41,7 @@ impl M3uPlaylistIterator { .map_err(|err| info_err!(format!("Could not deserialize file {m3u_path:?} - {err}")))?; let target_options = target.options.as_ref(); - let include_type_in_url = target_options.is_some_and( |opts| opts.m3u_include_type_in_url); + let include_type_in_url = target_options.is_some_and(|opts| opts.m3u_include_type_in_url); let mask_redirect_url = target_options.is_some_and(|opts| opts.m3u_mask_redirect_url); let server_info = cfg.get_user_server_info(user); @@ -58,7 +59,7 @@ impl M3uPlaylistIterator { }) } - fn get_stream_url(&self, m3u_pli: &M3uPlaylistItem, typed: bool) -> String { + fn get_rewritten_url(&self, m3u_pli: &M3uPlaylistItem, typed: bool, prefix_path: &str) -> String { if typed { let stream_type = match m3u_pli.item_type { PlaylistItemType::Live @@ -69,18 +70,26 @@ impl M3uPlaylistIterator { PlaylistItemType::Series | PlaylistItemType::SeriesInfo => "series", }; - format!("{}/{M3U_STREAM_PATH}/{stream_type}/{}/{}/{}", + format!("{}/{prefix_path}/{stream_type}/{}/{}/{}", &self.base_url, &self.username, &self.password, m3u_pli.virtual_id ) } else { - format!("{}/{M3U_STREAM_PATH}/{}/{}/{}", + format!("{}/{prefix_path}/{}/{}/{}", &self.base_url, &self.username, &self.password, m3u_pli.virtual_id ) } } + + fn get_stream_url(&self, m3u_pli: &M3uPlaylistItem, typed: bool) -> String { + self.get_rewritten_url(m3u_pli, typed, M3U_STREAM_PATH) + } + fn get_resource_url(&self, m3u_pli: &M3uPlaylistItem) -> String { + self.get_rewritten_url(m3u_pli, false, M3U_RESOURCE_PATH) + } + } impl Iterator for M3uPlaylistIterator { @@ -94,19 +103,19 @@ impl Iterator for M3uPlaylistIterator { // TODO hls and unknown reverse proxy self.reader.next().map(|m3u_pli| { - let stream_url = match m3u_pli.item_type { + let rewrite_urls = match m3u_pli.item_type { PlaylistItemType::LiveHls => None, - _ => match &self.proxy_type { - ProxyType::Reverse => Some(self.get_stream_url(&m3u_pli, self.include_type_in_url)), - ProxyType::Redirect => if self.mask_redirect_url { - Some(self.get_stream_url(&m3u_pli,self.include_type_in_url)) - } else { - None - } + _ => if match &self.proxy_type { + ProxyType::Reverse => true, + ProxyType::Redirect => self.mask_redirect_url, + } { + Some((self.get_stream_url(&m3u_pli, self.include_type_in_url), self.get_resource_url(&m3u_pli))) + } else { + None } }; let target_options = self.target_options.as_ref(); - m3u_pli.to_m3u(target_options, stream_url.as_deref()) + m3u_pli.to_m3u(target_options, rewrite_urls.as_ref()) }) } }