diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f52d561..2f0192528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - no error log for xtream api when content is skipped with options `xtream_skip_[live|vod|series]` - 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. -- The `kodi` format for movies can contain the `tmdb-id` (_optional_). To add the `tmdb-id` you can set now `kodi_style`, `xtream_resolve_video` and `xtream_resolve_video_delay` to target options. +- 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. # 2.0.10 (2024-12-03) - added Target Output Option `m3u_include_type_in_url`, default false. This adds `live`, `movie`, `series` to the url of the stream in reverse proxy mode. diff --git a/README.md b/README.md index 1938f3bc6..ff720b661 100644 --- a/README.md +++ b/README.md @@ -397,15 +397,15 @@ each series info entry needs to be fetched one by one and the provider can ban y The `kodi` format for movies can contain the `tmdb-id` (_optional_). Because xtream api delivers the data only on request, we need to fetch this info for each movie entry. But be aware the provider can ban you if you are doing request too frequently. -- `xtream_resolve_video` if is set to `true` and you have xtream input, the movies info are fetched and stored. +- `xtream_resolve_vod` if is set to `true` and you have xtream input, the movies info are fetched and stored. This can cause a lot of requests to the provider. Be cautious when using this option. -- `xtream_resolve_video_delay` to avoid a provider ban you can set the seconds between vod_info_request's. Default is 2 seconds. +- `xtream_resolve_vod_delay` to avoid a provider ban you can set the seconds between vod_info_request's. Default is 2 seconds. But be aware that the more series entries there are, the longer the process takes. Unlike `series info` `movie info` is only fetched once for each movie. If the data is stored locally there will be no update. -There is a difference for `xtream_resolve_video` and `xtream_resolve_series`. +There is a difference for `xtream_resolve_vod` and `xtream_resolve_series`. `xtream_resolve_series` works only when input: `xtream` and output: `m3u`. -`xtream_resolve_video` works only when input: `xtream`. +`xtream_resolve_vod` works only when input: `xtream`. ### 2.2.2.5 `filter` The filter is a string with a filter statement. diff --git a/frontend/src/model/server-config.ts b/frontend/src/model/server-config.ts index 486eedc0e..06899428c 100644 --- a/frontend/src/model/server-config.ts +++ b/frontend/src/model/server-config.ts @@ -52,8 +52,8 @@ export interface TargetConfig { xtream_skip_series_direct_source: boolean, xtream_resolve_series: boolean, xtream_resolve_series_delay: number, - xtream_resolve_video: boolean, - xtream_resolve_video_delay: number, + xtream_resolve_vod: boolean, + xtream_resolve_vod_delay: number, m3u_include_type_in_url: boolean, m3u_mask_redirect_url: boolean, share_live_streams: boolean, diff --git a/src/model/config.rs b/src/model/config.rs index 9e9697a85..26b218e88 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -306,9 +306,9 @@ pub struct ConfigTargetOptions { #[serde(default = "default_as_two_u16")] pub xtream_resolve_series_delay: u16, #[serde(default)] - pub xtream_resolve_video: bool, + pub xtream_resolve_vod: bool, #[serde(default = "default_as_two_u16")] - pub xtream_resolve_video_delay: u16, + pub xtream_resolve_vod_delay: u16, #[serde(default)] pub m3u_include_type_in_url: bool, #[serde(default)] diff --git a/src/model/playlist.rs b/src/model/playlist.rs index d1d49ef7e..2f20fd16a 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -11,7 +11,7 @@ use crate::model::xmltv::TVGuide; use crate::model::xtream::{xtream_playlistitem_to_document, XtreamMappingOptions}; use crate::processing::m3u_parser::extract_id_from_url; use crate::repository::storage::hash_string; -use crate::utils::json_utils::{get_string_from_serde_value, get_u32_from_serde_value, get_u64_from_serde_value}; +use crate::utils::json_utils::{get_string_from_serde_value, get_u64_from_serde_value}; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html @@ -190,12 +190,12 @@ impl PlaylistItemHeader { _ => None, }) } - pub fn get_additional_property_as_u32(&self, field: &str) -> Option { - match self.get_additional_property(field) { - Some(value) => get_u32_from_serde_value(value), - None => None - } - } + // pub fn get_additional_property_as_u32(&self, field: &str) -> Option { + // match self.get_additional_property(field) { + // Some(value) => get_u32_from_serde_value(value), + // None => None + // } + // } pub fn get_additional_property_as_u64(&self, field: &str) -> Option { match self.get_additional_property(field) { Some(value) => get_u64_from_serde_value(value), diff --git a/src/model/xtream.rs b/src/model/xtream.rs index dd50842ad..b2bd91a0e 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -403,6 +403,7 @@ impl XtreamSeriesInfoEpisode { add_str_property_if_exists!(result, self.info.releasedate, "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"); if result.is_empty() { None } else { Some(Value::Object(result)) } } diff --git a/src/processing/xtream_processor.rs b/src/processing/xtream_processor.rs index 2b939eab4..825bc9b1e 100644 --- a/src/processing/xtream_processor.rs +++ b/src/processing/xtream_processor.rs @@ -5,7 +5,6 @@ use crate::repository::storage::get_input_storage_path; use crate::utils::download; use crate::{info_err, notify_err}; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; use std::fs::File; use std::io::{BufWriter, Error, ErrorKind, Write}; @@ -37,7 +36,6 @@ macro_rules! handle_error_and_return { use crate::repository::bplustree::BPlusTree; use crate::repository::xtream_repository::xtream_get_record_file_path; use crate::utils::file_utils::append_or_crate_file; -use crate::utils::json_utils::get_u64_from_serde_value; #[macro_export] macro_rules! create_resolve_options_function_for_xtream_target { diff --git a/src/processing/xtream_processor_vod.rs b/src/processing/xtream_processor_vod.rs index 8e649a5b6..535ac2f6c 100644 --- a/src/processing/xtream_processor_vod.rs +++ b/src/processing/xtream_processor_vod.rs @@ -16,7 +16,7 @@ const TAG_VOD_INFO_TMDB_ID: &str = "tmdb_id"; const TAG_VOD_INFO_STREAM_ID: &str = "stream_id"; const TAG_VOD_INFO_ADDED: &str = "added"; -create_resolve_options_function_for_xtream_target!(video); +create_resolve_options_function_for_xtream_target!(vod); async fn read_processed_vod_info_ids(cfg: &Config, errors: &mut Vec, fpl: &FetchedPlaylist<'_>) -> HashMap { read_processed_info_ids(cfg, errors, fpl, PlaylistItemType::Video, |record: &InputVodInfoRecord| record.ts).await @@ -62,7 +62,7 @@ fn should_update_vod_info(pli: &PlaylistItem, processed_provider_ids: &HashMap, fpl: &FetchedPlaylist<'_>) { - let (resolve_movies, resolve_delay) = get_resolve_video_options(target, fpl); + let (resolve_movies, resolve_delay) = get_resolve_vod_options(target, fpl); if !resolve_movies { return; } // we cant write to the indexed-document directly because of the write lock and time-consuming operation. diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index 91fcaac4c..21681aee9 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -1,108 +1,178 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget}; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemType}; -use crate::model::xtream::{XtreamSeriesEpisode}; +use crate::model::xtream::XtreamSeriesEpisode; use crate::repository::bplustree::BPlusTree; use crate::repository::storage::get_input_storage_path; -use crate::repository::xtream_repository::xtream_get_record_file_path; +use crate::repository::xtream_repository::{xtream_get_record_file_path, InputVodInfoRecord}; use crate::utils::file_lock_manager::FileReadGuard; use crate::utils::file_utils; -use crate::utils::json_utils::{get_string_from_serde_value}; -use crate::{create_m3u_filter_error, create_m3u_filter_error_result, notify_err}; +use crate::{create_m3u_filter_error_result, notify_err}; use chrono::Datelike; use log::error; +use regex::Regex; use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::path::PathBuf; use std::rc::Rc; use std::sync::LazyLock; -use serde_json::Value; struct KodiStyle { - year: regex::Regex, - season: regex::Regex, - episode: regex::Regex, - whitespace: regex::Regex, + year: Regex, + season: Regex, + episode: Regex, + whitespace: Regex, + alphanumeric: Regex, } +static KODI_STYLE: LazyLock = LazyLock::new(|| KodiStyle { + season: regex::Regex::new(r"[Ss]\d{1,2}").unwrap(), + episode: regex::Regex::new(r"[Ee]\d{1,2}").unwrap(), + year: regex::Regex::new(r"\d{4}").unwrap(), + whitespace: regex::Regex::new(r"\s+").unwrap(), + alphanumeric: regex::Regex::new(r"[^\w\s]").unwrap(), +}); + fn sanitize_for_filename(text: &str, underscore_whitespace: bool) -> String { - text.chars().filter(|c| c.is_alphanumeric() || c.is_whitespace()) + text.trim().chars().filter(|c| c.is_alphanumeric() || c.is_whitespace() || (*c == '(') || (*c == ')')) .map(|c| if underscore_whitespace { if c.is_whitespace() { '_' } else { c } } else { c }) .collect::() } -fn kodi_style_rename_year(name: &str, style: &KodiStyle) -> (String, Option) { - let current_date = chrono::Utc::now(); - let cur_year = current_date.year(); - match style.year.find(name) { - Some(m) => { - let s_year = &name[m.start()..m.end()]; - let t_year: i32 = s_year.parse().unwrap(); - if t_year > 1900 && t_year <= cur_year { - let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]); - return (new_name, Some(String::from(s_year))); +fn extract_match(name: &str, pattern: &Regex) -> (String, Option) { + pattern.find(&name).map_or_else(|| (name.to_string(), None), |m| { + let matched = String::from(&name[m.start()..m.end()]); + let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]); + (new_name, Some(matched)) + }) +} + +fn extract_season_or_episode_match_with_default(name: &str, pattern: &Regex, default_value: Option<&String>) -> (String, Option) { + let (new_name, value) = extract_match(name, pattern); + let new_value = match value { + // we skip the prefix S or E to parse the num + Some(num_value) => num_value[1..].parse::().ok(), + None => default_value.and_then(|val| val.parse::().ok()), + }; + (new_name, new_value) +} + +fn kodi_style_rename_season(name: &str, style: &KodiStyle, series_season: Option<&String>) -> (String, Option) { + extract_season_or_episode_match_with_default(name, &style.season, series_season) +} + +fn kodi_style_rename_episode(name: &str, style: &KodiStyle, series_episode: Option<&String>) -> (String, Option) { + extract_season_or_episode_match_with_default(name, &style.episode, series_episode) +} + +fn kodi_style_rename_year( + name: &str, + style: &KodiStyle, + release_date: Option<&String>, +) -> (String, Option) { + let (new_name, possible_year) = extract_match(name, &style.year); + + if let Some(year) = possible_year.as_deref() { + if let Ok(num_year) = year.parse::() { + let cur_year = u32::try_from(chrono::Utc::now().year()).unwrap_or(0); + if (1900..=cur_year).contains(&num_year) { + return (new_name, Some(num_year)); } - (String::from(name), Some(cur_year.to_string())) } - _ => (String::from(name), Some(cur_year.to_string())), } -} -fn kodi_style_rename_season(name: &str, style: &KodiStyle) -> (String, Option) { - style.season.find(name).map_or_else(|| (String::from(name), Some(String::from("01"))), |m| { - let s_season = &name[m.start()..m.end()]; - let season = Some(String::from(&s_season[1..])); - let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]); - (new_name, season) - }) -} - -fn kodi_style_rename_episode(name: &str, style: &KodiStyle) -> (String, Option) { - style.episode.find(name).map_or_else(|| (String::from(name), None), |m| { - let s_episode = &name[m.start()..m.end()]; - let episode = Some(String::from(&s_episode[1..])); - let new_name = format!("{}{}", &name[0..m.start()], &name[m.end()..]); - (new_name, episode) - }) -} - -fn kodi_style_rename(name: &str, style: &KodiStyle) -> (Vec, String) { - let (work_name, year) = kodi_style_rename_year(name, style); - - // TODO use this for non xtream input - // let (work_name_2, season) = kodi_style_rename_season(&work_name_1, style); - // let (work_name_3, episode) = kodi_style_rename_episode(&work_name_2, style); - - let mut filename = work_name; - let mut filedir = vec![String::from(style.whitespace.replace_all(filename.as_str(), " "))]; - if year.is_some() { - if year.is_some() { - filename = format!("{filename} ({})", year.unwrap()); - filedir = vec![String::from(style.whitespace.replace_all(filename.as_str(), " "))]; + if let Some(rel_date) = release_date { + if let Some(year) = extract_match(&rel_date, &style.year).1.and_then(|y| y.parse::().ok()) { + return (name.to_string(), Some(year)); } - return (filedir, String::from(style.whitespace.replace_all(filename.as_str(), " "))); } - let new_name = String::from(style.whitespace.replace_all(name, " ")); - (vec![new_name.clone()], new_name) + (name.to_string(), None) } -static KODI_STYLE: LazyLock = LazyLock::new(|| KodiStyle { - season: regex::Regex::new(r"[Ss]\d\d").unwrap(), - episode: regex::Regex::new(r"[Ee]\d\d").unwrap(), - year: regex::Regex::new(r"\d\d\d\d").unwrap(), - whitespace: regex::Regex::new(r"\s+").unwrap(), -}); +fn trim_string_after_pos(input: &str, start_pos: usize) -> Option { + if let Some(slice) = input.get(start_pos..) { + if let Some(index) = slice.find(|c: char| c.is_alphanumeric()) { + return Some(String::from(&slice[index..])); + } + } + None +} + +fn trim_whitespace(pattern: &Regex, input: &str) -> String { + pattern.replace_all(input, " ").to_string() +} + +async fn kodi_style_rename(cfg: &Config, strm_item_info: &StrmItemInfo, style: &KodiStyle, input_tmdb_indexes: &mut InputTmdbIndexMap, underscore_whitespace: bool) -> (PathBuf, String) { + let separator = if underscore_whitespace { "_" } else { " " }; + let (name_1, year) = kodi_style_rename_year(&strm_item_info.title, style, strm_item_info.release_date.as_ref()); + let (name_2, season) = kodi_style_rename_season(&name_1, style, strm_item_info.season.as_ref()); + let (name_3, episode) = kodi_style_rename_episode(&name_2, style, strm_item_info.episode.as_ref()); + let name_4 = trim_whitespace(&style.whitespace, &*style.alphanumeric.replace_all(&name_3, "")); + let title = &strm_item_info.series_name.as_ref() + .filter(|&series_name| name_4.starts_with(series_name)) + .and_then(|series_name| trim_string_after_pos(&name_3, series_name.len())); + let tmdb_value = match strm_item_info.item_type { + PlaylistItemType::Series | PlaylistItemType::Video => get_tmdb_value(cfg, strm_item_info.provider_id, strm_item_info.input_id, input_tmdb_indexes, strm_item_info.item_type).await, + _ => None, + }; + + let mut file_dir = vec![strm_item_info.group.to_string()]; + let mut filename = vec![]; + + let name = if let Some(series_name) = &strm_item_info.series_name { + series_name + } else { &name_4 }; + let sanitized_name = sanitize_for_filename(name, underscore_whitespace); + filename.push(sanitized_name.clone()); + + if let Some(value) = year { + filename.push(format!("{separator}({value})")); + file_dir.push(format!("{sanitized_name}{separator}({value})")); + } else { + file_dir.push(sanitized_name); + } + + if let Some(value) = season { + filename.push(format!("{separator}S{value:02}")); + file_dir.push(format!("Season{separator}{value}")); + }; + + if let Some(value) = episode { + if season.is_none() { + filename.push(separator.to_string()); + } + filename.push(format!("E{value:02}")); + }; + if let Some(value) = title { + filename.push(format!("{separator}-{separator}{}", sanitize_for_filename(value, underscore_whitespace))); + } + + + if let Some(value) = tmdb_value { + match value { + InputTmdbIndexValue::Video(vod_record) => filename.push(format!("{separator}{{tmdb={}}}", vod_record.tmdb_id)), + InputTmdbIndexValue::Series(episode) => filename.push(format!("{separator}{{tmdb={}}}", episode.tmdb_id)), + } + } + let kodi_filename = filename.join(""); + + let mut path = PathBuf::new(); + for dir in file_dir { + path.push(sanitize_for_filename(&dir, underscore_whitespace)); + } + (path, kodi_filename) +} #[derive(Clone)] enum InputTmdbIndexTree { - Video(BPlusTree), + Video(BPlusTree), Series(BPlusTree), } #[derive(Clone)] enum InputTmdbIndexValue { - Video(u32), + Video(InputVodInfoRecord), Series(XtreamSeriesEpisode), } @@ -118,7 +188,7 @@ async fn get_tmdb_value(cfg: &Config, provider_id: Option, input_id: u16, std::collections::hash_map::Entry::Occupied(entry) => { if let Some((_, tree_value)) = entry.get() { match tree_value { - InputTmdbIndexTree::Video(tree) => tree.query(&pid).map(|id| InputTmdbIndexValue::Video(*id)), + InputTmdbIndexTree::Video(tree) => tree.query(&pid).map(|vod_record| InputTmdbIndexValue::Video(vod_record.clone())), InputTmdbIndexTree::Series(tree) => tree.query(&pid).map(|episode| InputTmdbIndexValue::Series(episode.clone())) } } else { @@ -139,8 +209,8 @@ async fn get_tmdb_value(cfg: &Config, provider_id: Option, input_id: u16, } } PlaylistItemType::Video => { - if let Ok(tree) = BPlusTree::::load(&tmdb_path) { - let tmdb_id = tree.query(&pid).map(|id| InputTmdbIndexValue::Video(*id)); + if let Ok(tree) = BPlusTree::::load(&tmdb_path) { + let tmdb_id = tree.query(&pid).map(|vod_record| InputTmdbIndexValue::Video(vod_record.clone())); entry.insert(Some((file_lock, InputTmdbIndexTree::Video(tree)))); return tmdb_id; } @@ -167,22 +237,28 @@ struct StrmItemInfo { url: Rc, series_name: Option, release_date: Option, + season: Option, + episode: Option, } -fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo { +fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo { let mut header = pli.header.borrow_mut(); let group = Rc::clone(&header.group); let title = Rc::clone(&header.title); let item_type = header.item_type; let provider_id = header.get_provider_id(); let input_id = header.input_id; + // TODO reverse proxy url let url = Rc::clone(&header.url); - let (series_name, series_release_date) = if header.item_type == PlaylistItemType::Series { + let (series_name, release_date, season, episode) = if header.item_type == PlaylistItemType::Series { let series_name = header.get_additional_property_as_str("series_name"); let release_date = header.get_additional_property_as_str("release_date"); - (series_name, release_date) - } else { (None, None) }; - StrmItemInfo { group, title, item_type, provider_id, input_id, url, series_name, release_date } + let season = header.get_additional_property_as_str("season"); + let episode = header.get_additional_property_as_str("episode"); + (series_name, release_date, season, episode) + } else { (None, None, None, None) }; + + StrmItemInfo { group, title, item_type, provider_id, input_id, url, series_name, release_date, season, episode } } fn prepare_strm_output_directory(cleanup: bool, path: &PathBuf) -> Result<(), M3uFilterError> { @@ -199,25 +275,27 @@ fn prepare_strm_output_directory(cleanup: bool, path: &PathBuf) -> Result<(), M3 fn filter_strm_item(&pli: &&PlaylistItem) -> bool { let item_type = pli.header.borrow().item_type; item_type == PlaylistItemType::Series - || item_type == PlaylistItemType::Live - || item_type == PlaylistItemType::Video + || item_type == PlaylistItemType::Live + || item_type == PlaylistItemType::Video } fn get_strm_output_options(target: &ConfigTarget) -> (bool, bool, bool) { target.options.as_ref().map_or_else( || (false, false, false), - |o| (o.underscore_whitespace, o.cleanup, o.kodi_style)) + |o| (o.underscore_whitespace, o.cleanup, o.kodi_style)) } pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playlist: &[PlaylistGroup], filename: Option<&str>) -> Result<(), M3uFilterError> { + let mut result = Ok(()); if !new_playlist.is_empty() { if filename.is_none() { return Err(notify_err!("write strm playlist failed: ".to_string())); } let (underscore_whitespace, cleanup, kodi_style) = get_strm_output_options(target); - let Some(path) = file_utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&filename.as_ref().unwrap()))) - else { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, format!("Failed to get file path for {}", filename.unwrap_or(""))) }; - if _ = prepare_strm_output_directory(cleanup, &path)?; + let Some(path) = file_utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(&filename.as_ref().unwrap()))) else { + return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "Failed to get file path for {}", filename.unwrap_or("")); + }; + let _ = prepare_strm_output_directory(cleanup, &path)?; let mut input_tmdb_indexes: InputTmdbIndexMap = HashMap::new(); for pg in new_playlist { for pli in pg.channels.iter().filter(filter_strm_item) { @@ -229,55 +307,36 @@ pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_p // - M3u Vod (no additional infos, need to extract from title) let str_item_info = extract_item_info(pli); - let mut dir_path = path.join(sanitize_for_filename(&str_item_info.group, underscore_whitespace)); - let mut kodi_file_name = sanitize_for_filename(&str_item_info.title, underscore_whitespace); - let mut additional_info = String::new(); - if kodi_style { - let (kodi_file_dir_name, kodi_style_filename) = if str_item_info.item_type == PlaylistItemType::Series && str_item_info.series_name.is_some() && str_item_info.release_date.is_some() { - (series_name, - } else { - kodi_style_rename(&kodi_file_name, &KODI_STYLE) - }; - kodi_file_name = kodi_style_filename; - kodi_file_dir_name.iter().for_each(|p| dir_path = dir_path.join(p)); - let tmdb_value = match str_item_info.item_type { - PlaylistItemType::Series | PlaylistItemType::Video => get_tmdb_value(cfg, str_item_info.provider_id, str_item_info.input_id, &mut input_tmdb_indexes, str_item_info.item_type).await, - _ => None, - }; - additional_info = match tmdb_value { - None => { String::new() } - Some(value) => { - match value { - InputTmdbIndexValue::Video(tmdb_id) => format!(" {{tmdb={tmdb_id}}}"), - InputTmdbIndexValue::Series(episode) => { - let mut episode_ext = format!(" S{:02}E{:02}", episode.season, episode.episode_num); - if episode.tmdb_id > 0 { - episode_ext = format!("{episode_ext} {{tmdb={}}}", episode.tmdb_id); - } - episode_ext - } - } - } - }; - } - if let Err(e) = std::fs::create_dir_all(&dir_path) { - error!("cant create directory: {:?}", &dir_path); - return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to write strm playlist: {}", e); + let (dir_path, strm_file_name) = if kodi_style { + kodi_style_rename(cfg, &str_item_info, &KODI_STYLE, &mut input_tmdb_indexes, underscore_whitespace).await + } else { + let dir_path = path.join(sanitize_for_filename(&str_item_info.group, underscore_whitespace)); + let strm_file_name = sanitize_for_filename(&str_item_info.title, underscore_whitespace); + (dir_path, strm_file_name) }; - let file_path = dir_path.join(format!("{kodi_file_name}{additional_info}.strm")); + let output_path = path.join(dir_path); + if let Err(e) = std::fs::create_dir_all(&output_path) { + error!("cant create directory: {output_path:?}"); + return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to create directory for strm playlist:{output_path:?} {e}"); + }; + let file_path = output_path.join(format!("{strm_file_name}.strm")); match File::create(&file_path) { Ok(mut strm_file) => { - match file_utils::check_write(&strm_file.write_all(url.as_bytes())) { - Ok(()) => (), - Err(e) => return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to write strm playlist: {}", e), + match file_utils::check_write(&strm_file.write_all(str_item_info.url.as_bytes())) { + Ok(()) => {} + Err(err) => { + error!("failed to write strm playlist: {err}"); + result = Err(notify_err!(format!("failed to write strm playlist: {}", err))); + } } } Err(err) => { - return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to write strm playlist: {}", err); + error!("failed to write strm playlist: {err}"); + result = Err(notify_err!(format!("failed to write strm playlist: {}", err))); } - } + }; } } } - Ok(()) + result } diff --git a/src/utils/download.rs b/src/utils/download.rs index 13d947331..c66080cc8 100644 --- a/src/utils/download.rs +++ b/src/utils/download.rs @@ -138,7 +138,7 @@ where return Ok(content); } // Check if the content has been resolved - let resolve_vod = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_video); + let resolve_vod = target.options.as_ref().is_some_and(|opt| opt.xtream_resolve_vod); if resolve_vod { if let Some(provider_id) = pli.get_provider_id() { if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Video).await { diff --git a/src/utils/json_utils.rs b/src/utils/json_utils.rs index 2c6ff7823..06514449b 100644 --- a/src/utils/json_utils.rs +++ b/src/utils/json_utils.rs @@ -186,9 +186,8 @@ pub fn get_u32_from_serde_value(value: &Value) -> Option { pub fn get_string_from_serde_value(value: &Value) -> Option { match value { - Value::String(str_val) => { - Some(str_val.to_string()) - } + Value::Number(num_val) => num_val.as_i64().map(|num| num.to_string()), + Value::String(str_val) => Some(str_val.clone()), _ => None, } } \ No newline at end of file diff --git a/test/rest-api.http b/test/rest-api.http index 8dfa2d40b..f9949eb66 100644 --- a/test/rest-api.http +++ b/test/rest-api.http @@ -42,11 +42,11 @@ GET {{local}}/player_api.php?username=xt&password=xt&action=get_live_streams&cat ### xtream vod_categories GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_categories -### xtream vod info -GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_streams&category_id=44 +### xtream vod streams +GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_streams&category_id=57 ### xtream vod info -GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_info&vod_id=8051 +GET {{local}}/player_api.php?username=xt&password=xt&action=get_vod_info&vod_id=7590 ### xtream series_categories GET {{local}}/player_api.php?username=xt&password=xt&action=get_series_categories