diff --git a/README.md b/README.md index b7dfb2e0c..cae032993 100644 --- a/README.md +++ b/README.md @@ -487,19 +487,37 @@ mappings: group: '|FR|TNT' ``` -## 3. User +## 3. Api-Proxy Config If you use the proxy functionality, -you need to create a `user.yml` configuration. +you need to create a `api-proxy.yml` configuration. + +You can specify the path for the file with the `-a` cli argument. ```yaml +server: + protocol: http + ip: 192.168.0.3 + http_port: 80 + https_port: 443 + rtmp_port: 0 + timezone: Europe/Paris + message: Welcome to m3u-filter user: - target: pl1 credentials: - - {username: xtream, password: xtream.secret} - + - {username: x3452, password: ztrhgrGZrt83hjerter} ``` -## 4. Compilation +## 4. Logging +Following log levels are supported: +- `debug` +- `info` _default_ +- `warn` +- `error` + +Use the `-l` or `-log-level` cli-argument to specify the log-level. + +## 5. Compilation ### Static binary for docker #### Install prerequisites @@ -567,7 +585,7 @@ Compile it with: cargo build --release --target x86_64-pc-windows-gnu ``` -## 5. The EXTM3U format is an extension of the M3U format. +## 6. The EXTM3U format is an extension of the M3U format. m3u has become almost a standard for the formation of playlists of media players and media devices. A file in the EXTM3U format is a text file with the extension m3u or m3u8. diff --git a/api-proxy.yml b/api-proxy.yml new file mode 100644 index 000000000..fb47b469a --- /dev/null +++ b/api-proxy.yml @@ -0,0 +1,12 @@ +server: + protocol: http + ip: 102.168.9.3 + http_port: 80 + https_port: + rtmp_port: + timezone: Europe/Paris + message: Welcome to m3u-filter +user: + - target: pl1 + credentials: + - {username: x3452, password: ztrhgrGZrt83hjerter} \ No newline at end of file diff --git a/src/api.rs b/src/api/main_api.rs similarity index 93% rename from src/api.rs rename to src/api/main_api.rs index 7c950dc4f..677b74ac0 100644 --- a/src/api.rs +++ b/src/api/main_api.rs @@ -7,13 +7,14 @@ use actix_web::{App, get, HttpRequest, HttpResponse, HttpServer, web}; use chrono::{Local}; use cron::Schedule; use std::str::FromStr; +use actix_web::middleware::Logger; use actix_web::web::Data; -use crate::model_api::{AppState, PlaylistRequest, ServerConfig}; -use crate::config::{Config, ConfigInput, InputType, ProcessTargets}; +use crate::api::model_api::{AppState, PlaylistRequest, ServerConfig}; +use crate::model::config::{Config, ConfigInput, InputType, ProcessTargets}; use crate::{exit, playlist_processor}; use crate::download::get_m3u_playlist; -use crate::xtream_player_api::xtream_player_api; +use crate::api::xtream_player_api::xtream_player_api; use log::{error}; #[get("/")] @@ -84,6 +85,7 @@ pub(crate) async fn start_server(cfg: Config, targets: ProcessTargets) -> future // Web Server HttpServer::new(move || App::new() + .wrap(Logger::default()) .wrap(Cors::default() .supports_credentials() .allow_any_origin() diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 000000000..fd9eeebfc --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod model_api; +pub(crate) mod main_api; +pub(crate) mod xtream_player_api; diff --git a/src/api/model_api.rs b/src/api/model_api.rs new file mode 100644 index 000000000..a195326b8 --- /dev/null +++ b/src/api/model_api.rs @@ -0,0 +1,60 @@ +use actix_web::web; +use serde::{Deserialize, Serialize}; +use crate::model::config::{Config, ProcessTargets}; + +pub(crate) struct AppState { + pub config: Config, + pub targets: ProcessTargets, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub(crate) struct PlaylistRequest { + pub url: String, +} + +impl From> for PlaylistRequest { + fn from(req: web::Json) -> Self { + PlaylistRequest { + url: String::from(&req.url), + } + } +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub(crate) struct ServerConfig { + pub sources: Vec, +} + +#[derive(Serialize)] +pub(crate) struct XtreamUserInfo { + pub active_cons: u16, // 0 + pub allowed_output_formats: Vec, //["ts"], + pub auth: u16, // 0 | 1 + pub created_at: i64, //1623429679, + pub exp_date: i64, //1628755200, + pub is_trial: u16, // 0 | 1 + pub max_connections: u16, + pub message: String, + pub password: String, + pub username: String, + pub status: String, // "Active" +} + +#[derive(Serialize)] +pub(crate) struct XtreamServerInfo { + pub url: String, + pub port:u16, + pub https_port:u16, + pub server_protocol: String, // http, https + pub rtmp_port:u16, + pub timezone:String, + pub timestamp_now:i64, + pub time_now: String, //"2021-06-28 17:07:37" +} + +#[derive(Serialize)] +pub(crate) struct XtreamAuthorizationResponse { + pub user_info: XtreamUserInfo, + pub server_info: XtreamServerInfo, +} + diff --git a/src/api/xtream_player_api.rs b/src/api/xtream_player_api.rs new file mode 100644 index 000000000..5477de789 --- /dev/null +++ b/src/api/xtream_player_api.rs @@ -0,0 +1,86 @@ +// https://github.com/tellytv/go.xtream-codes/blob/master/structs.go + +use actix_web::{HttpResponse, web, get, HttpRequest}; +use chrono::{Duration, Local}; +use crate::api::model_api::{AppState, XtreamAuthorizationResponse, XtreamServerInfo, XtreamUserInfo}; +use crate::model::config::Config; +use crate::model::model_config::{default_as_empty_str}; +use crate::repository::xtream_repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, xtream_get_all}; + +fn get_user_info(user_name: &str, cfg: &Config) -> XtreamAuthorizationResponse { + let server = cfg._api_proxy.as_ref().unwrap().server.clone(); + let now = Local::now(); + XtreamAuthorizationResponse { + user_info: XtreamUserInfo { + active_cons: 0, + allowed_output_formats: Vec::from(["ts".to_string()]), + auth: 1, + created_at: (now - Duration::days(365)).timestamp(), + exp_date: (now + Duration::days(365)).timestamp(), + is_trial: 0, + max_connections: 1, + message: server.message.to_string(), + password: "dfdfdf".to_string(), + username: user_name.to_string(), + status: "Active".to_string(), + }, + server_info: XtreamServerInfo { + url: server.ip.to_string(), + port: server.http_port, + https_port: server.https_port, + server_protocol: server.protocol.clone(), + rtmp_port: server.rtmp_port, + timezone: server.timezone.to_string(), + timestamp_now: now.timestamp(), + time_now: now.format("%Y-%m-%d %H:%M:%S").to_string(), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct XtreamApiRequest { + username: String, + password: String, + #[serde(default = "default_as_empty_str")] + action: String, +} + +#[get("/player_api.php")] +pub(crate) async fn xtream_player_api( + api_req: web::Query, + req: HttpRequest, + _app_state: web::Data, +) -> HttpResponse { + match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { + Some(target_name) => { + let target = target_name.as_str(); + if api_req.action.is_empty() { + return HttpResponse::Ok().json(get_user_info(api_req.username.as_str(), &_app_state.config)); + } + match match api_req.action.as_str() { + "get_live_categories" => xtream_get_all(&_app_state.config, target, COL_CAT_LIVE), + "get_vod_categories" => xtream_get_all(&_app_state.config, target, COL_CAT_VOD), + "get_series_categories" => xtream_get_all(&_app_state.config, target, COL_CAT_SERIES), + "get_live_streams" => xtream_get_all(&_app_state.config, target, COL_LIVE), + "get_vod_streams" => xtream_get_all(&_app_state.config, target, COL_VOD), + "get_series" => xtream_get_all(&_app_state.config, target, COL_SERIES), + _ => Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cant find action: {}/{}", target, &api_req.action))), + } { + Ok(file_path) => { + let file = actix_files::NamedFile::open_async(file_path).await.unwrap() + .set_content_type(mime::APPLICATION_JSON) + .disable_content_disposition(); + file.into_response(&req) + } + Err(_) => HttpResponse::BadRequest().finish() + } + } + _ => { + if api_req.action.is_empty() { + HttpResponse::Unauthorized().finish() + } else { + HttpResponse::BadRequest().finish() + } + } + } +} \ No newline at end of file diff --git a/src/download.rs b/src/download.rs index 568817c6d..5e744e6a3 100644 --- a/src/download.rs +++ b/src/download.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use std::sync::atomic::AtomicI32; use log::debug; use crate::{m3u_parser, utils, xtream_parser}; -use crate::config::{Config, ConfigInput}; -use crate::model_m3u::{PlaylistGroup, XtreamCluster}; +use crate::model::config::{Config, ConfigInput}; +use crate::model::model_m3u::{PlaylistGroup, XtreamCluster}; fn prepare_file_path(input: &ConfigInput, working_dir: &String, action: &str) -> Option { let persist_file: Option = diff --git a/src/filter.rs b/src/filter.rs index 3d0c59186..d5b07e0d0 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -5,8 +5,8 @@ use log::{debug, error}; use pest::iterators::Pair; use pest::Parser; use petgraph::algo::toposort; -use crate::model_m3u::PlaylistItem; -use crate::model_config::ItemField; +use crate::model::model_m3u::PlaylistItem; +use crate::model::model_config::ItemField; use petgraph::graph::DiGraph; use crate::exit; diff --git a/src/m3u_parser.rs b/src/m3u_parser.rs index f482a902c..6a2b08f4b 100644 --- a/src/m3u_parser.rs +++ b/src/m3u_parser.rs @@ -1,6 +1,6 @@ use std::cell::RefCell; -use crate::config::Config; -use crate::model_m3u::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, XtreamCluster}; +use crate::model::config::Config; +use crate::model::model_m3u::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, XtreamCluster}; fn token_value(it: &mut std::str::Chars) -> String { if let Some(oc) = it.next() { diff --git a/src/main.rs b/src/main.rs index 043b189c2..b35de5ebd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,19 +6,13 @@ use env_logger::{Builder}; use log::{debug, error, info, LevelFilter, warn}; use clap::Parser; -use crate::config::{Config, validate_targets}; -use crate::mapping::Mappings; -use crate::user::User; +use crate::model::api_proxy::ApiProxyConfig; +use crate::model::config::{Config, validate_targets}; +use crate::model::mapping::Mappings; -mod api; -mod config; +mod model; mod filter; mod m3u_parser; -mod m3u_repository; -mod mapping; -mod model_api; -mod model_config; -mod model_m3u; mod playlist_processor; mod repository; mod download; @@ -26,8 +20,7 @@ mod utils; mod messaging; mod xtream_parser; mod test; -mod xtream_player_api; -mod user; +mod api; #[derive(Parser)] #[command(name = "m3u-filter")] @@ -48,8 +41,8 @@ struct Args { mapping: Option, /// The user file - #[arg(short, long)] - user: Option, + #[arg(short, long = "api-proxy")] + api_proxy: Option, /// Run in server mode #[arg(short, long, default_value_t = false, default_missing_value = "true")] @@ -75,10 +68,10 @@ fn main() { read_mappings(args.mapping, &mut cfg); if args.server { - read_users(args.user, &mut cfg); + read_api_proxy_config(args.api_proxy, &mut cfg); debug!("web_root: {}", &cfg.api.web_root); info!("server running: http://{}:{}", &cfg.api.host, &cfg.api.port); - match api::start_server(cfg, targets) { + match api::main_api::start_server(cfg, targets) { Ok(_) => {} Err(e) => { exit!("cant start server: {}", e); @@ -111,14 +104,19 @@ fn read_mappings(args_mapping: Option, cfg: &mut Config) { cfg.set_mappings(mappings); } -fn read_users(args_user: Option, cfg: &mut Config) { - let user_file: String = args_user.unwrap_or(utils::get_default_user_path()); +fn read_api_proxy_config(args_api_proxy_config: Option, cfg: &mut Config) { + let api_proxy_config_file: String = args_api_proxy_config.unwrap_or(utils::get_default_api_proxy_config_path()); - let user = read_user(user_file.as_str()); - if user.is_none() { - warn!("cant read user file: {}", user_file.as_str()); + let api_proxy_config = read_api_proxy(api_proxy_config_file.as_str()); + if api_proxy_config.is_none() { + if cfg.has_published_targets() { + exit!("cant read api_proxy_config file: {}", api_proxy_config_file.as_str()); + } else { + warn!("cant read api_proxy_config file: {}", api_proxy_config_file.as_str()); + } + } else { + cfg.set_api_proxy(api_proxy_config); } - cfg.set_user(user); } @@ -152,17 +150,17 @@ fn read_mapping(mapping_file: &str) -> Option { } } -fn read_user(user_file: &str) -> Option { - match utils::open_file(&std::path::PathBuf::from(user_file), false) { +fn read_api_proxy(api_proxy_file: &str) -> Option { + match utils::open_file(&std::path::PathBuf::from(api_proxy_file), false) { Some(file) => { - let mapping: Result = serde_yaml::from_reader(file); + let mapping: Result = serde_yaml::from_reader(file); match mapping { Ok(result) => { result.prepare(); Some(result) } Err(err) => { - error!("cant read user file: {}", err); + error!("cant read api-proxy-config file: {}", err); None } } diff --git a/src/user.rs b/src/model/api_proxy.rs similarity index 56% rename from src/user.rs rename to src/model/api_proxy.rs index 30dfce686..7d075e488 100644 --- a/src/user.rs +++ b/src/model/api_proxy.rs @@ -1,3 +1,7 @@ +use std::collections::{HashSet}; +use crate::exit; +use log::{error}; + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub(crate) struct UserCredentials { pub username: String, @@ -11,6 +15,7 @@ pub(crate) struct TargetUser { } impl TargetUser { + pub fn get_target_name(&self, username: &str, password: &str) -> Option { if self.credentials.iter().find(|c| c.username.eq_ignore_ascii_case(username) && c.password.eq(password)).is_some() { return Some(self.target.clone()); @@ -20,13 +25,35 @@ impl TargetUser { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub(crate) struct User { +pub(crate) struct ServerInfo { + pub protocol: String, + pub ip: String, + pub http_port: u16, + pub https_port: u16, + pub rtmp_port: u16, + pub timezone: String, + pub message: String, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct ApiProxyConfig { + pub server: ServerInfo, pub user: Vec, } -impl User { +impl ApiProxyConfig { + pub fn prepare(&self) { - // TODO check if username is unique, a user can only access one target + let mut usernames = HashSet::new(); + for target_user in &self.user { + for user in &target_user.credentials { + if usernames.contains(&user.username) { + exit!("Non unique username found {}", &user.username); + } else { + usernames.insert(user.username.to_string()); + } + } + } } pub fn get_target_name(&self, username: &str, password: &str) -> Option { @@ -37,4 +64,4 @@ impl User { } None } -} +} \ No newline at end of file diff --git a/src/config.rs b/src/model/config.rs similarity index 94% rename from src/config.rs rename to src/model/config.rs index 0cc6a0c96..258ae9599 100644 --- a/src/config.rs +++ b/src/model/config.rs @@ -4,11 +4,11 @@ use enum_iterator::Sequence; use log::{debug, error}; use crate::filter::{Filter, get_filter, MockValueProcessor, PatternTemplate, prepare_templates, ValueProvider}; -use crate::mapping::Mappings; -use crate::mapping::Mapping; -use crate::model_config::{ItemField, ProcessingOrder, SortOrder, TargetType, default_as_zero, default_as_false, default_as_true}; -use crate::user::{User}; +use crate::model::mapping::Mappings; +use crate::model::mapping::Mapping; +use crate::model::model_config::{ItemField, ProcessingOrder, SortOrder, TargetType, default_as_zero, default_as_false, default_as_true}; use crate::{exit, utils}; +use crate::model::api_proxy::ApiProxyConfig; use crate::utils::get_working_path; fn default_as_frm() -> ProcessingOrder { ProcessingOrder::FRM } @@ -260,17 +260,29 @@ pub(crate) struct Config { pub video_suffix: Option>, pub schedule: Option, #[serde(skip_serializing, skip_deserializing)] - pub _user: Option, + pub _api_proxy: Option, } impl Config { - pub fn set_user(&mut self, user: Option) { - self._user = user; + + pub(crate) fn has_published_targets(&self) -> bool { + for source in &self.sources { + for target in &source.targets { + if target.publish { + return true; + } + } + } + false + } + + pub fn set_api_proxy(&mut self, api_proxy: Option) { + self._api_proxy = api_proxy; } pub fn get_target_for_user(&self, username: &str, password: &str) -> Option { - match &self._user { - Some(user) => user.get_target_name(username, password), + match &self._api_proxy { + Some(api_proxy) => api_proxy.get_target_name(username, password), _ => None } } diff --git a/src/mapping.rs b/src/model/mapping.rs similarity index 98% rename from src/mapping.rs rename to src/model/mapping.rs index ef003fb6a..d5e8cf141 100644 --- a/src/mapping.rs +++ b/src/model/mapping.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use log::{debug, error}; use regex::Regex; use crate::filter::{Filter, get_filter, PatternTemplate, prepare_templates, RegexWithCaptures, ValueProcessor}; -use crate::model_m3u::{FieldAccessor, PlaylistItem}; -use crate::model_config::{ItemField, MAPPER_ATTRIBUTE_FIELDS, AFFIX_FIELDS, +use crate::model::model_m3u::{FieldAccessor, PlaylistItem}; +use crate::model::model_config::{ItemField, MAPPER_ATTRIBUTE_FIELDS, AFFIX_FIELDS, default_as_empty_str, default_as_false, default_as_empty_map, }; use crate::valid_property; diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 000000000..d5a45f29a --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod config; +pub(crate) mod model_config; +pub(crate) mod model_m3u; +pub(crate) mod mapping; +pub(crate) mod api_proxy; \ No newline at end of file diff --git a/src/model_config.rs b/src/model/model_config.rs similarity index 100% rename from src/model_config.rs rename to src/model/model_config.rs diff --git a/src/model_m3u.rs b/src/model/model_m3u.rs similarity index 99% rename from src/model_m3u.rs rename to src/model/model_m3u.rs index e95b58727..6667669be 100644 --- a/src/model_m3u.rs +++ b/src/model/model_m3u.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::config::{ConfigOptions}; +use crate::model::config::ConfigOptions; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html diff --git a/src/model_api.rs b/src/model_api.rs deleted file mode 100644 index cfa5ea9dc..000000000 --- a/src/model_api.rs +++ /dev/null @@ -1,26 +0,0 @@ -use actix_web::web; -use serde::{Deserialize, Serialize}; -use crate::config::{Config, ProcessTargets}; - -pub(crate) struct AppState { - pub config: Config, - pub targets: ProcessTargets, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct PlaylistRequest { - pub url: String, -} - -impl From> for PlaylistRequest { - fn from(req: web::Json) -> Self { - PlaylistRequest { - url: String::from(&req.url), - } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub(crate) struct ServerConfig { - pub sources: Vec, -} \ No newline at end of file diff --git a/src/playlist_processor.rs b/src/playlist_processor.rs index 9d3877b82..c9cd42f20 100644 --- a/src/playlist_processor.rs +++ b/src/playlist_processor.rs @@ -4,18 +4,16 @@ use std::cell::RefCell; use std::sync::{Arc}; use std::thread; use log::{debug, error}; -use config::ConfigTarget; use unidecode::unidecode; -use crate::{config, Config, valid_property}; -use crate::config::{ConfigInput, InputAffix, InputType, ProcessTargets}; -use crate::model_config::SortOrder::{Asc, Desc}; +use crate::{model::config, Config, valid_property}; +use crate::model::config::{ConfigInput, ConfigTarget, InputAffix, InputType, ProcessTargets}; +use crate::model::model_config::{SortOrder::{Asc, Desc}, ItemField, AFFIX_FIELDS, ProcessingOrder}; use crate::filter::{get_field_value, MockValueProcessor, set_field_value, ValueProvider}; -use crate::m3u_repository::write_playlist; -use crate::model_m3u::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; -use crate::mapping::{Mapping, MappingValueProcessor}; -use crate::model_config::{ItemField, AFFIX_FIELDS, ProcessingOrder}; +use crate::repository::m3u_repository::write_playlist; +use crate::model::model_m3u::{FieldAccessor, PlaylistGroup, PlaylistItem, PlaylistItemHeader}; +use crate::model::mapping::{Mapping, MappingValueProcessor}; use crate::download::{get_m3u_playlist, get_xtream_playlist}; -use crate::repository::save_playlist; +use crate::repository::xtream_repository::xtream_save_playlist; fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option> { debug!("Filtering {} groups", playlist.len()); @@ -349,7 +347,7 @@ pub(crate) fn process_playlist(playlist: &mut [PlaylistGroup], } if target.publish { - return save_playlist(target, cfg, &mut new_playlist); + return xtream_save_playlist(target, cfg, &mut new_playlist); } Err(std::io::Error::new(std::io::ErrorKind::Other, format!("Persisting playlist failed: {}", &target.name))) } diff --git a/src/m3u_repository.rs b/src/repository/m3u_repository.rs similarity index 97% rename from src/m3u_repository.rs rename to src/repository/m3u_repository.rs index 53d911ef7..0c2ace45f 100644 --- a/src/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -1,9 +1,9 @@ -use crate::config::{Config, ConfigTarget}; -use crate::model_m3u::PlaylistGroup; +use crate::model::config::{Config, ConfigTarget}; +use crate::model::model_m3u::PlaylistGroup; use crate::utils; use std::io::Write; use chrono::Datelike; -use crate::model_config::TargetType; +use crate::model::model_config::TargetType; use log::{error}; macro_rules! open_file { diff --git a/src/repository/mod.rs b/src/repository/mod.rs new file mode 100644 index 000000000..e8a45a373 --- /dev/null +++ b/src/repository/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod m3u_repository; +pub(crate) mod xtream_repository; \ No newline at end of file diff --git a/src/repository.rs b/src/repository/xtream_repository.rs similarity index 84% rename from src/repository.rs rename to src/repository/xtream_repository.rs index 53f862748..5b0fff532 100644 --- a/src/repository.rs +++ b/src/repository/xtream_repository.rs @@ -5,9 +5,9 @@ use std::iter::FromIterator; use std::path::PathBuf; use serde::Serialize; use serde_json::{json, Value}; -use crate::config::{Config, ConfigTarget}; +use crate::model::config::{Config, ConfigTarget}; use crate::messaging::send_message; -use crate::model_m3u::{PlaylistGroup, XtreamCluster}; +use crate::model::model_m3u::{PlaylistGroup, XtreamCluster}; use crate::utils; use log::{error}; @@ -65,7 +65,7 @@ fn get_collection_path(path: &PathBuf, collection: &str) -> PathBuf { path.join(format!("{}.json", collection)) } -pub(crate) fn save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut Vec) -> Result<(), std::io::Error> { +pub(crate) fn xtream_save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut Vec) -> Result<(), std::io::Error> { let mut failed = false; if let Some(path) = get_storage_path(&cfg, &target.name) { if fs::create_dir_all(&path).is_err() { @@ -81,6 +81,7 @@ pub(crate) fn save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut let mut series_col = vec![]; let mut vod_col = vec![]; + let mut channel_num: i32 = 0; for plg in playlist { match &plg.xtream_cluster { XtreamCluster::LIVE => &mut cat_live_col, @@ -95,20 +96,30 @@ pub(crate) fn save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut ); for pli in &plg.channels { + channel_num += 1; let header = &pli.header.borrow(); let mut document = serde_json::Map::from_iter([ ("category_id".to_string(), Value::String(format!("{}", &plg.id))), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from((&plg.id).to_owned()))]))), - //("custom_sid", ""), ("direct_source".to_string(), Value::String(header.source.clone())), ("name".to_string(), Value::String(header.name.clone())), + ("num".to_string(), Value::Number(serde_json::Number::from(channel_num.clone()))), ("title".to_string(), Value::String(header.title.clone())), - //"num": channel_num), ("stream_icon".to_string(), Value::String(header.logo.clone())), - ("stream_id".to_string(), Value::String(header.id.clone())), ("thumbnail".to_string(), Value::String(header.logo_small.clone())), ]); + match header.xtream_cluster { + XtreamCluster::SERIES => + document.insert("series_id".to_string(), Value::String(header.id.clone())), + _ => document.insert("stream_id".to_string(), Value::Number(serde_json::Number::from(header.id.parse::().unwrap()))), + }; + + if let XtreamCluster::LIVE = header.xtream_cluster { + document.insert("custom_sid".to_string(), Value::String("".to_string())); // is this necessary ? + }; + + if let Some(add_props) = &header.additional_properties { for (field_name, field_value) in add_props { document.insert(field_name.to_string(), field_value.to_owned()); @@ -148,7 +159,7 @@ pub(crate) fn save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut Ok(()) } -pub(crate) fn 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 { if let Some(path) = get_storage_path(&cfg, target_name) { let col_path = get_collection_path(&path, collection_name); if col_path.exists() { diff --git a/src/utils.rs b/src/utils.rs index 61d01333e..ef80dbae1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,7 @@ use log::{debug, error}; use path_absolutize::*; use reqwest::header; use reqwest::header::{HeaderName, HeaderValue}; -use crate::config::ConfigInput; +use crate::model::config::ConfigInput; use crate::messaging::send_message; #[macro_export] @@ -51,8 +51,8 @@ pub(crate) fn get_default_mappings_path() -> String { get_default_file_path("mapping.yml".to_string()) } -pub(crate) fn get_default_user_path() -> String { - get_default_file_path("user.yml".to_string()) +pub(crate) fn get_default_api_proxy_config_path() -> String { + get_default_file_path("api-proxy.yml".to_string()) } pub(crate) fn get_working_path(wd: &String) -> String { diff --git a/src/xtream_parser.rs b/src/xtream_parser.rs index 147007c4b..5c6d6f6ea 100644 --- a/src/xtream_parser.rs +++ b/src/xtream_parser.rs @@ -1,11 +1,11 @@ use std::cell::{RefCell}; use std::collections::HashMap; use std::sync::atomic::{AtomicI32, Ordering}; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::DeserializeOwned; use serde_json::Value; -use crate::model_m3u::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, XtreamCluster}; -use crate::model_config::{default_as_empty_str}; +use crate::model::model_m3u::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, XtreamCluster}; +use crate::model::model_config::{default_as_empty_str}; use log::{error}; fn default_as_empty_list() -> Vec { vec![] } @@ -44,11 +44,11 @@ fn deserialize_number_from_string<'de, D, T: DeserializeOwned>( } } -fn value_to_string_array(value : &Vec) -> Option> { +fn value_to_string_array(value: &Vec) -> Option> { Some(value.iter().map(|i| value_to_string(i)).filter(|i| i.is_some()).map(|i| i.unwrap()).collect()) } -fn value_to_string(v : &Value) -> Option { +fn value_to_string(v: &Value) -> Option { match v { Value::Bool(value) => Some(value.to_string()), Value::Number(value) => Some(value.to_string()), @@ -65,7 +65,7 @@ fn deserialize_as_option_string<'de, D>(deserializer: D) -> Result Ok(Some(s.clone())), - _ => Ok(Some(value.to_string())), + _ => Ok(None), } } @@ -108,7 +108,7 @@ impl XtreamCategory { } } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] struct XtreamStream { #[serde(default, deserialize_with = "deserialize_as_string")] pub name: String, @@ -141,9 +141,9 @@ struct XtreamStream { #[serde(default, deserialize_with = "deserialize_as_option_string")] plot: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] - rating: Option, + rating: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] - rating_5based: Option, + rating_5based: Option, #[serde(default, deserialize_with = "deserialize_as_option_string")] release_date: Option, #[serde(default, deserialize_with = "deserialize_as_option_string")] @@ -169,7 +169,7 @@ macro_rules! add_str_property_if_exists { } macro_rules! add_i64_property_if_exists { ($vec:expr, $prop:expr, $prop_name:expr) => { - $prop.as_ref().map(|v| $vec.push((String::from($prop_name), Value::Number(serde_json::value::Number::from_f64(f64::from(*v)).unwrap())))); + $prop.as_ref().map(|v| $vec.push((String::from($prop_name), Value::Number(serde_json::value::Number::from(i64::from(*v)))))); } } diff --git a/src/xtream_player_api.rs b/src/xtream_player_api.rs deleted file mode 100644 index 935b3b356..000000000 --- a/src/xtream_player_api.rs +++ /dev/null @@ -1,41 +0,0 @@ -use actix_web::{HttpResponse, web, get, HttpRequest}; -use crate::model_api::AppState; -use crate::repository::{COL_CAT_LIVE, COL_CAT_SERIES, COL_CAT_VOD, COL_LIVE, COL_SERIES, COL_VOD, get_all}; - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct XtreamApiRequest { - username: String, - password: String, - action: String, -} - -#[get("/player_api.php")] -pub(crate) async fn xtream_player_api( - api_req: web::Query, - req: HttpRequest, - _app_state: web::Data, -) -> HttpResponse { - match _app_state.config.get_target_for_user(api_req.username.as_str(), api_req.password.as_str()) { - Some(target_name) => { - let target = target_name.as_str(); - match match api_req.action.as_str() { - "get_live_categories" => get_all(&_app_state.config, target, COL_CAT_LIVE), - "get_vod_categories" => get_all(&_app_state.config, target, COL_CAT_VOD), - "get_series_categories" => get_all(&_app_state.config, target, COL_CAT_SERIES), - "get_live_streams" => get_all(&_app_state.config, target, COL_LIVE), - "get_vod_streams" => get_all(&_app_state.config, target, COL_VOD), - "get_series" => get_all(&_app_state.config, target, COL_SERIES), - _ => Err(std::io::Error::new(std::io::ErrorKind::Unsupported, format!("Cant find collection: {}/{}", target, &api_req.action))), - } { - Ok(file_path) => { - let file = actix_files::NamedFile::open_async(file_path).await.unwrap() - .set_content_type(mime::APPLICATION_JSON) - .disable_content_disposition(); - file.into_response(&req) - } - Err(_) => HttpResponse::BadRequest().finish() - } - } - _ => HttpResponse::BadRequest().finish() - } -} \ No newline at end of file diff --git a/user.yml b/user.yml deleted file mode 100644 index a11a48c81..000000000 --- a/user.yml +++ /dev/null @@ -1,4 +0,0 @@ -user: - - target: pl1 - credentials: - - {username: x3452, password: ztrhgrGZrt83hjerter} \ No newline at end of file