From ee97fd0637d94710960188583f0ca33781fb36ae Mon Sep 17 00:00:00 2001 From: euzu Date: Fri, 20 Oct 2023 12:18:44 +0200 Subject: [PATCH] removed publish attribute for target, instead changed target.output to list of TargetType, now it is possible to write multiple output formats. --- CHANGELOG.md | 1 + Cargo.toml | 12 ++-- README.md | 32 ++++++---- src/api/api_utils.rs | 17 +++++ src/api/m3u_api.rs | 32 ++++++++++ src/api/main_api.rs | 2 + src/api/mod.rs | 2 + src/api/xtream_player_api.rs | 20 +++--- src/config_reader.rs | 9 +-- src/main.rs | 2 +- src/model/api_proxy.rs | 5 ++ src/model/config.rs | 95 ++++++++++++++-------------- src/model/model_config.rs | 7 +- src/model/model_m3u.rs | 2 +- src/processing/playlist_processor.rs | 71 +++++++++++++-------- src/repository/m3u_repository.rs | 38 ++++++----- src/repository/xtream_repository.rs | 2 +- src/utils.rs | 7 ++ 18 files changed, 225 insertions(+), 131 deletions(-) create mode 100644 src/api/api_utils.rs create mode 100644 src/api/m3u_api.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index a8318696d..561711957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Added Xtream Cluster Live, Video, Series. M3u Playlist cluster guessing through video file endings. * Added api-proxy config for xtream proxy, to define server info and user credentials * Added Xtream Api Endpoints. +* Added M3u Api Endpoints. * Added multiple input support * Added Messaging with opt in message types [info, error, stats] * Added Telegram message support diff --git a/Cargo.toml b/Cargo.toml index 2287ec531..41923105e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,14 +28,14 @@ actix-cors = "0" actix-rt = "2.8" futures = "0.3" path-absolutize = "3.0" -pest = "2.5.5" +pest = "2.5" pest_derive = "2.5" enum-iterator = "1.3" unidecode = "0" petgraph = "0" openssl = { version = "*", features = ["vendored"] } #https://docs.rs/openssl/0.10.34/openssl/#vendored -mime = "0.3.17" -log = "0.4.20" -env_logger = "0.10.0" -rustelebot = "0.3.2" -bincode = "1.3.3" +mime = "0.3" +log = "0.4" +env_logger = "0.10" +rustelebot = "0.3" +bincode = "1.3" diff --git a/README.md b/README.md index 8b9c67bed..2ad63a9fe 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ m3u-filter is a simple application which can: ``` ------------ |----> playlist_1 - | M3U_URL_1 | --> filter|map|rename|sort --|----> playlist_2 (optional) + | M3U_URL_1 | --> filter|map|rename|sort --|----> playlist_2 (optional) --> publish in server mode for user | M3U_URL_2 | (optional) ------------ |----> playlist_3 (optional) ``` @@ -155,10 +155,9 @@ sources: Has the following top level entries: * `enabled` _optional_ default is `true`, if you disable the processing is skipped * `name` _optional_ default is `default`, if not default it has to be unique, for running selective targets -* `publish` _optional_ default is false, only required if type is `xtream`, otherwise ignored. If true the contents can be published through xtream proxy. -* `filename` _mandatory_ if type `m3u` or `publish`= false, otherwise _optional_ +* `filename` _mandatory_ if type `m3u` or `strm`, otherwise _optional_ * `sort` _optional_ -* `output` _optional_ default is `m3u` +* `output` _optional_ list of output types [`m3u`, `strm`, `xtream`], default is [`m3u`] * `processing_order` _optional_ default is `frm` * `options` _optional_ * `filter` _mandatory_, @@ -166,11 +165,10 @@ Has the following top level entries: * `mapping` _optional_ * `watch` _optional_ -### 1.5.2.1 `publish` and `filename` +### 1.5.2.1 `filename` -`publish` only required if type is `xtream`, otherwise it is ignored. -If true the contents can be published through xtream proxy. -`filename` is the file for resulting playlist. It is _mandatory_ if type `m3u` or `publish`= false +`filename` is the file for resulting playlist. +It is _mandatory_ if output type contains `m3u` or `strm` ### 1.5.2.2 `sort` Has three top level attributes @@ -197,7 +195,8 @@ sort: ``` ### 1.5.2.3 `output` -There are two types of targets ```m3u``` and ```strm```. +Is a list of target types. +There are three types of targets ```m3u```, ```strm``` and ```xtream```. If the attribute is not specified ```m3u``` is created by default. You can set options for each `output` type. @@ -299,7 +298,8 @@ sources: - name: pl1strm enabled: false filename: playlist_strm - output: strm + output: + - strm options: ignore_logo: true underscore_whitespace: false @@ -335,8 +335,18 @@ the configured messaging (f.e. telegram channel). ### 1.6 `messaging` `messaging` is an optional configuration for receiving messages. Currently only telegram is supported. + +Messaging is Opt-In, you need to set the `notify_on` message types which are +- `info` +- `stats` +- `error` + ```yaml messaging: + notify_on: + - info + - stats + - error telegram: bot_token: '' chat_ids: @@ -558,7 +568,7 @@ mappings: If you use the proxy functionality, you need to create a `api-proxy.yml` configuration. -You can specify the path for the file with the `-a` cli argument. +You can specify the path for the file with the `-a` cli argument. ```yaml server: diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs new file mode 100644 index 000000000..5635527ef --- /dev/null +++ b/src/api/api_utils.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; +use actix_web::http::header::{CACHE_CONTROL, HeaderValue}; +use actix_web::{HttpRequest, HttpResponse}; + +pub async fn serve_file(file_path: &PathBuf, req: &HttpRequest) -> HttpResponse { + if file_path.exists() { + let file = actix_files::NamedFile::open_async(file_path).await.unwrap() + .set_content_type(mime::APPLICATION_JSON) + .disable_content_disposition(); + let mut result = file.into_response(req); + let headers = result.headers_mut(); + headers.insert(CACHE_CONTROL, HeaderValue::from_bytes("no-cache".as_bytes()).unwrap()); + result + } else { + HttpResponse::NoContent().finish() + } +} \ No newline at end of file diff --git a/src/api/m3u_api.rs b/src/api/m3u_api.rs new file mode 100644 index 000000000..967e3f3b9 --- /dev/null +++ b/src/api/m3u_api.rs @@ -0,0 +1,32 @@ +use actix_web::{get, HttpRequest, HttpResponse, web}; + +use crate::api::api_utils::serve_file; +use crate::api::model_api::AppState; +use crate::repository::m3u_repository::get_m3u_file_path; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct M3uApiRequest { + username: String, + password: String, +} + +#[get("/get.php")] +pub(crate) async fn m3u_api( + api_req: web::Query, + req: HttpRequest, + _app_state: web::Data, +) -> HttpResponse { + match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { + Some(target) => { + match get_m3u_file_path(&_app_state.config, target) { + Some(file_path) => { + serve_file(&file_path, &req).await + } + None => HttpResponse::BadRequest().finish() + } + } + None => { + HttpResponse::BadRequest().finish() + } + } +} \ No newline at end of file diff --git a/src/api/main_api.rs b/src/api/main_api.rs index 95b58a2f1..115a5e57f 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -12,6 +12,7 @@ use actix_web::web::Data; use chrono::Local; use cron::Schedule; use serde_json::json; +use crate::api::m3u_api::m3u_api; use crate::api::model_api::{AppState, PlaylistRequest, ServerConfig}; use crate::api::xtream_player_api::xtream_player_api; @@ -108,6 +109,7 @@ pub(crate) async fn start_server(cfg: Arc, targets: Arc) .route("/playlist", web::post().to(playlist)) .route("/config", web::get().to(config)) ).service(xtream_player_api) + .service(m3u_api) .service(index) .service(actix_files::Files::new("/", web_dir.to_string())) ) diff --git a/src/api/mod.rs b/src/api/mod.rs index fd9eeebfc..653ad6714 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ pub(crate) mod model_api; pub(crate) mod main_api; pub(crate) mod xtream_player_api; +pub(crate) mod m3u_api; +pub(crate) mod api_utils; \ No newline at end of file diff --git a/src/api/xtream_player_api.rs b/src/api/xtream_player_api.rs index 2ab6c1d1b..19225ec84 100644 --- a/src/api/xtream_player_api.rs +++ b/src/api/xtream_player_api.rs @@ -1,11 +1,12 @@ // https://github.com/tellytv/go.xtream-codes/blob/master/structs.go -use actix_web::{HttpResponse, web, get, HttpRequest}; -use actix_web::http::header::{CACHE_CONTROL, HeaderValue}; +use actix_web::{get, HttpRequest, HttpResponse, web}; use chrono::{Duration, Local}; + +use crate::api::api_utils::serve_file; use crate::api::model_api::{AppState, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; use crate::model::config::Config; -use crate::model::model_config::{default_as_empty_str}; +use crate::model::model_config::default_as_empty_str; use crate::repository::xtream_repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, xtream_get_all}; fn get_user_info(user_name: &str, cfg: &Config) -> XtreamAuthorizationResponse { @@ -53,7 +54,8 @@ pub(crate) async fn xtream_player_api( _app_state: web::Data, ) -> HttpResponse { match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { - Some(target_name) => { + Some(target) => { + let target_name = &target.name; if api_req.action.is_empty() { return HttpResponse::Ok().json(get_user_info(api_req.username.as_str(), &_app_state.config)); } @@ -67,13 +69,7 @@ pub(crate) async fn xtream_player_api( _ => Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cant find action: {}/{}", target_name, &api_req.action))), } { Ok(file_path) => { - let file = actix_files::NamedFile::open_async(file_path).await.unwrap() - .set_content_type(mime::APPLICATION_JSON) - .disable_content_disposition(); - let mut result = file.into_response(&req); - let headers = result.headers_mut(); - headers.insert(CACHE_CONTROL, HeaderValue::from_bytes("no-cache".as_bytes()).unwrap()); - result + serve_file(&file_path, &req).await } Err(_) => HttpResponse::BadRequest().finish() } @@ -86,4 +82,4 @@ pub(crate) async fn xtream_player_api( } } } -} \ No newline at end of file +} diff --git a/src/config_reader.rs b/src/config_reader.rs index 11ada15ca..374b84bd5 100644 --- a/src/config_reader.rs +++ b/src/config_reader.rs @@ -18,19 +18,14 @@ pub(crate) fn read_mappings(args_mapping: Option, cfg: &mut Config) -> R } } -pub(crate) fn read_api_proxy_config(args_api_proxy_config: Option, cfg: &mut Config) -> Result<(), M3uFilterError>{ +pub(crate) fn read_api_proxy_config(args_api_proxy_config: Option, cfg: &mut Config) { let api_proxy_config_file: String = args_api_proxy_config.unwrap_or(utils::get_default_api_proxy_config_path()); let api_proxy_config = read_api_proxy(api_proxy_config_file.as_str()); if api_proxy_config.is_none() { - if cfg.has_published_targets() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "cant read api_proxy_config file: {}", api_proxy_config_file.as_str()); - } else { - warn!("cant read api_proxy_config file: {}", api_proxy_config_file.as_str()); - } + warn!("cant read api_proxy_config file: {}", api_proxy_config_file.as_str()); } else { cfg.set_api_proxy(api_proxy_config); } - Ok(()) } diff --git a/src/main.rs b/src/main.rs index c117167c7..d872f3a49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,7 +72,7 @@ fn main() { } if args.server { - if let Err(err) = read_api_proxy_config(args.api_proxy, &mut cfg) { exit!("{}", err) }; + read_api_proxy_config(args.api_proxy, &mut cfg); start_in_server_mode(Arc::new(cfg), Arc::new(targets)); } else { start_in_cli_mode(Arc::new(cfg), Arc::new(targets)) diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index 4e7897c65..0e266d4a5 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -23,12 +23,17 @@ impl TargetUser { } } +fn default_as_443() -> u16 { 443 } +fn default_as_1935() -> u16 { 1935 } + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ServerInfo { pub protocol: String, pub ip: String, pub http_port: u16, + #[serde(default = "default_as_443")] pub https_port: u16, + #[serde(default = "default_as_1935")] pub rtmp_port: u16, pub timezone: String, pub message: String, diff --git a/src/model/config.rs b/src/model/config.rs index d1462ab3e..5d92ceca2 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1,16 +1,18 @@ use std::collections::{HashMap, HashSet}; -use path_absolutize::*; + +use enum_iterator::Sequence; use log::{debug, error}; +use path_absolutize::*; use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider}; -use crate::model::mapping::Mappings; -use crate::model::mapping::Mapping; -use crate::model::model_config::{ItemField, ProcessingOrder, SortOrder, TargetType, default_as_zero, default_as_false, default_as_true}; -use crate::{utils}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::messaging::MsgKind; use crate::model::api_proxy::ApiProxyConfig; -use crate::utils::get_working_path; +use crate::model::mapping::Mapping; +use crate::model::mapping::Mappings; +use crate::model::model_config::{default_as_false, default_as_true, default_as_zero, ItemField, ProcessingOrder, SortOrder, TargetType}; +use crate::utils; +use crate::utils::{dedup, get_working_path}; fn default_as_frm() -> ProcessingOrder { ProcessingOrder::Frm } @@ -146,6 +148,8 @@ pub(crate) struct ConfigOptions { pub kodi_style: bool, } +fn default_as_empty_output() -> Vec:: { vec![] } + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct ConfigTarget { #[serde(skip)] @@ -154,14 +158,12 @@ pub(crate) struct ConfigTarget { pub enabled: bool, #[serde(default = "default_as_default")] pub name: String, - #[serde(default = "default_as_false")] - pub publish: bool, pub filename: Option, pub options: Option, pub sort: Option, pub filter: String, - #[serde(alias = "type")] - pub output: Option, + #[serde(alias = "type", default = "default_as_empty_output")] + pub output: Vec, pub rename: Option>, pub mapping: Option>, #[serde(default = "default_as_frm")] @@ -176,6 +178,7 @@ pub(crate) struct ConfigTarget { impl ConfigTarget { pub fn prepare(&mut self, id: u16, templates: Option<&Vec>) -> Result<(), M3uFilterError> { self.id = id; + dedup::(&mut self.output); match get_filter(&self.filter, templates) { Ok(fltr) => { debug!("Filter: {}", fltr); @@ -187,7 +190,7 @@ impl ConfigTarget { handle_m3u_filter_error_result!(M3uFilterErrorKind::Info, sort.prepare()); } Ok(()) - }, + } Err(err) => Err(err), } } @@ -206,7 +209,7 @@ pub(crate) struct ConfigSource { impl ConfigSource { pub fn prepare(&mut self, index: u16) -> Result { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16)))); - Ok(index+(self.inputs.len() as u16)) + Ok(index + (self.inputs.len() as u16)) } } @@ -216,7 +219,7 @@ pub(crate) struct InputAffix { pub value: String, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)] pub(crate) enum InputType { #[serde(rename = "m3u")] M3u, @@ -255,7 +258,7 @@ pub(crate) struct ConfigInput { } impl ConfigInput { - pub fn prepare(&mut self, id: u16) -> Result<(), M3uFilterError>{ + pub fn prepare(&mut self, id: u16) -> Result<(), M3uFilterError> { self.id = id; if self.url.trim().is_empty() { return Err(M3uFilterError::new(M3uFilterErrorKind::Info, "url for input is mandatory".to_string())); @@ -316,7 +319,6 @@ fn default_as_empty_list() -> Vec { vec![] } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct MessagingConfig { - #[serde(default = "default_as_empty_list")] pub notify_on: Vec, pub telegram: Option, @@ -338,51 +340,49 @@ pub(crate) struct Config { } impl Config { - pub(crate) fn has_published_targets(&self) -> bool { - for source in &self.sources { - for target in &source.targets { - if target.publish { - return true; - } - } - } - false - } - pub fn set_api_proxy(&mut self, api_proxy: Option) { self._api_proxy = api_proxy; } - pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<&str> { + pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<&ConfigTarget> { match &self._api_proxy { - Some(api_proxy) => api_proxy.get_target_name(username, password), + Some(api_proxy) => { + match api_proxy.get_target_name(username, password) { + Some(target_name) => { + for source in &self.sources { + for target in &source.targets { + if target_name.eq_ignore_ascii_case(&target.name) { + return Some(target); + } + } + } + None + } + None => None + } + } _ => None } } - pub fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError>{ + pub fn set_mappings(&mut self, mappings: Option) -> Result<(), M3uFilterError> { if let Some(mapping_list) = mappings { for source in &mut self.sources { - for input in &source.inputs { - let is_m3u = matches!(input.input_type, InputType::M3u); - for target in &mut source.targets { - if is_m3u && target.filename.is_none() { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for m3u type: {}", target.name); - } - if !is_m3u && target.filename.is_none() && !target.publish { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename or publish is required for xtream type: {}", target.name); - } + for target in &mut source.targets { + let needs_filename = target.output.is_empty() || target.output.contains(&TargetType::M3u) || target.output.contains(&TargetType::Strm); + if needs_filename && target.filename.is_none() { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "filename is required for m3u type: {}", target.name); + } - if let Some(mapping_ids) = &target.mapping { - let mut target_mappings = Vec::new(); - for mapping_id in mapping_ids { - let mapping = mapping_list.get_mapping(mapping_id); - if let Some(mappings) = mapping { - target_mappings.push(mappings); - } + if let Some(mapping_ids) = &target.mapping { + let mut target_mappings = Vec::new(); + for mapping_id in mapping_ids { + let mapping = mapping_list.get_mapping(mapping_id); + if let Some(mappings) = mapping { + target_mappings.push(mappings); } - target._mapping = if !target_mappings.is_empty() { Some(target_mappings) } else { None }; } + target._mapping = if !target_mappings.is_empty() { Some(target_mappings) } else { None }; } } } @@ -390,7 +390,7 @@ impl Config { Ok(()) } - pub fn prepare(&mut self) -> Result<(), M3uFilterError>{ + pub fn prepare(&mut self) -> Result<(), M3uFilterError> { self.working_dir = get_working_path(&self.working_dir); self.api.prepare(); self.prepare_api_web_root(); @@ -403,7 +403,6 @@ impl Config { return Err(err); } } - }; // prepare sources and set id's let mut target_names_check = HashSet::::new(); diff --git a/src/model/model_config.rs b/src/model/model_config.rs index 0225e8506..ec3760fb9 100644 --- a/src/model/model_config.rs +++ b/src/model/model_config.rs @@ -31,12 +31,14 @@ pub(crate) fn default_as_empty_map() -> HashMap { HashMap::new() pub(crate) fn default_as_zero() -> u8 { 0 } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)] +#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq, Eq, Hash)] pub(crate) enum TargetType { #[serde(rename = "m3u")] M3u, #[serde(rename = "strm")] Strm, + #[serde(rename = "xtream")] + Xtream, } impl std::fmt::Display for TargetType { @@ -44,11 +46,12 @@ impl std::fmt::Display for TargetType { match *self { TargetType::M3u => write!(f, "M3u"), TargetType::Strm => write!(f, "Strm"), + TargetType::Xtream => write!(f, "Xtream"), } } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq)] pub(crate) enum ProcessingOrder { #[serde(rename = "frm")] Frm, diff --git a/src/model/model_m3u.rs b/src/model/model_m3u.rs index 8d709f23f..2b00352b3 100644 --- a/src/model/model_m3u.rs +++ b/src/model/model_m3u.rs @@ -12,7 +12,7 @@ pub(crate) struct FetchedPlaylist<'a> { pub playlist: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum XtreamCluster { Live = 1, Video = 2, diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index df6b008b9..fd482ee0d 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -1,26 +1,26 @@ extern crate unidecode; use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap}; use std::sync::{Arc, Mutex}; use std::thread; use log::{debug, error, info}; use unidecode::unidecode; -use crate::{Config, create_m3u_filter_error_result, get_errors_notify_message, model::config, valid_property}; +use crate::{Config, get_errors_notify_message, model::config, valid_property}; use crate::download::{get_m3u_playlist, get_xtream_playlist}; use crate::filter::{get_field_value, MockValueProcessor, set_field_value, ValueProvider}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::messaging::{MsgKind, send_message}; use crate::model::config::{ConfigTarget, default_as_default, InputAffix, InputType, ProcessTargets}; use crate::model::mapping::{Mapping, MappingValueProcessor}; -use crate::model::model_config::{AFFIX_FIELDS, ItemField, ProcessingOrder, SortOrder::{Asc, Desc}}; +use crate::model::model_config::{AFFIX_FIELDS, ItemField, ProcessingOrder, SortOrder::{Asc, Desc}, TargetType}; use crate::model::model_m3u::{FetchedPlaylist, FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; use crate::model::stats::{InputStats, PlaylistStats}; use crate::processing::playlist_watch::process_group_watch; -use crate::repository::m3u_repository::write_playlist; -use crate::repository::xtream_repository::xtream_save_playlist; +use crate::repository::m3u_repository::{write_m3u_playlist, write_strm_playlist}; +use crate::repository::xtream_repository::{write_xtream_playlist}; fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option> { debug!("Filtering {} groups", playlist.len()); @@ -327,7 +327,7 @@ fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc {} - Err(err) => errors.push(err) + Err(mut err) => err.drain(..).for_each(|e| errors.push(e)) } } }); @@ -387,7 +387,7 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe { pub(crate) fn process_playlist(playlists: &mut [FetchedPlaylist], target: &ConfigTarget, cfg: &Config, - stats: &mut HashMap) -> Result<(), M3uFilterError> { + stats: &mut HashMap) -> Result<(), Vec> { let pipe = get_processing_pipe(target); debug!("Processing order is {}", &target.processing_order); @@ -439,30 +439,51 @@ pub(crate) fn process_playlist(playlists: &mut [FetchedPlaylist], } } - let publish = target.publish; - if target.filename.is_some() { - let result = write_playlist(target, cfg, &mut new_playlist); - match &result { - Ok(..) => {} - Err(e) => { - error!("failed to write playlist {} {}", target.name, e); - } - } - if !publish { - return result; - } - } - - if target.publish { - return xtream_save_playlist(target, cfg, &mut new_playlist); - } - create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Persisting playlist failed: {}", &target.name) + persist_playlist(&new_playlist, target, cfg) } else { info!("Playlist is empty: {}", &target.name); Ok(()) } } +fn persist_playlist(playlist: &[PlaylistGroup], + target: &ConfigTarget, cfg: &Config,) -> Result<(), Vec> { + let mut errors = vec![]; + for target_type in &target.output { + match match target_type { + TargetType::M3u => write_m3u_playlist(target, cfg, playlist), + TargetType::Strm => write_strm_playlist(target, cfg, playlist), + TargetType::Xtream => write_xtream_playlist(target, cfg, playlist) + } { + Ok(_) => {} + Err(err) => { + errors.push(err) + } + } + } + + // + // if target.filename.is_some() { + // let result = write_playlist(target, cfg, &mut new_playlist); + // match &result { + // Ok(..) => {} + // Err(e) => { + // error!("failed to write playlist {} {}", target.name, e); + // } + // } + // if !publish { + // return result; + // } + // } + // + // if publish { + // if let Some(TargetType::Xtream) = target.output { + // return xtream_save_playlist(target, cfg, &mut new_playlist); + // } + // } + Err(vec![M3uFilterError::new(M3uFilterErrorKind::Notify, format!("Persisting playlist failed: {}", &target.name))]) +} + pub(crate) fn start_processing(cfg: Arc, targets: Arc) { let (stats, errors) = process_sources(cfg.clone(), targets.clone()); let stats_msg = format!("Stats: {}", stats.iter().map(|stat| stat.to_string()).collect::>().join("\n")); diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index 59409e074..42d3c899a 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -1,12 +1,13 @@ use std::fs::File; +use std::io::Write; + +use chrono::Datelike; +use log::error; + +use crate::{create_m3u_filter_error_result, utils}; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget}; use crate::model::model_m3u::PlaylistGroup; -use crate::{create_m3u_filter_error_result, utils}; -use std::io::{Write}; -use chrono::Datelike; -use crate::model::model_config::TargetType; -use log::{error}; -use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; fn check_write(res: std::io::Result) -> Result<(), std::io::Error> { match res { @@ -80,14 +81,21 @@ fn kodi_style_rename(name: &String, style: &KodiStyle) -> String { String::from(name) } -fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Vec) -> Result<(), M3uFilterError> { +pub(crate) fn get_m3u_file_path(cfg: &Config, target: &ConfigTarget) -> Option { + utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&target.filename.as_ref().unwrap()))) +} + +pub(crate) fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &[PlaylistGroup]) -> Result<(), M3uFilterError> { macro_rules! cant_write_result { ($path:expr, $err:expr) => { create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to write m3u playlist: {} - {}", $path.to_str().unwrap() ,$err) } } if !new_playlist.is_empty() { - if let Some(path) = utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&target.filename.as_ref().unwrap()))) { + if target.filename.is_none() { + return Err(M3uFilterError::new(M3uFilterErrorKind::Notify, "write m3u playlist failed: ".to_string())); + } + if let Some(path) = get_m3u_file_path(cfg, target) { match File::create(&path) { Ok(mut m3u_file) => { match check_write(m3u_file.write(b"#EXTM3U\n")) { @@ -107,7 +115,7 @@ fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Ve } } } - }, + } Err(e) => return cant_write_result!(&path, e), } } @@ -115,8 +123,11 @@ fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Ve Ok(()) } -fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut Vec) -> Result<(), M3uFilterError> { +pub(crate) fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &[PlaylistGroup]) -> Result<(), M3uFilterError> { if !new_playlist.is_empty() { + if target.filename.is_none() { + return Err(M3uFilterError::new(M3uFilterErrorKind::Notify, "write strm playlist failed: ".to_string())); + } let underscore_whitespace = target.options.as_ref().map_or(false, |o| o.underscore_whitespace); let cleanup = target.options.as_ref().map_or(false, |o| o.cleanup); let kodi_style = target.options.as_ref().map_or(false, |o| o.kodi_style); @@ -165,10 +176,3 @@ fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &mut V Ok(()) } -pub(crate) fn write_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut Vec) -> Result<(), M3uFilterError> { - let mut new_playlist = playlist.to_owned(); - if let Some(TargetType::Strm) = &target.output { - return write_strm_playlist(target, cfg, &mut new_playlist); - } - write_m3u_playlist(target, cfg, &mut new_playlist) -} diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 0a4263771..9401fe348 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -82,7 +82,7 @@ fn get_collection_path(path: &Path, collection: &str) -> PathBuf { path.join(format!("{}.json", collection)) } -pub(crate) fn xtream_save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut [PlaylistGroup]) -> Result<(), M3uFilterError> { +pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlist: &[PlaylistGroup]) -> Result<(), M3uFilterError> { if let Some(path) = get_storage_path(cfg, &target.name) { if fs::create_dir_all(&path).is_err() { let msg = format!("Failed to save, can't create directory {}", &path.to_str().unwrap()); diff --git a/src/utils.rs b/src/utils.rs index 6cf1cf342..fc4083f7c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,6 @@ +use std::collections::HashSet; use std::fs; +use std::hash::Hash; use std::io::{BufRead, Write}; use std::path::PathBuf; use log::{debug, error}; @@ -246,4 +248,9 @@ pub(crate) fn get_input_json_content(input: &ConfigInput, url_str: &String, pers }, Err(_) => create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "malformed input url: {}", url_str) } +} + +pub(crate) fn dedup(v: &mut Vec) { + let mut uniques = HashSet::new(); + v.retain(|e| uniques.insert(*e)); } \ No newline at end of file