From fd69a835b6d787b28dabc4bc3878d3e17ba2d64f Mon Sep 17 00:00:00 2001 From: euzu Date: Wed, 22 Nov 2023 21:06:58 +0100 Subject: [PATCH] some xtream refactorings --- config.yml | 1 + .../component/playlist-tree/playlist-tree.tsx | 5 + src/api/api_model.rs | 4 + src/api/api_utils.rs | 13 +- src/api/main_api.rs | 7 +- src/api/xtream_player_api.rs | 133 ++++++++-- src/config_reader.rs | 7 +- src/model/config.rs | 26 +- src/model/model_m3u.rs | 11 + src/repository/m3u_repository.rs | 10 +- src/repository/xtream_repository.rs | 235 ++++++++++++++++-- src/test.rs | 1 - src/utils.rs | 4 +- 13 files changed, 403 insertions(+), 54 deletions(-) diff --git a/config.yml b/config.yml index 5187a89ca..b1b2d5ca6 100644 --- a/config.yml +++ b/config.yml @@ -59,6 +59,7 @@ sources: sort: groups: order: asc + mapping: - France TNT - France Premium diff --git a/frontend/src/component/playlist-tree/playlist-tree.tsx b/frontend/src/component/playlist-tree/playlist-tree.tsx index b91e37a9c..3502a7e3b 100644 --- a/frontend/src/component/playlist-tree/playlist-tree.tsx +++ b/frontend/src/component/playlist-tree/playlist-tree.tsx @@ -91,6 +91,11 @@ export default function PlaylistTree(props: PlaylistTreeProps) { title = title.substring(0, idx).trim(); } } + const dateSuffixMatch = title.match(/(.*?).\(\d+\)/); + if (dateSuffixMatch?.length > 1) { + title = dateSuffixMatch[1]; + } + const url = serverConfig.video.web_search.replace("{}", title); window.open(url, "imdb"); } diff --git a/src/api/api_model.rs b/src/api/api_model.rs index 703a41a77..8e098644f 100644 --- a/src/api/api_model.rs +++ b/src/api/api_model.rs @@ -149,6 +149,10 @@ pub(crate) struct UserApiRequest { pub token: String, #[serde(default = "default_as_empty_str")] pub action: String, + #[serde(default = "default_as_empty_str")] + pub series_id: String, + #[serde(default = "default_as_empty_str")] + pub vod_id: String, } #[derive(Deserialize, Serialize, Debug, Clone)] diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 49d32a1c8..18997a40b 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -19,11 +19,9 @@ pub(crate) async fn serve_file(file_path: &Path, req: &HttpRequest) -> HttpRespo } } -pub(crate) fn get_user_target<'a>(api_req: &'a web::Query, app_state: &'a web::Data) -> Option<(UserCredentials, &'a ConfigTarget)> { - let username = api_req.username.as_str().trim(); - let password = api_req.password.as_str().trim(); +pub(crate) fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a web::Query, app_state: &'a web::Data) -> Option<(UserCredentials, &'a ConfigTarget)> { if !username.is_empty() && !password.is_empty() { - app_state.config.get_target_for_user(username, password) + app_state.config.get_target_for_user(username, password) } else { let token = api_req.token.as_str().trim(); if !token.is_empty() { @@ -32,4 +30,11 @@ pub(crate) fn get_user_target<'a>(api_req: &'a web::Query, app_s None } } +} + + +pub(crate) fn get_user_target<'a>(api_req: &'a web::Query, app_state: &'a web::Data) -> Option<(UserCredentials, &'a ConfigTarget)> { + let username = api_req.username.as_str().trim(); + let password = api_req.password.as_str().trim(); + get_user_target_by_credentials(username, password, api_req, app_state) } \ No newline at end of file diff --git a/src/api/main_api.rs b/src/api/main_api.rs index 01673643e..4495979a9 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -32,8 +32,9 @@ pub(crate) async fn start_server(cfg: Arc, targets: Arc) let port = cfg.api.port; let web_dir = cfg.api.web_root.to_string(); let web_dir_path = PathBuf::from(&web_dir); - if !web_dir_path.exists() || !web_dir_path.is_dir() { - return Err(std::io::Error::new(ErrorKind::NotFound, format!("web_root does not exists or is not an directory: {:?}", &web_dir_path))); + if !&web_dir_path.exists() || !&web_dir_path.is_dir() { + return Err(std::io::Error::new(ErrorKind::NotFound, + format!("web_root does not exists or is not an directory: {:?}", &web_dir_path))); } let schedule = cfg.schedule.clone(); @@ -72,7 +73,7 @@ pub(crate) async fn start_server(cfg: Arc, targets: Arc) .service(m3u_api_register()) .service(xmltv_api_register()) .service(index) - .service(actix_files::Files::new("/", web_dir.to_string())) + .service(actix_files::Files::new("/", &web_dir_path)) ) .bind(format!("{}:{}", host, port))? .run().await diff --git a/src/api/xtream_player_api.rs b/src/api/xtream_player_api.rs index f291c2607..7e1a9c130 100644 --- a/src/api/xtream_player_api.rs +++ b/src/api/xtream_player_api.rs @@ -1,15 +1,18 @@ // https://github.com/tellytv/go.xtream-codes/blob/master/structs.go +use std::io::Error; +use std::str::FromStr; use actix_web::{HttpRequest, HttpResponse, web, Resource}; use chrono::{Duration, Local}; use log::debug; -use crate::api::api_utils::{get_user_target, serve_file}; +use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, serve_file}; use crate::api::api_model::{AppState, UserApiRequest, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; use crate::model::api_proxy::{UserCredentials}; -use crate::model::config::Config; +use crate::model::config::{Config}; use crate::model::model_config::{TargetType}; -use crate::repository::xtream_repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, xtream_get_all}; +use crate::repository::xtream_repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, xtream_get_all, xtream_get_series_info, xtream_get_vod_info}; +use crate::utils::get_client_request; fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationResponse { let server = cfg._api_proxy.read().unwrap().as_ref().unwrap().server.clone(); @@ -41,6 +44,65 @@ fn get_user_info(user: &UserCredentials, cfg: &Config) -> XtreamAuthorizationRes } } +async fn xtream_player_api_stream( + api_req: &web::Query, + _app_state: &web::Data, + context: &str, + username: &str, + password: &str, + stream_id: &str, +) -> HttpResponse { + if let Some((_user, target)) = get_user_target_by_credentials(&username, &password, api_req, _app_state) { + let target_name = &target.name; + if target.has_output(&TargetType::Xtream) { + match _app_state.config.get_xtream_input_for_target(target_name) { + None => {} + Some(input) => { + let username = input.username.as_ref().unwrap().clone(); + let password = input.password.as_ref().unwrap().clone(); + let stream_url = format!("{}/{}/{}/{}/{}", input.url, context, username, password, stream_id); + let url = reqwest::Url::parse(&stream_url).unwrap(); + let client = get_client_request(input, url); + if let Ok(response) = client.send().await { + if response.status().is_success() { + return HttpResponse::Ok().streaming(response.bytes_stream()); + } + } + } + } + } + } + HttpResponse::BadRequest().finish() +} + +async fn xtream_player_api_live_stream( + api_req: web::Query, + path: web::Path<(String, String, String)>, + _app_state: web::Data, +) -> HttpResponse { + let (username, password, stream_id) = path.into_inner(); + xtream_player_api_stream(&api_req, &_app_state, "live", &username, &password, &stream_id).await +} + +async fn xtream_player_api_series_stream( + api_req: web::Query, + path: web::Path<(String, String, String)>, + _app_state: web::Data, +) -> HttpResponse { + let (username, password, stream_id) = path.into_inner(); + xtream_player_api_stream(&api_req, &_app_state, "series", &username, &password, &stream_id).await +} + +async fn xtream_player_api_movie_stream( + api_req: web::Query, + path: web::Path<(String, String, String)>, + _app_state: web::Data, +) -> HttpResponse { + let (username, password, stream_id) = path.into_inner(); + xtream_player_api_stream(&api_req, &_app_state, "movie", &username, &password, &stream_id).await +} + + async fn xtream_player_api( api_req: web::Query, req: HttpRequest, @@ -54,21 +116,55 @@ async fn xtream_player_api( if action.is_empty() { return HttpResponse::Ok().json(get_user_info(&user, &_app_state.config)); } - match match action { - "get_live_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_LIVE), - "get_vod_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_VOD), - "get_series_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_SERIES), - "get_live_streams" => xtream_get_all(&_app_state.config, target_name, COL_LIVE), - "get_vod_streams" => xtream_get_all(&_app_state.config, target_name, COL_VOD), - "get_series" => xtream_get_all(&_app_state.config, target_name, COL_SERIES), - _ => Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cant find action: {}/{}", target_name, action))), - } { - Ok(file_path) => { - serve_file(&file_path, &req).await + + match action { + "get_series_info" => { + match FromStr::from_str(api_req.series_id.trim()) { + Ok(stream_id) => { + match xtream_get_series_info(&_app_state.config, target_name, stream_id) { + Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), + Err(_) => HttpResponse::NoContent().finish() + } + } + Err(_) => HttpResponse::BadRequest().finish() + } } - Err(err) => { - debug!("Could not open file for xtream target: {} {}", target_name, err); - HttpResponse::NoContent().finish() + "get_vod_info" => { + match FromStr::from_str(api_req.vod_id.trim()) { + Ok(stream_id) => { + match xtream_get_vod_info(&_app_state.config, target_name, stream_id) { + Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), + Err(_) => HttpResponse::NoContent().finish() + } + } + Err(_) => HttpResponse::BadRequest().finish() + } + } + _ => { + match match action { + "get_live_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_LIVE), + "get_vod_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_VOD), + "get_series_categories" => xtream_get_all(&_app_state.config, target_name, COL_CAT_SERIES), + "get_live_streams" => xtream_get_all(&_app_state.config, target_name, COL_LIVE), + "get_vod_streams" => xtream_get_all(&_app_state.config, target_name, COL_VOD), + "get_series" => xtream_get_all(&_app_state.config, target_name, COL_SERIES), + _ => Err(Error::new(std::io::ErrorKind::Unsupported, format!("Cant find action: {}/{}", target_name, action))), + } { + Ok(maybe_file_path_or_content) => { + let (path, content) = maybe_file_path_or_content; + if let Some(file_path) = path { + serve_file(&file_path, &req).await + } else if let Some(payload) = content { + HttpResponse::Ok().body(payload) + } else { + HttpResponse::NoContent().finish() + } + } + Err(err) => { + debug!("Could not open file for xtream target: {} {}", target_name, err); + HttpResponse::NoContent().finish() + } + } } } } else { @@ -92,5 +188,8 @@ pub(crate) fn xtream_api_register() -> Vec { vec![ web::resource("/player_api.php").route(web::get().to(xtream_player_api)), web::resource("/xtream").route(web::get().to(xtream_player_api)), + web::resource("/live/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_live_stream)), + web::resource("/movie/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_movie_stream)), + web::resource("/series/{username}/{password}/{stream_id}").route(web::get().to(xtream_player_api_series_stream)), ] } \ No newline at end of file diff --git a/src/config_reader.rs b/src/config_reader.rs index 1d0864f52..849736646 100644 --- a/src/config_reader.rs +++ b/src/config_reader.rs @@ -1,4 +1,4 @@ -use std::fs::OpenOptions; +use std::fs::{File}; use std::path::PathBuf; use chrono::Local; use log::{debug, error, info, warn}; @@ -111,10 +111,7 @@ pub(crate) fn save_api_proxy(config: &ApiProxyConfig) -> Result<(), M3uFilterErr Err(err) => {error!("Could not backup file {}:{}", &backup_path.to_str().unwrap_or("?"), err)} } info!("Saving api proxy to {}", &path.to_str().unwrap_or("?")); - match OpenOptions::new().write(true) - .truncate(true) - .create(true) - .open(&path) { + match File::create(&path) { Ok(f) => { serde_yaml::to_writer(f, &config).unwrap(); Ok(()) diff --git a/src/model/config.rs b/src/model/config.rs index a9cac6c97..b0542b1db 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -289,6 +289,19 @@ impl ConfigSource { handle_m3u_filter_error_result_list!(M3uFilterErrorKind::Info, self.inputs.iter_mut().enumerate().map(|(idx, i)| i.prepare(index+(idx as u16)))); Ok(index + (self.inputs.len() as u16)) } + + pub(crate) fn get_xtream_input_for_target(&self, target_name: &str) -> Option<&ConfigInput> { + for target in &self.targets { + if target.name.eq(target_name) { + for input in &self.inputs { + if input.input_type.eq(&InputType::Xtream) { + return Some(input) + } + } + } + } + None + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -297,7 +310,7 @@ pub(crate) struct InputAffix { pub value: String, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Sequence, PartialEq)] pub(crate) enum InputType { #[serde(rename = "m3u")] M3u, @@ -511,6 +524,17 @@ impl Config { } } + pub(crate) fn get_xtream_input_for_target(&self, target_name: &str) -> Option<&ConfigInput> { + for source in &self.sources { + match source.get_xtream_input_for_target(target_name) { + Some(cfg) => return Some(cfg), + _ => {} + } + } + None + } + + pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(UserCredentials, &ConfigTarget)> { match self._api_proxy.read().unwrap().as_ref() { Some(api_proxy) => { diff --git a/src/model/model_m3u.rs b/src/model/model_m3u.rs index 7d7c79515..c1e98028f 100644 --- a/src/model/model_m3u.rs +++ b/src/model/model_m3u.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::fmt::{Display, Formatter}; use std::rc::Rc; use serde::{Deserialize, Serialize}; @@ -24,6 +25,16 @@ pub(crate) enum XtreamCluster { Series = 3, } +impl Display for XtreamCluster { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", match self { + XtreamCluster::Live => "live", + XtreamCluster::Video => "video", + XtreamCluster::Series => "series", + }) + } +} + pub(crate) fn default_stream_cluster() -> XtreamCluster { XtreamCluster::Live } pub(crate) trait FieldAccessor { diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index 286d9d6c0..540d7a170 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -10,7 +10,7 @@ use crate::model::config::{Config, ConfigTarget}; use crate::model::model_m3u::PlaylistGroup; use crate::utils::add_prefix_to_filename; -fn check_write(res: std::io::Result) -> Result<(), std::io::Error> { +fn check_write(res: std::io::Result<()>) -> Result<(), std::io::Error> { match res { Ok(_) => Ok(()), Err(_) => Err(std::io::Error::new(std::io::ErrorKind::Other, "Unable to write file")), @@ -107,18 +107,18 @@ pub(crate) fn write_m3u_playlist(target: &ConfigTarget, cfg: &Config, new_playli if let Some(path) = get_m3u_file_path(cfg, filename) { match File::create(&path) { Ok(mut m3u_file) => { - match check_write(m3u_file.write(b"#EXTM3U\n")) { + match check_write(m3u_file.write_all(b"#EXTM3U\n")) { Ok(_) => (), Err(e) => return cant_write_result!(&path, e), } for pg in new_playlist { for pli in &pg.channels { let content = pli.to_m3u(&target.options); - match check_write(m3u_file.write(content.as_bytes())) { + match check_write(m3u_file.write_all(content.as_bytes())) { Ok(_) => (), Err(e) => return cant_write_result!(&path, e), } - match check_write(m3u_file.write(b"\n")) { + match check_write(m3u_file.write_all(b"\n")) { Ok(_) => (), Err(e) => return cant_write_result!(&path, e), } @@ -170,7 +170,7 @@ pub(crate) fn write_strm_playlist(target: &ConfigTarget, cfg: &Config, new_playl let file_path = dir_path.join(format!("{}.strm", file_name)); match File::create(&file_path) { Ok(mut strm_file) => { - match check_write(strm_file.write(header.url.as_bytes())) { + match check_write(strm_file.write_all(header.url.as_bytes())) { Ok(_) => (), Err(e) => return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "failed to write strm playlist: {}", e), } diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index c370ff690..aa4c33bc3 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -1,7 +1,8 @@ use std::cell::Ref; +use std::collections::{BTreeMap, HashMap}; use std::fs; use std::fs::File; -use std::io::{BufWriter, Error, Write}; +use std::io::{BufReader, BufWriter, Error, Read, Seek, SeekFrom, Write}; use std::iter::FromIterator; use std::path::{Path, PathBuf}; use serde::Serialize; @@ -18,8 +19,7 @@ pub(crate) static COL_LIVE: &str = "live"; pub(crate) static COL_SERIES: &str = "series"; pub(crate) static COL_VOD: &str = "vod"; -const LIVE_STREAM_FIELDS: &[&str] = &[ -]; +const LIVE_STREAM_FIELDS: &[&str] = &[]; const VIDEO_STREAM_FIELDS: &[&str] = &[ "release_date", "cast", @@ -50,6 +50,53 @@ fn write_to_file(file: &Path, value: &T) -> Result<(), Error> } } +fn get_collection_and_idx_path(path: &Path, cluster: &XtreamCluster) -> (PathBuf, PathBuf) { + let collection = match cluster { + XtreamCluster::Live => COL_LIVE, + XtreamCluster::Video => COL_VOD, + XtreamCluster::Series => COL_SERIES, + }; + (get_collection_path(path, collection), get_idx_path(path, collection)) +} + +fn write_to_file_width_idx(path: &Path, values: &[(i32, Value)], cluster: &XtreamCluster) -> Result<(), Error> { + let (file, file_idx) = get_collection_and_idx_path(path, cluster); + match File::create(file) { + Ok(file) => { + let mut index = BTreeMap::::new(); + let mut writer = BufWriter::new(file); + writer.write_all("[".as_bytes())?; + let mut offset = 1; + let value_cnt = values.len(); + let mut value_idx = 0; + for (stream_id, data) in values { + let content = serde_json::to_string(data).unwrap(); + let bytes = content.as_bytes(); + let size = bytes.len(); + index.insert(*stream_id, (offset as u32, size as u16)); + offset += size; + let _ = writer.write_all(bytes); + value_idx += 1; + if value_idx < value_cnt { + writer.write_all(",".as_bytes())?; + offset += 1; + } + } + writer.write_all("]".as_bytes())?; + match writer.flush() { + Ok(_) => { + let encoded: Vec = bincode::serialize(&index).unwrap(); + let _ = fs::write(file_idx, encoded); + Ok(()) + } + Err(e) => Err(e) + } + } + Err(e) => Err(e) + } +} + + pub(crate) fn get_xtream_storage_path(cfg: &Config, target_name: &str) -> Option { utils::get_file_path(&cfg.working_dir, Some(std::path::PathBuf::from(target_name.replace(' ', "_")))) } @@ -58,6 +105,11 @@ fn get_collection_path(path: &Path, collection: &str) -> PathBuf { path.join(format!("{}.json", collection)) } +fn get_idx_path(path: &Path, collection: &str) -> PathBuf { + path.join(format!("{}.idx", collection)) +} + + pub(crate) fn get_xtream_epg_file_path(path: &Path) -> PathBuf { path.join("epg.xml") } @@ -69,8 +121,7 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis return Err(M3uFilterError::new(M3uFilterErrorKind::Notify, msg)); } - - let (skip_live_direct_source, skip_video_direct_source) = target.options.as_ref() + let (skip_live_direct_source, skip_video_direct_source) = target.options.as_ref() .map_or((false, false), |o| (o.xtream_skip_live_direct_source, o.xtream_skip_video_direct_source)); let mut cat_live_col = vec![]; @@ -80,6 +131,9 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis let mut series_col = vec![]; let mut vod_col = vec![]; + let mut vod_map = HashMap::::new(); + let mut series_map = HashMap::::new(); + let mut channel_num: i32 = 0; for plg in playlist { if !&plg.channels.is_empty() { @@ -106,10 +160,11 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis ("stream_icon".to_string(), Value::String(header.logo.as_ref().clone())), ]); - let stream_id = Value::Number(serde_json::Number::from(header.id.parse::().unwrap())); + let stream_id = header.id.parse::().unwrap(); + let stream_id_value = Value::Number(serde_json::Number::from(stream_id)); match header.xtream_cluster { XtreamCluster::Live => { - document.insert("stream_id".to_string(), stream_id); + document.insert("stream_id".to_string(), stream_id_value); if !skip_live_direct_source { document.insert("direct_source".to_string(), Value::String(header.url.as_ref().clone())); } @@ -121,14 +176,14 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis }); } XtreamCluster::Video => { - document.insert("stream_id".to_string(), stream_id); + document.insert("stream_id".to_string(), stream_id_value); if !skip_video_direct_source { document.insert("direct_source".to_string(), Value::String(header.url.as_ref().clone())); } document.insert("custom_sid".to_string(), Value::String("".to_string())); } XtreamCluster::Series => { - document.insert("series_id".to_string(), stream_id); + document.insert("series_id".to_string(), stream_id_value); } }; @@ -152,11 +207,21 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis } }; + match header.xtream_cluster { + XtreamCluster::Live => {} + XtreamCluster::Series => { + series_map.insert(stream_id, serde_json::to_string(&document).unwrap()); + } + XtreamCluster::Video => { + vod_map.insert(stream_id, serde_json::to_string(&document).unwrap()); + } + } + match header.xtream_cluster { XtreamCluster::Live => &mut live_col, XtreamCluster::Series => &mut series_col, XtreamCluster::Video => &mut vod_col, - }.push(Value::Object(document)); + }.push((stream_id, Value::Object(document))); } } } @@ -165,10 +230,7 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis for (col_path, data) in [ (get_collection_path(&path, COL_CAT_LIVE), &cat_live_col), (get_collection_path(&path, COL_CAT_VOD), &cat_vod_col), - (get_collection_path(&path, COL_CAT_SERIES), &cat_series_col), - (get_collection_path(&path, COL_LIVE), &live_col), - (get_collection_path(&path, COL_VOD), &vod_col), - (get_collection_path(&path, COL_SERIES), &series_col)] { + (get_collection_path(&path, COL_CAT_SERIES), &cat_series_col)] { match write_to_file(&col_path, data) { Ok(()) => {} Err(err) => { @@ -176,6 +238,17 @@ pub(crate) fn write_xtream_playlist(target: &ConfigTarget, cfg: &Config, playlis } } } + for (data, cluster) in [ + (&live_col, XtreamCluster::Live), + (&vod_col, XtreamCluster::Video), + (&series_col, XtreamCluster::Series)] { + match write_to_file_width_idx(&path, data, &cluster) { + Ok(()) => {} + Err(err) => { + errors.push(format!("Persisting collection failed: {}: {}", cluster, err)); + } + } + } if !errors.is_empty() { return create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "{}", errors.join("\n")); } @@ -232,12 +305,142 @@ fn append_mandatory_fields(document: &mut Map, fields: &[&str]) { } } -pub(crate) fn xtream_get_all(cfg: &Config, target_name: &str, collection_name: &str) -> Result { +pub(crate) fn xtream_get_all(cfg: &Config, target_name: &str, collection_name: &str) -> Result<(Option, Option), Error> { if let Some(path) = get_xtream_storage_path(cfg, target_name) { let col_path = get_collection_path(&path, collection_name); if col_path.exists() { - return Ok(col_path); + return Ok((Some(col_path), None)); } } Err(Error::new(std::io::ErrorKind::Other, format!("Cant find collection: {}/{}", target_name, collection_name))) } + +fn load_map(path: &Path) -> Option> { + match std::fs::read(path) { + Ok(encoded) => { + let decoded: BTreeMap = bincode::deserialize(&encoded[..]).unwrap(); + Some(decoded) + } + Err(_) => None, + } +} + +fn seek_read( + reader: &mut (impl Read + Seek), + offset: u32, + amount_to_read: u16, +) -> Result, Error> { + // A buffer filled with as many zeros as we'll read with read_exact + let mut buf = vec![0; amount_to_read as usize]; + reader.seek(SeekFrom::Start(offset as u64))?; + reader.read_exact(&mut buf)?; + Ok(buf) +} + +fn xtream_get_stream_info(cfg: &Config, target_name: &str, stream_id: i32, cluster: XtreamCluster) -> Result { + if let Some(path) = get_xtream_storage_path(cfg, target_name) { + let (col_path, idx_path) = get_collection_and_idx_path(&path, &cluster); + if idx_path.exists() && col_path.exists() { + if let Some(idx_map) = load_map(&idx_path) { + if let Some((offset, size)) = idx_map.get(&stream_id) { + let mut reader = BufReader::new(File::open(&col_path).unwrap()); + if let Ok(bytes) = seek_read(&mut reader, *offset, *size) { + return Ok(String::from_utf8(bytes).unwrap()); + } + } + } + } + } + Err(Error::new(std::io::ErrorKind::Other, format!("Cant find stream with id: {}/{}/{}", target_name, &cluster, stream_id))) +} + +pub(crate) fn xtream_get_series_info(cfg: &Config, target_name: &str, stream_id: i32) -> Result { + /* + { + "episodes": { + "": [ + { + "added": string, + "container_extension": string, + "custom_sid": string, + "direct_source": string, + "episode_num": int, + "id": string, + "info": { + "bitrate": int, + "duration": string, + "duration_secs": int, + "movie_image": string, + "name": string, + "plot": string, + "rating": float, + "releasedate": string, + "audio": FFMPEGStreamInfo, + "video": FFMPEGStreamInfo + } + "season": int, + "title": string + } + ] + }, + "info": { + "backdrop_path: [string], + "cast": string, + "category_id": string, + "cover": string, + "director": string, + "episode_run_time": string, + "genre": string, + "last_modified": string, + "name": string, + "num": int, + "plot": string, + "rating, string, + "rating_5based": float, + "releaseDate": string, + "series_id": int, + "stream_type": string, + "youtube_trailer": string, + } + } + "seasons": [] +} + */ + // TODO restructure + xtream_get_stream_info(cfg, target_name, stream_id, XtreamCluster::Series) +} + +pub(crate) fn xtream_get_vod_info(cfg: &Config, target_name: &str, stream_id: i32) -> Result { + /* + { + "info": { + "backdrop_path": [string], + "bitrate": FlexInt, + "cast": string, + "director": string, + "duration": string, + "duration_secs": FlexInt, + "genre": string, + "movie_image": string, + "plot": string, + "rating": FlexFloat, + "releasedate": string, + "tmdb_id": int, + "youtube_trailer": string, + "audio": FFMPEGStreamInfo, + "video": FFMPEGStreamInfo, + } `json:"info"` + "movie_data": { + "added": string, + "category_id": string, + "container_extension": string, + "custom_sid": string, + "direct_source": string, + "name": string, + "stream_id": int + } + } + */ + // TODO restructure + xtream_get_stream_info(cfg, target_name, stream_id, XtreamCluster::Video) +} diff --git a/src/test.rs b/src/test.rs index 6c747fe2e..d805020f8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -12,5 +12,4 @@ mod tests { Err(e) => {} } } - } \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 9d7101678..19d12f42c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -196,7 +196,7 @@ pub(crate) fn get_file_path(wd: &String, path: Option) -> Option reqwest::RequestBuilder { +pub(crate) fn get_client_request(input: &ConfigInput, url: url::Url) -> reqwest::RequestBuilder { let mut request = reqwest::Client::new().get(url); if input.headers.is_empty() { let headers = get_request_headers(&input.headers); @@ -205,7 +205,7 @@ fn get_client_request(input: &ConfigInput, url: url::Url) -> reqwest::RequestBui request } -pub fn get_request_headers(defined_headers: &HashMap) -> HeaderMap { +pub(crate) fn get_request_headers(defined_headers: &HashMap) -> HeaderMap { let mut headers = header::HeaderMap::new(); for (key, value) in defined_headers { headers.insert(