Files
tuliprox/backend/src/processing/processor/xtream_series.rs
T

272 lines
11 KiB
Rust
Raw Normal View History

2025-12-17 09:03:07 +01:00
use crate::model::FetchedPlaylist;
2025-12-27 21:42:09 +01:00
use crate::model::{AppConfig, ConfigTarget};
2025-02-05 20:01:49 +01:00
use crate::processing::parser::xtream::parse_xtream_series_info;
2026-01-02 13:18:08 +01:00
use crate::processing::processor::create_resolve_options_function_for_xtream_target;
2025-12-17 09:03:07 +01:00
use crate::processing::processor::playlist::ProcessingPipe;
2025-12-27 21:42:09 +01:00
use crate::processing::processor::xtream::playlist_resolve_download_playlist_item;
2026-01-20 18:51:46 +01:00
use crate::repository::{get_input_storage_path, persist_input_series_info_batch, MemoryPlaylistSource, PlaylistSource};
2025-12-25 12:33:28 +01:00
use log::{error, info, log_enabled, Level};
2025-12-17 09:03:07 +01:00
use shared::error::TuliproxError;
2025-12-25 12:33:28 +01:00
use shared::model::{InputType, PlaylistEntry, SeriesStreamProperties, StreamProperties, XtreamSeriesInfo};
2026-02-10 19:37:20 +01:00
use shared::model::{PlaylistGroup, PlaylistItemType, UUIDType, XtreamCluster};
use indexmap::IndexMap;
2026-02-10 19:37:20 +01:00
use std::collections::{HashMap, HashSet};
2025-12-25 12:33:28 +01:00
use std::sync::Arc;
2025-12-17 09:03:07 +01:00
use std::time::Instant;
2024-12-19 13:02:10 +01:00
2024-12-22 10:16:23 +01:00
create_resolve_options_function_for_xtream_target!(series);
2024-12-19 13:02:10 +01:00
const BATCH_SIZE: usize = 100;
#[allow(clippy::too_many_lines)]
2026-01-02 13:18:08 +01:00
async fn playlist_resolve_series_info(app_config: &Arc<AppConfig>, client: &reqwest::Client,
errors: &mut Vec<TuliproxError>,
fpl: &mut FetchedPlaylist<'_>,
resolve_series: bool,
2026-01-20 10:09:58 +01:00
resolve_delay: u16) -> Vec<PlaylistGroup> {
2025-12-25 12:33:28 +01:00
let input = fpl.input;
let working_dir = &app_config.config.load().working_dir;
2026-01-30 00:20:13 +01:00
let storage_path = match get_input_storage_path(&input.name, working_dir).await {
2025-12-25 12:33:28 +01:00
Ok(storage_path) => storage_path,
Err(err) => {
2026-01-20 10:09:58 +01:00
error!("Can't resolve series info, input storage directory for input '{}' failed: {err}", input.name);
2025-12-25 12:33:28 +01:00
return vec![];
}
2025-12-25 12:33:28 +01:00
};
2024-12-19 13:02:10 +01:00
2026-01-20 10:09:58 +01:00
let series_info_count = if resolve_series {
let series_info_count = fpl.get_missing_series_info_count();
if series_info_count > 0 {
info!("Found {series_info_count} series info to resolve");
}
series_info_count
} else {
0
};
2024-12-30 10:27:25 +01:00
let mut last_log_time = Instant::now();
2024-12-31 14:40:31 +01:00
let mut processed_series_info_count = 0;
let mut group_series: IndexMap<u32, PlaylistGroup> = IndexMap::new();
let mut batch = Vec::with_capacity(BATCH_SIZE);
2026-01-02 13:18:08 +01:00
let input = fpl.input;
for pli in fpl.items_mut() {
if pli.header.xtream_cluster != XtreamCluster::Series
|| pli.header.item_type != PlaylistItemType::SeriesInfo {
continue;
}
2026-01-02 13:18:08 +01:00
let Some(provider_id) = pli.get_provider_id() else { continue; };
if provider_id == 0 {
continue;
}
let should_download = resolve_series && !pli.has_details();
if should_download {
processed_series_info_count += 1;
2026-01-20 18:51:46 +01:00
if let Some(content) = playlist_resolve_download_playlist_item(
app_config,
2026-01-20 18:51:46 +01:00
client,
pli,
input,
errors,
resolve_delay,
XtreamCluster::Series,
)
.await
{
if !content.is_empty() {
2026-01-21 15:32:46 +01:00
//tokio::fs::write(&storage_path.join(format!("{provider_id}_series_info.json")), &content).await.ok();
match serde_json::from_str::<XtreamSeriesInfo>(&content) {
Ok(info) => {
let series_stream_props = SeriesStreamProperties::from_info(&info, pli);
batch.push((provider_id, series_stream_props.clone()));
if batch.len() >= BATCH_SIZE {
if let Err(err) = persist_input_series_info_batch(app_config, &storage_path, XtreamCluster::Series, &input.name, std::mem::take(&mut batch)).await {
error!("Failed to persist batch series info: {err}");
}
2025-12-27 21:42:09 +01:00
}
// Update in-memory playlist items with the newly fetched vod info.
// This makes the data available for later processing steps like STRM export.
pli.header.additional_properties = Some(StreamProperties::Series(Box::new(series_stream_props)));
}
Err(err) => {
error!("Failed to parse series info for provider_id {provider_id}: {err}");
}
}
}
}
}
2026-01-02 13:18:08 +01:00
// extract episodes from info
if let Some(StreamProperties::Series(properties)) = pli.header.additional_properties.as_ref() {
let (group, series_name) = {
let header = &pli.header;
(header.group.clone(), if header.name.is_empty() { header.title.clone() } else { header.name.clone() })
};
2026-01-20 10:09:58 +01:00
if let Some(episodes) = parse_xtream_series_info(&pli.get_uuid(), properties, &group, &series_name, input) {
let group = group_series.entry(pli.header.category_id)
.or_insert_with(|| {
PlaylistGroup {
id: pli.header.category_id,
title: pli.header.group.clone(),
channels: Vec::new(),
xtream_cluster: XtreamCluster::Series,
}
});
group.channels.extend(episodes.into_iter());
2026-01-02 13:18:08 +01:00
}
}
2026-01-02 13:18:08 +01:00
if resolve_series && log_enabled!(Level::Info) && last_log_time.elapsed().as_secs() >= 30 {
info!("resolved {processed_series_info_count}/{series_info_count} series info");
last_log_time = Instant::now();
2024-12-23 11:30:33 +01:00
}
}
if !batch.is_empty() {
if let Err(err) = persist_input_series_info_batch(app_config, &storage_path, XtreamCluster::Series, &input.name, batch).await {
error!("Failed to persist final batch series info: {err}");
2024-12-23 11:30:33 +01:00
}
}
2026-01-02 13:18:08 +01:00
if resolve_series {
info!("resolved {processed_series_info_count}/{series_info_count} series info");
}
group_series.into_values().collect()
2024-12-23 11:30:33 +01:00
}
2025-12-25 12:33:28 +01:00
2026-02-10 19:37:20 +01:00
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
2025-12-25 12:33:28 +01:00
pub async fn playlist_resolve_series(cfg: &Arc<AppConfig>,
2025-12-11 09:53:49 +01:00
client: &reqwest::Client,
2025-06-24 01:06:25 +02:00
target: &ConfigTarget,
2025-05-13 21:50:08 +02:00
errors: &mut Vec<TuliproxError>,
2024-12-22 10:16:23 +01:00
pipe: &ProcessingPipe,
provider_fpl: &mut FetchedPlaylist<'_>,
processed_fpl: &mut FetchedPlaylist<'_>,
) {
let (resolve_series, resolve_delay) = get_resolve_series_options(target, processed_fpl);
2026-01-02 13:18:08 +01:00
provider_fpl.source.release_resources(XtreamCluster::Series);
2026-01-20 10:09:58 +01:00
let series_playlist = playlist_resolve_series_info(cfg, client, errors, processed_fpl, resolve_series, resolve_delay).await;
provider_fpl.source.obtain_resources().await;
2025-12-30 12:31:55 +01:00
2026-02-10 19:37:20 +01:00
// Keep input playlist as canonical source:
// - Store resolved series details back into provider_fpl items
// - Rebuild base (unmodified) episodes from provider_fpl metadata only
if provider_fpl.is_memory() {
2026-02-10 19:37:20 +01:00
let mut resolved_series_by_provider_id: HashMap<u32, SeriesStreamProperties> = HashMap::new();
for pli in processed_fpl.items() {
if pli.header.xtream_cluster != XtreamCluster::Series
|| pli.header.item_type != PlaylistItemType::SeriesInfo
{
continue;
}
let Some(provider_id) = pli.get_provider_id() else { continue; };
if provider_id == 0 {
continue;
}
if let Some(StreamProperties::Series(properties)) = pli.header.additional_properties.as_ref() {
resolved_series_by_provider_id
.entry(provider_id)
.or_insert_with(|| properties.as_ref().clone());
}
}
if !resolved_series_by_provider_id.is_empty() {
let mut existing_uuids: HashSet<UUIDType> = provider_fpl
.items()
.filter(|pli| {
pli.header.xtream_cluster == XtreamCluster::Series
&& pli.header.item_type == PlaylistItemType::Series
})
.map(|pli| pli.get_uuid())
.collect();
let mut base_series_groups: IndexMap<u32, PlaylistGroup> = IndexMap::new();
let input = provider_fpl.input;
for pli in provider_fpl.items_mut() {
if pli.header.xtream_cluster != XtreamCluster::Series
|| pli.header.item_type != PlaylistItemType::SeriesInfo
{
continue;
}
let Some(provider_id) = pli.get_provider_id() else { continue; };
if provider_id == 0 {
continue;
}
if let Some(resolved) = resolved_series_by_provider_id.get(&provider_id) {
pli.header.additional_properties = Some(StreamProperties::Series(Box::new(resolved.clone())));
}
let Some(StreamProperties::Series(series_props)) = pli.header.additional_properties.as_ref() else {
continue;
};
let (category_id, group_title, series_name) = {
let header = &pli.header;
(
header.category_id,
header.group.clone(),
if header.name.is_empty() {
header.title.clone()
} else {
header.name.clone()
},
)
};
if let Some(episodes) = parse_xtream_series_info(
&pli.get_uuid(),
series_props,
&group_title,
&series_name,
input,
) {
let group = base_series_groups.entry(category_id).or_insert_with(|| PlaylistGroup {
id: category_id,
title: group_title,
channels: Vec::new(),
xtream_cluster: XtreamCluster::Series,
});
for episode in episodes {
if existing_uuids.insert(episode.get_uuid()) {
group.channels.push(episode);
}
}
}
}
for group in base_series_groups.values().filter(|g| !g.channels.is_empty()) {
provider_fpl.update_playlist(group).await;
}
}
2024-12-23 11:30:33 +01:00
}
2026-02-10 19:37:20 +01:00
if series_playlist.is_empty() {
return;
}
// run the target processing pipe over newly derived episodes
// run the processing pipe over new items
let mut new_playlist = series_playlist;
2024-12-23 11:30:33 +01:00
for f in pipe {
let mut source = MemoryPlaylistSource::new(new_playlist);
2026-01-20 10:09:58 +01:00
if let Some(v) = f(&mut source, target) {
new_playlist = v;
} else {
new_playlist = source.take_groups();
2024-12-22 10:16:23 +01:00
}
}
2024-12-23 11:30:33 +01:00
// assign new items to the new playlist
for plg in &new_playlist {
processed_fpl.update_playlist(plg).await;
2024-12-23 11:30:33 +01:00
}
2024-12-19 13:02:10 +01:00
}