mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-08 08:52:05 +02:00
Code restructuring
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
use crate::model::api_proxy::ProxyUserCredentials;
|
||||
use std::str;
|
||||
use crate::model::config::TargetType;
|
||||
|
||||
// /hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}
|
||||
#[derive(Debug)]
|
||||
pub struct HlsrPath {
|
||||
token: String,
|
||||
// username: String,
|
||||
// password: String,
|
||||
// channel: String,
|
||||
hash: String,
|
||||
chunk: String,
|
||||
}
|
||||
fn parse_hlsr_path(input: &str) -> Option<HlsrPath> {
|
||||
let parts: Vec<&str> = input.split('/').collect();
|
||||
|
||||
if parts.len() != 8 || !parts[0].is_empty() || parts[1] != "hlsr" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(HlsrPath {
|
||||
token: parts[2].to_string(),
|
||||
// username: parts[3].to_string(),
|
||||
// password: parts[4].to_string(),
|
||||
// channel: parts[5].to_string(),
|
||||
hash: parts[6].to_string(),
|
||||
chunk: parts[7].to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub const M3U_HLSR_PREFIX: &str = "mhlsr";
|
||||
|
||||
pub fn rewrite_hls_url(stream_id: u32, username: &str, password: &str, hlsr: &HlsrPath, target_type: &TargetType) -> String {
|
||||
let prefix = if *target_type == TargetType::Xtream { "hlsr" } else { M3U_HLSR_PREFIX };
|
||||
format!("/{prefix}/{}/{username}/{password}/{stream_id}/{}/{}", hlsr.token, hlsr.hash, hlsr.chunk)
|
||||
}
|
||||
|
||||
pub fn rewrite_hls(content: &str, virtual_id: u32, user: &ProxyUserCredentials, target_type: &TargetType) -> String {
|
||||
content.lines().map(|line| {
|
||||
if line.starts_with('#') {
|
||||
line.to_string()
|
||||
} else {
|
||||
match parse_hlsr_path(line) {
|
||||
None => line.to_string(),
|
||||
Some(hlsr) => rewrite_hls_url(virtual_id, &user.username, &user.password, &hlsr, target_type)
|
||||
}
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
.join("\r\n")
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
use std::borrow::BorrowMut;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::model::config::{Config, ConfigInput};
|
||||
use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
|
||||
use crate::utils::hash_utils::extract_id_from_url;
|
||||
use crate::utils::string_utils;
|
||||
|
||||
#[inline]
|
||||
fn token_value(it: &mut std::str::Chars) -> String {
|
||||
// Use .find() to skip until the first double quote (") character.
|
||||
if it.any(|ch| ch == '"') {
|
||||
// If a quote is found, call get_value to extract the value.
|
||||
return get_value(it);
|
||||
}
|
||||
// If no double quote is found, return an empty string.
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn get_value(it: &mut std::str::Chars) -> String {
|
||||
let mut result = String::with_capacity(128);
|
||||
for oc in it.by_ref() {
|
||||
if oc == '"' {
|
||||
break;
|
||||
}
|
||||
result.push(oc);
|
||||
}
|
||||
result.shrink_to_fit();
|
||||
result
|
||||
}
|
||||
|
||||
fn token_till(it: &mut std::str::Chars, stop_char: char, start_with_alpha: bool) -> Option<String> {
|
||||
let mut result = String::with_capacity(128);
|
||||
let mut skip_non_alpha = start_with_alpha;
|
||||
|
||||
for ch in it.by_ref() {
|
||||
if ch == stop_char {
|
||||
break;
|
||||
}
|
||||
if result.is_empty() && ch.is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if skip_non_alpha {
|
||||
if ch.is_alphabetic() {
|
||||
skip_non_alpha = false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(ch);
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
None
|
||||
} else {
|
||||
result.shrink_to_fit();
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn skip_digit(it: &mut std::str::Chars) -> Option<char> {
|
||||
loop {
|
||||
match it.next() {
|
||||
Some(c) => {
|
||||
if !(c == '-' || c == '+' || c.is_ascii_digit()) {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_empty_playlistitem_header(input_name: &str, url: &str) -> PlaylistItemHeader {
|
||||
PlaylistItemHeader {
|
||||
url: Rc::new(url.to_owned()),
|
||||
category_id: 0,
|
||||
input_name: Rc::new(input_name.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! process_header_fields {
|
||||
($header:expr, $token:expr, $(($prop:ident, $field:expr)),*; $val:expr) => {
|
||||
match $token {
|
||||
$(
|
||||
$field => $header.$prop = Rc::new($val),
|
||||
)*
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, url: &str) -> PlaylistItemHeader {
|
||||
let mut plih = create_empty_playlistitem_header(input.name.as_str(), url);
|
||||
let mut it = content.chars();
|
||||
let line_token = token_till(&mut it, ':', false);
|
||||
if line_token.as_deref() == Some("#EXTINF") {
|
||||
let mut c = skip_digit(&mut it);
|
||||
loop {
|
||||
if c.is_none() {
|
||||
break;
|
||||
}
|
||||
if c.unwrap() == ',' {
|
||||
plih.title = Rc::new(get_value(&mut it));
|
||||
} else {
|
||||
let token = token_till(&mut it, '=', true);
|
||||
if let Some(t) = token {
|
||||
let value = token_value(&mut it);
|
||||
process_header_fields!(plih, t.to_lowercase().as_str(),
|
||||
(id, "tvg-id"),
|
||||
(group, "group-title"),
|
||||
(name, "tvg-name"),
|
||||
(chno, "tvg-chno"),
|
||||
(parent_code, "parent-code"),
|
||||
(audio_track, "audio-track"),
|
||||
(logo, "tvg-logo"),
|
||||
(logo_small, "tvg-logo-small"),
|
||||
(time_shift, "timeshift"),
|
||||
(rec, "tvg-rec"); value);
|
||||
}
|
||||
}
|
||||
c = it.next();
|
||||
}
|
||||
if plih.id.is_empty() {
|
||||
if let Some(chanid) = extract_id_from_url(url) {
|
||||
plih.id = Rc::new(chanid);
|
||||
}
|
||||
}
|
||||
// plih.virtual_id = plih.id;
|
||||
plih.epg_channel_id = Some(Rc::clone(&plih.id));
|
||||
}
|
||||
|
||||
if video_suffixes.iter().any(|suffix| url.ends_with(suffix)) {
|
||||
// TODO find Series based on group or configured names
|
||||
plih.xtream_cluster = XtreamCluster::Video;
|
||||
plih.item_type = PlaylistItemType::Video;
|
||||
}
|
||||
|
||||
{
|
||||
let header = plih.borrow_mut();
|
||||
if header.name.is_empty() {
|
||||
if !header.title.is_empty() {
|
||||
header.name = header.title.clone();
|
||||
} else if !header.id.is_empty() {
|
||||
header.name = header.id.clone();
|
||||
header.title = header.id.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plih
|
||||
}
|
||||
|
||||
|
||||
pub fn consume_m3u<'a, I, F: FnMut(PlaylistItem)>(cfg: &Config, input: &ConfigInput, lines: I, mut visit: F)
|
||||
where
|
||||
I: Iterator<Item=&'a str>,
|
||||
{
|
||||
let mut header: Option<String> = None;
|
||||
let mut group: Option<String> = None;
|
||||
|
||||
let video_suffixes = cfg.video.as_ref().unwrap().extensions.iter().map(String::as_str).collect::<Vec<&str>>();
|
||||
for line in lines {
|
||||
if line.starts_with("#EXTINF") {
|
||||
header = Some(String::from(line));
|
||||
continue;
|
||||
}
|
||||
if line.starts_with("#EXTGRP") {
|
||||
group = Some(String::from(&line[8..]));
|
||||
continue;
|
||||
}
|
||||
if line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
if let Some(header_value) = header {
|
||||
let item = PlaylistItem { header: RefCell::new(process_header(input, &video_suffixes, &header_value, line)) };
|
||||
let mut header = item.header.borrow_mut();
|
||||
if header.group.is_empty() {
|
||||
if let Some(group_value) = group {
|
||||
header.group = Rc::new(group_value);
|
||||
} else {
|
||||
let current_title = header.title.clone();
|
||||
header.group = Rc::new(string_utils::get_title_group(current_title.as_str()));
|
||||
}
|
||||
}
|
||||
drop(header);
|
||||
visit(item);
|
||||
}
|
||||
header = None;
|
||||
group = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_m3u<'a, I>(cfg: &Config, input: &ConfigInput, lines: I) -> Vec<PlaylistGroup>
|
||||
where
|
||||
I: Iterator<Item=&'a str>,
|
||||
{
|
||||
let mut sort_order: Vec<Vec<PlaylistItem>> = vec![];
|
||||
let mut sort_order_idx: usize = 0;
|
||||
let mut group_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
consume_m3u(cfg, input, lines, |item| {
|
||||
// keep the original sort order for groups and group the playlist items
|
||||
let key = {
|
||||
let header = item.header.borrow();
|
||||
format!("{}{}", &header.xtream_cluster, &header.group)
|
||||
};
|
||||
match group_map.entry(key) {
|
||||
std::collections::hash_map::Entry::Vacant(v) => {
|
||||
v.insert(sort_order_idx);
|
||||
sort_order.push(vec![item]);
|
||||
sort_order_idx += 1;
|
||||
}
|
||||
std::collections::hash_map::Entry::Occupied(o) => {
|
||||
sort_order.get_mut(*o.get()).unwrap().push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut grp_id = 0;
|
||||
let result: Vec<PlaylistGroup> = sort_order.into_iter().map(|channels| {
|
||||
// create a group based on the first playlist item
|
||||
let channel = channels.first();
|
||||
let (cluster, group_title) = channel.map(|pli|
|
||||
(pli.header.borrow().xtream_cluster, Rc::clone(&pli.header.borrow().group))).unwrap();
|
||||
grp_id += 1;
|
||||
PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: Rc::clone(&group_title), channels }
|
||||
}).collect();
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod m3u;
|
||||
pub mod xtream;
|
||||
pub mod xmltv;
|
||||
pub mod hls;
|
||||
@@ -0,0 +1,196 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader;
|
||||
|
||||
use crate::model::xmltv::{Epg, EPG_ATTRIB_CHANNEL, EPG_ATTRIB_ID, EPG_TAG_TV, EPG_TAG_CHANNEL, EPG_TAG_PROGRAMME, TVGuide, XmlTag};
|
||||
use crate::utils::compression::compressed_file_reader::CompressedFileReader;
|
||||
|
||||
impl TVGuide {
|
||||
pub fn filter(&self, channel_ids: &HashSet<Rc<String>>) -> Option<Epg> {
|
||||
if channel_ids.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match CompressedFileReader::new(&self.file) {
|
||||
Ok(mut reader) => {
|
||||
let mut children: Vec<XmlTag> = vec![];
|
||||
let mut tv_attributes: Option<Rc<HashMap<String, String>>> = None;
|
||||
let mut filter_tags = |tag: XmlTag| {
|
||||
if match tag.name.as_str() {
|
||||
EPG_TAG_CHANNEL => {
|
||||
tag.get_attribute_value(EPG_ATTRIB_ID).is_some_and(|val| channel_ids.contains(val))
|
||||
}
|
||||
EPG_TAG_PROGRAMME => {
|
||||
tag.get_attribute_value(EPG_ATTRIB_CHANNEL).is_some_and(|val| channel_ids.contains(val))
|
||||
},
|
||||
EPG_TAG_TV => {
|
||||
tv_attributes.clone_from(&tag.attributes);
|
||||
false
|
||||
},
|
||||
_ => false,
|
||||
} {
|
||||
children.push(tag);
|
||||
}
|
||||
};
|
||||
|
||||
parse_tvguide(&mut reader, &mut filter_tags);
|
||||
|
||||
if children.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Epg {
|
||||
attributes: tv_attributes,
|
||||
children,
|
||||
})
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_tvguide<R, F>(content: R, callback: &mut F)
|
||||
where
|
||||
R: std::io::BufRead,
|
||||
F: FnMut(XmlTag),
|
||||
{
|
||||
let mut stack: Vec<XmlTag> = vec![];
|
||||
let mut reader = Reader::from_reader(content);
|
||||
let mut buf = Vec::<u8>::new();
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Eof) => break,
|
||||
Ok(Event::Start(e)) => {
|
||||
let name = String::from_utf8_lossy(e.name().as_ref()).to_string();
|
||||
let is_tv_tag = name == EPG_TAG_TV;
|
||||
let attributes = e.attributes().filter_map(Result::ok)
|
||||
.filter_map(|a| {
|
||||
let key = String::from_utf8_lossy(a.key.as_ref()).to_string();
|
||||
let value = String::from(a.unescape_value().unwrap().as_ref());
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((key, value))
|
||||
}
|
||||
}).collect::<HashMap<String, String>>();
|
||||
let tag = XmlTag {
|
||||
name,
|
||||
value: None,
|
||||
attributes: if attributes.is_empty() { None } else { Some(Rc::new(attributes)) },
|
||||
children: None,
|
||||
};
|
||||
|
||||
if is_tv_tag {
|
||||
callback(tag);
|
||||
} else {
|
||||
stack.push(tag);
|
||||
}
|
||||
}
|
||||
Ok(Event::End(_e)) => {
|
||||
if !stack.is_empty() {
|
||||
if let Some(tag) = stack.pop() {
|
||||
if tag.name == EPG_TAG_CHANNEL {
|
||||
if let Some(chan_id) = tag.get_attribute_value(EPG_ATTRIB_ID) {
|
||||
if !chan_id.is_empty() {
|
||||
callback(tag);
|
||||
}
|
||||
}
|
||||
} else if tag.name == EPG_TAG_PROGRAMME {
|
||||
if let Some(chan_id) = tag.get_attribute_value(EPG_ATTRIB_CHANNEL) {
|
||||
if !chan_id.is_empty() {
|
||||
callback(tag);
|
||||
}
|
||||
}
|
||||
} else if !stack.is_empty() {
|
||||
if let Some(old_tag) = stack.pop().map(|mut r| {
|
||||
let rc_tag = Rc::new(tag);
|
||||
r.children = Some(
|
||||
r.children.map_or_else(|| vec![rc_tag.clone()], |mut c| {
|
||||
c.push(rc_tag.clone());
|
||||
c
|
||||
}));
|
||||
r
|
||||
}) {
|
||||
stack.push(old_tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::Text(e)) => {
|
||||
let text = e.unescape().unwrap().trim().to_owned();
|
||||
if !text.is_empty() && !stack.is_empty() {
|
||||
stack.last_mut().unwrap().value = Some(text);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flatten_tvguide(tv_guides: &[Epg]) -> Option<Epg> {
|
||||
if tv_guides.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut epg = Epg {
|
||||
attributes: None,
|
||||
children: vec![],
|
||||
};
|
||||
let mut channel_ids: Vec<&String> = vec![];
|
||||
for guide in tv_guides {
|
||||
if epg.attributes.is_none() {
|
||||
epg.attributes.clone_from(&guide.attributes);
|
||||
}
|
||||
guide.children.iter().for_each(|c| {
|
||||
if c.name.as_str() == EPG_TAG_CHANNEL {
|
||||
if let Some(chan_id) = c.get_attribute_value(EPG_ATTRIB_ID) {
|
||||
if !channel_ids.contains(&chan_id) {
|
||||
channel_ids.push(chan_id);
|
||||
epg.children.push(c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
guide.children.iter().for_each(|c| {
|
||||
if c.name.as_str() == EPG_TAG_PROGRAMME {
|
||||
if let Some(chan_id) = c.get_attribute_value(EPG_TAG_CHANNEL) {
|
||||
if channel_ids.contains(&chan_id) {
|
||||
epg.children.push(c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Some(epg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::model::xmltv::{TVGuide};
|
||||
|
||||
#[test]
|
||||
fn parse_test() -> io::Result<()> {
|
||||
let file_path = "/tmp/epg.xml.gz";
|
||||
|
||||
let tv_guide = TVGuide { file: PathBuf::from(file_path) };
|
||||
|
||||
let channel_ids = vec!["channel.1", "channel.2", "channel.3"];
|
||||
let channel_ids : HashSet<Rc<String>> = channel_ids.into_iter().map(|s| Rc::new(s.to_string())).collect();
|
||||
|
||||
match tv_guide.filter(&channel_ids) {
|
||||
None => assert!(false, "No epg filtered"),
|
||||
Some(epg) => {
|
||||
assert_eq!(epg.children.len(), channel_ids.len() * 2, "Epg size does not match")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result};
|
||||
use crate::model::config::ConfigInput;
|
||||
use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster};
|
||||
use crate::model::xtream::{XtreamCategory, XtreamSeriesInfo, XtreamSeriesInfoEpisode, XtreamStream};
|
||||
use crate::utils::hash_utils::generate_playlist_uuid;
|
||||
use crate::utils::network::xtream::{get_xtream_stream_url_base, ACTION_GET_SERIES_INFO};
|
||||
|
||||
fn map_to_xtream_category(categories: &Value) -> Result<Vec<XtreamCategory>, M3uFilterError> {
|
||||
match serde_json::from_value::<Vec<XtreamCategory>>(categories.to_owned()) {
|
||||
Ok(xtream_categories) => Ok(xtream_categories),
|
||||
Err(err) => {
|
||||
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to process categories {}", &err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: &Value) -> Result<Vec<XtreamStream>, M3uFilterError> {
|
||||
match serde_json::from_value::<Vec<XtreamStream>>(streams.to_owned()) {
|
||||
Ok(stream_list) => Ok(stream_list),
|
||||
Err(err) => {
|
||||
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to map to xtream streams {:?}: {}", xtream_cluster, &err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &XtreamSeriesInfoEpisode) -> Rc<String> {
|
||||
if episode.direct_source.is_empty() {
|
||||
let ext = episode.container_extension.clone();
|
||||
let stream_base_url = format!("{url}/series/{username}/{password}/{}.{ext}", episode.id);
|
||||
Rc::new(stream_base_url)
|
||||
} else {
|
||||
Rc::new(episode.direct_source.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &str, input: &ConfigInput) -> Result<Option<Vec<(XtreamSeriesInfoEpisode, PlaylistItem)>>, M3uFilterError> {
|
||||
let url = input.url.as_str();
|
||||
let username = input.username.as_ref().map_or("", |v| v);
|
||||
let password = input.password.as_ref().map_or("", |v| v);
|
||||
|
||||
match serde_json::from_value::<XtreamSeriesInfo>(info.to_owned()) {
|
||||
Ok(series_info) => {
|
||||
if let Some(episodes) = &series_info.episodes {
|
||||
let result: Vec<(XtreamSeriesInfoEpisode, PlaylistItem)> = episodes.values().flatten().map(|episode| {
|
||||
let episode_url = create_xtream_series_episode_url(url, username, password, episode);
|
||||
(episode.clone(),
|
||||
PlaylistItem {
|
||||
header: RefCell::new(PlaylistItemHeader {
|
||||
id: Rc::new(episode.id.to_string()),
|
||||
uuid: Rc::new(generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url)),
|
||||
name: Rc::new(series_name.to_string()),
|
||||
logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())),
|
||||
group: Rc::new(group_title.to_string()),
|
||||
title: Rc::new(episode.title.clone()),
|
||||
url: episode_url,
|
||||
item_type: PlaylistItemType::Series,
|
||||
xtream_cluster: XtreamCluster::Series,
|
||||
additional_properties: episode.get_additional_properties(&series_info),
|
||||
category_id: 0,
|
||||
input_name: Rc::new(input.name.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
return if result.is_empty() { Ok(None) } else { Ok(Some(result)) };
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
create_m3u_filter_error_result!(M3uFilterErrorKind::Notify, "Failed to process series info for {series_name} {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str,
|
||||
stream: &XtreamStream, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> Rc<String> {
|
||||
if stream.direct_source.is_empty() {
|
||||
let stream_base_url = match xtream_cluster {
|
||||
XtreamCluster::Live => {
|
||||
let ctx_path = if live_stream_use_prefix {"live/"} else { "" };
|
||||
let suffix = if live_stream_without_extension { "" } else { ".ts" };
|
||||
format!("{url}/{ctx_path}{username}/{password}/{}{suffix}", &stream.get_stream_id())
|
||||
},
|
||||
XtreamCluster::Video => {
|
||||
let ext = stream.container_extension.as_ref().map_or("mp4", |e| e.as_str());
|
||||
format!("{url}/movie/{username}/{password}/{}.{ext}", &stream.get_stream_id())
|
||||
}
|
||||
XtreamCluster::Series =>
|
||||
format!("{}&action={ACTION_GET_SERIES_INFO}&series_id={}", get_xtream_stream_url_base(url, username, password), &stream.get_stream_id())
|
||||
};
|
||||
Rc::new(stream_base_url)
|
||||
} else {
|
||||
Rc::clone(&stream.direct_source)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_xtream(input: &ConfigInput,
|
||||
xtream_cluster: XtreamCluster,
|
||||
categories: &Value,
|
||||
streams: &Value) -> Result<Option<Vec<PlaylistGroup>>, M3uFilterError> {
|
||||
match map_to_xtream_category(categories) {
|
||||
Ok(xtream_categories) => {
|
||||
let input_name = Rc::new(input.name.to_string());
|
||||
let url = input.url.as_str();
|
||||
let username = input.username.as_ref().map_or("", |v| v);
|
||||
let password = input.password.as_ref().map_or("", |v| v);
|
||||
|
||||
match map_to_xtream_streams(xtream_cluster, streams) {
|
||||
Ok(xtream_streams) => {
|
||||
let mut group_map: HashMap::<Rc<String>, RefCell<XtreamCategory>> =
|
||||
xtream_categories.into_iter().map(|category|
|
||||
(Rc::clone(&category.category_id), RefCell::new(category))
|
||||
).collect();
|
||||
let unknown_grp = RefCell::new(XtreamCategory {
|
||||
category_id: Rc::new("0".to_string()),
|
||||
category_name: Rc::new("Unknown".to_string()),
|
||||
channels: vec![],
|
||||
});
|
||||
|
||||
let (live_stream_use_prefix, live_stream_without_extension) = input.options.as_ref()
|
||||
.map_or((true, false), |o| (o.xtream_live_stream_use_prefix, o.xtream_live_stream_without_extension));
|
||||
|
||||
for stream in xtream_streams {
|
||||
let group = group_map.get(&stream.category_id).unwrap_or(&unknown_grp);
|
||||
let mut grp = group.borrow_mut();
|
||||
let category_name = &grp.category_name;
|
||||
let stream_url = create_xtream_url(xtream_cluster, url, username, password, &stream, live_stream_use_prefix, live_stream_without_extension);
|
||||
let item_type = PlaylistItemType::from(xtream_cluster);
|
||||
let item = PlaylistItem {
|
||||
header: RefCell::new(PlaylistItemHeader {
|
||||
id: Rc::new(stream.get_stream_id().to_string()),
|
||||
uuid: Rc::new(generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url)),
|
||||
name: Rc::clone(&stream.name),
|
||||
logo: Rc::clone(&stream.stream_icon),
|
||||
group: Rc::clone(category_name),
|
||||
title: Rc::clone(&stream.name),
|
||||
url: stream_url,
|
||||
epg_channel_id: stream.epg_channel_id.clone(),
|
||||
item_type,
|
||||
xtream_cluster,
|
||||
additional_properties: stream.get_additional_properties(),
|
||||
category_id: 0,
|
||||
input_name: Rc::clone(&input_name),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
grp.add(item);
|
||||
}
|
||||
let has_channels = !unknown_grp.borrow().channels.is_empty();
|
||||
if has_channels {
|
||||
group_map.insert(Rc::new("0".to_string()), unknown_grp);
|
||||
}
|
||||
|
||||
Ok(Some(group_map.values().filter(|category| !category.borrow().channels.is_empty())
|
||||
.map(|category| {
|
||||
let cat = category.borrow();
|
||||
PlaylistGroup {
|
||||
id: cat.category_id.parse::<u32>().unwrap_or(0),
|
||||
xtream_cluster,
|
||||
title: Rc::clone(&cat.category_name),
|
||||
channels: cat.channels.clone(),
|
||||
}
|
||||
}).collect()))
|
||||
}
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user