diff --git a/src/api/xtream_api.rs b/src/api/xtream_api.rs index 12fb38130..e94b0da53 100644 --- a/src/api/xtream_api.rs +++ b/src/api/xtream_api.rs @@ -17,8 +17,10 @@ use crate::api::api_utils::{get_user_server_info, get_user_target, get_user_targ use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; use crate::model::config::{Config, ConfigInput, ConfigTarget}; use crate::model::config::TargetType; -use crate::model::playlist::{XtreamCluster, XtreamPlaylistItem}; +use crate::model::playlist::{PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; use crate::model::xtream::XtreamMappingOptions; +use crate::repository::storage::{get_target_id_mapping_file, get_target_storage_path, hash_string}; +use crate::repository::target_id_mapping_record::TargetIdMapping; use crate::repository::xtream_repository; use crate::utils::{json_utils, request_utils}; @@ -161,12 +163,12 @@ async fn xtream_player_api_stream( let target_name = &target.name; if target.has_output(&TargetType::Xtream) { let (action_stream_id, stream_ext) = xtream_api_request_separate_number_and_rest(stream_req.stream_id); - let req_stream_id: u32 = match FromStr::from_str(action_stream_id.trim()) { + let virtual_id: u32 = match FromStr::from_str(action_stream_id.trim()) { Ok(id) => id, Err(_) => return HttpResponse::BadRequest().finish() }; - if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(req_stream_id, &app_state.config, target, None) { + if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None) { let input_id: u16 = pli.input_id; if let Some(input) = app_state.config.get_input_by_id(input_id) { let mut query_path = if stream_req.action_path.is_empty() { String::new() } else { format!("{}/", stream_req.action_path) }; @@ -178,12 +180,12 @@ async fn xtream_player_api_stream( } return stream_response(&stream_url, req, Some(input)).await; } - error!("Cant find stream url for target {target_name}, context {}, stream_id {req_stream_id}", stream_req.context); + error!("Cant find stream url for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context); } else { - error!("Cant find input for target {target_name}, context {}, stream_id {req_stream_id}", stream_req.context); + error!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", stream_req.context); } } else { - error!("Failed to read xtream item for stream id {}", req_stream_id); + error!("Failed to read xtream item for stream id {}", virtual_id); } } else { debug!("Target has no xtream output {}", target_name); @@ -281,10 +283,11 @@ fn get_xtream_vod_info(target: &ConfigTarget, pli: &XtreamPlaylistItem, content: Err(Error::new(ErrorKind::Other, format!("Failed to get vod info for id {}", pli.virtual_id))) } -fn get_xtream_series_info(config: &Config, target: &ConfigTarget, pli: &XtreamPlaylistItem, content: &str) -> Result { +fn get_and_write_xtream_series_info(config: &Config, target: &ConfigTarget, pli_series_info: &XtreamPlaylistItem, content: &str) -> Result { if let Ok(mut doc) = serde_json::from_str::(content) { - let mut new_id_to_provider_id_mapping: Vec<(u32, u32)> = Vec::new(); - if let Some(mut new_id) = xtream_repository::xtream_get_max_series_info_episode_id(config, target.name.replace(' ', "_").as_str()) { + if let Some(target_path) = get_target_storage_path(config, target.name.as_str()) { + let mut target_id_mapping = TargetIdMapping::from_path(&get_target_id_mapping_file(&target_path)); + if let Some(episodes) = doc.get_mut("episodes") { if let Some(episodes_map) = episodes.as_object_mut() { let options = XtreamMappingOptions::from_target_options(target.options.as_ref()); @@ -295,9 +298,10 @@ fn get_xtream_series_info(config: &Config, target: &ConfigTarget, pli: &XtreamPl if let Some(episode) = entry.as_object_mut() { if let Some(episode_id) = episode.get("id") { if let Ok(provider_id) = episode_id.as_str().unwrap().parse::() { - new_id += 1; - new_id_to_provider_id_mapping.push((new_id, provider_id)); - episode.insert("id".to_string(), Value::String(new_id.to_string())); + let uuid_str = format!("{}/{}", pli_series_info.url, provider_id); + let uuid = hash_string(uuid_str.as_str()); + let virtual_id = target_id_mapping.insert_entry(provider_id, uuid, &PlaylistItemType::Series, pli_series_info.virtual_id); + episode.insert("id".to_string(), Value::String(virtual_id.to_string())); } } if options.skip_series_direct_source { @@ -309,13 +313,13 @@ fn get_xtream_series_info(config: &Config, target: &ConfigTarget, pli: &XtreamPl } } if let Ok(result) = serde_json::to_string(&doc) { - let _ = xtream_repository::xtream_write_series_info(config, target.name.replace(' ', "_").as_str(), pli.virtual_id, &new_id_to_provider_id_mapping, &result); + let _ = xtream_repository::xtream_write_series_info(config, target.name.as_str(), pli_series_info.virtual_id, &result); return Ok(result); } } } } - Err(Error::new(ErrorKind::Other, format!("Failed to get series info for id {}", pli.virtual_id))) + Err(Error::new(ErrorKind::Other, format!("Failed to get series info for id {}", pli_series_info.virtual_id))) } async fn xtream_get_stream_info_content(info_url: &str, input: &ConfigInput) -> Result { @@ -325,7 +329,7 @@ async fn xtream_get_stream_info_content(info_url: &str, input: &ConfigInput) -> async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &ConfigTarget, pli: &XtreamPlaylistItem, info_url: &str, cluster: XtreamCluster) -> Result { if cluster == XtreamCluster::Series { - if let Ok(content) = xtream_repository::xtream_load_series_info(config, target.name.replace(' ', "_").as_str(), pli.virtual_id) { + if let Ok(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.virtual_id) { return Ok(content); } } @@ -333,12 +337,8 @@ async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &C if let Ok(content) = xtream_get_stream_info_content(info_url, input).await { return match cluster { XtreamCluster::Live => Ok(content), - XtreamCluster::Video => { - return get_xtream_vod_info(target, pli, &content); - } - XtreamCluster::Series => { - return get_xtream_series_info(config, target, pli, &content); - } + XtreamCluster::Video => get_xtream_vod_info(target, pli, &content), + XtreamCluster::Series => get_and_write_xtream_series_info(config, target, pli, &content), }; } @@ -349,12 +349,12 @@ async fn xtream_get_stream_info(config: &Config, input: &ConfigInput, target: &C async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, cluster: XtreamCluster) -> HttpResponse { - let req_stream_id: u32 = match FromStr::from_str(stream_id) { + let virtual_id: u32 = match FromStr::from_str(stream_id) { Ok(id) => id, Err(_) => return HttpResponse::BadRequest().finish() }; - if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(req_stream_id, &app_state.config, target, Some(cluster)) { + if let Ok(pli) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)) { let input_id = pli.input_id; if let Some(input) = app_state.config.get_input_by_id(input_id) { if let Some(info_url) = get_xtream_player_api_info_url(input, cluster, pli.provider_id) { @@ -530,9 +530,9 @@ async fn xtream_player_api( } else { let cat_id = if category_id.is_empty() { 0 } else { category_id.parse::().unwrap_or(0) }; match match action { - "get_live_streams" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Live, &app_state.config, target, cat_id), - "get_vod_streams" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Video, &app_state.config, target, cat_id), - "get_series" => xtream_repository::xtream_load_rewrite_playlist(XtreamCluster::Series, &app_state.config, target, cat_id), + "get_live_streams" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Live, &app_state.config, target, cat_id), + "get_vod_streams" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Video, &app_state.config, target, cat_id), + "get_series" => xtream_repository::xtream_load_rewrite_playlist(&XtreamCluster::Series, &app_state.config, target, cat_id), _ => Err(Error::new(ErrorKind::Unsupported, format!("Cant find action: {action} for target: {target_name}"))), } { Ok(payload) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(payload), diff --git a/src/filter.rs b/src/filter.rs index f488843d4..2502d57bb 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -194,7 +194,7 @@ impl std::fmt::Display for Filter { Filter::TypeComparison(field, item_type) => { write!(f, "{} = {}", field, match item_type { PlaylistItemType::Live => "live", - PlaylistItemType::Movie => "vod", + PlaylistItemType::Video => "vod", PlaylistItemType::Series => "series", PlaylistItemType::SeriesInfo => "series" // yes series-info is handled as series in filter }) @@ -268,8 +268,8 @@ fn get_parser_field_comparison(expr: Pair, templates: &Vec Option { if text_item_type.eq_ignore_ascii_case("live") { Some(PlaylistItemType::Live) - } else if text_item_type.eq_ignore_ascii_case("movie") || text_item_type.eq_ignore_ascii_case("vod") { - Some(PlaylistItemType::Movie) + } else if text_item_type.eq_ignore_ascii_case("movie") || text_item_type.eq_ignore_ascii_case("video") || text_item_type.eq_ignore_ascii_case("vod") { + Some(PlaylistItemType::Video) } else if text_item_type.eq_ignore_ascii_case("series") { Some(PlaylistItemType::Series) } else if text_item_type.eq_ignore_ascii_case("series-info") { diff --git a/src/model/playlist.rs b/src/model/playlist.rs index ec883a60f..0d0e5abe7 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -2,13 +2,15 @@ use std::cell::RefCell; use std::cmp::PartialEq; use std::fmt::{Display, Formatter}; use std::rc::Rc; + use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; +use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{ConfigInput, ConfigTarget}; 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::default_utils::{default_as_false, default_as_zero_u16, default_as_zero_u32}; @@ -41,6 +43,29 @@ pub(crate) enum XtreamCluster { Series = 3, } +impl XtreamCluster { + // pub fn to_bytes(self) -> [u8; 1] { + // [self as u8] + // } + // + // pub fn from_bytes(bytes: [u8; 1]) -> Result { + // match bytes[0] { + // 1 => Ok(XtreamCluster::Live), + // 2 => Ok(XtreamCluster::Video), + // 3 => Ok(XtreamCluster::Series), + // _ => Err(format!("Invalid value for XtreamCluster: {}", bytes[0])), + // } + // } + + pub fn as_str(&self) -> &str { + match self { + XtreamCluster::Live => "Live", + XtreamCluster::Video => "Video", + XtreamCluster::Series => "Series", + } + } +} + impl Default for XtreamCluster { fn default() -> Self { XtreamCluster::Live @@ -49,22 +74,48 @@ impl Default for XtreamCluster { impl Display for XtreamCluster { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - XtreamCluster::Live => "live", - XtreamCluster::Video => "movie", - XtreamCluster::Series => "series", - }) + write!(f, "{}", self.as_str()) } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +impl TryFrom for XtreamCluster { + type Error = String; + fn try_from(item_type: PlaylistItemType) -> Result { + match item_type { + PlaylistItemType::Live => Ok(XtreamCluster::Live), + PlaylistItemType::Video => Ok(XtreamCluster::Video), + PlaylistItemType::Series => Ok(XtreamCluster::Series), + PlaylistItemType::SeriesInfo => Err(format!("Cant convert {item_type}")), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[repr(u8)] pub(crate) enum PlaylistItemType { Live = 1, - Movie = 2, + Video = 2, Series = 3, SeriesInfo = 4, } +impl PlaylistItemType { + pub fn to_bytes(self) -> [u8; 1] { + [self as u8] + } + + pub fn from_bytes(bytes: [u8; 1]) -> Result { + match bytes[0] { + 1 => Ok(PlaylistItemType::Live), + 2 => Ok(PlaylistItemType::Video), + 3 => Ok(PlaylistItemType::Series), + 4 => Ok(PlaylistItemType::SeriesInfo), + _ => Err(format!("Invalid value for PlaylistItemType: {}", bytes[0])), + } + } +} + + impl Default for PlaylistItemType { fn default() -> Self { PlaylistItemType::Live @@ -75,7 +126,7 @@ impl From for PlaylistItemType { fn from(xtream_cluster: XtreamCluster) -> Self { match xtream_cluster { XtreamCluster::Live => PlaylistItemType::Live, - XtreamCluster::Video => PlaylistItemType::Movie, + XtreamCluster::Video => PlaylistItemType::Video, XtreamCluster::Series => PlaylistItemType::SeriesInfo, } } @@ -85,7 +136,7 @@ impl Display for PlaylistItemType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", match self { PlaylistItemType::Live => "live", - PlaylistItemType::Movie => "movie", + PlaylistItemType::Video => "video", PlaylistItemType::Series => "series", PlaylistItemType::SeriesInfo => "series-info", }) @@ -99,7 +150,7 @@ pub(crate) trait FieldAccessor { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub(crate) struct PlaylistItemHeader { - pub uuid: Rc<[u8;32]>, // calculated + pub uuid: Rc<[u8; 32]>, // calculated pub id: Rc, // provider id pub virtual_id: u32, // virtual id pub name: Rc, @@ -127,13 +178,28 @@ pub(crate) struct PlaylistItemHeader { } impl PlaylistItemHeader { - pub(crate) fn gen_uuid(&mut self) { self.uuid = Rc::new(hash_string(&self.url)) } pub(crate) fn get_uuid(&self) -> &Rc<[u8; 32]> { &self.uuid } + + pub(crate) fn get_provider_id(&mut self) -> Option { + match self.id.parse::() { + Ok(id) => Some(id), + Err(_) => match extract_id_from_url(&self.url) { + Some(id) => match id.parse::() { + Ok(newid) => { + self.id = Rc::new(newid.to_string()); + Some(newid) + } + Err(_) => None, + }, + None => None, + } + } + } } macro_rules! to_m3u_non_empty_fields { @@ -325,7 +391,6 @@ pub(crate) struct PlaylistGroup { } impl PlaylistGroup { - pub(crate) fn on_load(&mut self) { self.channels.iter().for_each(|pl| pl.header.borrow_mut().gen_uuid()); } diff --git a/src/processing/m3u_parser.rs b/src/processing/m3u_parser.rs index ba1cb96e0..e366f5d7f 100644 --- a/src/processing/m3u_parser.rs +++ b/src/processing/m3u_parser.rs @@ -136,7 +136,7 @@ fn process_header(input: &ConfigInput, video_suffixes: &Vec<&str>, content: &str if video_suffixes.iter().any(|suffix| url.ends_with(suffix)) { // TODO find Series based on group or configured names plih.xtream_cluster = XtreamCluster::Video; - plih.item_type = PlaylistItemType::Movie; + plih.item_type = PlaylistItemType::Video; } { diff --git a/src/repository/index_record.rs b/src/repository/index_record.rs deleted file mode 100644 index c4cf37ac1..000000000 --- a/src/repository/index_record.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::fs::File; -use std::io::{Error, Read, Seek, SeekFrom, Write}; -use std::path::Path; -use crate::utils::file_utils; - -/** -We write the structs with `bincode::encode` to a file. -To access each entry we need a index file where we can find the -Entries with offset and size of the encoded struct. -This is used for the index file where first entry is the index -of the encoded file, and size is the size of the encoded struct. - -We also use it for different purposes, like storing id -> to id mapping. - */ -pub(in crate::repository) struct IndexRecord { - pub left: u32, - pub right: u32, -} - -impl IndexRecord { - pub fn from_file(file: &mut File, offset: u32) -> Result { - file.seek(SeekFrom::Start(u64::from(offset)))?; - let mut left_bytes = [0u8; 4]; - let mut right_bytes = [0u8; 4]; - file.read_exact(&mut left_bytes)?; - file.read_exact(&mut right_bytes)?; - let left = u32::from_le_bytes(left_bytes); - let right = u32::from_le_bytes(right_bytes); - Ok(IndexRecord { left, right }) - } - - pub fn to_file(path: &Path, left: u32, right: u32, append: bool) -> Result<(), Error> { - match file_utils::open_file_append(path, append) { - Ok(mut file) => { - let bytes = IndexRecord::to_bytes(left, right); - file.write_all(&bytes) - }, - Err(err) => Err(err) - } - } - - pub fn to_bytes(left: u32, right: u32) -> [u8; 8] { - let left_bytes: [u8; 4] = left.to_le_bytes(); - let right_bytes: [u8; 4] = right.to_le_bytes(); - let mut combined_bytes: [u8; 8] = [0; 8]; - combined_bytes[..4].copy_from_slice(&left_bytes); - combined_bytes[4..].copy_from_slice(&right_bytes); - combined_bytes - } - - pub fn get_record_size() -> u32 { 8 } - pub fn get_index_offset(index: u32) -> u32 { index * 8 } -} \ No newline at end of file diff --git a/src/repository/indexed_document_reader.rs b/src/repository/indexed_document_reader.rs index ee727f5d7..702786bc1 100644 --- a/src/repository/indexed_document_reader.rs +++ b/src/repository/indexed_document_reader.rs @@ -3,12 +3,18 @@ use std::fs::File; use std::io::{Error, ErrorKind, Read, Seek, SeekFrom}; use std::marker::PhantomData; use std::path::Path; +use crate::repository::indexed_document_writer::get_record_size; -use crate::repository::index_record::IndexRecord; + +fn read_content_size(main_file: &mut File) -> Result { + let mut size_bytes = [0u8; 4]; + main_file.read_exact(&mut size_bytes)?; + let buf_size = u32::from_le_bytes(size_bytes) as usize; + Ok(buf_size) +} pub(in crate::repository) struct IndexedDocumentReader { main_file: File, - index_file: File, cursor: u32, size: u32, failed: bool, @@ -31,7 +37,6 @@ impl IndexedDocumentReader { }; Ok(Self { main_file, - index_file, cursor: 0, size: u32::try_from(size).map_err(|err| Error::new(ErrorKind::Other, err))?, failed: false, @@ -59,33 +64,31 @@ impl IndexedDocumentReader { !self.failed && self.cursor < self.size } pub fn read_next(&mut self) -> Result, Error> { - if self.has_next() { - let record = IndexRecord::from_file(&mut self.index_file, self.cursor); - self.cursor += IndexRecord::get_record_size(); - match record { - Ok(index_record) => { - let offset = u64::from(index_record.left); - let buf_size = index_record.right as usize; - if self.t_buffer.len() < buf_size { - self.t_buffer.resize(buf_size, 0u8); - } - self.main_file.seek(SeekFrom::Start(offset))?; - self.main_file.read_exact(&mut self.t_buffer[0..buf_size])?; - return match bincode::deserialize::(&self.t_buffer[0..buf_size]) { - Ok(value) => Ok(Some(value)), - Err(err) => { - self.failed = true; - Err(Error::new(ErrorKind::Other, format!("Failed to deserialize document {err}"))) - } - }; - } - Err(err) => { - self.failed = true; - return Err(Error::new(ErrorKind::Other, format!("Failed to deserialize document {err}"))); - } - } + if !self.has_next() { + return Ok(None); } - Ok(None) + // read content-size + let buf_size: usize = read_content_size(&mut self.main_file)?; + // resize buffer if necessary + if self.t_buffer.capacity() < buf_size { + self.t_buffer.reserve(buf_size - self.t_buffer.capacity()); + } + self.t_buffer.resize(buf_size, 0u8); + + // read content + self.main_file.read_exact(&mut self.t_buffer[0..buf_size])?; + + // deserialize buffer + return match bincode::deserialize::(&self.t_buffer[0..buf_size]) { + Ok(value) => { + self.cursor += get_record_size(); + Ok(Some(value)) + } + Err(err) => { + self.failed = true; + Err(Error::new(ErrorKind::Other, format!("Failed to deserialize document {err}"))) + } + }; } } @@ -103,19 +106,34 @@ impl Iterator for IndexedDocumentReader } } -pub(in crate::repository) fn read_indexed_item(main_path: &Path, index_path: &Path, offset: u32) -> Result - where T: ?Sized + serde::de::DeserializeOwned +fn get_offset(file: &mut File, doc_id: u32) -> Result { + // TODO optimize this, sequential searching trough complete file! + let mut bytes = [0u8; 8]; + file.seek(SeekFrom::Start(0))?; + loop { + file.read_exact(&mut bytes)?; + let id = u32::from_le_bytes(bytes[..4].try_into().unwrap()); + if id == doc_id { + return Ok(u64::from(u32::from_le_bytes(bytes[4..].try_into().unwrap()))); + } + } +} + +pub(in crate::repository) fn read_indexed_item(main_path: &Path, index_path: &Path, doc_id: u32) -> Result +where + T: ?Sized + serde::de::DeserializeOwned, { if main_path.exists() && index_path.exists() { let mut index_file = File::open(index_path)?; let mut main_file = File::open(main_path)?; - let index_record = IndexRecord::from_file(&mut index_file, offset)?; - main_file.seek(SeekFrom::Start(u64::from(index_record.left)))?; - let mut buffer: Vec = vec![0; index_record.right as usize]; + let offset = get_offset(&mut index_file, doc_id)?; + main_file.seek(SeekFrom::Start(offset))?; + let buf_size = read_content_size(&mut main_file)?; + let mut buffer: Vec = vec![0; buf_size]; main_file.read_exact(&mut buffer)?; if let Ok(item) = bincode::deserialize::(&buffer[..]) { return Ok(item); } } - Err(Error::new(ErrorKind::Other, format!("Failed to read item for offset {} - {}", offset, main_path.to_str().unwrap()))) -} \ No newline at end of file + Err(Error::new(ErrorKind::Other, format!("Failed to read item for id {} - {}", doc_id, main_path.to_str().unwrap()))) +} diff --git a/src/repository/indexed_document_writer.rs b/src/repository/indexed_document_writer.rs index b05d7439f..2c55d6c2d 100644 --- a/src/repository/indexed_document_writer.rs +++ b/src/repository/indexed_document_writer.rs @@ -3,10 +3,31 @@ use std::fs::File; use std::io::{Error, ErrorKind, Write}; use std::path::PathBuf; -use crate::repository::index_record::IndexRecord; use crate::utils::file_utils; use crate::utils::file_utils::create_file_tuple; +pub(in crate::repository) fn get_record_size() -> u32 { 8 } + +fn to_bytes(left: u32, right: u32) -> [u8; 8] { + let left_bytes: [u8; 4] = left.to_le_bytes(); + let right_bytes: [u8; 4] = right.to_le_bytes(); + let mut combined_bytes: [u8; 8] = [0; 8]; + combined_bytes[..4].copy_from_slice(&left_bytes); + combined_bytes[4..].copy_from_slice(&right_bytes); + combined_bytes +} + +/** +* Creates two files, +* - content +* - index +* +* Layout of content file record is: +* - content-size (u32) + content +* +* Layout of index file record is: +* - document_id (u32) + content file offset + (u32) +*/ pub(in crate::repository) struct IndexedDocumentWriter { main_path: PathBuf, index_path: PathBuf, @@ -55,15 +76,18 @@ impl IndexedDocumentWriter { let current_main_index = self.main_offset; let current_index_index = self.index_offset; if let Ok(encoded) = bincode::serialize(doc) { - match file_utils::check_write(&self.main_file.write_all(&encoded)) { + let content_bytes = u32::try_from(encoded.len()).map_err(|err| Error::new(ErrorKind::Other, err))?; + let mut data: Vec = content_bytes.to_le_bytes().to_vec(); + data.extend(&encoded); + match file_utils::check_write(&self.main_file.write_all(&data)) { Ok(()) => { - let bytes_written = u32::try_from(encoded.len()).map_err(|err| Error::new(ErrorKind::Other, err))?; - let combined_bytes = IndexRecord::to_bytes(doc_id, bytes_written); + let combined_bytes = to_bytes(doc_id, self.main_offset); if let Err(err) = file_utils::check_write(&self.index_file.write_all(&combined_bytes)) { return Err(Error::new(ErrorKind::Other, format!("failed to write document: {} - {}", self.index_path.to_str().unwrap(), err))); } - self.main_offset += bytes_written; - self.index_offset += IndexRecord::get_record_size(); + let written_bytes = u32::try_from(data.len()).map_err(|err| Error::new(ErrorKind::Other, err))?; + self.main_offset += written_bytes; + self.index_offset += get_record_size(); } Err(err) => { return Err(Error::new(ErrorKind::Other, format!("failed to write document: {} - {}", self.main_path.to_str().unwrap(), err))); diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index 214c73b4d..26fa249d7 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -10,7 +10,6 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; use crate::model::config::{Config, ConfigTarget}; use crate::model::playlist::{M3uPlaylistItem, PlaylistGroup, PlaylistItem, PlaylistItemType}; -use crate::repository::index_record::IndexRecord; use crate::repository::indexed_document_reader::{IndexedDocumentReader, read_indexed_item}; use crate::repository::indexed_document_writer::IndexedDocumentWriter; use crate::repository::storage::{ensure_target_storage_path}; @@ -135,5 +134,5 @@ pub(crate) fn m3u_get_item_for_stream_id(stream_id: u32, m3u_path: &Path, idx_pa if stream_id < 1 { return Err(Error::new(ErrorKind::Other, "id should start with 1")); } - read_indexed_item::(m3u_path, idx_path, IndexRecord::get_index_offset(stream_id - 1)) + read_indexed_item::(m3u_path, idx_path, stream_id) } \ No newline at end of file diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 8ca12a2fa..8ffa7c0d8 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -5,7 +5,6 @@ pub(crate) mod epg_repository; pub(crate) mod kodi_repository; pub(crate) mod storage; -mod index_record; mod indexed_document_writer; mod indexed_document_reader; -mod target_id_mapping_record; \ No newline at end of file +pub(crate) mod target_id_mapping_record; \ No newline at end of file diff --git a/src/repository/playlist_repository.rs b/src/repository/playlist_repository.rs index 51662fc53..e7ded5468 100644 --- a/src/repository/playlist_repository.rs +++ b/src/repository/playlist_repository.rs @@ -20,13 +20,14 @@ pub(crate) fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg> for group in &mut *playlist { for channel in &group.channels { let mut header = channel.header.borrow_mut(); - match header.id.parse::() { - Ok(provider_id) => { + match header.get_provider_id() { + Some(provider_id) => { let uuid = header.get_uuid(); - header.virtual_id = target_id_mapping.insert_entry(provider_id, **uuid); + let item_type = header.item_type; + header.virtual_id = target_id_mapping.insert_entry(provider_id, **uuid, &item_type, 0); } - Err(err) => { - errors.push(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string())); + None => { + errors.push(M3uFilterError::new(M3uFilterErrorKind::Info, format!("Playlistitem has no provider id: {}", &header.title))); } } } @@ -35,7 +36,7 @@ pub(crate) fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg> for output in &target.output { match match output.target { TargetType::M3u => m3u_write_playlist(target, cfg, &target_path, playlist), - TargetType::Xtream => xtream_write_playlist(target, cfg, &target_path, playlist), + TargetType::Xtream => xtream_write_playlist(target, cfg, playlist), TargetType::Strm => kodi_write_strm_playlist(target, cfg, playlist, &output.filename), } { Ok(()) => { @@ -52,9 +53,14 @@ pub(crate) fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg> Err(err) => errors.push(err) } } + if let Err(err) = target_id_mapping.persist() { + errors.push(M3uFilterError::new(M3uFilterErrorKind::Info, err.to_string())); + } } - Err(err) => errors.push(err), - } + Err(err) => { + errors.push(err); + }, + }; if errors.is_empty() { Ok(()) } else { Err(errors) } } diff --git a/src/repository/target_id_mapping_record.rs b/src/repository/target_id_mapping_record.rs index 7e638ddae..565fdec2f 100644 --- a/src/repository/target_id_mapping_record.rs +++ b/src/repository/target_id_mapping_record.rs @@ -1,9 +1,11 @@ use std::cmp::max; use std::collections::BTreeMap; use std::fs::File; -use std::io::{Error, Read, Seek, SeekFrom, Write}; +use std::io::{Error, ErrorKind, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::rc::Rc; +use actix_web::body::MessageBody; +use crate::model::playlist::{PlaylistItemType}; use crate::utils::file_utils; @@ -11,42 +13,67 @@ use crate::utils::file_utils; This file contains the provider id, the virtual id, and an uuid */ -pub(in crate::repository) struct TargetIdMappingRecord { +pub(crate) struct TargetIdMappingRecord { pub provider_id: u32, pub virtual_id: u32, pub uuid: [u8; 32], + pub item_type: PlaylistItemType, + pub parent_virtual_id: u32, // only for series to hold series info id. } impl TargetIdMappingRecord { - fn to_bytes(&self) -> [u8; 40] { + fn to_bytes(&self) -> [u8; 45] { let provider_id_bytes: [u8; 4] = self.provider_id.to_le_bytes(); let virtual_id_bytes: [u8; 4] = self.virtual_id.to_le_bytes(); - let mut combined_bytes: [u8; 40] = [0; 40]; - combined_bytes[..4].copy_from_slice(&provider_id_bytes); - combined_bytes[4..].copy_from_slice(&virtual_id_bytes); - combined_bytes[8..].copy_from_slice(&self.uuid); + let parent_virtual_id_bytes: [u8; 4] = self.parent_virtual_id.to_le_bytes(); + let item_type_bytes: [u8; 1] = self.item_type.to_bytes(); + + let mut combined_bytes: [u8; 45] = [0; 45]; + combined_bytes[0..4].copy_from_slice(&provider_id_bytes); + combined_bytes[4..8].copy_from_slice(&virtual_id_bytes); + combined_bytes[8..12].copy_from_slice(&parent_virtual_id_bytes); + combined_bytes[12..13].copy_from_slice(&item_type_bytes); + combined_bytes[13..45].copy_from_slice(&self.uuid); + combined_bytes } + + fn from_bytes(bytes: &[u8; 45]) -> Self { + let provider_id_bytes: [u8; 4] = bytes[0..4].try_into().expect("Slice with incorrect length"); + let virtual_id_bytes: [u8; 4] = bytes[4..8].try_into().expect("Slice with incorrect length"); + let parent_virtual_id_bytes: [u8; 4] = bytes[8..12].try_into().expect("Slice with incorrect length"); + let item_type_bytes: [u8; 1] = bytes[12..13].try_into().expect("Slice with incorrect length"); + let uuid: [u8; 32] = bytes[13..45].try_into().expect("Slice with incorrect length"); + + TargetIdMappingRecord { + provider_id: u32::from_le_bytes(provider_id_bytes), + virtual_id: u32::from_le_bytes(virtual_id_bytes), + parent_virtual_id: u32::from_le_bytes(parent_virtual_id_bytes), + item_type: PlaylistItemType::from_bytes(item_type_bytes).unwrap(), + uuid, + } + } } -pub(in crate::repository) struct TargetIdMapping { +pub(crate) struct TargetIdMapping { dirty: bool, virtual_id_counter: u32, by_provider_id: BTreeMap>, by_virtual_id: BTreeMap>, by_uuid: BTreeMap<[u8; 32], Rc>, records: Vec>, + file: Option } impl TargetIdMapping { - pub(crate) fn insert_entry(&mut self, provider_id: u32, uuid: [u8; 32]) -> u32 { + pub(crate) fn insert_entry(&mut self, provider_id: u32, uuid: [u8; 32], item_type: &PlaylistItemType, parent_virtual_id: u32) -> u32 { self.dirty = true; self.virtual_id_counter += 1; - self.insert(TargetIdMappingRecord { provider_id, virtual_id: self.virtual_id_counter, uuid }); + self.insert(TargetIdMappingRecord { provider_id, virtual_id: self.virtual_id_counter, uuid, item_type: *item_type, parent_virtual_id}); self.virtual_id_counter } - fn new(records: Vec>) -> Self { + fn new(records: Vec>, file: Option) -> Self { let mut by_provider_id: BTreeMap> = BTreeMap::new(); let mut by_virtual_id: BTreeMap> = BTreeMap::new(); let mut by_uuid: BTreeMap<[u8; 32], Rc> = BTreeMap::new(); @@ -64,6 +91,7 @@ impl TargetIdMapping { by_virtual_id, by_uuid, records, + file } } @@ -78,36 +106,42 @@ impl TargetIdMapping { self.records.push(shared_record); } - fn get_by_provider_id(&self, provider_id: u32) -> Option<&Rc> { + pub(crate) fn get_by_provider_id(&self, provider_id: u32) -> Option<&Rc> { self.by_provider_id.get(&provider_id) } - fn get_by_virtual_id(&self, virtual_id: u32) -> Option<&Rc> { + pub(crate) fn get_by_virtual_id(&self, virtual_id: u32) -> Option<&Rc> { self.by_virtual_id.get(&virtual_id) } - fn get_by_uuid(&self, uuid: &[u8; 32]) -> Option<&Rc> { + pub(crate) fn get_by_uuid(&self, uuid: &[u8; 32]) -> Option<&Rc> { self.by_uuid.get(uuid) } + + pub fn persist(&mut self) -> Result<(), Error> { + let mut file = match self.file.take() { + Some(file) => file, + None => return Err(Error::new(ErrorKind::NotFound, "No file given")), + }; + let result = self.to_file(&mut file); + self.file = Some(file); + result + } + pub fn to_file(&mut self, file: &mut File) -> Result<(), Error> { - for record in &self.records { - let bytes = record.to_bytes(); - if let Err(err) = file.write_all(&bytes) { - return Err(err); + if self.dirty { + for record in &self.records { + let bytes = record.to_bytes(); + if let Err(err) = file.write_all(&bytes) { + return Err(err); + } } } self.dirty = false; Ok(()) } - pub fn from_path(path: &Path) -> Self { - match file_utils::open_file_append(path, false) { - Ok(mut file) => TargetIdMapping::from_file(&mut file), - _ => TargetIdMapping::new(vec![]) - } - } - pub fn to_path(&mut self, path: &Path) -> Result<(), Error> { match file_utils::open_file_append(path, false) { Ok(mut file) => self.to_file(&mut file), @@ -115,27 +149,27 @@ impl TargetIdMapping { } } - pub fn from_file(file: &mut File) -> Self { + pub fn from_path(path: &Path) -> Self { + match file_utils::open_file_append(path, false) { + Ok(mut file) => { + let records = TargetIdMapping::read_records_from_file(&mut file); + TargetIdMapping::new(records, Some(file)) + }, + _ => TargetIdMapping::new(vec![], None) + } + } + + fn read_records_from_file(file: &mut File) -> Vec> { let mut records = vec![]; if let Ok(_) = file.seek(SeekFrom::Start(0)) { + let mut bytes = [0u8;45]; loop { - let mut provider_id_bytes = [0u8; 4]; - let mut virtual_id_bytes = [0u8; 4]; - let mut uuid = [0u8; 32]; - if let Err(_) = file.read_exact(&mut provider_id_bytes) { + if let Err(_) = file.read_exact(&mut bytes) { break; } - if let Err(_) = file.read_exact(&mut virtual_id_bytes) { - break; - } - if let Err(_) = file.read_exact(&mut uuid) { - break; - } - let provider_id = u32::from_le_bytes(provider_id_bytes); - let virtual_id = u32::from_le_bytes(virtual_id_bytes); - records.push(Rc::new(TargetIdMappingRecord { provider_id, virtual_id, uuid })); + records.push(Rc::new(TargetIdMappingRecord::from_bytes(&bytes))); } } - TargetIdMapping::new(records) + records } } \ No newline at end of file diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index 7fbed66c8..70d9152d9 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -1,9 +1,8 @@ use std::collections::HashMap; use std::convert::TryInto; use std::fs::File; -use std::io::{BufReader, Error, ErrorKind, Read, Seek, SeekFrom, Write}; +use std::io::{BufReader, Error, ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; -use std::rc::Rc; use log::error; use serde_json::{json, Value}; @@ -13,20 +12,16 @@ use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind}; use crate::model::config::{Config, ConfigTarget}; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemType, XtreamCluster, XtreamPlaylistItem}; use crate::model::xtream::XtreamMappingOptions; -use crate::processing::m3u_parser::extract_id_from_url; -use crate::repository::index_record::IndexRecord; use crate::repository::indexed_document_reader::{IndexedDocumentReader, read_indexed_item}; use crate::repository::indexed_document_writer::IndexedDocumentWriter; -use crate::repository::storage::get_target_storage_path; +use crate::repository::storage::{get_target_id_mapping_file, get_target_storage_path}; +use crate::repository::target_id_mapping_record::TargetIdMapping; use crate::utils::file_utils; use crate::utils::json_utils::{json_iter_array, json_write_documents_to_file}; pub(crate) static COL_CAT_LIVE: &str = "cat_live"; pub(crate) static COL_CAT_SERIES: &str = "cat_series"; pub(crate) static COL_CAT_VOD: &str = "cat_vod"; -pub(crate) static COL_LIVE: &str = "live"; -pub(crate) static COL_SERIES: &str = "series"; -pub(crate) static COL_VOD: &str = "vod"; macro_rules! cant_write_result { ($path:expr, $err:expr) => { @@ -38,10 +33,6 @@ fn get_collection_path(path: &Path, collection: &str) -> PathBuf { path.join(format!("{collection}.json")) } -pub(crate) fn xtream_get_stream_id_cluster_index_file_path(storage_path: &Path) -> PathBuf { - storage_path.join("cluster_index.db") -} - // Maps episode id -> to provider_episode_id and series_id fn xtream_get_series_episode_id_mapping_file_path(storage_path: &Path) -> PathBuf { storage_path.join("mapping_episode.db") @@ -95,67 +86,34 @@ fn xtream_clear_catchup(storage_path: &Path) { let _ = std::fs::remove_file(xtream_get_catchup_id_mapping_file_path(storage_path)); } -fn write_playlist_to_file(storage_path: &Path, stream_id: &mut u32, cluster: XtreamCluster, playlist: &mut [PlaylistItem]) -> Result<(), M3uFilterError> { - let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster); - match IndexedDocumentWriter::new(xtream_path.clone(), idx_path) { - Ok(mut writer) => { - for pli in playlist.iter_mut() { - if let Ok(mut xtream) = pli.to_xtream() { - xtream.virtual_id = *stream_id; - match writer.write_doc(xtream.virtual_id, &xtream) { - Ok(_) => *stream_id += 1, +fn write_playlists_to_file(storage_path: &Path, collections: Vec<(XtreamCluster, &mut [PlaylistItem])>) -> Result<(), M3uFilterError> { + for (cluster, playlist) in collections { + let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, &cluster); + match IndexedDocumentWriter::new(xtream_path.clone(), idx_path) { + Ok(mut writer) => { + for item in playlist { + match item.to_xtream() { + Ok(xtream) => { + match writer.write_doc(item.header.borrow().virtual_id, &xtream) { + Ok(_) => {} + Err(err) => return cant_write_result!(&xtream_path, err) + } + }, Err(err) => return cant_write_result!(&xtream_path, err) } } } - if cluster == XtreamCluster::Live { - xtream_clear_catchup(storage_path); - } else if cluster == XtreamCluster::Series { - xtream_clear_series_info(storage_path); - } - Ok(()) + Err(err) => return cant_write_result!(&xtream_path, err) } - Err(err) => cant_write_result!(&xtream_path, err) - } -} - -fn save_stream_id_cluster_mapping(storage_path: &Path, id_data: &mut Vec<(XtreamCluster, u32, u32)>) -> Result<(), Error> { - let stream_id_path = xtream_get_stream_id_cluster_index_file_path(storage_path); - id_data.sort_by(|(_, start_a, _), (_, start_b, _)| start_b.cmp(start_a)); - let encoded: Vec = bincode::serialize(id_data).unwrap(); - std::fs::write(stream_id_path, encoded) -} - -fn load_stream_id_cluster_mapping(storage_path: &Path) -> Option> { - let path = xtream_get_stream_id_cluster_index_file_path(storage_path); - if path.exists() { - match std::fs::read(path) { - Ok(encoded) => { - let decoded: Vec<(XtreamCluster, u32, u32)> = bincode::deserialize(&encoded[..]).unwrap(); - Some(decoded) - } - Err(_) => None, + if cluster == XtreamCluster::Live { + xtream_clear_catchup(storage_path); + } else if cluster == XtreamCluster::Series { + xtream_clear_series_info(storage_path); } - } else { - None } + Ok(()) } -fn write_playlists_to_file(storage_path: &Path, collections: Vec<(XtreamCluster, &mut [PlaylistItem])>) -> Result<(), M3uFilterError> { - let mut id_list: Vec<(XtreamCluster, u32, u32)> = vec![]; - let mut stream_id: u32 = 1; - for (cluster, playlist) in collections { - let start = stream_id; - write_playlist_to_file(storage_path, &mut stream_id, cluster, playlist)?; - id_list.push((cluster, start, stream_id)); - } - match save_stream_id_cluster_mapping(storage_path, &mut id_list) { - Ok(()) => Ok(()), - Err(err) => Err(M3uFilterError::new(M3uFilterErrorKind::Notify, format!("failed to write xtream playlist: {} - {}", storage_path.to_str().unwrap(), err))) - } -} - - fn get_map_item_as_str(map: &serde_json::Map, key: &str) -> Option { if let Some(value) = map.get(key) { if let Some(result) = value.as_str() { @@ -204,19 +162,23 @@ pub(crate) fn xtream_get_epg_file_path(path: &Path) -> PathBuf { path.join("epg.xml") } -pub(crate) fn xtream_get_file_paths(storage_path: &Path, cluster: XtreamCluster) -> (PathBuf, PathBuf) { - let xtream_path = storage_path.join(format!("{}.db", match cluster { - XtreamCluster::Live => COL_LIVE, - XtreamCluster::Video => COL_VOD, - XtreamCluster::Series => COL_SERIES - })); +fn xtream_get_file_paths_for_name(storage_path: &Path, name: &str) -> (PathBuf, PathBuf) { + let xtream_path = storage_path.join(format!("{}.db", name)); let extension = xtream_path.extension().map(|ext| format!("{}_", ext.to_str().unwrap_or(""))); let index_path = xtream_path.with_extension(format!("{}idx", &extension.unwrap_or_default())); (xtream_path, index_path) } -pub(crate) fn xtream_write_playlist(target: &ConfigTarget, cfg: &Config, target_path: &Path, playlist: &mut [PlaylistGroup]) -> Result<(), M3uFilterError> { - match ensure_xtream_storage_path(cfg, target.name.replace(' ', "_").as_str()) { +pub(crate) fn xtream_get_file_paths(storage_path: &Path, cluster: &XtreamCluster) -> (PathBuf, PathBuf) { + xtream_get_file_paths_for_name(storage_path, &cluster.as_str().to_lowercase()) +} + +pub(crate) fn xtream_get_file_paths_for_series(storage_path: &Path) -> (PathBuf, PathBuf) { + xtream_get_file_paths_for_name(storage_path, "series") +} + +pub(crate) fn xtream_write_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut [PlaylistGroup]) -> Result<(), M3uFilterError> { + match ensure_xtream_storage_path(cfg, target.name.as_str()) { Ok(path) => { let mut cat_live_col = vec![]; let mut cat_series_col = vec![]; @@ -225,7 +187,7 @@ pub(crate) fn xtream_write_playlist(target: &ConfigTarget, cfg: &Config, target_ let mut series_col = vec![]; let mut vod_col = vec![]; let mut errors = Vec::new(); -// TODO.EUZU old catgeory ids + // preserve category_ids let (max_cat_id, existing_cat_ids) = load_old_category_ids(&path); let mut cat_id_counter = max_cat_id; @@ -253,30 +215,8 @@ pub(crate) fn xtream_write_playlist(target: &ConfigTarget, cfg: &Config, target_ let col = if header.item_type == PlaylistItemType::Series { None } else { - if header.id.parse::().is_err() { - let id_from_url = match extract_id_from_url(&header.url) { - Some(id) => match id.parse::() { - Ok(newid) => Some(newid), - Err(_) => None, - }, - None => None, - }; - - let has_id = match id_from_url { - Some(newid) => { - header.id = Rc::new(newid.to_string()); - Ok(()) - } - None => { - let title = header.title.as_str(); - errors.push(format!("Channel does not have an id: {title}")); - Err(()) - } - }; - // Instead of returning from the function, handle the error by assigning None to col. - if has_id.is_err() { - None - } else { + match header.get_provider_id() { + Some(_) => { header.category_id = *cat_id; Some(match header.xtream_cluster { XtreamCluster::Live => &mut live_col, @@ -284,13 +224,11 @@ pub(crate) fn xtream_write_playlist(target: &ConfigTarget, cfg: &Config, target_ XtreamCluster::Video => &mut vod_col, }) } - } else { - header.category_id = *cat_id; - Some(match header.xtream_cluster { - XtreamCluster::Live => &mut live_col, - XtreamCluster::Series => &mut series_col, - XtreamCluster::Video => &mut vod_col, - }) + None => { + let title = header.title.as_str(); + errors.push(format!("Channel does not have an id: {title}")); + None + } } }; drop(header); @@ -342,44 +280,45 @@ pub(crate) fn xtream_get_collection_path(cfg: &Config, target_name: &str, collec Err(Error::new(ErrorKind::Other, format!("Cant find collection: {target_name}/{collection_name}"))) } -fn _xtream_get_item_for_stream_id(stream_id: u32, storage_path: &Path, xtream_cluster: Option, mapping: &[(XtreamCluster, u32, u32)]) -> Result { - if let Some((cluster, cluster_start, _end)) = match xtream_cluster { - Some(clus) => mapping.iter().find(|(c, _, _)| *c == clus), - None => mapping.iter().find(|(_cluster, start, _end)| stream_id >= *start), - } { - let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, *cluster); - if stream_id >= *cluster_start { - return read_indexed_item::(&xtream_path, &idx_path, IndexRecord::get_index_offset(stream_id - cluster_start)); - } - } - Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}"))) +fn xtream_read_item_for_stream_id(stream_id: u32, storage_path: &Path, cluster: &XtreamCluster) -> Result { + let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster); + return read_indexed_item::(&xtream_path, &idx_path, stream_id); +} + +fn xtream_read_series_item_for_stream_id(stream_id: u32, storage_path: &Path) -> Result { + let (xtream_path, idx_path) = xtream_get_file_paths_for_series(storage_path); + return read_indexed_item::(&xtream_path, &idx_path, stream_id); } pub(crate) fn xtream_get_item_for_stream_id(stream_id: u32, config: &Config, target: &ConfigTarget, xtream_cluster: Option) -> Result { - if let Some(storage_path) = xtream_get_storage_path(config, target.name.replace(' ', "_").as_str()) { - if let Some(mapping) = load_stream_id_cluster_mapping(&storage_path) { - if let Some(max) = mapping.iter().map(|(_cluster, _start, end)| end).max().copied() { - if max < stream_id { - // episoden id's fangen bei (max cluster id + 1) an. - // episode mapping har 3 u32 also 12 bytes - let index = stream_id - (max + 1); - if let Ok((_episode_id, provider_id, series_id)) = xtream_read_episode_id_mapping(&storage_path, index) { - if let Ok(mut pli) = _xtream_get_item_for_stream_id(series_id, &storage_path, xtream_cluster, &mapping) { - pli.provider_id = provider_id; - return Ok(pli); + if let Some(storage_path) = xtream_get_storage_path(config, target.name.as_str()) { + let cluster = match xtream_cluster { + Some(c) => c, + None => { + if let Some(target_path) = get_target_storage_path(config, target.name.as_str()) { + let target_id_mapping = TargetIdMapping::from_path(&get_target_id_mapping_file(&target_path)); + if let Some(mapping) = target_id_mapping.get_by_virtual_id(stream_id) { + if mapping.item_type == PlaylistItemType::SeriesInfo { + return xtream_read_series_item_for_stream_id(stream_id, &storage_path); + } else { + XtreamCluster::try_from(mapping.item_type).unwrap() } + } else { + return Err(Error::new(ErrorKind::Other, format!("Could not determine cluster for xtream item with stream-id {stream_id}"))); } - return Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}"))); + } else { + return Err(Error::new(ErrorKind::Other, format!("Could not find path for target {}", &target.name))); } } - return _xtream_get_item_for_stream_id(stream_id, &storage_path, xtream_cluster, &mapping); - } + }; + xtream_read_item_for_stream_id(stream_id, &storage_path, &cluster) + } else { + Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}, storage path not found"))) } - Err(Error::new(ErrorKind::Other, format!("Failed to read xtream item for stream-id {stream_id}"))) } -pub(crate) fn xtream_load_rewrite_playlist(cluster: XtreamCluster, config: &Config, target: &ConfigTarget, category_id: u32) -> Result { - if let Some(storage_path) = xtream_get_storage_path(config, target.name.replace(' ', "_").as_str()) { +pub(crate) fn xtream_load_rewrite_playlist(cluster: &XtreamCluster, config: &Config, target: &ConfigTarget, category_id: u32) -> Result { + if let Some(storage_path) = xtream_get_storage_path(config, target.name.as_str()) { let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, cluster); match IndexedDocumentReader::::new(&xtream_path, &idx_path) { Ok(mut reader) => { @@ -400,91 +339,17 @@ pub(crate) fn xtream_load_rewrite_playlist(cluster: XtreamCluster, config: &Conf Err(Error::new(ErrorKind::Other, format!("Failed to find xtream storage for target {}", &target.name))) } -// The mapping file record is episode_id 4bytes, episode_provider_id 4bytes, series_id 4bytes -fn read_last_id_from_episode_id_mapping_file(path: &Path) -> Result { - let mut file = File::open(path)?; - file.seek(SeekFrom::End(-12))?; - let mut buffer = [0; 4]; - file.read_exact(&mut buffer)?; - Ok(u32::from_le_bytes(buffer)) -} - -pub(crate) fn xtream_get_max_series_info_episode_id(config: &Config, target_name: &str) -> Option { - if let Some(storage_path) = xtream_get_storage_path(config, target_name) { - let mapping_file_path = xtream_get_series_episode_id_mapping_file_path(&storage_path); - if mapping_file_path.exists() { - if let Ok(last_id) = read_last_id_from_episode_id_mapping_file(&mapping_file_path) { - return Some(last_id); - } - } - - if let Some(storage_path) = xtream_get_storage_path(config, target_name) { - if let Some(mapping) = load_stream_id_cluster_mapping(&storage_path) { - return mapping.iter().map(|(_, _, end)| end).max().copied(); - } - } - } - None -} - -fn xtream_write_episode_id_mapping(storage_path: &Path, series_id: u32, episode_id_mapping: &[(u32, u32)]) -> Result<(), Error> { - let file_path = xtream_get_series_episode_id_mapping_file_path(storage_path); - if let Ok(mut file) = if file_path.exists() { - std::fs::OpenOptions::new() - .append(true) // Open in append mode - .open(file_path) - } else { - File::create(file_path) - } { - let stream_id_bytes: [u8; 4] = series_id.to_le_bytes(); - let mut bytes: Vec = Vec::new(); - for (episode_id, provider_id) in episode_id_mapping { - bytes.extend_from_slice(&episode_id.to_le_bytes()); - bytes.extend_from_slice(&provider_id.to_le_bytes()); - bytes.extend_from_slice(&stream_id_bytes); - } - return file_utils::check_write(&file.write_all(&bytes[..])); - } - Err(Error::new(ErrorKind::Other, format!("Failed to open series info idmapping file inside {}", storage_path.to_str().unwrap()))) -} - -fn xtream_read_episode_id_mapping(storage_path: &Path, index: u32) -> Result<(u32, u32, u32), Error> { - let offset = index * 12; - let episode_file = xtream_get_series_episode_id_mapping_file_path(storage_path); - if episode_file.exists() { - if let Ok(mut file) = File::open(episode_file) { - file.seek(SeekFrom::Start(u64::from(offset)))?; - let mut bytes = [0u8; 4]; - file.read_exact(&mut bytes)?; - let episode_id = u32::from_le_bytes(bytes); - file.read_exact(&mut bytes)?; - let provider_id = u32::from_le_bytes(bytes); - file.read_exact(&mut bytes)?; - let series_id = u32::from_le_bytes(bytes); - Ok((episode_id, provider_id, series_id)) - } else { - Err(Error::new(ErrorKind::Other, format!("Could not find episode mapping at offset {offset}"))) - } - } else { - Err(Error::new(ErrorKind::Other, format!("Episode mapping not found at {}", storage_path.to_str().unwrap_or("")))) - } -} - pub(crate) fn xtream_write_series_info(config: &Config, target_name: &str, - series_id: u32, episode_id_mapping: &[(u32, u32)], + series_id: u32, content: &str) -> Result<(), Error> { if let Some(storage_path) = xtream_get_storage_path(config, target_name) { if let Some((info_path, idx_path)) = xtream_get_info_file_paths(&storage_path, XtreamCluster::Series) { return match IndexedDocumentWriter::new_append(info_path.clone(), idx_path) { Ok(mut writer) => { match writer.write_doc(series_id, content) { - Ok((_, index_offset)) => { - let series_id_index_mapping_path = xtream_get_series_id_series_info_mapping_file_path(&storage_path); - IndexRecord::to_file(&series_id_index_mapping_path, series_id, index_offset, true)?; - } + Ok(_) => Ok(()), Err(_) => return Err(Error::new(ErrorKind::Other, format!("failed to write xtream series info for target {target_name}"))) } - xtream_write_episode_id_mapping(&storage_path, series_id, episode_id_mapping) } Err(err) => Err(err) }; @@ -493,6 +358,7 @@ pub(crate) fn xtream_write_series_info(config: &Config, target_name: &str, Ok(()) } +// Reads the series info entry if exists, otherwise error pub(crate) fn xtream_load_series_info(config: &Config, target_name: &str, series_id: u32) -> Result { if let Some(storage_path) = xtream_get_storage_path(config, target_name) { let series_id_index_mapping_path = xtream_get_series_id_series_info_mapping_file_path(&storage_path); diff --git a/test/rest-api.http b/test/rest-api.http index 01c45fe29..cb51d05e4 100644 --- a/test/rest-api.http +++ b/test/rest-api.http @@ -17,10 +17,10 @@ Content-Type: application/json {"username": "test", "password": "test"} ### xtream live_categories -GET http://localhost:8901/m3u?username=me&password=me +GET http://localhost:8901/player_api.php?action=get_live_categories&username=xt&password=xt ### xtream live_streams -GET http://localhost:8901/player_api.php?action=get_live_streams&username=xt&password=xt&category_id=203 +GET http://localhost:8901/player_api.php?action=get_live_streams&username=xt&password=xt&category_id=102 ### xtream series_categories GET http://localhost:8901/player_api.php?action=get_series_categories&username=xt&password=xt