some xtream refactorings

This commit is contained in:
euzu
2023-11-22 21:06:58 +01:00
parent fce81e2cb7
commit fd69a835b6
13 changed files with 403 additions and 54 deletions
+1
View File
@@ -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");
}
+4
View File
@@ -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)]
+9 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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)),
]
}
+2 -5
View File
@@ -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
View File
@@ -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) => {
+11
View File
@@ -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 {
+5 -5
View File
@@ -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),
}
+219 -16
View File
@@ -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)
}
-1
View File
@@ -12,5 +12,4 @@ mod tests {
Err(e) => {}
}
}
}
+2 -2
View File
@@ -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(