diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 92a3b4184..dd50842ad 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -283,7 +283,7 @@ pub struct XtreamSeriesInfoSeason { #[derive(Debug, Clone, Serialize, Deserialize)] #[allow(non_snake_case)] pub struct XtreamSeriesInfoInfo { - name: String, + pub(crate) name: String, cover: String, plot: String, cast: String, @@ -322,6 +322,7 @@ pub struct XtreamSeriesInfoEpisodeInfo { pub season: u32, } +// Used for serde_json deserialization, can not be used with bincode #[derive(Debug, Clone, Serialize, Deserialize)] pub struct XtreamSeriesInfoEpisode { pub id: String, @@ -337,17 +338,41 @@ pub struct XtreamSeriesInfoEpisode { pub direct_source: String, } -// impl XtreamSeriesInfoEpisode { -// pub fn get_id(&self) -> u32 { -// match self.id.parse::() { -// Ok(id) => id, -// Err(_) => { -// error!("Failed to convert id to number {}", self.id); -// 0 -// } -// } -// } -// } +impl XtreamSeriesInfoEpisode { + pub fn get_id(&self) -> u32 { + self.id.parse::().unwrap_or_else(|_| 0) + } +} + +//bincode does not support deserialize_with. We use this struct for db +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct XtreamSeriesEpisode { + pub id: u32, + pub episode_num: u32, + pub title: String, + pub container_extension: String, + pub custom_sid: String, + pub added: String, + pub season: u32, + pub tmdb_id: u32, + pub direct_source: String, +} + +impl XtreamSeriesEpisode { + pub fn from(info_episode: &XtreamSeriesInfoEpisode) -> Self { + Self { + id: info_episode.get_id(), + episode_num: info_episode.episode_num, + title: info_episode.title.to_string(), + container_extension: info_episode.container_extension.to_string(), + 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), + direct_source: info_episode.direct_source.to_string(), + } + } +} #[derive(Debug, Clone, Serialize, Deserialize)] pub struct XtreamSeriesInfo { @@ -364,6 +389,7 @@ impl XtreamSeriesInfoEpisode { if !bdpath.is_empty() { result.insert(String::from("backdrop_path"), Value::Array(Vec::from([Value::String(String::from(bdpath.first()?))]))); } + add_str_property_if_exists!(result, series_info.info.name.as_str(), "series_name"); 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, self.container_extension.as_str(), "container_extension"); diff --git a/src/processing/xtream_processor.rs b/src/processing/xtream_processor.rs index 4988249e1..386cdfa8f 100644 --- a/src/processing/xtream_processor.rs +++ b/src/processing/xtream_processor.rs @@ -14,6 +14,7 @@ use std::path::PathBuf; const FILE_SERIES_INFO: &str = "xtream_series_info"; const FILE_VOD_INFO: &str = "xtream_vod_info"; const FILE_SUFFIX_WAL: &str = "wal"; +const FILE_SERIES_EPISODE_RECORD: &str = "series_episode_record"; #[macro_export] macro_rules! handle_error { @@ -36,6 +37,7 @@ 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 { @@ -53,27 +55,9 @@ macro_rules! create_resolve_options_function_for_xtream_target { }; } - -pub fn get_u64_from_serde_value(value: &Value) -> Option { - match value { - Value::Number(num_val) => num_val.as_u64(), - Value::String(str_val) => { - match str_val.parse::() { - Ok(val) => Some(val), - Err(_) => None - } - } - _ => None, - } -} - -pub fn get_u32_from_serde_value(value: &Value) -> Option { - get_u64_from_serde_value(value).and_then(|val| u32::try_from(val).ok()) -} - -pub(in crate::processing) async fn playlist_resolve_process_playlist_item(pli: &PlaylistItem, input: &ConfigInput, errors: &mut Vec, resolve_delay: u16, cluster: XtreamCluster) -> Option { +pub(in crate::processing) async fn playlist_resolve_download_playlist_item(pli: &PlaylistItem, input: &ConfigInput, errors: &mut Vec, resolve_delay: u16, cluster: XtreamCluster) -> Option { let mut result = None; - let provider_id = pli.get_provider_id().unwrap_or(0); + let Some(provider_id) = pli.get_provider_id() else { return None; }; if let Some(info_url) = download::get_xtream_player_api_info_url(input, cluster, provider_id) { result = match download::get_xtream_stream_info_content(&info_url, input).await { Ok(content) => Some(content), @@ -102,7 +86,7 @@ pub(in crate::processing) fn write_info_content_to_wal_file(writer: &mut BufWrit pub(in crate::processing) fn create_resolve_episode_wal_files(cfg: &Config, input: &ConfigInput) -> Option<(File, PathBuf)> { match get_input_storage_path(input, &cfg.working_dir) { Ok(storage_path) => { - let info_path = storage_path.join(format!("series_episode_record.{FILE_SUFFIX_WAL}")); + let info_path = storage_path.join(format!("{FILE_SERIES_EPISODE_RECORD}.{FILE_SUFFIX_WAL}")); let info_file = append_or_crate_file(&info_path).ok()?; Some((info_file, info_path)) } diff --git a/src/processing/xtream_processor_series.rs b/src/processing/xtream_processor_series.rs index cbe441579..9ee8adc05 100644 --- a/src/processing/xtream_processor_series.rs +++ b/src/processing/xtream_processor_series.rs @@ -3,7 +3,7 @@ use crate::model::config::{Config, ConfigTarget, InputType}; use crate::model::playlist::{FetchedPlaylist, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster}; use crate::processing::playlist_processor::ProcessingPipe; use crate::processing::xtream_parser::parse_xtream_series_info; -use crate::processing::xtream_processor::{create_resolve_episode_wal_files, create_resolve_info_wal_files, playlist_resolve_process_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; +use crate::processing::xtream_processor::{create_resolve_episode_wal_files, create_resolve_info_wal_files, playlist_resolve_download_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; use crate::repository::storage::get_input_storage_path; use crate::repository::xtream_repository::{xtream_get_info_file_paths, xtream_update_input_info_file, xtream_update_input_series_episodes_record_from_wal_file, xtream_update_input_series_record_from_wal_file}; use crate::repository::IndexedDocumentReader; @@ -11,7 +11,7 @@ use crate::{create_resolve_options_function_for_xtream_target, handle_error, han use std::collections::HashMap; use std::fs::File; use std::io::{BufWriter, Write}; -use crate::model::xtream::XtreamSeriesInfoEpisode; +use crate::model::xtream::{XtreamSeriesEpisode, XtreamSeriesInfoEpisode}; const TAG_SERIES_INFO_LAST_MODIFIED: &str = "last_modified"; @@ -36,9 +36,9 @@ fn write_series_episode_record_to_wal_file( provider_id: u32, episode: &XtreamSeriesInfoEpisode, ) -> std::io::Result<()> { - if let Ok(content) = serde_json::to_string(episode) { + let series_episode = XtreamSeriesEpisode::from(episode); + if let Ok(content_bytes) = bincode::serialize(&series_episode) { writer.write_all(&provider_id.to_le_bytes())?; - let content_bytes = content.as_bytes(); let len = u32::try_from(content_bytes.len()).unwrap(); writer.write_all(&len.to_le_bytes())?; writer.write_all(&content_bytes)?; @@ -51,14 +51,14 @@ fn should_update_series_info(pli: &PlaylistItem, processed_provider_ids: &HashMa } async fn playlist_resolve_series_info(cfg: &Config, errors: &mut Vec, - processed_fpl: &mut FetchedPlaylist<'_>, resolve_delay: u16) -> HashMap { + processed_fpl: &mut FetchedPlaylist<'_>, resolve_delay: u16) -> bool { + let mut processed_info_ids = read_processed_series_info_ids(cfg, errors, processed_fpl).await; // we cant write to the indexed-document directly because of the write lock and time-consuming operation. // All readers would be waiting for the lock and the app would be unresponsive. // We collect the content into a wal file and write it once we collected everything. let Some((wal_content_file, wal_record_file, wal_content_path, wal_record_path)) = create_resolve_info_wal_files(cfg, processed_fpl.input, XtreamCluster::Series) - else { return HashMap::new(); }; + else { return !processed_info_ids.is_empty(); }; - let mut processed_info_ids = read_processed_series_info_ids(cfg, errors, processed_fpl).await; let mut content_writer = BufWriter::new(&wal_content_file); let mut record_writer = BufWriter::new(&wal_record_file); let mut content_updated = false; @@ -69,17 +69,19 @@ async fn playlist_resolve_series_info(cfg: &Config, errors: &mut Vec, errors: &mut Vec, - processed_ids: &HashMap, ) -> Vec { let mut result: Vec = vec![]; let input = provider_fpl.input; @@ -118,7 +122,8 @@ async fn process_series_info( return result; }; - let Ok(mut doc_reader) = IndexedDocumentReader::::new(&info_path, &idx_path) else { return result; }; + // Contains the Series Info with episode listing + let Ok(mut info_reader) = IndexedDocumentReader::::new(&info_path, &idx_path) else { return result; }; let Some((wal_file, wal_path)) = create_resolve_episode_wal_files(cfg, input) else { errors.push(notify_err!("Could not create wal file for series episodes record".to_string())); @@ -139,16 +144,13 @@ async fn process_series_info( .filter(|pli| pli.header.borrow().item_type == PlaylistItemType::SeriesInfo) { let Some(provider_id) = pli.header.borrow_mut().get_provider_id() else { continue; }; - if !processed_ids.contains_key(&provider_id) { continue; } - let Ok(content) = doc_reader.get(&provider_id) else { continue; }; + let Ok(content) = info_reader.get(&provider_id) else { continue; }; match serde_json::from_str::(&content) { Ok(series_content) => { match parse_xtream_series_info(&series_content, pli.header.borrow().group.as_str(), input) { Ok(Some(series)) => { for (episode, pli_episode) in &series { let Some(provider_id) = &pli_episode.header.borrow_mut().get_provider_id() else { continue; }; - // TODO only write to wal if not previously handled - like tmdb_id handled or not handle_error!(write_series_episode_record_to_wal_file(&mut wal_writer, *provider_id, episode), |err| errors.push(info_err!(format!("Failed to write to series episode wal file: {err}")))); } @@ -192,9 +194,9 @@ pub async fn playlist_resolve_series(cfg: &Config, target: &ConfigTarget, let (resolve_series, resolve_delay) = get_resolve_series_options(target, processed_fpl); if !resolve_series { return; } - let processed_ids = playlist_resolve_series_info(cfg, errors, processed_fpl, resolve_delay).await; - if processed_ids.is_empty() { return; } - let mut series_playlist = process_series_info(cfg, provider_fpl, errors, &processed_ids).await; + if !playlist_resolve_series_info(cfg, errors, processed_fpl, resolve_delay).await { return; } + let mut series_playlist = process_series_info(cfg, provider_fpl, errors).await; + if series_playlist.is_empty() { return; } // original content saved into original list for plg in &series_playlist { provider_fpl.update_playlist(plg); diff --git a/src/processing/xtream_processor_vod.rs b/src/processing/xtream_processor_vod.rs index f1440c7a9..8e649a5b6 100644 --- a/src/processing/xtream_processor_vod.rs +++ b/src/processing/xtream_processor_vod.rs @@ -1,9 +1,10 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget, InputType}; use crate::model::playlist::{FetchedPlaylist, PlaylistItem, PlaylistItemType, XtreamCluster}; -use crate::processing::xtream_processor::{create_resolve_info_wal_files, get_u32_from_serde_value, get_u64_from_serde_value, playlist_resolve_process_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; +use crate::processing::xtream_processor::{create_resolve_info_wal_files, playlist_resolve_download_playlist_item, read_processed_info_ids, should_update_info, write_info_content_to_wal_file}; use crate::repository::xtream_repository::{xtream_update_input_info_file, xtream_update_input_vod_record_from_wal_file, InputVodInfoRecord}; use crate::{create_resolve_options_function_for_xtream_target, handle_error, handle_error_and_return, notify_err}; +use crate::utils::json_utils::{get_u32_from_serde_value, get_u64_from_serde_value}; use serde_json::{Map, Value}; use std::collections::HashMap; use std::fs::File; @@ -80,7 +81,7 @@ pub async fn playlist_resolve_vod(cfg: &Config, target: &ConfigTarget, errors: & .filter(|&pli| pli.header.borrow().xtream_cluster == XtreamCluster::Video) { let (should_update, _provider_id, _ts) = should_update_vod_info(pli, &processed_info_ids); if should_update { - if let Some(content) = playlist_resolve_process_playlist_item(pli, fpl.input, errors, resolve_delay, XtreamCluster::Video).await { + if let Some(content) = playlist_resolve_download_playlist_item(pli, fpl.input, errors, resolve_delay, XtreamCluster::Video).await { if let Some((provider_id, info_record)) = extract_info_record_from_vod_info(&content) { let ts = info_record.ts; handle_error_and_return!(write_info_content_to_wal_file(&mut content_writer, provider_id, &content), diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index 0b6c91095..6760ea432 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -1,12 +1,13 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget}; -use crate::model::playlist::{PlaylistGroup, PlaylistItemType}; -use crate::model::xtream::XtreamSeriesInfoEpisode; +use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemType}; +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::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_result, notify_err}; use chrono::Datelike; use log::error; @@ -15,6 +16,7 @@ use std::fs::File; use std::io::Write; use std::rc::Rc; use std::sync::LazyLock; +use serde_json::Value; struct KodiStyle { year: regex::Regex, @@ -94,18 +96,20 @@ static KODI_STYLE: LazyLock = LazyLock::new(|| KodiStyle { #[derive(Clone)] enum InputTmdbIndexTree { Video(BPlusTree), - Series(BPlusTree), + Series(BPlusTree), } #[derive(Clone)] enum InputTmdbIndexValue { Video(u32), - Series(XtreamSeriesInfoEpisode), + Series(XtreamSeriesEpisode), } type InputTmdbIndexMap = HashMap>; async fn get_tmdb_value(cfg: &Config, provider_id: Option, input_id: u16, input_indexes: &mut InputTmdbIndexMap, item_type: PlaylistItemType) -> Option { + // the tmdb_ids are stored inside record files for xtream input. + // we load this record files on request for each input and item_type. match provider_id { None => None, Some(pid) => { @@ -127,7 +131,7 @@ async fn get_tmdb_value(cfg: &Config, provider_id: Option, input_id: u16, if let Ok(file_lock) = cfg.file_locks.read_lock(&tmdb_path).await { match item_type { PlaylistItemType::Series => { - if let Ok(tree) = BPlusTree::::load(&tmdb_path) { + if let Ok(tree) = BPlusTree::::load(&tmdb_path) { let tmdb_id = tree.query(&pid).map(|episode| InputTmdbIndexValue::Series(episode.clone())); entry.insert(Some((file_lock, InputTmdbIndexTree::Series(tree)))); return tmdb_id; @@ -173,32 +177,65 @@ pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_p let mut input_tmdb_indexes: InputTmdbIndexMap = HashMap::new(); - for pg in new_playlist { - for pli in pg.channels.iter().filter(|&pli| { - let item_type = pli.header.borrow().item_type; - item_type == PlaylistItemType::Series - || item_type == PlaylistItemType::Live - || item_type == PlaylistItemType::Video - }) { - let (group, title, item_type, provider_id, input_id, url) = { - 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; - let url = Rc::clone(&header.url); - (group, title, item_type, provider_id, input_id, url) - }; + let filter_item = |&pli: &&PlaylistItem| { + let item_type = pli.header.borrow().item_type; + item_type == PlaylistItemType::Series + || item_type == PlaylistItemType::Live + || item_type == PlaylistItemType::Video + }; + let extract_item_info = |pli: &PlaylistItem| { + 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; + let url = Rc::clone(&header.url); + let (series_name, series_release_date) = if header.item_type == PlaylistItemType::Series { + header.additional_properties.as_ref().map_or((None, None), |v| match v { + Value::Object(map) => { + let series_name = if let Some(updated) = map.get("series_name") { + get_string_from_serde_value(updated) + } else { + None + }; + let release_date = if let Some(updated) = map.get("release_date") { + get_string_from_serde_value(updated) + } else { + None + }; + (series_name, release_date) + } + _ => (None, None), + }) + } else { + (None, None) + }; + + (group, title, item_type, provider_id, input_id, url, series_name, series_release_date) + }; + + for pg in new_playlist { + for pli in pg.channels.iter().filter(filter_item) { + // we need to consider + // - Xtream Series Episode (has series_name and release_date) + // - Xtream VOD (should have year or release_date) + // - M3u Series (TODO we dont have this currently, should be guessed through m3u parser) + // - M3u Vod (no additional infos, need to extract from title) + + let (group, title, item_type, provider_id, input_id, url, series_name, series_release_date) = extract_item_info(pli); let mut dir_path = path.join(sanitize_for_filename(&group, underscore_whitespace)); let mut kodi_file_name = sanitize_for_filename(&title, underscore_whitespace); let mut additional_info = String::new(); if kodi_style { - let (kodi_file_dir_name, kodi_style_filename) = kodi_style_rename(&kodi_file_name, &KODI_STYLE); + let (kodi_file_dir_name, kodi_style_filename) = if item_type == PlaylistItemType::Series && series_name.is_some() && series_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 item_type { PlaylistItemType::Series | PlaylistItemType::Video => get_tmdb_value(cfg, provider_id, input_id, &mut input_tmdb_indexes, item_type).await, _ => None, @@ -210,8 +247,8 @@ pub async fn kodi_write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_p 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 let Some(tmdb_id) = episode.info.tmdb_id { - episode_ext = format!("{episode_ext} {{tmdb={tmdb_id}}}"); + if episode.tmdb_id > 0 { + episode_ext = format!("{episode_ext} {{tmdb={}}}", episode.tmdb_id); } episode_ext } diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 216079c18..8a851c0a5 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -10,7 +10,7 @@ use serde_json::{json, Map, Value}; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigInput, ConfigTarget}; use crate::model::playlist::{PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; -use crate::model::xtream::{XtreamMappingOptions, XtreamSeriesInfoEpisode}; +use crate::model::xtream::{XtreamMappingOptions, XtreamSeriesEpisode}; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery, BPlusTreeUpdate}; use crate::repository::indexed_document::{IndexedDocumentGarbageCollector, IndexedDocumentWriter, IndexedDocumentDirectAccess}; use crate::repository::storage::{get_input_storage_path, get_target_id_mapping_file, get_target_storage_path, hash_string, FILE_SUFFIX_DB, FILE_SUFFIX_INDEX}; @@ -23,7 +23,7 @@ use crate::utils::file_utils::open_readonly_file; pub static COL_CAT_LIVE: &str = "cat_live"; pub static COL_CAT_SERIES: &str = "cat_series"; pub static COL_CAT_VOD: &str = "cat_vod"; -const FILE_SERIES_EPISODES: &str = "series_episodes"; +const FILE_SERIES_INFO: &str = "series_info"; const FILE_VOD_INFO: &str = "vod_info"; const FILE_VOD_INFO_RECORD: &str = "vod_info_record"; const FILE_SERIES_INFO_RECORD: &str = "series_info_record"; @@ -90,8 +90,8 @@ pub fn xtream_get_info_file_paths( cluster: XtreamCluster, ) -> Option<(PathBuf, PathBuf)> { if cluster == XtreamCluster::Series { - let xtream_path = storage_path.join(format!("{FILE_SERIES_EPISODES}.{FILE_SUFFIX_DB}")); - let index_path = storage_path.join(format!("{FILE_SERIES_EPISODES}.{FILE_SUFFIX_INDEX}")); + let xtream_path = storage_path.join(format!("{FILE_SERIES_INFO}.{FILE_SUFFIX_DB}")); + let index_path = storage_path.join(format!("{FILE_SERIES_INFO}.{FILE_SUFFIX_INDEX}")); return Some((xtream_path, index_path)); } else if cluster == XtreamCluster::Video { let xtream_path = storage_path.join(format!("{FILE_VOD_INFO}.{FILE_SUFFIX_DB}")); @@ -806,7 +806,7 @@ pub async fn xtream_update_input_series_episodes_record_from_wal_file( let mut reader = BufReader::new(open_readonly_file(wal_path).map_err(|err| notify_err!(format!("Could not read series episode wal info {err}")))?); let mut provider_id_bytes = [0u8; 4]; let mut len_bytes = [0u8; 4]; - let mut tree_record_index: BPlusTree = BPlusTree::load(&record_path).unwrap_or_else(|_| BPlusTree::new()); + let mut tree_record_index: BPlusTree = BPlusTree::load(&record_path).unwrap_or_else(|_| BPlusTree::new()); let mut buffer = vec![0u8; 4096]; loop { if reader.read_exact(&mut provider_id_bytes).is_err() { @@ -826,9 +826,7 @@ pub async fn xtream_update_input_series_episodes_record_from_wal_file( if reader.read_exact(&mut buffer[0..len]).is_err() { break; } - let content = String::from_utf8_lossy(&buffer[0..len]); - - match serde_json::from_str(&content) { + match bincode::deserialize(&buffer[0..len]) { Ok(episode) => { tree_record_index.insert(provider_id, episode); }, diff --git a/src/utils/json_utils.rs b/src/utils/json_utils.rs index ddd73ef5c..2c6ff7823 100644 --- a/src/utils/json_utils.rs +++ b/src/utils/json_utils.rs @@ -164,4 +164,31 @@ where .map_err(|_| serde::de::Error::custom("Invalid string number")), _ => Err(serde::de::Error::custom("Expected number or string")), } +} + + +pub fn get_u64_from_serde_value(value: &Value) -> Option { + match value { + Value::Number(num_val) => num_val.as_u64(), + Value::String(str_val) => { + match str_val.parse::() { + Ok(val) => Some(val), + Err(_) => None + } + } + _ => None, + } +} + +pub fn get_u32_from_serde_value(value: &Value) -> Option { + get_u64_from_serde_value(value).and_then(|val| u32::try_from(val).ok()) +} + +pub fn get_string_from_serde_value(value: &Value) -> Option { + match value { + Value::String(str_val) => { + Some(str_val.to_string()) + } + _ => None, + } } \ No newline at end of file