mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 14:52:22 +02:00
811 lines
39 KiB
Rust
811 lines
39 KiB
Rust
use crate::model::{ PlaylistEntry, PlaylistItemType, VirtualId, XtreamCluster, XtreamMappingOptions, XtreamSeriesInfo, XtreamVideoInfo};
|
|
use crate::utils::{deserialize_as_option_string, deserialize_number_from_string,
|
|
deserialize_number_from_string_or_zero, string_default_on_null,
|
|
deserialize_as_string, deserialize_as_string_array, deserialize_json_as_string,
|
|
string_or_number_u32};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::borrow::Cow;
|
|
use std::collections::HashMap;
|
|
use serde_json::Value;
|
|
use crate::model::info_doc_utils::InfoDocUtils;
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct LiveStreamProperties {
|
|
#[serde(default)]
|
|
pub name: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub category_id: u32,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub stream_id: u32,
|
|
#[serde(default)]
|
|
pub stream_icon: String,
|
|
#[serde(default)]
|
|
pub direct_source: String,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub custom_sid: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub added: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub stream_type: 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_or_zero")]
|
|
pub is_adult: i32,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct VideoStreamDetailProperties {
|
|
pub kinopoisk_url: Option<String>,
|
|
pub o_name: Option<String>,
|
|
pub cover_big: Option<String>,
|
|
pub movie_image: Option<String>,
|
|
pub release_date: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub episode_run_time: Option<u32>,
|
|
pub youtube_trailer: Option<String>,
|
|
pub director: Option<String>,
|
|
pub actors: Option<String>,
|
|
pub cast: Option<String>,
|
|
pub description: Option<String>,
|
|
pub plot: Option<String>,
|
|
pub age: Option<String>,
|
|
pub mpaa_rating: Option<String>,
|
|
#[serde(default)]
|
|
pub rating_count_kinopoisk: u32,
|
|
pub country: Option<String>,
|
|
pub genre: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_string_array")]
|
|
pub backdrop_path: Option<Vec<String>>,
|
|
pub duration_secs: Option<String>,
|
|
pub duration: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub video: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub audio: Option<String>,
|
|
#[serde(default)]
|
|
pub bitrate: u32,
|
|
pub runtime: Option<String>,
|
|
pub status: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct VideoStreamProperties {
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub name: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub category_id: u32,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub stream_id: u32,
|
|
#[serde(default)]
|
|
pub stream_icon: String,
|
|
#[serde(default)]
|
|
pub direct_source: String,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub custom_sid: Option<String>,
|
|
#[serde(default)]
|
|
pub added: String,
|
|
#[serde(default)]
|
|
pub container_extension: 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)]
|
|
pub stream_type: Option<String>,
|
|
#[serde(default)]
|
|
pub trailer: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub is_adult: i32,
|
|
#[serde(default)]
|
|
pub details: Option<VideoStreamDetailProperties>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct SeriesStreamDetailEpisodeProperties {
|
|
#[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_or_number_u32")]
|
|
pub season: 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 custom_sid: Option<String>,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub added: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub direct_source: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub release_date: String,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub plot: Option<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, deserialize_with = "string_or_number_u32")]
|
|
pub bitrate: u32,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub rating: Option<f64>,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub video: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub audio: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct SeriesStreamDetailProperties {
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub year: Option<u32>,
|
|
pub episodes: Option<Vec<SeriesStreamDetailEpisodeProperties>>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct SeriesStreamProperties {
|
|
#[serde(default, deserialize_with = "deserialize_as_string")]
|
|
pub name: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub category_id: u32,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub series_id: u32,
|
|
#[serde(default, deserialize_with = "deserialize_as_string_array")]
|
|
pub backdrop_path: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
pub cast: String,
|
|
#[serde(default)]
|
|
pub cover: String,
|
|
#[serde(default)]
|
|
pub director: 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_or_zero")]
|
|
pub rating: f64,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string_or_zero")]
|
|
pub rating_5based: f64,
|
|
pub release_date: Option<String>,
|
|
#[serde(default)]
|
|
pub youtube_trailer: String,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default)]
|
|
pub details: Option<SeriesStreamDetailProperties>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub struct EpisodeStreamProperties {
|
|
pub episode_id: u32,
|
|
pub episode: u32,
|
|
pub season: u32,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub added: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_as_option_string")]
|
|
pub release_date: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_number_from_string")]
|
|
pub tmdb: Option<u32>,
|
|
#[serde(default)]
|
|
pub movie_image: String,
|
|
#[serde(default, deserialize_with = "string_default_on_null")]
|
|
pub container_extension: String,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub video: Option<String>,
|
|
#[serde(default, deserialize_with = "deserialize_json_as_string")]
|
|
pub audio: Option<String>,
|
|
}
|
|
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
|
pub enum StreamProperties {
|
|
Live(LiveStreamProperties),
|
|
Video(Box<VideoStreamProperties>),
|
|
Series(Box<SeriesStreamProperties>),
|
|
Episode(EpisodeStreamProperties),
|
|
}
|
|
|
|
impl StreamProperties {
|
|
pub fn has_details(&self) -> bool {
|
|
match self {
|
|
StreamProperties::Video(video) => video.details.is_some(),
|
|
StreamProperties::Series(series) => series.details.is_some(),
|
|
StreamProperties::Live(_)
|
|
| StreamProperties::Episode(_) => false,
|
|
}
|
|
}
|
|
|
|
pub fn prepare(&mut self) {
|
|
match self {
|
|
StreamProperties::Live(live) => {
|
|
live.epg_channel_id = live.epg_channel_id.as_ref()
|
|
.filter(|epg_id| !epg_id.trim().is_empty())
|
|
.map(|epg_id| epg_id.to_lowercase())
|
|
.or(None);
|
|
}
|
|
StreamProperties::Video(_) => {}
|
|
StreamProperties::Series(_) => {}
|
|
StreamProperties::Episode(_) => {}
|
|
}
|
|
}
|
|
|
|
pub fn get_category_id(&self) -> u32 {
|
|
match self {
|
|
StreamProperties::Live(live) => live.category_id,
|
|
StreamProperties::Video(video) => video.category_id,
|
|
StreamProperties::Series(series) => series.category_id,
|
|
StreamProperties::Episode(_episode) => 0,
|
|
}
|
|
}
|
|
|
|
pub fn get_stream_id(&self) -> u32 {
|
|
match self {
|
|
StreamProperties::Live(live) => live.stream_id,
|
|
StreamProperties::Video(video) => video.stream_id,
|
|
StreamProperties::Series(series) => series.series_id,
|
|
StreamProperties::Episode(episode) => episode.episode_id,
|
|
}
|
|
}
|
|
|
|
pub fn get_stream_icon(&self) -> Cow<'_, str> {
|
|
match self {
|
|
StreamProperties::Live(live) => Cow::Borrowed(&live.stream_icon),
|
|
StreamProperties::Video(video) => Cow::Borrowed(&video.stream_icon),
|
|
StreamProperties::Series(series) => Cow::Borrowed(&series.cover),
|
|
StreamProperties::Episode(episode) => Cow::Borrowed(&episode.movie_image),
|
|
}
|
|
}
|
|
|
|
pub fn get_name(&self) -> Cow<'_, str> {
|
|
match self {
|
|
StreamProperties::Live(live) => Cow::Borrowed(&live.name),
|
|
StreamProperties::Video(video) => Cow::Borrowed(&video.name),
|
|
StreamProperties::Series(series) => Cow::Borrowed(&series.name),
|
|
StreamProperties::Episode(_episode) => Cow::Borrowed(""),
|
|
}
|
|
}
|
|
|
|
pub fn get_epg_channel_id(&self) -> Option<String> {
|
|
match self {
|
|
StreamProperties::Live(live) => live.epg_channel_id.clone(),
|
|
StreamProperties::Video(_) => None,
|
|
StreamProperties::Series(_) => None,
|
|
StreamProperties::Episode(_) => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_tmdb_id(&self) -> Option<u32> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => video.tmdb,
|
|
StreamProperties::Series(series) => series.tmdb,
|
|
StreamProperties::Episode(episode) => episode.tmdb,
|
|
}
|
|
}
|
|
|
|
pub fn get_release_date(&self) -> Option<String> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => video.details.as_ref().and_then(|d| d.release_date.clone()),
|
|
StreamProperties::Series(series) => series.release_date.clone(),
|
|
StreamProperties::Episode(episode) => episode.release_date.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn get_added(&self) -> Option<String> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => Some(video.added.clone()),
|
|
StreamProperties::Series(series) => series.details.as_ref()
|
|
.and_then(|d| d.episodes.as_ref())
|
|
.and_then(|e| e.first().map(|e| e.added.clone())),
|
|
StreamProperties::Episode(episode) => episode.added.clone(),
|
|
}
|
|
}
|
|
|
|
pub fn get_container_extension(&self) -> Option<String> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => if video.container_extension.is_empty() { None } else { Some(video.container_extension.clone()) },
|
|
StreamProperties::Series(series) => series.details.as_ref()
|
|
.and_then(|d| d.episodes.as_ref())
|
|
.and_then(|e| e.first().and_then(|e| {
|
|
if e.container_extension.is_empty() { None } else { Some(e.container_extension.clone()) }
|
|
})),
|
|
StreamProperties::Episode(episode) => if episode.container_extension.is_empty() { None } else { Some(episode.container_extension.clone()) },
|
|
}
|
|
}
|
|
|
|
pub fn get_direct_source(&self) -> Option<String> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => if video.direct_source.is_empty() { None } else { Some(video.direct_source.clone()) },
|
|
StreamProperties::Series(series) => series.details.as_ref()
|
|
.and_then(|d| d.episodes.as_ref())
|
|
.and_then(|e| e.first().and_then(|e| {
|
|
if e.direct_source.is_empty() { None } else { Some(e.direct_source.clone()) }
|
|
})),
|
|
StreamProperties::Episode(_episode) => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_season(&self) -> Option<u32> {
|
|
match self {
|
|
StreamProperties::Live(_)
|
|
| StreamProperties::Video(_)
|
|
| StreamProperties::Series(_) => None,
|
|
StreamProperties::Episode(episode) => Some(episode.season),
|
|
}
|
|
}
|
|
|
|
pub fn get_episode(&self) -> Option<u32> {
|
|
match self {
|
|
StreamProperties::Live(_)
|
|
| StreamProperties::Video(_)
|
|
| StreamProperties::Series(_) => None,
|
|
StreamProperties::Episode(episode) => Some(episode.episode),
|
|
}
|
|
}
|
|
|
|
pub fn get_last_modified(&self) -> Option<u64> {
|
|
match self {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => video.added.parse::<u64>().ok(),
|
|
StreamProperties::Series(series) => series.last_modified.as_ref().and_then(|v| v.parse::<u64>().ok()),
|
|
StreamProperties::Episode(episode) => episode.added.as_ref().and_then(|v| v.parse::<u64>().ok()),
|
|
}
|
|
}
|
|
|
|
pub fn resolve_resource_url<'a>(&'a self, field: &str) -> Option<Cow<'a, str>> {
|
|
if field.starts_with("backdrop_path") {
|
|
if let StreamProperties::Series(series) = self {
|
|
if let Some(backdrop) = series.backdrop_path.as_ref() {
|
|
if let Some(url) = backdrop.first() {
|
|
return Some(Cow::Borrowed(url));
|
|
}
|
|
}
|
|
}
|
|
return None;
|
|
} else if field.starts_with("nfo_backdrop_path") {
|
|
if let StreamProperties::Video(video) = self {
|
|
if let Some(details) = video.details.as_ref() {
|
|
if let Some(backdrop) = details.backdrop_path.as_ref() {
|
|
if let Some(url) = backdrop.first() {
|
|
return Some(Cow::Borrowed(url));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
|
|
if field == "cover" {
|
|
if let StreamProperties::Series(series) = self {
|
|
return Some(Cow::Borrowed(series.cover.as_str()));
|
|
}
|
|
return None;
|
|
}
|
|
if field == "logo" || field == "logo_small" {
|
|
return match self {
|
|
StreamProperties::Live(live) => {
|
|
Some(Cow::Borrowed(live.stream_icon.as_str()))
|
|
}
|
|
StreamProperties::Video(video) => {
|
|
Some(Cow::Borrowed(video.stream_icon.as_str()))
|
|
}
|
|
StreamProperties::Series(series) => {
|
|
Some(Cow::Borrowed(series.cover.as_str()))
|
|
}
|
|
StreamProperties::Episode(episode) => {
|
|
Some(Cow::Borrowed(episode.movie_image.as_str()))
|
|
}
|
|
}
|
|
}
|
|
if field == "movie_image" {
|
|
if let StreamProperties::Episode(episode) = self {
|
|
return Some(Cow::Borrowed(episode.movie_image.as_str()))
|
|
}
|
|
return None;
|
|
}
|
|
if field == "nfo_cover_big" {
|
|
if let StreamProperties::Video(video) = self {
|
|
if let Some(details) = video.details.as_ref() {
|
|
if let Some(cover_big) = details.cover_big.as_ref() {
|
|
return Some(Cow::Borrowed(cover_big.as_str()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if field == "nfo_movie_image" {
|
|
if let StreamProperties::Video(video) = self {
|
|
if let Some(details) = video.details.as_ref() {
|
|
if let Some(movie_image) = details.movie_image.as_ref() {
|
|
return Some(Cow::Borrowed(movie_image.as_str()));
|
|
}
|
|
}
|
|
}
|
|
return None;
|
|
}
|
|
if field.starts_with("nfo_ep_") {
|
|
if let Some((season, episode_num, field)) = parse_season_episode_field(field) {
|
|
if let StreamProperties::Series(series) = self {
|
|
if let Some(details) = series.details.as_ref() {
|
|
if let Some(episodes) = details.episodes.as_ref() {
|
|
for episode in episodes {
|
|
if episode.season == season && episode_num == episode.episode_num && field == "movie_image" {
|
|
return Some(Cow::Borrowed(episode.movie_image.as_str()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn to_info_document(&self, options: &XtreamMappingOptions, item_type: PlaylistItemType,
|
|
virtual_id: VirtualId, category_id: u32) -> Value {
|
|
match self {
|
|
StreamProperties::Live(_live) => {
|
|
Value::Object(serde_json::Map::new())
|
|
}
|
|
StreamProperties::Video(video) => {
|
|
self.video_to_info_document(options, video, item_type, virtual_id, category_id)
|
|
}
|
|
StreamProperties::Series(series) => {
|
|
self.series_to_info_document(options, series, item_type, virtual_id, category_id)
|
|
}
|
|
StreamProperties::Episode(_episode) => {
|
|
Value::Object(serde_json::Map::new())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn series_to_info_document(&self, options: &XtreamMappingOptions, series: &SeriesStreamProperties,
|
|
item_type: PlaylistItemType, virtual_id: VirtualId, category_id: u32) -> Value {
|
|
let resource_url = options.get_resource_url(XtreamCluster::Series, item_type, virtual_id);
|
|
Value::Object(serde_json::Map::from_iter([
|
|
("seasons".to_string(), Value::Array(Vec::new())),
|
|
("info".to_string(), Value::Object(serde_json::Map::from_iter([
|
|
("name".to_string(), Value::String(series.name.clone())),
|
|
("cover".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), series.cover.as_ref(), "cover"))),
|
|
("plot".to_string(), InfoDocUtils::as_string(series.plot.as_ref())),
|
|
("cast".to_string(), Value::String(series.cast.clone())),
|
|
("director".to_string(), Value::String(series.director.clone())),
|
|
("genre".to_string(), InfoDocUtils::as_string(series.genre.as_ref())),
|
|
("release_date".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())),
|
|
("releaseDate".to_string(), InfoDocUtils::as_string(series.release_date.as_ref())),
|
|
("last_modified".to_string(), InfoDocUtils::as_string(series.last_modified.as_ref())),
|
|
("rating".to_string(), Value::String(InfoDocUtils::limited(series.rating))),
|
|
("rating_5based".to_string(), Value::String(InfoDocUtils::limited(series.rating_5based))),
|
|
("backdrop_path".to_string(), Value::Array(
|
|
series.backdrop_path.as_ref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)|
|
|
Value::String(
|
|
InfoDocUtils::make_bdpath_resource_url(resource_url.as_ref(), p, idx, "")
|
|
)
|
|
).collect())
|
|
)),
|
|
("tmdb".to_string(), Value::String(series.tmdb.unwrap_or_default().to_string())),
|
|
("youtube_trailer".to_string(), Value::String(series.youtube_trailer.clone())),
|
|
("episode_run_time".to_string(), InfoDocUtils::as_string(series.episode_run_time.as_ref())),
|
|
("category_id".to_string(), Value::String(category_id.to_string())),
|
|
("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::as_u32(category_id)]))),
|
|
]))),
|
|
if let Some(episodes) = series.details.as_ref().and_then(|d| d.episodes.as_ref()) {
|
|
("episodes".to_string(), self.series_episodes_to_info_document(options, resource_url.as_ref(), episodes))
|
|
} else {
|
|
("episodes".to_string(), Value::Object(serde_json::Map::new()))
|
|
}
|
|
]))
|
|
}
|
|
|
|
fn video_to_info_document(&self, options: &XtreamMappingOptions, video: &VideoStreamProperties,
|
|
item_type: PlaylistItemType, virtual_id: VirtualId, category_id: u32) -> Value {
|
|
let resource_url = options.get_resource_url(XtreamCluster::Video, item_type, virtual_id);
|
|
let stream_icon = InfoDocUtils::make_resource_url(resource_url.as_ref(), &self.get_stream_icon(), "logo");
|
|
|
|
Value::Object(serde_json::Map::from_iter([
|
|
if let Some(details) = video.details.as_ref() {
|
|
("info".to_string(), Value::Object(serde_json::Map::from_iter([
|
|
("actors".to_string(), InfoDocUtils::as_string(details.actors.as_ref())),
|
|
("age".to_string(), InfoDocUtils::as_string(details.age.as_ref())),
|
|
("audio".to_string(), InfoDocUtils::as_value(details.audio.as_ref())),
|
|
("video".to_string(), InfoDocUtils::as_value(details.video.as_ref())),
|
|
("backdrop_path".to_string(), Value::Array(
|
|
details.backdrop_path.as_ref().map_or_else(Vec::new, |b| b.iter().enumerate().map(|(idx, p)|
|
|
Value::String(
|
|
InfoDocUtils::make_bdpath_resource_url(resource_url.as_ref(), p, idx, "nfo_")
|
|
)
|
|
).collect())
|
|
)),
|
|
("bitrate".to_string(), InfoDocUtils::as_u32(details.bitrate)),
|
|
("cast".to_string(), InfoDocUtils::as_string(details.cast.as_ref())),
|
|
("country".to_string(), InfoDocUtils::as_string(details.country.as_ref())),
|
|
("cover_big".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_cover_big"))),
|
|
("description".to_string(), InfoDocUtils::as_string(details.description.as_ref())),
|
|
("director".to_string(), InfoDocUtils::as_string(details.director.as_ref())),
|
|
("duration".to_string(), InfoDocUtils::as_string(details.duration.as_ref())),
|
|
("duration_secs".to_string(), InfoDocUtils::as_string(details.duration_secs.as_ref())),
|
|
("episode_run_time".to_string(), InfoDocUtils::as_u32(details.episode_run_time.unwrap_or_default())),
|
|
("genre".to_string(), InfoDocUtils::as_string(details.genre.as_ref())),
|
|
("kinopoisk_url".to_string(), InfoDocUtils::as_string(details.kinopoisk_url.as_ref())),
|
|
("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url.as_ref(), &details.cover_big.as_ref().map_or_else(String::new, Clone::clone), "nfo_movie_image"))),
|
|
("mpaa_rating".to_string(), InfoDocUtils::as_string(details.mpaa_rating.as_ref())),
|
|
("name".to_string(), Value::String(video.name.clone())),
|
|
("o_name".to_string(), InfoDocUtils::as_string(details.o_name.as_ref())),
|
|
("plot".to_string(), InfoDocUtils::as_string(details.plot.as_ref())),
|
|
("rating".to_string(), Value::String(InfoDocUtils::limited(video.rating.unwrap_or_default()))),
|
|
("rating_count_kinopoisk".to_string(), InfoDocUtils::as_u32(details.rating_count_kinopoisk)),
|
|
("release_date".to_string(), InfoDocUtils::as_string(details.release_date.as_ref())),
|
|
("runtime".to_string(), InfoDocUtils::as_string(details.runtime.as_ref())),
|
|
("status".to_string(), InfoDocUtils::as_string(details.status.as_ref())),
|
|
("tmdb_id".to_string(), Value::String(video.tmdb.unwrap_or_default().to_string())),
|
|
("youtube_trailer".to_string(), InfoDocUtils::as_string(details.youtube_trailer.as_ref())),
|
|
])))
|
|
} else {
|
|
("info".to_string(), Value::Object(serde_json::Map::from_iter([
|
|
("actors".to_string(), InfoDocUtils::empty_string()),
|
|
("age".to_string(), InfoDocUtils::empty_string()),
|
|
("audio".to_string(), InfoDocUtils::as_value(None)),
|
|
("video".to_string(), InfoDocUtils::as_value(None)),
|
|
("backdrop_path".to_string(), Value::Array(Vec::from_iter([
|
|
Value::String(stream_icon.clone()),
|
|
]))),
|
|
("bitrate".to_string(), InfoDocUtils::as_u32(0)),
|
|
("cast".to_string(), InfoDocUtils::empty_string()),
|
|
("country".to_string(), InfoDocUtils::empty_string()),
|
|
("cover_big".to_string(), Value::String(stream_icon.clone())),
|
|
("description".to_string(), InfoDocUtils::empty_string()),
|
|
("director".to_string(), InfoDocUtils::empty_string()),
|
|
("duration".to_string(), Value::String("0".to_string())),
|
|
("duration_secs".to_string(), Value::String("0".to_string())),
|
|
("episode_run_time".to_string(), InfoDocUtils::as_u32(0)),
|
|
("genre".to_string(), InfoDocUtils::empty_string()),
|
|
("kinopoisk_url".to_string(), InfoDocUtils::empty_string()),
|
|
("movie_image".to_string(), Value::String(stream_icon.clone())),
|
|
("mpaa_rating".to_string(), InfoDocUtils::empty_string()),
|
|
("name".to_string(), Value::String(video.name.clone())),
|
|
("o_name".to_string(), Value::String(video.name.clone())),
|
|
("plot".to_string(), InfoDocUtils::empty_string()),
|
|
("rating".to_string(), Value::String(InfoDocUtils::limited(video.rating.unwrap_or_default()))),
|
|
("rating_count_kinopoisk".to_string(), InfoDocUtils::as_u32(0)),
|
|
("release_date".to_string(), InfoDocUtils::empty_string()),
|
|
("runtime".to_string(), Value::String("0".to_string())),
|
|
("status".to_string(), Value::String("Released".to_string())),
|
|
("tmdb_id".to_string(), Value::String(video.tmdb.unwrap_or_default().to_string())),
|
|
("youtube_trailer".to_string(), InfoDocUtils::empty_string()),
|
|
])))
|
|
},
|
|
("movie_data".to_string(), Value::Object(serde_json::Map::from_iter([
|
|
("added".to_string(), Value::String(video.added.clone())),
|
|
("category_id".to_string(), Value::String(category_id.to_string())),
|
|
("category_ids".to_string(), Value::Array(Vec::from([InfoDocUtils::as_u32(category_id)]))),
|
|
("container_extension".to_string(), Value::String(video.container_extension.clone())),
|
|
("custom_sid".to_string(), video.custom_sid.as_ref().map_or(Value::Null, |s| Value::String(s.clone()))),
|
|
("direct_source".to_string(), if options.skip_video_direct_source { InfoDocUtils::empty_string() } else { Value::String(video.direct_source.clone()) }),
|
|
("name".to_string(), Value::String(video.name.clone())),
|
|
("stream_id".to_string(), Value::Number(serde_json::Number::from(virtual_id))),
|
|
])))
|
|
]))
|
|
}
|
|
|
|
fn series_episodes_to_info_document(&self, options: &XtreamMappingOptions,
|
|
resource_url: Option<&String>,
|
|
episodes: &Vec<SeriesStreamDetailEpisodeProperties>) -> Value {
|
|
let mut map: HashMap<u32, Vec<Value>> = HashMap::new();
|
|
for ep in episodes {
|
|
let value = Value::Object(serde_json::Map::from_iter([
|
|
("id".to_string(), InfoDocUtils::as_u32(ep.id)),
|
|
("episode_num".to_string(), InfoDocUtils::as_u32(ep.episode_num)),
|
|
("title".to_string(), Value::String(ep.title.clone())),
|
|
("container_extension".to_string(), Value::String(ep.container_extension.clone())),
|
|
("info".to_string(), Value::Object(serde_json::Map::from_iter([
|
|
("id".to_string(), InfoDocUtils::as_u32(ep.tmdb.unwrap_or_default())),
|
|
("air_date".to_string(), Value::String(ep.release_date.clone())),
|
|
("rating".to_string(), Value::Number(serde_json::Number::from_f64(ep.rating.unwrap_or_default()).unwrap_or_else(|| serde_json::Number::from(0)))),
|
|
("movie_image".to_string(), Value::String(InfoDocUtils::make_resource_url(resource_url, &ep.movie_image, "nfo_movie_image"))),
|
|
("duration".to_string(), Value::String(ep.duration.clone())),
|
|
("duration_secs".to_string(), InfoDocUtils::as_u32(ep.duration_secs)),
|
|
("video".to_string(), InfoDocUtils::as_value(ep.video.as_ref())),
|
|
("audio".to_string(), InfoDocUtils::as_value(ep.audio.as_ref())),
|
|
("bitrate".to_string(), InfoDocUtils::as_u32(ep.bitrate)),
|
|
]))),
|
|
("custom_sid".to_string(), ep.custom_sid.as_ref().map_or(Value::Null, |s| Value::String(s.clone()))),
|
|
("added".to_string(), Value::String(ep.added.clone())),
|
|
("season".to_string(), InfoDocUtils::as_u32(ep.season)),
|
|
("direct_source".to_string(), if options.skip_series_direct_source { InfoDocUtils::empty_string() } else { Value::String(ep.direct_source.clone()) }),
|
|
]));
|
|
map.entry(ep.season).or_default().push(value);
|
|
}
|
|
|
|
Value::Object(
|
|
map.into_iter()
|
|
.map(|(season, episodes)| {
|
|
(
|
|
season.to_string(),
|
|
Value::Array(episodes)
|
|
)
|
|
})
|
|
.collect::<serde_json::Map<String, Value>>()
|
|
)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
fn parse_season_episode_field(s: &str) -> Option<(u32, u32, String)> {
|
|
let mut parts = s.split('_');
|
|
|
|
let (prefix, ep) = (parts.next()?, parts.next()?);
|
|
if prefix != "nfo" || ep != "ep" {
|
|
return None;
|
|
}
|
|
|
|
let season: u32 = parts.next()?.parse().ok()?;
|
|
let episode: u32 = parts.next()?.parse().ok()?;
|
|
|
|
let kind = parts.collect::<Vec<_>>().join("_");
|
|
if kind.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some((season, episode, kind))
|
|
}
|
|
|
|
|
|
impl VideoStreamProperties {
|
|
pub fn from_info<P>(info: &XtreamVideoInfo, pli: &P) -> VideoStreamProperties
|
|
where
|
|
P: PlaylistEntry,
|
|
{
|
|
let mut props = VideoStreamProperties {
|
|
name: info.info.name.clone(),
|
|
category_id: info.movie_data.category_id,
|
|
stream_id: info.movie_data.stream_id,
|
|
stream_icon: info.info.movie_image.as_ref().map_or_else(String::new, Clone::clone),
|
|
direct_source: info.movie_data.direct_source.clone(),
|
|
custom_sid: info.movie_data.custom_sid.clone(),
|
|
added: info.movie_data.added.clone(),
|
|
container_extension: info.movie_data.container_extension.clone(),
|
|
rating: None, // from PlaylistItem
|
|
rating_5based: None, // from PlaylistItem
|
|
stream_type: None, // from PlaylistItem
|
|
trailer: info.info.youtube_trailer.clone(),
|
|
tmdb: info.info.tmdb_id.parse::<u32>().ok(),
|
|
is_adult: 0, // from PlaylistItem
|
|
details: Some(VideoStreamDetailProperties {
|
|
kinopoisk_url: info.info.kinopoisk_url.clone(),
|
|
o_name: info.info.o_name.clone(),
|
|
cover_big: info.info.cover_big.clone(),
|
|
movie_image: info.info.movie_image.clone(),
|
|
release_date: info.info.releasedate.clone(),
|
|
episode_run_time: info.info.episode_run_time,
|
|
youtube_trailer: info.info.youtube_trailer.clone(),
|
|
director: info.info.director.clone(),
|
|
actors: info.info.actors.clone(),
|
|
cast: info.info.cast.clone(),
|
|
description: info.info.description.clone(),
|
|
plot: info.info.plot.clone(),
|
|
age: info.info.age.clone(),
|
|
mpaa_rating: info.info.mpaa_rating.clone(),
|
|
rating_count_kinopoisk: info.info.rating_count_kinopoisk,
|
|
country: info.info.country.clone(),
|
|
genre: info.info.genre.clone(),
|
|
backdrop_path: info.info.backdrop_path.clone(),
|
|
duration_secs: info.info.duration_secs.clone(),
|
|
duration: info.info.duration.clone(),
|
|
video: info.info.video.clone(),
|
|
audio: info.info.audio.clone(),
|
|
bitrate: info.info.bitrate,
|
|
runtime: info.info.runtime.clone(),
|
|
status: info.info.status.clone(),
|
|
}),
|
|
};
|
|
|
|
if let Some(StreamProperties::Video(video)) = pli.get_additional_properties() {
|
|
props.rating = video.rating;
|
|
props.rating_5based = video.rating_5based;
|
|
props.stream_type = video.stream_type.clone();
|
|
if props.tmdb.is_none() {
|
|
props.tmdb = video.tmdb;
|
|
}
|
|
if props.trailer.is_none() {
|
|
props.trailer = video.trailer.clone();
|
|
}
|
|
props.is_adult = video.is_adult;
|
|
} else {
|
|
props.stream_type = Some("movie".to_string());
|
|
}
|
|
|
|
props
|
|
}
|
|
}
|
|
|
|
impl SeriesStreamProperties {
|
|
pub fn from_info<P>(info: &XtreamSeriesInfo, pli: &P) -> SeriesStreamProperties
|
|
where
|
|
P: PlaylistEntry,
|
|
{
|
|
SeriesStreamProperties {
|
|
name: info.info.name.clone(),
|
|
category_id: info.info.category_id,
|
|
series_id: pli.get_virtual_id(),
|
|
backdrop_path: info.info.backdrop_path.clone(),
|
|
cast: info.info.cast.clone(),
|
|
cover: info.info.cover.clone(),
|
|
director: info.info.director.clone(),
|
|
episode_run_time: Some(info.info.episode_run_time.clone()),
|
|
genre: Some(info.info.genre.clone()),
|
|
last_modified: Some(info.info.last_modified.clone()),
|
|
plot: Some(info.info.plot.clone()),
|
|
rating: info.info.rating,
|
|
rating_5based: info.info.rating_5based,
|
|
release_date: Some(info.info.release_date.clone()),
|
|
youtube_trailer: info.info.youtube_trailer.clone(),
|
|
tmdb: info.info.tmdb,
|
|
details: Some(SeriesStreamDetailProperties {
|
|
year: InfoDocUtils::extract_year_from_release_date(&info.info.release_date),
|
|
episodes: info.episodes.as_ref().map(|list|
|
|
list.iter().map(|e| {
|
|
SeriesStreamDetailEpisodeProperties {
|
|
id: e.id,
|
|
episode_num: e.episode_num,
|
|
season: e.season,
|
|
title: e.title.clone(),
|
|
container_extension: e.container_extension.clone(),
|
|
custom_sid: e.custom_sid.clone(),
|
|
added: e.added.clone(),
|
|
direct_source: e.direct_source.clone(),
|
|
tmdb: info.info.tmdb,
|
|
release_date: e.info.as_ref().map(|i| i.air_date.clone()).unwrap_or_default(),
|
|
plot: None,
|
|
duration_secs: e.info.as_ref().map(|i| i.duration_secs).unwrap_or_default(),
|
|
duration: e.info.as_ref().map(|i| i.duration.clone()).unwrap_or_default(),
|
|
movie_image: e.info.as_ref().map(|i| i.movie_image.clone()).unwrap_or_default(),
|
|
bitrate: e.info.as_ref().map(|i| i.bitrate).unwrap_or_default(),
|
|
rating: e.info.as_ref().map(|i| i.rating),
|
|
video: e.info.as_ref().map(|i| i.video.clone()).unwrap_or_default(),
|
|
audio: e.info.as_ref().map(|i| i.audio.clone()).unwrap_or_default(),
|
|
}
|
|
}).collect())
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl EpisodeStreamProperties {
|
|
|
|
pub fn from_series(series: &SeriesStreamProperties, episode: &SeriesStreamDetailEpisodeProperties) -> EpisodeStreamProperties {
|
|
EpisodeStreamProperties {
|
|
episode_id: episode.id,
|
|
episode: episode.episode_num,
|
|
season: episode.season,
|
|
added: Some(episode.added.clone()),
|
|
release_date: Some(episode.release_date.clone()),
|
|
tmdb: episode.tmdb.or(series.tmdb),
|
|
movie_image: episode.movie_image.clone(),
|
|
container_extension: episode.container_extension.clone(),
|
|
video: episode.video.clone(),
|
|
audio: episode.audio.clone(),
|
|
}
|
|
}
|
|
}
|