mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-15 20:32:27 +02:00
changed user.yml to api-proxy.yml because of some other needed properties,
refactored file structure. Added xtream authentication response.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
pub(crate) mod model_api;
|
||||
pub(crate) mod main_api;
|
||||
pub(crate) mod xtream_player_api;
|
||||
@@ -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<web::Json<PlaylistRequest>> for PlaylistRequest {
|
||||
fn from(req: web::Json<PlaylistRequest>) -> Self {
|
||||
PlaylistRequest {
|
||||
url: String::from(&req.url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub(crate) struct ServerConfig {
|
||||
pub sources: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct XtreamUserInfo {
|
||||
pub active_cons: u16, // 0
|
||||
pub allowed_output_formats: Vec<String>, //["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,
|
||||
}
|
||||
|
||||
@@ -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<XtreamApiRequest>,
|
||||
req: HttpRequest,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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<PathBuf> {
|
||||
let persist_file: Option<PathBuf> =
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -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() {
|
||||
|
||||
+24
-26
@@ -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<String>,
|
||||
|
||||
/// The user file
|
||||
#[arg(short, long)]
|
||||
user: Option<String>,
|
||||
#[arg(short, long = "api-proxy")]
|
||||
api_proxy: Option<String>,
|
||||
|
||||
/// 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<String>, cfg: &mut Config) {
|
||||
cfg.set_mappings(mappings);
|
||||
}
|
||||
|
||||
fn read_users(args_user: Option<String>, 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<String>, 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<Mappings> {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_user(user_file: &str) -> Option<User> {
|
||||
match utils::open_file(&std::path::PathBuf::from(user_file), false) {
|
||||
fn read_api_proxy(api_proxy_file: &str) -> Option<ApiProxyConfig> {
|
||||
match utils::open_file(&std::path::PathBuf::from(api_proxy_file), false) {
|
||||
Some(file) => {
|
||||
let mapping: Result<User, _> = serde_yaml::from_reader(file);
|
||||
let mapping: Result<ApiProxyConfig, _> = 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
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<TargetUser>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
@@ -37,4 +64,4 @@ impl User {
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Vec<String>>,
|
||||
pub schedule: Option<String>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub _user: Option<User>,
|
||||
pub _api_proxy: Option<ApiProxyConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn set_user(&mut self, user: Option<User>) {
|
||||
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<ApiProxyConfig>) {
|
||||
self._api_proxy = api_proxy;
|
||||
}
|
||||
|
||||
pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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<web::Json<PlaylistRequest>> for PlaylistRequest {
|
||||
fn from(req: web::Json<PlaylistRequest>) -> Self {
|
||||
PlaylistRequest {
|
||||
url: String::from(&req.url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub(crate) struct ServerConfig {
|
||||
pub sources: Vec<String>,
|
||||
}
|
||||
@@ -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<Vec<PlaylistGroup>> {
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod m3u_repository;
|
||||
pub(crate) mod xtream_repository;
|
||||
@@ -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<PlaylistGroup>) -> Result<(), std::io::Error> {
|
||||
pub(crate) fn xtream_save_playlist(target: &ConfigTarget, cfg: &Config, playlist: &mut Vec<PlaylistGroup>) -> 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::<i32>().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<PathBuf, Error> {
|
||||
pub(crate) fn xtream_get_all(cfg: &Config, target_name: &str, collection_name: &str) -> Result<PathBuf, Error> {
|
||||
if let Some(path) = get_storage_path(&cfg, target_name) {
|
||||
let col_path = get_collection_path(&path, collection_name);
|
||||
if col_path.exists() {
|
||||
+3
-3
@@ -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 {
|
||||
|
||||
+10
-10
@@ -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<PlaylistItem> { vec![] }
|
||||
@@ -44,11 +44,11 @@ fn deserialize_number_from_string<'de, D, T: DeserializeOwned>(
|
||||
}
|
||||
}
|
||||
|
||||
fn value_to_string_array(value : &Vec<Value>) -> Option<Vec<String>> {
|
||||
fn value_to_string_array(value: &Vec<Value>) -> Option<Vec<String>> {
|
||||
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<String> {
|
||||
fn value_to_string(v: &Value) -> Option<String> {
|
||||
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<Option<String
|
||||
|
||||
match &value {
|
||||
Value::String(s) => 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<String>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
rating: Option<f32>,
|
||||
rating: Option<f64>,
|
||||
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
||||
rating_5based: Option<f32>,
|
||||
rating_5based: Option<f64>,
|
||||
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
||||
release_date: Option<String>,
|
||||
#[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))))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<XtreamApiRequest>,
|
||||
req: HttpRequest,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user