mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-30 13:02:10 +02:00
some xtream refactorings
This commit is contained in:
@@ -59,6 +59,7 @@ sources:
|
||||
sort:
|
||||
groups:
|
||||
order: asc
|
||||
|
||||
mapping:
|
||||
- France TNT
|
||||
- France Premium
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<UserApiRequest>, app_state: &'a web::Data<AppState>) -> 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<UserApiRequest>, app_state: &'a web::Data<AppState>) -> 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<UserApiRequest>, app_s
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn get_user_target<'a>(api_req: &'a web::Query<UserApiRequest>, app_state: &'a web::Data<AppState>) -> 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)
|
||||
}
|
||||
+4
-3
@@ -32,8 +32,9 @@ pub(crate) async fn start_server(cfg: Arc<Config>, targets: Arc<ProcessTargets>)
|
||||
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<Config>, targets: Arc<ProcessTargets>)
|
||||
.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
|
||||
|
||||
+116
-17
@@ -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<UserApiRequest>,
|
||||
_app_state: &web::Data<AppState>,
|
||||
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<UserApiRequest>,
|
||||
path: web::Path<(String, String, String)>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> 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<UserApiRequest>,
|
||||
path: web::Path<(String, String, String)>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> 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<UserApiRequest>,
|
||||
path: web::Path<(String, String, String)>,
|
||||
_app_state: web::Data<AppState>,
|
||||
) -> 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<UserApiRequest>,
|
||||
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<Resource> {
|
||||
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)),
|
||||
]
|
||||
}
|
||||
@@ -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(())
|
||||
|
||||
+25
-1
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<usize>) -> 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),
|
||||
}
|
||||
|
||||
@@ -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<T>(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::<i32, (u32, u16)>::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<u8> = 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<PathBuf> {
|
||||
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::<i32, String>::new();
|
||||
let mut series_map = HashMap::<i32, String>::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::<i32>().unwrap()));
|
||||
let stream_id = header.id.parse::<i32>().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<String, Value>, fields: &[&str]) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn xtream_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<(Option<PathBuf>, Option<String>), 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<BTreeMap<i32, (u32, u16)>> {
|
||||
match std::fs::read(path) {
|
||||
Ok(encoded) => {
|
||||
let decoded: BTreeMap<i32, (u32, u16)> = bincode::deserialize(&encoded[..]).unwrap();
|
||||
Some(decoded)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn seek_read(
|
||||
reader: &mut (impl Read + Seek),
|
||||
offset: u32,
|
||||
amount_to_read: u16,
|
||||
) -> Result<Vec<u8>, 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<String, Error> {
|
||||
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<String, Error> {
|
||||
/*
|
||||
{
|
||||
"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<String, Error> {
|
||||
/*
|
||||
{
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -12,5 +12,4 @@ mod tests {
|
||||
Err(e) => {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -196,7 +196,7 @@ pub(crate) fn get_file_path(wd: &String, path: Option<PathBuf>) -> Option<PathBu
|
||||
}
|
||||
|
||||
|
||||
fn get_client_request(input: &ConfigInput, url: url::Url) -> 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<String, String>) -> HeaderMap {
|
||||
pub(crate) fn get_request_headers(defined_headers: &HashMap<String, String>) -> HeaderMap {
|
||||
let mut headers = header::HeaderMap::new();
|
||||
for (key, value) in defined_headers {
|
||||
headers.insert(
|
||||
|
||||
Reference in New Issue
Block a user