mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-19 22:32:26 +02:00
- Added cluster to favourites - Added Regex cache to avoid multiple compilations of same regex
1255 lines
44 KiB
Rust
1255 lines
44 KiB
Rust
use crate::utils::{arc_str_serde, extract_extension_from_url, generate_playlist_uuid, get_provider_id, intern, parse_uuid_hex};
|
|
use crate::model::{xtream_const, ClusterFlags, CommonPlaylistItem, ConfigTargetOptions, EpisodeStreamProperties, SeriesStreamProperties, StreamProperties, VideoStreamProperties, XtreamInfoDocument};
|
|
use enum_iterator::Sequence;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::borrow::Cow;
|
|
use std::fmt::Write;
|
|
use std::fmt::{Display, Formatter};
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
use hex::FromHex;
|
|
// https://de.wikipedia.org/wiki/M3U
|
|
// https://siptv.eu/howto/playlist.html
|
|
|
|
pub type VirtualId = u32;
|
|
|
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
|
pub struct UUIDType(pub [u8; 32]);
|
|
|
|
impl UUIDType {
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.0.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.is_empty()
|
|
}
|
|
|
|
/// Converts the first 16 bytes of this `UUIDType` into a valid UUID v4 string.
|
|
///
|
|
/// Note:
|
|
/// - Only the first 16 bytes are used, because a standard UUID is 16 bytes.
|
|
/// - The remaining 16 bytes of the 32-byte `UUIDType` are ignored in this operation.
|
|
/// - This conversion is **not reversible**, calling `from_valid_uuid` on the resulting string
|
|
/// will not recover the original 32-byte `UUIDType`.
|
|
pub fn to_valid_uuid(&self) -> String {
|
|
let mut bytes = [0u8; 16];
|
|
bytes.copy_from_slice(&self.0[0..16]);
|
|
|
|
// Set UUID version (v4)
|
|
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
|
// Set UUID variant (10xxxxxx)
|
|
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
|
|
|
format!(
|
|
"{}-{}-{}-{}-{}",
|
|
hex::encode_upper(&bytes[0..4]),
|
|
hex::encode_upper(&bytes[4..6]),
|
|
hex::encode_upper(&bytes[6..8]),
|
|
hex::encode_upper(&bytes[8..10]),
|
|
hex::encode_upper(&bytes[10..16]),
|
|
)
|
|
}
|
|
|
|
/// Creates a `UUIDType` from a valid UUID string.
|
|
///
|
|
/// Implementation details:
|
|
/// - A standard UUID is 16 bytes.
|
|
/// - The first 16 bytes of the resulting `UUIDType` are taken from the parsed UUID.
|
|
/// - The remaining 16 bytes are filled by hashing the first 16 bytes using Blake3.
|
|
/// - This ensures the resulting `UUIDType` is 32 bytes, but this operation is **not reversible**
|
|
/// to the original 32-byte `UUIDType` if the input was previously generated with `to_valid_uuid`.
|
|
pub fn from_valid_uuid(uuid: &str) -> Self {
|
|
let bytes = if let Some(parsed_uuid) = parse_uuid_hex(uuid) {
|
|
let mut bytes = [0u8; 32];
|
|
// die 16 UUID Bytes
|
|
bytes[..16].copy_from_slice(&parsed_uuid);
|
|
// die restlichen 16 Bytes = Hash der UUID (optional)
|
|
let hash = blake3::hash(&parsed_uuid);
|
|
bytes[16..].copy_from_slice(&hash.as_bytes()[..16]);
|
|
bytes
|
|
} else {
|
|
// fallback
|
|
*blake3::hash(uuid.as_bytes()).as_bytes()
|
|
};
|
|
|
|
UUIDType(bytes)
|
|
}
|
|
}
|
|
|
|
impl AsRef<[u8]> for UUIDType {
|
|
fn as_ref(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
|
|
impl FromStr for UUIDType {
|
|
type Err = hex::FromHexError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let bytes = <[u8; 32]>::from_hex(s)?;
|
|
Ok(UUIDType(bytes))
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for UUIDType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", hex::encode(self.0))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default)]
|
|
#[repr(u8)]
|
|
pub enum XtreamCluster {
|
|
#[default]
|
|
Live = 1,
|
|
Video = 2,
|
|
Series = 3,
|
|
}
|
|
|
|
impl XtreamCluster {
|
|
pub const fn as_str(&self) -> &str {
|
|
match self {
|
|
Self::Live => "Live",
|
|
Self::Video => "Video",
|
|
Self::Series => "Series",
|
|
}
|
|
}
|
|
pub const fn as_stream_type(&self) -> &str {
|
|
match self {
|
|
Self::Live => "live",
|
|
Self::Video => "movie",
|
|
Self::Series => "series",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for XtreamCluster {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"live" => Ok(XtreamCluster::Live),
|
|
"video" | "vod" | "movie" => Ok(XtreamCluster::Video),
|
|
"series" => Ok(XtreamCluster::Series),
|
|
_ => Err(format!("Invalid XtreamCluster: {s}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for XtreamCluster {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.as_str())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<PlaylistItemType> for XtreamCluster {
|
|
type Error = String;
|
|
fn try_from(item_type: PlaylistItemType) -> Result<Self, Self::Error> {
|
|
match item_type {
|
|
PlaylistItemType::Live | PlaylistItemType::LiveHls | PlaylistItemType::LiveDash | PlaylistItemType::LiveUnknown => Ok(Self::Live),
|
|
PlaylistItemType::Catchup | PlaylistItemType::Video | PlaylistItemType::LocalVideo => Ok(Self::Video),
|
|
PlaylistItemType::Series | PlaylistItemType::SeriesInfo | PlaylistItemType::LocalSeries | PlaylistItemType::LocalSeriesInfo => Ok(Self::Series),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize, Default, Sequence)]
|
|
#[repr(u8)]
|
|
pub enum PlaylistItemType {
|
|
#[default]
|
|
Live = 1,
|
|
Video = 2,
|
|
Series = 3, // xtream series description
|
|
SeriesInfo = 4, // xtream series info fetched for series description
|
|
Catchup = 5,
|
|
LiveUnknown = 6, // No Provider id
|
|
LiveHls = 7, // m3u8 entry
|
|
LiveDash = 8, // mpd
|
|
LocalVideo = 9,
|
|
LocalSeries = 10,
|
|
LocalSeriesInfo = 11,
|
|
}
|
|
|
|
impl From<XtreamCluster> for PlaylistItemType {
|
|
fn from(xtream_cluster: XtreamCluster) -> Self {
|
|
match xtream_cluster {
|
|
XtreamCluster::Live => Self::Live,
|
|
XtreamCluster::Video => Self::Video,
|
|
XtreamCluster::Series => Self::SeriesInfo,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FromStr for PlaylistItemType {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s {
|
|
"Live" => Ok(PlaylistItemType::Live),
|
|
"Video" => Ok(PlaylistItemType::Video),
|
|
"LocalVideo" => Ok(PlaylistItemType::LocalVideo),
|
|
"Series" => Ok(PlaylistItemType::Series),
|
|
"SeriesInfo" => Ok(PlaylistItemType::SeriesInfo),
|
|
"LocalSeries" => Ok(PlaylistItemType::LocalSeries),
|
|
"LocalSeriesInfo" => Ok(PlaylistItemType::LocalSeriesInfo),
|
|
"Catchup" => Ok(PlaylistItemType::Catchup),
|
|
"LiveUnknown" => Ok(PlaylistItemType::LiveUnknown),
|
|
"LiveHls" => Ok(PlaylistItemType::LiveHls),
|
|
"LiveDash" => Ok(PlaylistItemType::LiveDash),
|
|
_ => Err(format!("Invalid PlaylistItemType: {s}")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PlaylistItemType {
|
|
const LIVE: &'static str = "live";
|
|
const VIDEO: &'static str = "video";
|
|
const SERIES: &'static str = "series";
|
|
const SERIES_INFO: &'static str = "series-info";
|
|
const CATCHUP: &'static str = "catchup";
|
|
|
|
pub fn is_local(&self) -> bool {
|
|
matches!(self, PlaylistItemType::LocalVideo | PlaylistItemType::LocalSeries | PlaylistItemType::LocalSeriesInfo)
|
|
}
|
|
|
|
pub fn is_live(&self) -> bool {
|
|
matches!(self, PlaylistItemType::Live | PlaylistItemType::LiveDash | PlaylistItemType::LiveHls | PlaylistItemType::LiveUnknown)
|
|
}
|
|
|
|
pub fn as_u8(self) -> u8 {
|
|
self as u8
|
|
}
|
|
|
|
pub fn as_str(&self) -> Cow<'static, str> {
|
|
match self {
|
|
Self::Live | Self::LiveHls | Self::LiveDash | Self::LiveUnknown => Cow::Borrowed(Self::LIVE),
|
|
Self::Video | Self::LocalVideo => Cow::Borrowed(Self::VIDEO),
|
|
Self::Series | Self::LocalSeries => Cow::Borrowed(Self::SERIES),
|
|
Self::SeriesInfo | Self::LocalSeriesInfo => Cow::Borrowed(Self::SERIES_INFO),
|
|
Self::Catchup => Cow::Borrowed(Self::CATCHUP),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for PlaylistItemType {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.as_str())
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Default, Debug)]
|
|
pub struct PlaylistItemTypeSet(u16);
|
|
impl PlaylistItemTypeSet {
|
|
#[inline]
|
|
pub fn empty() -> Self {
|
|
Self(0)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn from_item(item: PlaylistItemType) -> Self {
|
|
let bit = 1u16 << ((item as u8) - 1);
|
|
Self(bit)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn insert(&mut self, item: PlaylistItemType) {
|
|
self.0 |= 1u16 << ((item as u8) - 1);
|
|
}
|
|
|
|
#[inline]
|
|
pub fn remove(&mut self, item: PlaylistItemType) {
|
|
self.0 &= !(1u16 << ((item as u8) - 1));
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_set(&self, item: PlaylistItemType) -> bool {
|
|
(self.0 & (1u16 << ((item as u8) - 1))) != 0
|
|
}
|
|
|
|
#[inline]
|
|
pub fn bits(self) -> u16 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
|
|
pub trait FieldGetAccessor {
|
|
fn get_field(&self, field: &str) -> Option<Cow<'_, str>>;
|
|
}
|
|
pub trait FieldSetAccessor {
|
|
fn set_field(&mut self, field: &str, value: &str) -> bool;
|
|
}
|
|
|
|
pub trait PlaylistEntry: Send + Sync {
|
|
fn get_virtual_id(&self) -> VirtualId;
|
|
fn get_provider_id(&self) -> Option<u32>;
|
|
fn get_category_id(&self) -> Option<u32>;
|
|
fn get_provider_url(&self) -> Cow<'_, str>;
|
|
fn get_uuid(&self) -> UUIDType;
|
|
fn get_item_type(&self) -> PlaylistItemType;
|
|
fn get_group(&self) -> Cow<'_, str>;
|
|
fn get_name(&self) -> Cow<'_, str>;
|
|
fn get_resolved_info_document(&self, options: &XtreamMappingOptions) -> Option<XtreamInfoDocument>;
|
|
fn get_additional_properties(&self) -> Option<&StreamProperties>;
|
|
fn get_additional_properties_mut(&mut self) -> Option<&mut StreamProperties>;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PlaylistItemHeader {
|
|
#[serde(skip)]
|
|
pub uuid: UUIDType, // calculated
|
|
pub id: String, // provider id
|
|
pub virtual_id: VirtualId, // virtual id
|
|
pub name: String,
|
|
pub chno: u32,
|
|
pub logo: String,
|
|
pub logo_small: String,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub group: Arc<str>,
|
|
pub title: String,
|
|
pub parent_code: String,
|
|
pub audio_track: String,
|
|
pub time_shift: String,
|
|
pub rec: String,
|
|
pub url: String,
|
|
pub epg_channel_id: Option<String>,
|
|
pub xtream_cluster: XtreamCluster,
|
|
pub additional_properties: Option<StreamProperties>,
|
|
#[serde(default)]
|
|
pub item_type: PlaylistItemType,
|
|
#[serde(default)]
|
|
pub category_id: u32,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub input_name: Arc<str>,
|
|
#[serde(default)]
|
|
pub source_ordinal: u32,
|
|
}
|
|
|
|
impl Default for PlaylistItemHeader {
|
|
fn default() -> Self {
|
|
Self {
|
|
uuid: UUIDType::default(),
|
|
id: String::new(),
|
|
virtual_id: 0,
|
|
name: String::new(),
|
|
chno: 0,
|
|
logo: String::new(),
|
|
logo_small: String::new(),
|
|
group: intern(""),
|
|
title: String::new(),
|
|
parent_code: String::new(),
|
|
audio_track: String::new(),
|
|
time_shift: String::new(),
|
|
rec: String::new(),
|
|
url: String::new(),
|
|
epg_channel_id: None,
|
|
xtream_cluster: XtreamCluster::default(),
|
|
additional_properties: None,
|
|
item_type: PlaylistItemType::default(),
|
|
category_id: 0,
|
|
input_name: intern(""),
|
|
source_ordinal: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PlaylistItemHeader {
|
|
pub fn gen_uuid(&mut self) {
|
|
self.uuid = generate_playlist_uuid(&self.input_name, &self.id, self.item_type, &self.url);
|
|
}
|
|
pub const fn get_uuid(&self) -> &UUIDType {
|
|
&self.uuid
|
|
}
|
|
|
|
pub fn get_provider_id(&mut self) -> Option<u32> {
|
|
match get_provider_id(&self.id, &self.url) {
|
|
None => None,
|
|
Some(newid) => {
|
|
self.id = newid.to_string();
|
|
Some(newid)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_container_extension(&self) -> Option<Cow<'_, str>> {
|
|
self.additional_properties.as_ref().and_then(|a| a.get_container_extension())
|
|
}
|
|
}
|
|
|
|
macro_rules! to_m3u_non_empty_fields {
|
|
($header:expr, $line:expr, $(($prop:ident, $field:expr)),*;) => {
|
|
$(
|
|
if !$header.$prop.is_empty() {
|
|
let _ = write!($line," {}=\"{}\"", $field, $header.$prop );
|
|
}
|
|
)*
|
|
};
|
|
}
|
|
|
|
macro_rules! to_m3u_resource_non_empty_fields {
|
|
($header:expr, $url:expr, $line:expr, $(($prop:ident, $field:expr)),*;) => {
|
|
$(
|
|
if !$header.$prop.is_empty() {
|
|
let _ = write!($line, " {}=\"{}/{}\"", $field, $url, stringify!($prop));
|
|
}
|
|
)*
|
|
};
|
|
}
|
|
|
|
macro_rules! generate_field_accessor_impl_for_playlist_item_header {
|
|
($($prop:ident),*;) => {
|
|
impl crate::model::FieldGetAccessor for crate::model::PlaylistItemHeader {
|
|
fn get_field(&self, field: &str) -> Option<Cow<'_, str>> {
|
|
let bytes = field.as_bytes();
|
|
|
|
$(
|
|
{
|
|
let target = stringify!($prop).as_bytes();
|
|
if bytes.eq_ignore_ascii_case(target) {
|
|
return Some(Cow::Borrowed(&self.$prop));
|
|
}
|
|
}
|
|
)*
|
|
|
|
if bytes.eq_ignore_ascii_case(b"group") {
|
|
Some(Cow::Borrowed(&*self.group))
|
|
} else if bytes.eq_ignore_ascii_case(b"caption") {
|
|
Some(if self.title.is_empty() {
|
|
Cow::Borrowed(&self.name)
|
|
} else {
|
|
Cow::Borrowed(&self.title)
|
|
})
|
|
} else if bytes.eq_ignore_ascii_case(b"input") {
|
|
Some(Cow::Borrowed(&*self.input_name))
|
|
} else if bytes.eq_ignore_ascii_case(b"type") {
|
|
Some(Cow::Owned(self.item_type.to_string()))
|
|
} else if bytes.eq_ignore_ascii_case(b"epg_channel_id") || bytes.eq_ignore_ascii_case(b"epg_id") {
|
|
self.epg_channel_id.as_ref().map(|s| Cow::Borrowed(s.as_str()))
|
|
} else if bytes.eq_ignore_ascii_case(b"chno") {
|
|
Some(Cow::Owned(self.chno.to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl crate::model::FieldSetAccessor for crate::model::PlaylistItemHeader {
|
|
fn set_field(&mut self, field: &str, value: &str) -> bool {
|
|
let bytes = field.as_bytes();
|
|
$(
|
|
{
|
|
let target = stringify!($prop).as_bytes();
|
|
if bytes.eq_ignore_ascii_case(target) {
|
|
self.$prop = String::from(value);
|
|
return true;
|
|
}
|
|
}
|
|
)*
|
|
|
|
if bytes.eq_ignore_ascii_case(b"group") {
|
|
self.group = crate::utils::intern(value);
|
|
true
|
|
} else if bytes.eq_ignore_ascii_case(b"caption") {
|
|
self.title = String::from(value);
|
|
self.name = String::from(value);
|
|
true
|
|
} else if bytes.eq_ignore_ascii_case(b"epg_channel_id") || bytes.eq_ignore_ascii_case(b"epg_id") {
|
|
self.epg_channel_id = Some(String::from(value));
|
|
true
|
|
} else if bytes.eq_ignore_ascii_case(b"chno") {
|
|
if let Ok(parsed) = value.parse::<u32>() {
|
|
self.chno = parsed;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generate_field_accessor_impl_for_playlist_item_header!(id, /*virtual_id,*/ title, name, logo, logo_small, parent_code, audio_track, time_shift, rec, url;);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct M3uPlaylistItem {
|
|
pub virtual_id: VirtualId,
|
|
pub provider_id: String,
|
|
pub name: String,
|
|
pub chno: u32,
|
|
pub logo: String,
|
|
pub logo_small: String,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub group: Arc<str>,
|
|
pub title: String,
|
|
pub parent_code: String,
|
|
pub audio_track: String,
|
|
pub time_shift: String,
|
|
pub rec: String,
|
|
pub url: String,
|
|
pub epg_channel_id: Option<String>,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub input_name: Arc<str>,
|
|
pub item_type: PlaylistItemType,
|
|
#[serde(skip)]
|
|
pub t_stream_url: String,
|
|
#[serde(skip)]
|
|
pub t_resource_url: Option<String>,
|
|
#[serde(default)]
|
|
pub source_ordinal: u32,
|
|
}
|
|
|
|
impl M3uPlaylistItem {
|
|
#[allow(clippy::missing_panics_doc)]
|
|
pub fn to_m3u(&self, target_options: Option<&ConfigTargetOptions>, rewrite_urls: bool) -> String {
|
|
let options = target_options.as_ref();
|
|
let ignore_logo = options.is_some_and(|o| o.ignore_logo);
|
|
let mut line = String::with_capacity(256);
|
|
let _ = write!(&mut line, "#EXTINF:-1 tvg-id=\"{}\" tvg-name=\"{}\" group-title=\"{}\"",
|
|
self.epg_channel_id.as_ref().map_or("", |o| o.as_ref()),
|
|
self.name, self.group);
|
|
|
|
if !ignore_logo {
|
|
if let (true, Some(resource_url)) = (rewrite_urls, self.t_resource_url.as_ref()) {
|
|
to_m3u_resource_non_empty_fields!(self, resource_url, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small"););
|
|
} else {
|
|
to_m3u_non_empty_fields!(self, line, (logo, "tvg-logo"), (logo_small, "tvg-logo-small"););
|
|
}
|
|
}
|
|
|
|
if self.chno != 0 {
|
|
let _ = write!(line, " tvg-chno=\"{}\"", self.chno);
|
|
}
|
|
to_m3u_non_empty_fields!(self, line,
|
|
(parent_code, "parent-code"),
|
|
(audio_track, "audio-track"),
|
|
(time_shift, "timeshift"),
|
|
(rec, "tvg-rec"););
|
|
|
|
let url = if self.t_stream_url.is_empty() { &self.url } else { &self.t_stream_url };
|
|
let _ = write!(&mut line, ",{}\n{}", self.title, url);
|
|
line
|
|
}
|
|
|
|
pub fn to_common(&self) -> CommonPlaylistItem {
|
|
CommonPlaylistItem {
|
|
virtual_id: self.virtual_id,
|
|
provider_id: self.provider_id.to_string(),
|
|
name: self.name.clone(),
|
|
chno: self.chno,
|
|
logo: self.logo.clone(),
|
|
logo_small: self.logo_small.clone(),
|
|
group: self.group.clone(),
|
|
title: self.title.clone(),
|
|
parent_code: self.parent_code.clone(),
|
|
audio_track: self.audio_track.to_string(),
|
|
time_shift: self.time_shift.to_string(),
|
|
rec: self.rec.clone(),
|
|
url: self.url.clone(),
|
|
input_name: self.input_name.clone(),
|
|
item_type: self.item_type,
|
|
epg_channel_id: self.epg_channel_id.clone(),
|
|
xtream_cluster: XtreamCluster::try_from(self.item_type).ok(),
|
|
additional_properties: None,
|
|
category_id: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PlaylistEntry for M3uPlaylistItem {
|
|
#[inline]
|
|
fn get_virtual_id(&self) -> VirtualId {
|
|
self.virtual_id
|
|
}
|
|
|
|
fn get_provider_id(&self) -> Option<u32> {
|
|
get_provider_id(&self.provider_id, &self.url)
|
|
}
|
|
#[inline]
|
|
fn get_category_id(&self) -> Option<u32> {
|
|
None
|
|
}
|
|
#[inline]
|
|
fn get_provider_url(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(self.url.as_str())
|
|
}
|
|
|
|
fn get_uuid(&self) -> UUIDType {
|
|
generate_playlist_uuid(&self.input_name, &self.provider_id, self.item_type, &self.url)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_item_type(&self) -> PlaylistItemType {
|
|
self.item_type
|
|
}
|
|
|
|
#[inline]
|
|
fn get_group(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(&*self.group)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_name(&self) -> Cow<'_, str> {
|
|
if self.title.is_empty() {
|
|
Cow::Borrowed(self.name.as_str())
|
|
} else {
|
|
Cow::Borrowed(self.title.as_str())
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn get_resolved_info_document(&self, _options: &XtreamMappingOptions) -> Option<XtreamInfoDocument> {
|
|
None
|
|
}
|
|
#[inline]
|
|
fn get_additional_properties(&self) -> Option<&StreamProperties> {
|
|
None
|
|
}
|
|
#[inline]
|
|
fn get_additional_properties_mut(&mut self) -> Option<&mut StreamProperties> {
|
|
None
|
|
}
|
|
|
|
}
|
|
|
|
macro_rules! generate_field_accessor_impl_for_m3u_playlist_item {
|
|
($($prop:ident),*;) => {
|
|
impl crate::model::FieldGetAccessor for M3uPlaylistItem {
|
|
fn get_field(&self, field: &str) -> Option<Cow<'_, str>> {
|
|
let bytes = field.as_bytes();
|
|
$(
|
|
{
|
|
let target = stringify!($prop).as_bytes();
|
|
if bytes.len() == target.len() &&
|
|
bytes.iter().zip(target).all(|(a, b)| a.to_ascii_lowercase() == *b)
|
|
{
|
|
return Some(Cow::Borrowed(&self.$prop));
|
|
}
|
|
}
|
|
)*
|
|
if bytes.eq_ignore_ascii_case(b"group") {
|
|
Some(Cow::Borrowed(&*self.group))
|
|
} else if bytes.eq_ignore_ascii_case(b"caption") {
|
|
Some(if self.title.is_empty() {
|
|
Cow::Borrowed(&self.name)
|
|
} else {
|
|
Cow::Borrowed(&self.title)
|
|
})
|
|
} else if bytes.eq_ignore_ascii_case(b"epg_channel_id") || bytes.eq_ignore_ascii_case(b"epg_id") {
|
|
self.epg_channel_id.as_ref().map(|s| Cow::Borrowed(s.as_str()))
|
|
} else if bytes.eq_ignore_ascii_case(b"chno") {
|
|
Some(Cow::Owned(self.chno.to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
generate_field_accessor_impl_for_m3u_playlist_item!(title, name, provider_id, logo, logo_small, parent_code, audio_track, time_shift, rec, url;);
|
|
|
|
impl From<M3uPlaylistItem> for CommonPlaylistItem {
|
|
fn from(item: M3uPlaylistItem) -> Self {
|
|
item.to_common()
|
|
}
|
|
}
|
|
|
|
#[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>,
|
|
pub reverse_item_types: PlaylistItemTypeSet,
|
|
pub username: String,
|
|
pub password: String,
|
|
pub base_url: Option<String>,
|
|
}
|
|
|
|
impl XtreamMappingOptions {
|
|
#[inline]
|
|
pub fn is_reverse(&self, item_type: PlaylistItemType) -> bool {
|
|
self.reverse_item_types.is_set(item_type)
|
|
}
|
|
|
|
pub fn get_resource_url(&self, xtream_cluster: XtreamCluster, item_type: PlaylistItemType, virtual_id: VirtualId) -> Option<String> {
|
|
let is_reverse = self.is_reverse(item_type);
|
|
let resource_url = if is_reverse && self.rewrite_resource_url && self.base_url.is_some() {
|
|
let resource_url = format!("{}/resource/{}/{}/{}/{}", self.base_url.as_ref().map_or_else(String::new, |b| b.clone()),
|
|
xtream_cluster.as_stream_type(), self.username, self.password, virtual_id);
|
|
Some(resource_url)
|
|
} else {
|
|
None
|
|
};
|
|
resource_url
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct XtreamPlaylistItem {
|
|
pub virtual_id: VirtualId,
|
|
pub provider_id: u32,
|
|
pub name: String,
|
|
pub logo: String,
|
|
pub logo_small: String,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub group: Arc<str>,
|
|
pub title: String,
|
|
pub parent_code: String,
|
|
pub rec: String,
|
|
pub url: String,
|
|
pub epg_channel_id: Option<String>,
|
|
pub xtream_cluster: XtreamCluster,
|
|
pub additional_properties: Option<StreamProperties>,
|
|
pub item_type: PlaylistItemType,
|
|
pub category_id: u32,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub input_name: Arc<str>,
|
|
pub channel_no: u32,
|
|
#[serde(default)]
|
|
pub source_ordinal: u32,
|
|
}
|
|
|
|
impl XtreamPlaylistItem {
|
|
|
|
pub fn to_common(&self) -> CommonPlaylistItem {
|
|
CommonPlaylistItem {
|
|
virtual_id: self.virtual_id,
|
|
provider_id: self.provider_id.to_string(),
|
|
name: self.name.clone(),
|
|
chno: self.channel_no,
|
|
logo: self.logo.clone(),
|
|
logo_small: self.logo_small.clone(),
|
|
group: self.group.clone(),
|
|
title: self.title.clone(),
|
|
parent_code: self.parent_code.clone(),
|
|
audio_track: String::new(),
|
|
time_shift: String::new(),
|
|
rec: self.rec.clone(),
|
|
url: self.url.clone(),
|
|
input_name: self.input_name.clone(),
|
|
item_type: self.item_type,
|
|
epg_channel_id: self.epg_channel_id.clone(),
|
|
xtream_cluster: Some(self.xtream_cluster),
|
|
additional_properties: self.additional_properties.clone(),
|
|
category_id: Some(self.category_id),
|
|
}
|
|
}
|
|
|
|
pub fn get_container_extension(&self) -> Option<Cow<'_, str>> {
|
|
match self.additional_properties {
|
|
None => None,
|
|
Some(ref props) => {
|
|
match props {
|
|
StreamProperties::Live(_) => Some(Cow::Borrowed("ts")),
|
|
StreamProperties::Video(video) => Some(Cow::Borrowed(video.container_extension.as_str())),
|
|
StreamProperties::Series(_) => None,
|
|
StreamProperties::Episode(episode) => Some(Cow::Borrowed(episode.container_extension.as_str())),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn has_details(&self) -> bool {
|
|
self.additional_properties.as_ref().is_some_and(|p| p.has_details())
|
|
}
|
|
|
|
pub fn resolve_resource_url<'a>(&'a self, field: &str) -> Option<Cow<'a, str>> {
|
|
let bytes = field.as_bytes();
|
|
if bytes.eq_ignore_ascii_case(b"logo") && !self.logo.is_empty() {
|
|
return Some(Cow::Borrowed(self.logo.as_str()));
|
|
} else if bytes.eq_ignore_ascii_case(b"logo_small") && !self.logo_small.is_empty() {
|
|
return Some(Cow::Borrowed(self.logo_small.as_str()));
|
|
}
|
|
self.additional_properties.as_ref().and_then(|a| a.resolve_resource_url(field))
|
|
}
|
|
}
|
|
|
|
|
|
impl PlaylistEntry for XtreamPlaylistItem {
|
|
#[inline]
|
|
fn get_virtual_id(&self) -> VirtualId {
|
|
self.virtual_id
|
|
}
|
|
#[inline]
|
|
fn get_provider_id(&self) -> Option<u32> {
|
|
Some(self.provider_id)
|
|
}
|
|
#[inline]
|
|
fn get_category_id(&self) -> Option<u32> {
|
|
Some(self.category_id)
|
|
}
|
|
#[inline]
|
|
fn get_provider_url(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(self.url.as_str())
|
|
}
|
|
|
|
#[inline]
|
|
fn get_uuid(&self) -> UUIDType {
|
|
generate_playlist_uuid(&self.input_name, &self.provider_id.to_string(), self.item_type, &self.url)
|
|
}
|
|
#[inline]
|
|
fn get_item_type(&self) -> PlaylistItemType {
|
|
self.item_type
|
|
}
|
|
#[inline]
|
|
fn get_group(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(&*self.group)
|
|
}
|
|
#[inline]
|
|
fn get_name(&self) -> Cow<'_, str> {
|
|
if self.title.is_empty() {
|
|
Cow::Borrowed(self.name.as_str())
|
|
} else {
|
|
Cow::Borrowed(self.title.as_str())
|
|
}
|
|
}
|
|
|
|
fn get_resolved_info_document(&self, options: &XtreamMappingOptions) -> Option<XtreamInfoDocument> {
|
|
if self.has_details() {
|
|
self.additional_properties.as_ref()
|
|
.map(|p| p.to_info_document(options, self.get_item_type(),
|
|
self.get_virtual_id(), self.get_category_id().unwrap_or(0)))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn get_additional_properties(&self) -> Option<&StreamProperties> {
|
|
self.additional_properties.as_ref()
|
|
}
|
|
#[inline]
|
|
fn get_additional_properties_mut(&mut self) -> Option<&mut StreamProperties> {
|
|
self.additional_properties.as_mut()
|
|
}
|
|
}
|
|
|
|
macro_rules! generate_field_accessor_impl_for_xtream_playlist_item {
|
|
($($prop:ident),*;) => {
|
|
impl crate::model::FieldGetAccessor for crate::model::XtreamPlaylistItem {
|
|
fn get_field(&self, field: &str) -> Option<Cow<'_, str>> {
|
|
let bytes = field.as_bytes();
|
|
|
|
$(
|
|
{
|
|
let target = stringify!($prop).as_bytes();
|
|
if bytes.len() == target.len() &&
|
|
bytes.iter().zip(target).all(|(a, b)| a.to_ascii_lowercase() == *b)
|
|
{
|
|
return Some(Cow::Borrowed(&self.$prop));
|
|
}
|
|
}
|
|
)*
|
|
|
|
// Caption
|
|
if bytes.eq_ignore_ascii_case(b"caption") {
|
|
Some(if self.title.is_empty() {
|
|
Cow::Borrowed(self.name.as_str())
|
|
} else {
|
|
Cow::Borrowed(self.title.as_str())
|
|
})
|
|
}
|
|
// epg_channel_id / epg_id
|
|
else if bytes.eq_ignore_ascii_case(b"epg_channel_id") || bytes.eq_ignore_ascii_case(b"epg_id") {
|
|
self.epg_channel_id.as_ref().map(|s| Cow::<str>::Borrowed(s.as_str()))
|
|
}
|
|
// Additional Properties
|
|
else if field.starts_with(xtream_const::XC_PROP_BACKDROP_PATH)
|
|
|| bytes.eq_ignore_ascii_case(xtream_const::XC_PROP_COVER.as_bytes())
|
|
{
|
|
match self.additional_properties.as_ref() {
|
|
Some(additional_properties) => match additional_properties {
|
|
StreamProperties::Live(_) => None,
|
|
StreamProperties::Video(video) => {
|
|
if bytes.eq_ignore_ascii_case(xtream_const::XC_PROP_COVER.as_bytes()) {
|
|
video.details.as_ref().and_then(|details| {
|
|
details.cover_big.as_ref()
|
|
.or(details.movie_image.as_ref())
|
|
.or_else(|| details.backdrop_path.as_ref().and_then(|p| p.first()))
|
|
.map(|s| Cow::<str>::Borrowed(s.as_str()))
|
|
})
|
|
} else {
|
|
video.details.as_ref().and_then(|details| {
|
|
details.backdrop_path.as_ref().and_then(|p| p.first())
|
|
.or(details.movie_image.as_ref())
|
|
.or(details.cover_big.as_ref())
|
|
.map(|s| Cow::<str>::Borrowed(s.as_str()))
|
|
})
|
|
}
|
|
}
|
|
StreamProperties::Series(series) => {
|
|
if bytes.eq_ignore_ascii_case(xtream_const::XC_PROP_COVER.as_bytes()) {
|
|
if series.cover.is_empty() {
|
|
series.backdrop_path.as_ref().and_then(|p| p.first()).map(|s| Cow::<str>::Borrowed(s.as_str()))
|
|
} else {
|
|
Some(Cow::<str>::Borrowed(series.cover.as_str()))
|
|
}
|
|
} else {
|
|
match series.backdrop_path.as_ref() {
|
|
None => if series.cover.is_empty() { None } else { Some(Cow::Borrowed(&series.cover)) },
|
|
Some(p) => p.first().map(|s| Cow::<str>::Borrowed(s.as_str())),
|
|
}
|
|
}
|
|
}
|
|
StreamProperties::Episode(episode) => Some(Cow::<str>::Borrowed(episode.movie_image.as_str())),
|
|
},
|
|
None => None,
|
|
}
|
|
}
|
|
// Default fallback
|
|
else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<XtreamPlaylistItem> for CommonPlaylistItem {
|
|
fn from(item: XtreamPlaylistItem) -> Self {
|
|
item.to_common()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PlaylistItem {
|
|
#[serde(flatten)]
|
|
pub header: PlaylistItemHeader,
|
|
}
|
|
|
|
generate_field_accessor_impl_for_xtream_playlist_item!(group, title, name, logo, logo_small, parent_code, rec, url;);
|
|
|
|
impl PlaylistItem {
|
|
|
|
fn get_additional_properties(header: &PlaylistItemHeader) -> Option<StreamProperties> {
|
|
match &header.additional_properties {
|
|
Some(props) => Some(props.clone()),
|
|
None => {
|
|
match header.xtream_cluster {
|
|
XtreamCluster::Live => None,
|
|
XtreamCluster::Video => {
|
|
let container_extension = extract_extension_from_url(&header.url).map(|e| e.strip_prefix('.').unwrap_or(e).to_string()).unwrap_or_default();
|
|
Some(StreamProperties::Video(Box::new(VideoStreamProperties {
|
|
name: header.name.clone(),
|
|
category_id: header.category_id,
|
|
stream_id: header.virtual_id,
|
|
stream_icon: "".to_string(),
|
|
direct_source: "".to_string(),
|
|
custom_sid: None,
|
|
added: String::new(),
|
|
container_extension,
|
|
rating: None,
|
|
rating_5based: None,
|
|
stream_type: Some("movie".to_string()),
|
|
trailer: None,
|
|
tmdb: None,
|
|
is_adult: 0,
|
|
details: None,
|
|
})))
|
|
}
|
|
XtreamCluster::Series => {
|
|
if header.item_type == PlaylistItemType::Series {
|
|
let container_extension = extract_extension_from_url(&header.url).map(|e| e.strip_prefix('.').unwrap_or(e).to_string()).unwrap_or_default();
|
|
// TODO maybe from link ? like s01e02 or something like this
|
|
Some(StreamProperties::Episode(EpisodeStreamProperties {
|
|
episode_id: 0,
|
|
episode: 0,
|
|
season: 0,
|
|
added: None,
|
|
release_date: None,
|
|
tmdb: None,
|
|
movie_image: String::new(),
|
|
container_extension,
|
|
audio: None,
|
|
video: None,
|
|
}))
|
|
} else if header.item_type == PlaylistItemType::SeriesInfo {
|
|
Some(StreamProperties::Series(Box::new(SeriesStreamProperties {
|
|
name: header.name.clone(),
|
|
category_id: header.category_id,
|
|
tmdb: None,
|
|
series_id: 0,
|
|
backdrop_path: None,
|
|
cast: String::new(),
|
|
cover: String::new(),
|
|
director: String::new(),
|
|
episode_run_time: None,
|
|
genre: None,
|
|
last_modified: None,
|
|
plot: None,
|
|
rating: 0.0,
|
|
rating_5based: 0.0,
|
|
release_date: None,
|
|
youtube_trailer: String::new(),
|
|
details: None,
|
|
})))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn has_details(&self) -> bool {
|
|
self.header.additional_properties.as_ref().is_some_and(|p| p.has_details())
|
|
}
|
|
|
|
pub fn get_tmdb_id(&self) -> Option<u32> {
|
|
self.header.additional_properties.as_ref().and_then(|p| p.get_tmdb_id())
|
|
}
|
|
}
|
|
|
|
impl From<&PlaylistItem> for XtreamPlaylistItem {
|
|
fn from(item: &PlaylistItem) -> Self {
|
|
let header = &item.header;
|
|
let provider_id = header.id.parse::<u32>().unwrap_or_default();
|
|
let additional_properties = PlaylistItem::get_additional_properties(header);
|
|
|
|
XtreamPlaylistItem {
|
|
virtual_id: header.virtual_id,
|
|
provider_id,
|
|
name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(),
|
|
logo: header.logo.to_string(),
|
|
logo_small: header.logo_small.to_string(),
|
|
group: Arc::clone(&header.group),
|
|
title: header.title.to_string(),
|
|
parent_code: header.parent_code.to_string(),
|
|
rec: header.rec.to_string(),
|
|
url: header.url.to_string(),
|
|
epg_channel_id: header.epg_channel_id.clone(),
|
|
xtream_cluster: header.xtream_cluster,
|
|
additional_properties,
|
|
item_type: header.item_type,
|
|
category_id: header.category_id,
|
|
input_name: Arc::clone(&header.input_name),
|
|
channel_no: header.chno,
|
|
source_ordinal: header.source_ordinal,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&PlaylistItem> for M3uPlaylistItem {
|
|
fn from(item: &PlaylistItem) -> Self {
|
|
let header = &item.header;
|
|
M3uPlaylistItem {
|
|
virtual_id: header.virtual_id,
|
|
provider_id: header.id.to_string(),
|
|
name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(),
|
|
chno: header.chno,
|
|
logo: header.logo.to_string(),
|
|
logo_small: header.logo_small.to_string(),
|
|
group: Arc::clone(&header.group),
|
|
title: header.title.to_string(),
|
|
parent_code: header.parent_code.to_string(),
|
|
audio_track: header.audio_track.to_string(),
|
|
time_shift: header.time_shift.to_string(),
|
|
rec: header.rec.to_string(),
|
|
url: header.url.to_string(),
|
|
epg_channel_id: header.epg_channel_id.clone(),
|
|
input_name: Arc::clone(&header.input_name),
|
|
item_type: header.item_type,
|
|
t_stream_url: header.url.to_string(),
|
|
t_resource_url: None,
|
|
source_ordinal: header.source_ordinal,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&PlaylistItem> for CommonPlaylistItem {
|
|
fn from(item: &PlaylistItem) -> Self {
|
|
let header = &item.header;
|
|
|
|
let additional_properties = PlaylistItem::get_additional_properties(header);
|
|
|
|
CommonPlaylistItem {
|
|
virtual_id: header.virtual_id,
|
|
provider_id: header.id.clone(),
|
|
name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.clone(),
|
|
logo: header.logo.clone(),
|
|
logo_small: header.logo_small.clone(),
|
|
group: Arc::clone(&header.group),
|
|
title: header.title.clone(),
|
|
parent_code: header.parent_code.clone(),
|
|
audio_track: header.audio_track.clone(),
|
|
time_shift: header.time_shift.clone(),
|
|
rec: header.rec.clone(),
|
|
url: header.url.clone(),
|
|
epg_channel_id: header.epg_channel_id.clone(),
|
|
xtream_cluster: Some(header.xtream_cluster),
|
|
additional_properties,
|
|
item_type: header.item_type,
|
|
category_id: Some(header.category_id),
|
|
input_name: Arc::clone(&header.input_name),
|
|
chno: header.chno,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&XtreamPlaylistItem> for PlaylistItem {
|
|
fn from(item: &XtreamPlaylistItem) -> Self {
|
|
let header = PlaylistItemHeader {
|
|
uuid: item.get_uuid(),
|
|
virtual_id: item.virtual_id,
|
|
id: item.provider_id.to_string(), // u32 → String
|
|
name: item.name.clone(),
|
|
title: item.title.clone(),
|
|
logo: item.logo.clone(),
|
|
logo_small: item.logo_small.clone(),
|
|
group: item.group.clone(),
|
|
parent_code: item.parent_code.clone(),
|
|
rec: item.rec.clone(),
|
|
url: item.url.clone(),
|
|
epg_channel_id: item.epg_channel_id.clone(),
|
|
xtream_cluster: item.xtream_cluster,
|
|
item_type: item.item_type,
|
|
category_id: item.category_id,
|
|
input_name: item.input_name.clone(),
|
|
chno: item.channel_no,
|
|
audio_track: String::new(),
|
|
time_shift: String::new(),
|
|
additional_properties: item.additional_properties.clone(),
|
|
source_ordinal: item.source_ordinal,
|
|
};
|
|
|
|
PlaylistItem {
|
|
header
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<&M3uPlaylistItem> for PlaylistItem {
|
|
fn from(item: &M3uPlaylistItem) -> Self {
|
|
let header = PlaylistItemHeader {
|
|
uuid: item.get_uuid(),
|
|
virtual_id: item.virtual_id,
|
|
id: item.provider_id.clone(),
|
|
name: item.name.clone(),
|
|
title: item.title.clone(),
|
|
logo: item.logo.clone(),
|
|
logo_small: item.logo_small.clone(),
|
|
group: item.group.clone(),
|
|
parent_code: item.parent_code.clone(),
|
|
rec: item.rec.clone(),
|
|
url: item.url.clone(),
|
|
epg_channel_id: item.epg_channel_id.clone(),
|
|
xtream_cluster: XtreamCluster::try_from(item.item_type).unwrap_or(XtreamCluster::Live),
|
|
item_type: item.item_type,
|
|
category_id: 0,
|
|
input_name: item.input_name.clone(),
|
|
chno: item.chno,
|
|
audio_track: item.audio_track.clone(),
|
|
time_shift: item.time_shift.clone(),
|
|
additional_properties: None,
|
|
source_ordinal: item.source_ordinal,
|
|
};
|
|
|
|
PlaylistItem {
|
|
header
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
impl PlaylistEntry for PlaylistItem {
|
|
#[inline]
|
|
fn get_virtual_id(&self) -> VirtualId {
|
|
self.header.virtual_id
|
|
}
|
|
|
|
fn get_provider_id(&self) -> Option<u32> {
|
|
let header = &self.header;
|
|
get_provider_id(&header.id, &header.url)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_category_id(&self) -> Option<u32> {
|
|
Some(self.header.category_id)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_provider_url(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(self.header.url.as_str())
|
|
}
|
|
|
|
#[inline]
|
|
fn get_uuid(&self) -> UUIDType {
|
|
let header = &self.header;
|
|
generate_playlist_uuid(&header.input_name, &header.id, header.item_type, &header.url)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_item_type(&self) -> PlaylistItemType {
|
|
self.header.item_type
|
|
}
|
|
|
|
#[inline]
|
|
fn get_group(&self) -> Cow<'_, str> {
|
|
Cow::Borrowed(&*self.header.group)
|
|
}
|
|
|
|
#[inline]
|
|
fn get_name(&self) -> Cow<'_, str> {
|
|
if self.header.title.is_empty() {
|
|
Cow::Borrowed(self.header.name.as_str())
|
|
} else {
|
|
Cow::Borrowed(self.header.title.as_str())
|
|
}
|
|
}
|
|
|
|
fn get_resolved_info_document(&self, options: &XtreamMappingOptions) -> Option<XtreamInfoDocument> {
|
|
if self.has_details() {
|
|
self.header.additional_properties.as_ref().map(|p|
|
|
p.to_info_document(options, self.get_item_type(), self.get_virtual_id(),
|
|
self.get_category_id().unwrap_or(0)))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_additional_properties(&self) -> Option<&StreamProperties> {
|
|
self.header.additional_properties.as_ref()
|
|
}
|
|
#[inline]
|
|
fn get_additional_properties_mut(&mut self) -> Option<&mut StreamProperties> {
|
|
self.header.additional_properties.as_mut()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PlaylistGroup {
|
|
pub id: u32,
|
|
#[serde(with = "arc_str_serde")]
|
|
pub title: Arc<str>,
|
|
pub channels: Vec<PlaylistItem>,
|
|
pub xtream_cluster: XtreamCluster,
|
|
}
|
|
|
|
impl PlaylistGroup {
|
|
#[inline]
|
|
pub fn on_load(&mut self) {
|
|
for pl in &mut self.channels {
|
|
pl.header.gen_uuid();
|
|
pl.header.category_id = self.id;
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn filter_count<F>(&self, filter: F) -> usize
|
|
where
|
|
F: Fn(&PlaylistItem) -> bool,
|
|
{
|
|
self.channels.iter().filter(|&c| filter(c)).count()
|
|
}
|
|
}
|