diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 35ecbe0df..e89f7ed4d 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -3,7 +3,7 @@ use actix_web::http::header::{CACHE_CONTROL, HeaderValue}; use actix_web::{HttpRequest, HttpResponse, web}; use crate::api::api_model::{AppState, UserApiRequest}; use crate::model::api_proxy::{UserCredentials}; -use crate::model::config::ConfigTarget; +use crate::model::config::{ConfigTarget}; pub(crate) async fn serve_file(file_path: &Path, req: &HttpRequest) -> HttpResponse { if file_path.exists() { @@ -36,4 +36,4 @@ pub(crate) fn get_user_target<'a>(api_req: &'a UserApiRequest, app_state: &'a we let username = api_req.username.as_str().trim(); let password = api_req.password.as_str().trim(); get_user_target_by_credentials(username, password, api_req, app_state) -} \ No newline at end of file +} diff --git a/src/api/xmltv_api.rs b/src/api/xmltv_api.rs index 22422df60..3c4b29eda 100644 --- a/src/api/xmltv_api.rs +++ b/src/api/xmltv_api.rs @@ -12,7 +12,6 @@ use crate::repository::m3u_repository::get_m3u_epg_file_path; use crate::repository::xtream_repository::{get_xtream_epg_file_path, get_xtream_storage_path}; use crate::utils::{file_utils, request_utils}; - fn get_epg_path_for_target(config: &Config, target: &ConfigTarget) -> Option { for output in &target.output { match output.target { @@ -54,25 +53,27 @@ async fn xmltv_api( // If you want to use xmltv then provide the url in the config to filter unnecessary content. // If you have multiple xtream sources, the first one will be used for epg let target_name = &target.name; - if let Some(input) = _app_state.config.get_input_for_target(target_name, &InputType::Xtream) { - let epg_url = input.epg_url.as_ref().map_or("".to_string(), |s| s.to_owned()); - let api_url = if epg_url.is_empty() { - format!("{}/xmltv.php?username={}&password={}", - input.url.as_str(), - input.username.as_ref().unwrap_or(&"".to_string()).as_str(), - input.password.as_ref().unwrap_or(&"".to_string()).as_str(), - ) - } else { epg_url.to_string() }; - if let Ok(url) = Url::parse(&api_url) { - if user.proxy == ProxyType::Redirect { - debug!("Redirecting epg request to {}", api_url); - return HttpResponse::Found().insert_header(("Location", api_url)).finish(); - } - let client = request_utils::get_client_request(input, url, None); - if let Ok(response) = client.send().await { - if response.status().is_success() { - if let Ok(content) = response.text().await { - return HttpResponse::Ok().content_type(mime::TEXT_XML).body(content); + if let Some(inputs) = _app_state.config.get_input_for_target(target_name, &InputType::Xtream) { + if let Some(&input) = inputs.first() { + let epg_url = input.epg_url.as_ref().map_or("".to_string(), |s| s.to_owned()); + let api_url = if epg_url.is_empty() { + format!("{}/xmltv.php?username={}&password={}", + input.url.as_str(), + input.username.as_ref().unwrap_or(&"".to_string()).as_str(), + input.password.as_ref().unwrap_or(&"".to_string()).as_str(), + ) + } else { epg_url.to_string() }; + if let Ok(url) = Url::parse(&api_url) { + if user.proxy == ProxyType::Redirect { + debug!("Redirecting epg request to {}", api_url); + return HttpResponse::Found().insert_header(("Location", api_url)).finish(); + } + let client = request_utils::get_client_request(input, url, None); + if let Ok(response) = client.send().await { + if response.status().is_success() { + if let Ok(content) = response.text().await { + return HttpResponse::Ok().content_type(mime::TEXT_XML).body(content); + } } } } diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index a53fc179e..4d272f9a7 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -150,6 +150,16 @@ fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationRes } } +fn separate_number_and_rest(input: &str) -> (String, String) { + if let Some(dot_index) = input.find('.') { + let number_part = input[..dot_index].to_string(); + let rest = input[dot_index..].to_string(); + (number_part, rest) + } else { + (input.to_string(), String::new()) + } +} + async fn xtream_player_api_stream( req: &HttpRequest, api_req: &web::Query, @@ -162,11 +172,26 @@ async fn xtream_player_api_stream( if let Some((user, target)) = get_user_target_by_credentials(username, password, api_req, _app_state) { let target_name = &target.name; if target.has_output(&TargetType::Xtream) { - if let Some(target_input) = match _app_state.config.get_input_for_target(target_name, &InputType::Xtream) { - None => _app_state.config.get_input_for_target(target_name, &InputType::M3u), - Some(inp) => Some(inp) - } { - if let Some(stream_url) = get_xtream_player_api_stream_url(target_input, context, action_path) { + let mut stream_id = action_path.to_owned(); + let mut input: Option<&ConfigInput> = None; + + let (action_stream_id, action_ext) = separate_number_and_rest(action_path); + if let Ok(num) = action_stream_id.trim().parse() { + let (xtream_id, cfg_input) = get_xtream_mapped_id_and_input_for_stream_id(_app_state, target_name, num); + if cfg_input.is_some() { + input = cfg_input; + stream_id = format!("{}{}", xtream_id, action_ext); + } + } + + if input.is_none() { + if let Some(m3u_inputs) = _app_state.config.get_input_for_target(target_name, &InputType::M3u) { + input = m3u_inputs.first().cloned(); + } + } + + if let Some(target_input) = input { + if let Some(stream_url) = get_xtream_player_api_stream_url(target_input, context, stream_id.as_str()) { if user.proxy == ProxyType::Redirect { debug!("Redirecting stream request to {}", stream_url); return HttpResponse::Found().insert_header(("Location", stream_url)).finish(); @@ -262,14 +287,30 @@ async fn xtream_player_api_timeshift_stream( xtream_player_api_stream(&req, &api_req, &_app_state, "timeshift", &username, &password, &action_path).await } + +fn get_xtream_mapped_id_and_input_for_stream_id<'a>(app_state: &'a AppState, target_name: &str, stream_id: i32) -> (i32, Option<&'a ConfigInput>) { + if let Some(inputs) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) { + if let Ok(Some(mapping)) = xtream_repository::read_xtream_mapping(stream_id as u32, app_state.config.as_ref(), target_name) { + if let Some(cfg_input) = inputs.iter().find(|&&inp| inp.id == mapping.input_id).cloned() { + return (mapping.stream_id as i32, Some(cfg_input)); + } + } + return (stream_id, inputs.first().cloned()); + } + (stream_id, None) +} + +// TODO use u32 or i32 ?? async fn xtream_get_stream_info(app_state: &AppState, target_name: &str, stream_id: i32, cluster: &XtreamCluster) -> Result { - if let Some(target_input) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) { - if let Ok(content) = xtream_repository::xtream_get_stored_stream_info(app_state, target_name, stream_id, cluster, target_input).await { + + let (xtream_id, input) = get_xtream_mapped_id_and_input_for_stream_id(app_state, target_name, stream_id); + if let Some(target_input) = input { + if let Ok(content) = xtream_repository::xtream_get_stored_stream_info(app_state, target_name, xtream_id, cluster, target_input).await { return Ok(content); } - if let Some(info_url) = get_xtream_player_api_info_url(target_input, cluster, stream_id) { + if let Some(info_url) = get_xtream_player_api_info_url(target_input, cluster, xtream_id) { if let Ok(url) = Url::parse(&info_url) { let client = request_utils::get_client_request(target_input, url, None); if let Ok(response) = client.send().await { @@ -295,29 +336,36 @@ async fn xtream_get_stream_info(app_state: &AppState, target_name: &str, stream_ async fn xtream_get_stream_info_response(app_state: &AppState, user: &UserCredentials, target_name: &str, stream_id: &str, cluster: &XtreamCluster) -> HttpResponse { - match FromStr::from_str(stream_id) { - Ok(xtream_stream_id) => { - if user.proxy == ProxyType::Redirect { - if let Some(target_input) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) { - if let Some(info_url) = get_xtream_player_api_info_url(target_input, cluster, xtream_stream_id) { - return HttpResponse::Found().insert_header(("Location", info_url)).finish(); - } - } - } + let xtream_stream_id: i32 = match FromStr::from_str(stream_id) { + Ok(id) => id, + Err(_) => return HttpResponse::BadRequest().finish() + }; - match xtream_get_stream_info(app_state, target_name, xtream_stream_id, cluster).await { - Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), - Err(_) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{info:[]}"), + if user.proxy == ProxyType::Redirect { + let (xtream_id, input) = get_xtream_mapped_id_and_input_for_stream_id(app_state, target_name, xtream_stream_id); + if let Some(target_input) = input { + if let Some(info_url) = get_xtream_player_api_info_url(target_input, cluster, xtream_id) { + return HttpResponse::Found().insert_header(("Location", info_url)).finish(); } } - Err(_) => HttpResponse::BadRequest().finish() + } + + match xtream_get_stream_info(app_state, target_name, xtream_stream_id, cluster).await { + Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), + Err(_) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{info:[]}"), } } async fn xtream_get_short_epg(app_state: &AppState, user: &UserCredentials, target_name: &str, stream_id: &str, limit: &str) -> HttpResponse { - if let Some(target_input) = app_state.config.get_input_for_target(target_name, &InputType::Xtream) { + let xtream_stream_id: i32 = match FromStr::from_str(stream_id) { + Ok(id) => id, + Err(_) => return HttpResponse::BadRequest().finish() + }; + + let (xtream_id, input) = get_xtream_mapped_id_and_input_for_stream_id(app_state, target_name, xtream_stream_id); + if let Some(target_input) = input { if let Some(action_url) = get_xtream_player_api_action_url(target_input, "get_short_epg") { - let mut info_url = format!("{}&stream_id={}", action_url, stream_id); + let mut info_url = format!("{}&stream_id={}", action_url, xtream_id); if !(limit.is_empty() || limit.eq("0")) { info_url = format!("{}&limit={}", info_url, limit); } diff --git a/src/model/config.rs b/src/model/config.rs index c6eec7835..eaa6cc565 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -418,6 +418,8 @@ impl ConfigTarget { pub(crate) struct ConfigSource { pub inputs: Vec, pub targets: Vec, + #[serde(skip_serializing, skip_deserializing)] + pub _multi_xtream_input: bool, } impl ConfigSource { @@ -426,14 +428,16 @@ impl ConfigSource { Ok(index + (self.inputs.len() as u16)) } - pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option<&ConfigInput> { + pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option> { + let mut result = Vec::new(); for target in &self.targets { if target.name.eq(target_name) { for input in &self.inputs { - if input.input_type.eq(input_type) { - return Some(input); + if input.enabled && input.input_type.eq(input_type) { + result.push(input); } } + return Some(result); } } None @@ -627,7 +631,7 @@ impl VideoConfig { Some(downl) => { if downl.headers.is_empty() { downl.headers.borrow_mut().insert("Accept".to_string(), "video/*".to_string()); - downl.headers.borrow_mut().insert("User-Agent".to_string(), "AppleTV/tvOS/9.1.1.".to_string()); + downl.headers.borrow_mut().insert("User-Agent".to_string(), "Apple TV; tvOS 13.3.1".to_string()); } if let Some(episode_pattern) = &downl.episode_pattern { @@ -727,9 +731,11 @@ impl Config { } } - pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option<&ConfigInput> { + pub(crate) fn get_input_for_target(&self, target_name: &str, input_type: &InputType) -> Option> { for source in &self.sources { - if let Some(cfg) = source.get_input_for_target(target_name, input_type) { return Some(cfg); } + if let Some(cfg) = source.get_input_for_target(target_name, input_type) { + return Some(cfg); + } } None } diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 95a6de7dd..768b8b08d 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -68,6 +68,7 @@ pub(crate) trait FieldAccessor { #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct PlaylistItemHeader { pub id: Rc, + pub stream_id: Rc, pub name: Rc, pub logo: Rc, pub logo_small: Rc, @@ -86,7 +87,7 @@ pub(crate) struct PlaylistItemHeader { #[serde(skip_serializing, skip_deserializing)] pub additional_properties: Option>, #[serde(default = "default_playlist_item_type", skip_serializing, skip_deserializing)] - pub item_type: PlaylistItemType, + pub item_type: PlaylistItemType, #[serde(default = "default_as_false", skip_serializing, skip_deserializing)] pub series_fetched: bool, // only used for series_info } @@ -118,12 +119,12 @@ macro_rules! get_fields { impl FieldAccessor for PlaylistItemHeader { fn get_field(&self, field: &str) -> Option> { - get_fields!(self, field, id, name, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, source, url;) + get_fields!(self, field, id, stream_id, name, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, source, url;) } fn set_field(&mut self, field: &str, value: &str) -> bool { let val = String::from(value); - update_fields!(self, field, id, name, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, source, url; val) + update_fields!(self, field, id, stream_id, name, logo, logo_small, group, title, parent_code, audio_track, time_shift, rec, source, url; val) } } diff --git a/src/processing/m3u_parser.rs b/src/processing/m3u_parser.rs index 7f829541b..77b8588ae 100644 --- a/src/processing/m3u_parser.rs +++ b/src/processing/m3u_parser.rs @@ -56,6 +56,7 @@ fn skip_digit(it: &mut std::str::Chars) -> Option { fn create_empty_playlistitem_header(content: &String, url: String) -> PlaylistItemHeader { PlaylistItemHeader { id: default_as_empty_rc_str(), + stream_id: default_as_empty_rc_str(), name: default_as_empty_rc_str(), logo: default_as_empty_rc_str(), logo_small: default_as_empty_rc_str(), @@ -122,6 +123,7 @@ fn process_header(video_suffixes: &Vec<&str>, content: &String, url: String) -> plih.id = Rc::new(chanid); } } + plih.stream_id = Rc::clone(&plih.id); plih.epg_channel_id = Some(Rc::clone(&plih.id)); } diff --git a/src/processing/playlist_processor.rs b/src/processing/playlist_processor.rs index 4a16433b8..8109f046c 100644 --- a/src/processing/playlist_processor.rs +++ b/src/processing/playlist_processor.rs @@ -4,7 +4,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; use std::sync::{Arc, Mutex}; -use std::thread; +use std::{thread}; use actix_rt::System; use log::{debug, error, info, Level, log_enabled}; @@ -14,17 +14,18 @@ use crate::{Config, get_errors_notify_message, model::config, valid_property}; 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::config::{ConfigSource, ConfigTarget, default_as_default, InputAffix, InputType, ProcessTargets}; use crate::model::mapping::{Mapping, MappingValueProcessor}; use crate::model::config::{AFFIX_FIELDS, ItemField, ProcessingOrder, SortOrder::{Asc, Desc}, TargetType}; use crate::model::playlist::{FetchedPlaylist, FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; use crate::model::stats::{InputStats, PlaylistStats}; use crate::model::xmltv::{Epg}; +use crate::model::xtream::MultiXtreamMapping; use crate::processing::playlist_watch::process_group_watch; use crate::processing::xmltv_parser::flatten_tvguide; use crate::repository::epg_repository::write_epg; use crate::repository::m3u_repository::{write_m3u_playlist, write_strm_playlist}; -use crate::repository::xtream_repository::write_xtream_playlist; +use crate::repository::xtream_repository::{write_xtream_mapping, write_xtream_playlist}; use crate::utils::download; fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option> { @@ -48,7 +49,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.clone(), }); } }); @@ -266,7 +267,7 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option id: new_group_id, title: Rc::clone(title), channels: vec![channel.clone()], - xtream_cluster: cluster.clone() + xtream_cluster: cluster.clone(), }) } } @@ -362,7 +363,7 @@ async fn process_source(cfg: Arc, source_idx: usize, user_targets: Arc

{} Err(mut err) => err.drain(..).for_each(|e| errors.push(e)) } @@ -430,6 +431,7 @@ fn get_processing_pipe(target: &ConfigTarget) -> ProcessingPipe { } pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], + source: &ConfigSource, target: &ConfigTarget, cfg: &Config, stats: &mut HashMap, errors: &mut Vec) -> Result<(), Vec> { @@ -452,32 +454,7 @@ pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], new_fpl.playlist = v; } } - let (resolve_series, resolve_series_delay) = - if let Some(options) = &target.options { - (options.xtream_resolve_series && fpl.input.input_type == InputType::Xtream && target.has_output(&TargetType::M3u), - options.xtream_resolve_series_delay) - } else { - (false, 0) - }; - if resolve_series { - let mut series_playlist = download::get_xtream_playlist_series(fpl, errors, resolve_series_delay).await; - // original content saved into original list - for plg in &series_playlist { - fpl.update_playlist(plg); - } - // run processing pipe over new items - for f in &pipe { - let r = f(&mut series_playlist, target); - if let Some(v) = r { - series_playlist = v; - } - } - // assign new items to the new playlist - for plg in &series_playlist { - new_fpl.update_playlist(plg); - } - } - + 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 { @@ -491,6 +468,39 @@ pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], } apply_affixes(&mut new_fetched_playlists); + + if source._multi_xtream_input { + let mut stream_id_mappings: Vec = Vec::new(); + let mut counter: u32 = 0; + new_fetched_playlists.iter() + .flat_map(|pl| { + let input_id = &pl.input.id; + pl.playlist.iter().map(move |plg| (input_id, &plg.channels)) + }) + .flat_map(|(input_id, channels)| channels.iter().map(move |chan| (input_id, chan))) + .for_each(|(input_id, chan)| { + counter += 1; + let mut header = chan.header.borrow_mut(); + header.id = Rc::new(counter.to_string()); + let xtream_mapping = MultiXtreamMapping { + stream_id: header.stream_id.parse::().unwrap(), + input_id: *input_id, + }; + stream_id_mappings.push(xtream_mapping); + }); + + match write_xtream_mapping(&stream_id_mappings, cfg, &target.name) { + Ok(_) => { + debug!("wrote multi xtream input mapping for {}", &target.name); + } + Err(err) => { + return Err(vec![M3uFilterError::new( + M3uFilterErrorKind::Notify, + format!("Write multi xtream input mapping {} failed: {}", target.name, err))]); + } + } + } + let mut new_playlist = vec![]; let mut new_epg = vec![]; let mut tv_guides = vec![]; @@ -524,7 +534,7 @@ pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], let watch_re = target._watch_re.as_ref().unwrap(); new_playlist.iter().for_each(|pl| { if watch_re.iter().any(|r| r.is_match(&pl.title)) { - process_group_watch(cfg, &target.name, pl) + process_group_watch(cfg, &target.name, pl) } }); } @@ -537,6 +547,37 @@ pub(crate) async fn process_playlist<'a>(playlists: &mut [FetchedPlaylist<'a>], } } +async fn playlist_resolve_series<'a>(target: &ConfigTarget, errors: &mut Vec, + pipe: &ProcessingPipe, + fpl: &mut FetchedPlaylist<'_>, + new_fpl: &mut FetchedPlaylist<'_>) { + let (resolve_series, resolve_series_delay) = + if let Some(options) = &target.options { + (options.xtream_resolve_series && fpl.input.input_type == InputType::Xtream && target.has_output(&TargetType::M3u), + options.xtream_resolve_series_delay) + } else { + (false, 0) + }; + if resolve_series { + let mut series_playlist = download::get_xtream_playlist_series(fpl, errors, resolve_series_delay).await; + // original content saved into original list + for plg in &series_playlist { + fpl.update_playlist(plg); + } + // run processing pipe over new items + for f in pipe { + let r = f(&mut series_playlist, target); + if let Some(v) = r { + series_playlist = v; + } + } + // assign new items to the new playlist + for plg in &series_playlist { + new_fpl.update_playlist(plg); + } + } +} + fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option, target: &ConfigTarget, cfg: &Config) -> Result<(), Vec> { let mut errors = vec![]; @@ -572,7 +613,7 @@ pub(crate) async fn exec_processing(cfg: Arc, targets: Arc PathBuf { + path.join("id_mapping.db") +} + +pub(crate) fn write_xtream_mapping(mappings: &[MultiXtreamMapping], config: &Config, target_name: &str) -> io::Result<()> { + if let Some(path) = get_xtream_storage_path(config, target_name) { + let mut file = File::create(get_id_mapping_path(&path))?; + // We assume the mappings list is created with a counter as id + // and id 1 means the 0 index. We write all the data and can calculate the offset inside the + // file by (u32 size + u16 size) * index. + for mapping in mappings { + file.write_all(&mapping.stream_id.to_le_bytes())?; + file.write_all(&mapping.input_id.to_le_bytes())?; + }; + return Ok(()); + } + Err(io::Error::new(ErrorKind::Other, format!("Failed to find the xtream storage path for {}", target_name))) +} + +pub(crate) fn read_xtream_mapping(id: u32, config: &Config, target_name: &str) -> io::Result> { + if id < 1 { + return Err(io::Error::new(ErrorKind::Other, "id should start with 1")); + } + + if let Some(path) = get_xtream_storage_path(config, target_name) { + let mapping_file_path = get_id_mapping_path(&path); + if mapping_file_path.exists() { + let mut file = File::open(&mapping_file_path)?; + let index = (id - 1) as u64; + let mapping_size = 4 + 2; // u32 + u16 + let offset = mapping_size * index; + + file.seek(SeekFrom::Start(offset))?; + let mut stream_id_bytes = [0u8; 4]; + file.read_exact(&mut stream_id_bytes)?; + let stream_id = u32::from_le_bytes(stream_id_bytes); + let mut input_id_bytes = [0u8; 2]; + file.read_exact(&mut input_id_bytes)?; + let input_id = u16::from_le_bytes(input_id_bytes); + return Ok(Some(MultiXtreamMapping { stream_id, input_id })); + } + } + Ok(None) } \ No newline at end of file diff --git a/src/test.rs b/src/test.rs index ad0ccbfd2..93739355c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod tests { use crate::filter::get_filter; + use crate::model::model_xtream::MultiXtreamMapping; + use crate::repository::xtream_repository::{read_xtream_mapping, write_xtream_mapping}; #[test] fn test_filter() { diff --git a/src/utils/file_utils.rs b/src/utils/file_utils.rs index 414daf6af..aa5fe0c35 100644 --- a/src/utils/file_utils.rs +++ b/src/utils/file_utils.rs @@ -73,6 +73,7 @@ pub(crate) fn get_working_path(wd: &String) -> String { String::from(current_dir.to_str().unwrap_or(".")) } else { let work_path = std::path::PathBuf::from(wd); + let _ = fs::create_dir_all(&work_path); let wdpath = match fs::metadata(&work_path) { Ok(md) => { if md.is_dir() && !md.permissions().readonly() {