mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-03 06:22:15 +02:00
618 lines
27 KiB
Rust
618 lines
27 KiB
Rust
use crate::model::{AppConfig, ProxyUserCredentials};
|
|
use crate::model::{ConfigTarget, XtreamTargetOutput};
|
|
use serde::{Deserialize, Deserializer, Serialize};
|
|
use serde_json::{Map, Value};
|
|
use shared::model::{xtream_const, PlaylistItem, XtreamPlaylistItem};
|
|
use shared::model::{ClusterFlags, PlaylistEntry, XtreamCluster};
|
|
use shared::utils::{deserialize_as_option_string, deserialize_as_string, deserialize_as_string_array, deserialize_number_from_string,
|
|
get_non_empty_str, opt_string_or_number_u32, string_default_on_null, string_or_number_f64, string_or_number_u32};
|
|
use std::iter::FromIterator;
|
|
|
|
#[derive(Deserialize, Default)]
|
|
pub struct XtreamCategory {
|
|
#[serde(deserialize_with = "deserialize_as_string")]
|
|
pub category_id: String,
|
|
#[serde(deserialize_with = "deserialize_as_string")]
|
|
pub category_name: String,
|
|
//pub parent_id: i32,
|
|
#[serde(default)]
|
|
pub channels: Vec<PlaylistItem>,
|
|
}
|
|
|
|
impl XtreamCategory {
|
|
pub fn add(&mut self, item: PlaylistItem) {
|
|
self.channels.push(item);
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct XtreamStream {
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub name: String,
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub category_id: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub stream_id: Option<u32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub series_id: Option<u32>,
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub stream_icon: String,
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub direct_source: String,
|
|
|
|
// optional attributes
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub custom_sid: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_string_array")]
|
|
pub backdrop_path: Option<Vec<String>>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub added: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub cast: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub container_extension: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub cover: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub director: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub episode_run_time: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub genre: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub last_modified: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub plot: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub rating: Option<f64>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub rating_5based: Option<f64>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub release_date: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub stream_type: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub title: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub year: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub trailer: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub youtube_trailer: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub epg_channel_id: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tv_archive: Option<i32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tv_archive_duration: Option<i32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb_id: Option<u32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub is_adult: Option<i32>,
|
|
|
|
}
|
|
|
|
macro_rules! add_str_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$vec.insert(String::from($prop_name), Value::String($prop.to_string()));
|
|
}
|
|
}
|
|
macro_rules! add_rc_str_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$prop.as_ref().map(|v| $vec.insert(String::from($prop_name), Value::String(v.to_string())));
|
|
}
|
|
}
|
|
|
|
macro_rules! add_opt_i64_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$prop.as_ref().map(|v| $vec.insert(String::from($prop_name), Value::Number(serde_json::value::Number::from(i64::from(*v)))));
|
|
}
|
|
}
|
|
|
|
macro_rules! add_opt_f64_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$prop.as_ref().map(|v| $vec.insert(String::from($prop_name), Value::Number(serde_json::value::Number::from_f64(f64::from(*v)).unwrap_or_else(|| serde_json::Number::from(0)))));
|
|
}
|
|
}
|
|
|
|
macro_rules! add_f64_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$vec.insert(String::from($prop_name), Value::Number(serde_json::value::Number::from_f64(f64::from($prop)).unwrap_or_else(|| serde_json::Number::from(0))));
|
|
}
|
|
}
|
|
|
|
macro_rules! add_i64_property_if_exists {
|
|
($vec:expr, $prop:expr, $prop_name:expr) => {
|
|
$vec.insert(String::from($prop_name), Value::Number(serde_json::value::Number::from(i64::from($prop))));
|
|
}
|
|
}
|
|
|
|
macro_rules! add_to_doc_str_property_if_not_exists {
|
|
($document:expr, $prop_name:expr, $prop_value:expr) => {
|
|
match $document.get($prop_name) {
|
|
None => {
|
|
$document.insert(String::from($prop_name), $prop_value);
|
|
}
|
|
Some(value) => { if Value::is_null(value) {
|
|
$document.insert(String::from($prop_name), $prop_value);
|
|
}}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl XtreamStream {
|
|
pub fn get_stream_id(&self) -> u32 {
|
|
self.stream_id.unwrap_or_else(|| self.series_id.unwrap_or(0))
|
|
}
|
|
|
|
pub fn get_additional_properties(&self) -> Option<Value> {
|
|
let mut result = Map::new();
|
|
if let Some(bdpath) = self.backdrop_path.as_ref() {
|
|
if !bdpath.is_empty() {
|
|
result.insert(String::from(xtream_const::XC_PROP_BACKDROP_PATH), Value::Array(Vec::from([Value::String(String::from(bdpath.first()?))])));
|
|
}
|
|
}
|
|
|
|
add_rc_str_property_if_exists!(result, self.tmdb_id.or(self.tmdb), "tmdb");
|
|
add_rc_str_property_if_exists!(result, self.added, "added");
|
|
add_rc_str_property_if_exists!(result, self.cast, "cast");
|
|
add_rc_str_property_if_exists!(result, self.container_extension, "container_extension");
|
|
add_rc_str_property_if_exists!(result, self.cover, "cover");
|
|
add_rc_str_property_if_exists!(result, self.director, "director");
|
|
add_rc_str_property_if_exists!(result, self.episode_run_time, "episode_run_time");
|
|
add_rc_str_property_if_exists!(result, self.genre, "genre");
|
|
add_rc_str_property_if_exists!(result, self.last_modified, "last_modified");
|
|
add_rc_str_property_if_exists!(result, self.plot, "plot");
|
|
add_opt_f64_property_if_exists!(result, self.rating, "rating");
|
|
add_opt_f64_property_if_exists!(result, self.rating_5based, "rating_5based");
|
|
add_rc_str_property_if_exists!(result, self.release_date, "release_date");
|
|
add_rc_str_property_if_exists!(result, self.stream_type, "stream_type");
|
|
add_rc_str_property_if_exists!(result, self.title, "title");
|
|
add_rc_str_property_if_exists!(result, self.year, "year");
|
|
add_rc_str_property_if_exists!(result, self.trailer, "trailer");
|
|
add_rc_str_property_if_exists!(result, self.youtube_trailer, "youtube_trailer");
|
|
add_rc_str_property_if_exists!(result, self.epg_channel_id, "epg_channel_id");
|
|
add_opt_i64_property_if_exists!(result, self.tv_archive, "tv_archive");
|
|
add_opt_i64_property_if_exists!(result, self.tv_archive_duration, "tv_archive_duration");
|
|
add_opt_i64_property_if_exists!(result, self.is_adult, "is_adult");
|
|
if result.is_empty() { None } else { Some(Value::Object(result)) }
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XtreamSeriesInfoSeason {
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub air_date: String,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub episode_count: u32,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub id: u32,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub name: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub overview: String,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub season_number: u32,
|
|
#[serde(default, deserialize_with = "string_or_number_f64")]
|
|
pub vote_average: f64,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub cover: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub cover_big: String,
|
|
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[allow(non_snake_case)]
|
|
pub struct XtreamSeriesInfoInfo {
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub(crate) name: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
cover: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
plot: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
cast: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
director: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
genre: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
release_date: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
releaseDate: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
releasedate: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
last_modified: String,
|
|
#[serde(default, deserialize_with = "string_or_number_f64")]
|
|
rating: f64,
|
|
#[serde(default, deserialize_with = "string_or_number_f64")]
|
|
rating_5based: f64,
|
|
#[serde(default, deserialize_with = "deserialize_as_string_array")]
|
|
pub backdrop_path: Option<Vec<String>>,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
trailer: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
youtube_trailer: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
episode_run_time: String,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
category_id: u32,
|
|
#[serde(default, deserialize_with = "opt_string_or_number_u32")]
|
|
pub tmdb_id: Option<u32>,
|
|
#[serde(default, deserialize_with = "opt_string_or_number_u32")]
|
|
pub tmdb: Option<u32>,
|
|
}
|
|
|
|
#[allow(non_snake_case)]
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct XtreamSeriesInfoEpisodeInfo {
|
|
#[serde(default, deserialize_with = "opt_string_or_number_u32")]
|
|
pub tmdb_id: Option<u32>,
|
|
#[serde(default, deserialize_with = "opt_string_or_number_u32")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub release_date: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub releaseDate: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub releasedate: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub plot: String,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub duration_secs: u32,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub duration: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub movie_image: String,
|
|
#[serde(default)]
|
|
pub video: Value,
|
|
#[serde(default)]
|
|
pub audio: Value,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub bitrate: u32,
|
|
#[serde(default, deserialize_with = "string_or_number_f64")]
|
|
pub rating: f64,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub season: u32,
|
|
}
|
|
|
|
// Used for serde_json deserialization, can not be used with bincode
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XtreamSeriesInfoEpisode {
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub id: u32,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub episode_num: u32,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub title: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub container_extension: String,
|
|
#[serde(default)]
|
|
pub info: Option<XtreamSeriesInfoEpisodeInfo>,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub custom_sid: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub added: String,
|
|
#[serde(default, deserialize_with = "string_or_number_u32")]
|
|
pub season: u32,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub direct_source: String,
|
|
}
|
|
|
|
impl XtreamSeriesInfoEpisode {
|
|
pub fn get_id(&self) -> u32 {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
//bincode does not support deserialize_with. We use this struct for db
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XtreamSeriesEpisode {
|
|
pub id: u32,
|
|
pub episode_num: u32,
|
|
pub title: String,
|
|
pub container_extension: String,
|
|
pub custom_sid: String,
|
|
pub added: String,
|
|
pub season: u32,
|
|
pub tmdb_id: u32,
|
|
pub direct_source: String,
|
|
}
|
|
|
|
impl XtreamSeriesEpisode {
|
|
pub fn from(info_episode: &XtreamSeriesInfoEpisode) -> Self {
|
|
Self {
|
|
id: info_episode.get_id(),
|
|
episode_num: info_episode.episode_num,
|
|
title: info_episode.title.clone(),
|
|
container_extension: info_episode.container_extension.clone(),
|
|
custom_sid: info_episode.custom_sid.clone(),
|
|
added: info_episode.added.clone(),
|
|
season: info_episode.season,
|
|
tmdb_id: info_episode.info.as_ref().and_then(|info| info.tmdb_id.or(info.tmdb)).unwrap_or(0),
|
|
direct_source: info_episode.direct_source.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn deserialize_episodes<'de, D>(deserializer: D) -> Result<Option<Vec<XtreamSeriesInfoEpisode>>, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
// read as generic value
|
|
let value = Value::deserialize(deserializer)?;
|
|
match value {
|
|
Value::Null => Ok(None),
|
|
Value::Array(array) => {
|
|
if array.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
let mut result = Vec::new();
|
|
for inner in array {
|
|
if let Some(inner_array) = inner.as_array() {
|
|
for item in inner_array {
|
|
let ep: XtreamSeriesInfoEpisode = serde_json::from_value(item.clone())
|
|
.map_err(serde::de::Error::custom)?;
|
|
result.push(ep);
|
|
}
|
|
}
|
|
}
|
|
Ok(Some(result))
|
|
}
|
|
}
|
|
Value::Object(object) => {
|
|
if object.is_empty() {
|
|
Ok(None)
|
|
} else {
|
|
let mut result = Vec::new();
|
|
for (_key, val) in object {
|
|
if let Some(inner_array) = val.as_array() {
|
|
for item in inner_array {
|
|
let ep: XtreamSeriesInfoEpisode = serde_json::from_value(item.clone())
|
|
.map_err(serde::de::Error::custom)?;
|
|
result.push(ep);
|
|
}
|
|
}
|
|
}
|
|
Ok(Some(result))
|
|
}
|
|
}
|
|
_ => Err(serde::de::Error::custom("Invalid format for episodes")),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XtreamSeriesInfo {
|
|
#[serde(default)]
|
|
pub seasons: Option<Vec<XtreamSeriesInfoSeason>>,
|
|
#[serde(default)]
|
|
pub info: Option<XtreamSeriesInfoInfo>,
|
|
#[serde(default, deserialize_with = "deserialize_episodes")]
|
|
pub episodes: Option<Vec<XtreamSeriesInfoEpisode>>,
|
|
}
|
|
|
|
impl XtreamSeriesInfoEpisode {
|
|
pub fn get_additional_properties(&self, series_info: &XtreamSeriesInfo) -> Option<Value> {
|
|
let mut result = Map::new();
|
|
let info = series_info.info.as_ref();
|
|
let bdpath = info.and_then(|i| i.backdrop_path.as_ref());
|
|
let bdpath_is_set = bdpath.as_ref().is_some_and(|bdpath| !bdpath.is_empty());
|
|
if bdpath_is_set {
|
|
result.insert(String::from("backdrop_path"), Value::Array(Vec::from([Value::String(String::from(bdpath?.first()?))])));
|
|
}
|
|
add_str_property_if_exists!(result, info.map_or("", |i| i.name.as_str()), "series_name");
|
|
add_str_property_if_exists!(result, info.map_or("", |i| get_non_empty_str(i.release_date.as_str(), i.releaseDate.as_str(), i.releasedate.as_str())), "series_release_date");
|
|
add_str_property_if_exists!(result, self.added.as_str(), "added");
|
|
add_str_property_if_exists!(result, info.map_or("", |i| i.cast.as_str()), "cast");
|
|
add_str_property_if_exists!(result, self.container_extension.as_str(), "container_extension");
|
|
add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| info.movie_image.as_str()), "cover");
|
|
add_str_property_if_exists!(result, info.map_or("", |i| i.director.as_str()), "director");
|
|
add_str_property_if_exists!(result, info.map_or("", |i| i.episode_run_time.as_str()), "episode_run_time");
|
|
add_str_property_if_exists!(result, info.map_or("", |i| i.last_modified.as_str()), "last_modified");
|
|
add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| info.plot.as_str()), "plot");
|
|
add_f64_property_if_exists!(result, info.map_or(0_f64, |i| i.rating), "rating");
|
|
add_f64_property_if_exists!(result, info.map_or(0_f64, |i| i.rating_5based), "rating_5based");
|
|
add_str_property_if_exists!(result, self.info.as_ref().map_or("", |info| get_non_empty_str(info.release_date.as_str(), info.releaseDate.as_str(), info.releasedate.as_str())), "release_date");
|
|
add_str_property_if_exists!(result, self.title, "title");
|
|
add_i64_property_if_exists!(result, self.season, "season");
|
|
add_i64_property_if_exists!(result, self.episode_num, "episode");
|
|
let series_tmdb_id = info.and_then(|i| i.tmdb_id.or(i.tmdb));
|
|
add_opt_i64_property_if_exists!(result, self.info.as_ref().and_then(|info| info.tmdb_id.or(info.tmdb.or(series_tmdb_id))), "tmdb_id");
|
|
if result.is_empty() { None } else { Some(Value::Object(result)) }
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
pub struct XtreamMappingOptions {
|
|
pub skip_live_direct_source: bool,
|
|
pub skip_video_direct_source: bool,
|
|
pub skip_series_direct_source: bool,
|
|
pub rewrite_resource_url: bool,
|
|
pub force_redirect: Option<ClusterFlags>,
|
|
}
|
|
|
|
impl XtreamMappingOptions {
|
|
pub fn from_target_options(target: &ConfigTarget, target_output: &XtreamTargetOutput, cfg: &AppConfig) -> Self {
|
|
Self {
|
|
skip_live_direct_source: target_output.skip_live_direct_source,
|
|
skip_video_direct_source: target_output.skip_video_direct_source,
|
|
skip_series_direct_source: target_output.skip_series_direct_source,
|
|
rewrite_resource_url: cfg.is_reverse_proxy_resource_rewrite_enabled(),
|
|
force_redirect: target.options.as_ref().and_then(|o| o.force_redirect),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn normalize_release_date(document: &mut serde_json::Map<String, Value>) {
|
|
// Find the first non-empty release date key
|
|
let date_value = document.get("release_date")
|
|
.or_else(|| document.get("releaseDate"))
|
|
.or_else(|| document.get("releasedate"))
|
|
.filter(|v| v.as_str().is_some_and(|s| !s.is_empty()))
|
|
.cloned();
|
|
|
|
// Remove unused keys (optional)
|
|
document.remove("releaseDate");
|
|
document.remove("releasedate");
|
|
|
|
// Insert the normalized release date or null if not found
|
|
if let Some(date) = date_value {
|
|
document.insert("release_date".to_string(), date);
|
|
} else {
|
|
document.insert("release_date".to_string(), Value::Null);
|
|
}
|
|
}
|
|
|
|
fn append_mandatory_fields(document: &mut serde_json::Map<String, Value>, fields: &[&str]) {
|
|
for &field in fields {
|
|
if !document.contains_key(field) {
|
|
document.insert(field.to_string(), Value::Null);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn append_prepared_series_properties(add_props: Option<&Map<String, Value>>, document: &mut Map<String, Value>) {
|
|
if let Some(props) = add_props {
|
|
match props.get("rating") {
|
|
Some(value) => {
|
|
document.insert("rating".to_string(), match value {
|
|
Value::Number(val) => Value::String(format!("{:.0}", val.as_f64().unwrap_or(0f64))),
|
|
Value::String(val) => Value::String(val.clone()),
|
|
_ => Value::String("0".to_string()),
|
|
});
|
|
}
|
|
None => {
|
|
document.insert("rating".to_string(), Value::String("0".to_string()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn make_bdpath_resource_url(resource_url: &str, bd_path: &str, index: usize, field_prefix: &str) -> String {
|
|
if bd_path.starts_with("http") {
|
|
format!("{resource_url}/{field_prefix}{}_{index}", xtream_const::XC_PROP_BACKDROP_PATH)
|
|
} else {
|
|
bd_path.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, options: &XtreamMappingOptions, user: &ProxyUserCredentials) -> serde_json::Value {
|
|
let stream_id_value = Value::Number(serde_json::Number::from(pli.virtual_id));
|
|
|
|
let is_reverse = user.proxy.is_reverse(pli.item_type) && !options.force_redirect.as_ref().is_some_and(|o| o.has_cluster(pli.item_type));
|
|
let (resource_url, logo, logo_small) = if is_reverse && options.rewrite_resource_url {
|
|
let resource_url = format!("{url}/resource/{}/{}/{}/{}", pli.xtream_cluster.as_stream_type(), user.username, user.password, pli.get_virtual_id());
|
|
let logo_url = if pli.logo.is_empty() { String::new() } else { format!("{resource_url}/logo") };
|
|
let logo_small_url = if pli.logo_small.is_empty() { String::new() } else { format!("{resource_url}/logo_small") };
|
|
(Some(resource_url), logo_url, logo_small_url)
|
|
} else {
|
|
(None, pli.logo.clone(), pli.logo_small.clone())
|
|
};
|
|
let mut document = serde_json::Map::from_iter([
|
|
("category_id".to_string(), Value::String(format!("{}", &pli.category_id))),
|
|
("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(pli.category_id))]))),
|
|
("name".to_string(), Value::String(pli.name.clone())),
|
|
("num".to_string(), Value::Number(serde_json::Number::from(pli.channel_no))),
|
|
("title".to_string(), Value::String(pli.title.clone())),
|
|
("stream_icon".to_string(), Value::String(logo)),
|
|
]);
|
|
|
|
match pli.xtream_cluster {
|
|
XtreamCluster::Live => {
|
|
document.insert("stream_id".to_string(), stream_id_value);
|
|
if options.skip_live_direct_source {
|
|
document.insert("direct_source".to_string(), Value::String(String::new()));
|
|
} else {
|
|
document.insert("direct_source".to_string(), Value::String(pli.url.clone()));
|
|
}
|
|
document.insert("thumbnail".to_string(), Value::String(logo_small));
|
|
document.insert("custom_sid".to_string(), Value::String(String::new()));
|
|
document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.clone())));
|
|
}
|
|
XtreamCluster::Video => {
|
|
document.insert("stream_id".to_string(), stream_id_value);
|
|
if options.skip_video_direct_source {
|
|
document.insert("direct_source".to_string(), Value::String(String::new()));
|
|
} else {
|
|
document.insert("direct_source".to_string(), Value::String(pli.url.clone()));
|
|
}
|
|
document.insert("custom_sid".to_string(), Value::String(String::new()));
|
|
}
|
|
XtreamCluster::Series => {
|
|
document.insert("series_id".to_string(), stream_id_value);
|
|
}
|
|
}
|
|
|
|
let props = pli.additional_properties.as_ref().and_then(|add_props| serde_json::from_str::<Map<String, Value>>(add_props).ok());
|
|
|
|
if let Some(ref add_props) = props {
|
|
for (field_name, field_value) in add_props {
|
|
if !document.contains_key(field_name) {
|
|
document.insert(field_name.clone(), field_value.to_owned());
|
|
}
|
|
}
|
|
}
|
|
|
|
match pli.xtream_cluster {
|
|
XtreamCluster::Live => {
|
|
append_mandatory_fields(&mut document, xtream_const::LIVE_STREAM_FIELDS);
|
|
add_to_doc_str_property_if_not_exists!(document, "stream_type", Value::String(String::from("live")));
|
|
add_to_doc_str_property_if_not_exists!(document, "added", Value::String(chrono::Utc::now().timestamp().to_string()));
|
|
}
|
|
XtreamCluster::Video => {
|
|
append_mandatory_fields(&mut document, xtream_const::VIDEO_STREAM_FIELDS);
|
|
add_to_doc_str_property_if_not_exists!(document, "stream_type", Value::String(String::from("movie")));
|
|
add_to_doc_str_property_if_not_exists!(document, "added", Value::String(chrono::Utc::now().timestamp().to_string()));
|
|
}
|
|
XtreamCluster::Series => {
|
|
append_prepared_series_properties(props.as_ref(), &mut document);
|
|
append_mandatory_fields(&mut document, xtream_const::SERIES_STREAM_FIELDS);
|
|
}
|
|
}
|
|
|
|
rewrite_doc_urls(resource_url.as_ref(), &mut document, xtream_const::XTREAM_VOD_REWRITE_URL_PROPS, "");
|
|
|
|
Value::Object(document)
|
|
}
|
|
|
|
pub fn rewrite_doc_urls(resource_url: Option<&String>, document: &mut Map<String, Value>, fields: &[&str], field_prefix: &str) {
|
|
if let Some(rewrite_url) = resource_url {
|
|
if let Some(bdpath) = document.get(xtream_const::XC_PROP_BACKDROP_PATH) {
|
|
match bdpath {
|
|
Value::String(bd_path) => {
|
|
document.insert(xtream_const::XC_PROP_BACKDROP_PATH.to_string(), Value::Array(vec![Value::String(make_bdpath_resource_url(rewrite_url.as_str(), bd_path, 0, field_prefix))]));
|
|
}
|
|
Value::Array(bd_path) => {
|
|
document.insert(xtream_const::XC_PROP_BACKDROP_PATH.to_string(), Value::Array(
|
|
bd_path.iter()
|
|
.filter_map(|val| val.as_str())
|
|
.enumerate()
|
|
.map(|(index, value)| Value::String(make_bdpath_resource_url(rewrite_url.as_str(), value, index, field_prefix)))
|
|
.collect()));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
for &field in fields {
|
|
if let Some(Value::String(value)) = document.get(field) {
|
|
if value.starts_with("http") {
|
|
document.insert(field.to_string(), Value::String(format!("{rewrite_url}/{field_prefix}{field}")));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
pub struct PlaylistXtreamCategory {
|
|
#[serde(alias = "category_id")]
|
|
pub id: String,
|
|
#[serde(alias = "category_name")]
|
|
pub name: String,
|
|
} |