2025-06-24 01:06:25 +02:00
use crate ::model ::{ AppConfig , ConfigTarget };
2025-12-17 09:03:07 +01:00
use crate ::model ::FetchedPlaylist ;
2025-02-05 20:01:49 +01:00
use crate ::processing ::parser ::xtream ::parse_xtream_series_info ;
2025-12-17 09:03:07 +01:00
use crate ::processing ::processor ::playlist ::ProcessingPipe ;
2025-06-16 10:48:01 +02:00
use crate ::processing ::processor ::xtream ::{ create_resolve_episode_wal_files , create_resolve_info_wal_files , playlist_resolve_download_playlist_item , read_processed_info_ids , should_update_info };
2025-12-17 09:03:07 +01:00
use crate ::processing ::processor ::{ create_resolve_options_function_for_xtream_target , handle_error , handle_error_and_return };
2024-12-24 15:00:05 +01:00
use crate ::repository ::storage ::get_input_storage_path ;
2025-12-17 17:37:25 +01:00
use crate ::repository ::xtream_repository ::{ write_series_info_to_wal_file , xtream_get_info_file_paths ,
xtream_update_input_info_file , xtream_update_input_series_episodes_record_from_wal_file ,
xtream_update_input_series_record_from_wal_file };
2024-12-24 15:00:05 +01:00
use crate ::repository ::IndexedDocumentReader ;
2025-05-14 18:23:22 +02:00
use crate ::utils ;
2025-12-11 20:02:20 +01:00
use crate ::utils ::{ bincode_serialize , IO_BUFFER_SIZE };
2025-12-17 09:03:07 +01:00
use log ::{ error , info , log_enabled , warn , Level };
use shared ::error ::{ info_err , notify_err };
use shared ::error ::TuliproxError ;
2025-12-22 17:25:11 +01:00
use shared ::model ::{ InputType , SeriesStreamProperties , StreamProperties , XtreamSeriesInfo };
use shared ::model ::{ PlaylistGroup , PlaylistItemType , XtreamCluster };
2025-12-17 09:03:07 +01:00
use std ::collections ::{ HashMap , HashSet };
use std ::time ::Instant ;
use tokio ::io ::AsyncWriteExt ;
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
2025-06-24 01:06:25 +02:00
async fn read_processed_series_info_ids ( cfg : & AppConfig , errors : & mut Vec < TuliproxError > , fpl : & FetchedPlaylist < '_ > ) -> HashMap < u32 , u64 > {
2024-12-24 15:00:05 +01:00
read_processed_info_ids ( cfg , errors , fpl , PlaylistItemType ::SeriesInfo , | ts : & u64 | * ts ). await
2024-12-22 05:00:59 +01:00
}
2025-12-17 09:03:07 +01:00
async fn write_series_episode_record_to_wal_file (
writer : & mut tokio ::io ::BufWriter <& mut tokio ::fs ::File > ,
2024-12-24 15:00:05 +01:00
provider_id : u32 ,
2025-12-22 17:25:11 +01:00
additional_properties : Option <& StreamProperties > ,
2025-12-11 20:18:31 +01:00
) -> std ::io ::Result < usize > {
2025-12-22 17:25:11 +01:00
if let Some ( StreamProperties ::Episode ( series_episode )) = additional_properties {
if let Ok ( content_bytes ) = bincode_serialize ( & series_episode ) {
writer . write_all ( & provider_id . to_le_bytes ()). await ? ;
let content_len = content_bytes . len ();
if let Ok ( len ) = u32 ::try_from ( content_len ) {
writer . write_all ( & len . to_le_bytes ()). await ? ;
writer . write_all ( & content_bytes ). await ? ;
return Ok ( content_len + 4 usize );
}
error! ( "Cant write to WAL file, content length exceeds u32" );
2025-07-22 11:32:46 +02:00
}
2024-12-25 16:56:05 +01:00
}
2025-12-11 20:18:31 +01:00
Ok ( 0 )
2024-12-24 15:00:05 +01:00
}
2025-12-11 09:53:49 +01:00
async fn playlist_resolve_series_info ( cfg : & AppConfig , client : & reqwest ::Client , errors : & mut Vec < TuliproxError > ,
2024-12-30 10:27:25 +01:00
fpl : & mut FetchedPlaylist < '_ > , resolve_delay : u16 ) -> bool {
2025-12-17 09:03:07 +01:00
// TODO read existing WAL File and import it to avoid duplicate requests
2025-12-11 14:32:04 +01:00
let mut processed_info_ids : HashMap < u32 , u64 > = read_processed_series_info_ids ( cfg , errors , fpl ). await ;
2025-11-30 20:29:00 +01:00
let mut fetched_in_run : HashSet < u32 > = HashSet ::new ();
2025-12-17 09:03:07 +01:00
// we can't write to the indexed-document directly because of the write lock and time-consuming operation.
2024-12-19 13:02:10 +01:00
// All readers would be waiting for the lock and the app would be unresponsive.
// We collect the content into a wal file and write it once we collected everything.
2025-12-17 09:03:07 +01:00
let Some (( mut wal_content_file , mut wal_record_file , wal_content_path , wal_record_path ))
= create_resolve_info_wal_files ( & cfg . config . load (), fpl . input , XtreamCluster ::Series ). await
2024-12-26 12:11:28 +01:00
else { return ! processed_info_ids . is_empty (); };
2024-12-22 05:00:59 +01:00
2025-12-17 09:03:07 +01:00
let mut content_writer = utils ::async_file_writer ( & mut wal_content_file );
let mut record_writer = utils ::async_file_writer ( & mut wal_record_file );
2024-12-22 05:00:59 +01:00
let mut content_updated = false ;
2024-12-19 13:02:10 +01:00
2025-03-11 14:56:10 +01:00
let series_info_count = fpl . playlistgroups . iter ()
2024-12-22 05:00:59 +01:00
. filter ( |& plg | plg . xtream_cluster == XtreamCluster ::Series )
2024-12-19 13:02:10 +01:00
. flat_map ( | plg | & plg . channels )
2025-12-17 17:37:25 +01:00
. filter ( |& pli | pli . header . item_type == PlaylistItemType ::SeriesInfo ). count ();
2024-12-30 10:27:25 +01:00
info! ( "Found {series_info_count} series info to resolve" );
2025-12-11 14:32:04 +01:00
let mut last_log_time = Instant ::now ();
2024-12-31 14:40:31 +01:00
let mut processed_series_info_count = 0 ;
2025-12-11 14:32:04 +01:00
let mut write_counter = 0 usize ;
2025-12-17 17:37:25 +01:00
// LocalSeriesInfo entries are not resolved!
2025-12-11 14:32:04 +01:00
for plg in & mut fpl . playlistgroups {
for pli in & mut plg . channels {
2025-12-17 17:37:25 +01:00
if pli . header . item_type != PlaylistItemType ::SeriesInfo {
2025-12-11 14:32:04 +01:00
continue ;
}
2025-12-22 17:25:11 +01:00
let ( should_update , provider_id , ts ) = should_update_info ( pli , & processed_info_ids );
2025-12-11 14:32:04 +01:00
if should_update && provider_id != 0 && fetched_in_run . insert ( provider_id ) {
if let Some ( content ) = playlist_resolve_download_playlist_item ( client , pli , fpl . input , errors , resolve_delay , XtreamCluster ::Series ). await {
2025-12-22 17:25:11 +01:00
if let Ok ( info ) = serde_json ::from_str ::< XtreamSeriesInfo > ( & content ) {
let series_stream_props = SeriesStreamProperties ::from_info ( & info , pli );
handle_error_and_return! ( write_series_info_to_wal_file ( provider_id , ts , & serde_json ::to_string ( & series_stream_props ). unwrap_or_else ( | _ | String ::new ()),
& mut content_writer , & mut record_writer ). await ,
2025-12-11 14:32:04 +01:00
| err | errors . push ( notify_err! ( format! ( "Failed to resolve series, could not write to wal file {err} " ))));
2025-12-22 17:25:11 +01:00
processed_info_ids . insert ( provider_id , ts );
content_updated = true ;
write_counter += content . len ();
// periodic flush to bound BufWriter memory
if write_counter >= IO_BUFFER_SIZE {
write_counter = 0 ;
if let Err ( err ) = content_writer . flush (). await {
errors . push ( notify_err! ( format! ( "Failed periodic flush of wal content writer {err} " )));
}
if let Err ( err ) = record_writer . flush (). await {
errors . push ( notify_err! ( format! ( "Failed periodic flush of wal record writer {err} " )));
}
2025-12-11 14:32:04 +01:00
}
2025-12-22 17:25:11 +01:00
// Update in-memory playlist items with the newly fetched vod info.
// This makes the data available for subsequent processing steps like STRM export.
pli . header . additional_properties = Some ( StreamProperties ::Series ( Box ::new ( series_stream_props )));
2025-12-11 14:32:04 +01:00
}
}
}
if log_enabled! ( Level ::Info ) {
processed_series_info_count += 1 ;
if last_log_time . elapsed (). as_secs () >= 30 {
info! ( "resolved {processed_series_info_count}/{series_info_count} series info" );
last_log_time = Instant ::now ();
}
2025-01-01 11:09:27 +01:00
}
2024-12-30 10:27:25 +01:00
}
2024-12-19 13:02:10 +01:00
}
2025-12-11 14:32:04 +01:00
info! ( "resolved {processed_series_info_count}/{series_info_count} series info" );
2024-12-26 12:11:28 +01:00
// content_wal contains the provider_id and series_info with episode listing
// record_wal contains provider_id and timestamp
2024-12-22 05:00:59 +01:00
if content_updated {
2025-12-17 09:03:07 +01:00
handle_error! ( content_writer . flush (). await ,
2025-12-11 20:02:20 +01:00
| err | errors . push ( notify_err! ( format! ( "Failed to resolve series, could not write to wal file {err} " ))));
2025-12-17 09:03:07 +01:00
handle_error! ( record_writer . flush (). await ,
2025-12-11 20:02:20 +01:00
| err | errors . push ( notify_err! ( format! ( "Failed to resolve series tmdb, could not write to wal file {err} " ))));
2025-12-17 09:03:07 +01:00
handle_error! ( content_writer . get_ref (). sync_all (). await , | err | errors . push ( notify_err! ( format! ( "Failed to sync series info to wal file {err} " ))));
handle_error! ( record_writer . get_ref (). sync_all (). await , | err | errors . push ( notify_err! ( format! ( "Failed to sync series info record to wal file {err} " ))));
2025-06-16 10:48:01 +02:00
drop ( content_writer );
2024-12-22 05:00:59 +01:00
drop ( record_writer );
2025-12-11 14:32:04 +01:00
drop ( wal_content_file );
2024-12-25 12:02:00 +01:00
drop ( wal_record_file );
2024-12-30 10:27:25 +01:00
handle_error! ( xtream_update_input_info_file ( cfg , fpl . input , & wal_content_path , XtreamCluster ::Series ). await ,
2024-12-23 11:30:33 +01:00
| err | errors . push ( err ));
2024-12-30 10:27:25 +01:00
handle_error! ( xtream_update_input_series_record_from_wal_file ( cfg , fpl . input , & wal_record_path ). await ,
2024-12-23 11:30:33 +01:00
| err | errors . push ( err ));
2024-12-19 13:02:10 +01:00
}
2024-12-22 05:00:59 +01:00
2024-12-26 12:11:28 +01:00
// we updated now
// - series_info.db which contains the original series_info json
// - series_record.db which contains the series_info provider_id and timestamp
! processed_info_ids . is_empty ()
2024-12-22 10:16:23 +01:00
}
2025-12-22 17:25:11 +01:00
2024-12-23 11:30:33 +01:00
async fn process_series_info (
2025-06-24 01:06:25 +02:00
app_config : & AppConfig ,
2024-12-30 10:27:25 +01:00
fpl : & mut FetchedPlaylist < '_ > ,
2025-05-13 21:50:08 +02:00
errors : & mut Vec < TuliproxError > ,
2024-12-23 11:30:33 +01:00
) -> Vec < PlaylistGroup > {
let mut result : Vec < PlaylistGroup > = vec! [];
2024-12-30 10:27:25 +01:00
let input = fpl . input ;
2025-06-24 01:06:25 +02:00
let config = app_config . config . load ();
let Ok ( Some (( info_path , idx_path ))) = get_input_storage_path ( & input . name , & config . working_dir )
2024-12-23 11:30:33 +01:00
. map ( | storage_path | xtream_get_info_file_paths ( & storage_path , XtreamCluster ::Series ))
2024-12-24 15:00:05 +01:00
else {
errors . push ( notify_err! ( "Failed to open input info file for series" . to_string ()));
return result ;
2024-12-23 11:30:33 +01:00
};
2025-12-11 14:32:04 +01:00
let mut write_counter = 0 usize ;
2025-11-14 09:49:48 +01:00
let _file_lock = app_config . file_locks . read_lock ( & info_path ). await ;
2024-12-23 11:30:33 +01:00
2024-12-26 12:11:28 +01:00
// Contains the Series Info with episode listing
let Ok ( mut info_reader ) = IndexedDocumentReader ::< u32 , String > ::new ( & info_path , & idx_path ) else { return result ; };
2024-12-24 15:00:05 +01:00
2025-12-17 09:03:07 +01:00
let Some (( mut wal_file , wal_path )) = create_resolve_episode_wal_files ( & config , input ). await else {
2024-12-24 15:00:05 +01:00
errors . push ( notify_err! ( "Could not create wal file for series episodes record" . to_string ()));
return result ;
2024-12-23 11:30:33 +01:00
};
2025-12-17 09:03:07 +01:00
let mut wal_writer = utils ::async_file_writer ( & mut wal_file );
2024-12-23 11:30:33 +01:00
2024-12-30 10:27:25 +01:00
for plg in fpl
2024-12-23 11:30:33 +01:00
. playlistgroups
. iter_mut ()
. filter ( | plg | plg . xtream_cluster == XtreamCluster ::Series )
{
let mut group_series = vec! [];
2025-12-17 17:37:25 +01:00
// Resolve does not handle LocalSeriesInfo
2024-12-23 11:30:33 +01:00
for pli in plg
. channels
2025-03-11 14:56:10 +01:00
. iter_mut ()
2025-12-17 17:37:25 +01:00
. filter ( | pli | pli . header . item_type == PlaylistItemType ::SeriesInfo )
2024-12-23 11:30:33 +01:00
{
2025-03-11 14:56:10 +01:00
let Some ( provider_id ) = pli . header . get_provider_id () else { continue ; };
2024-12-26 12:11:28 +01:00
let Ok ( content ) = info_reader . get ( & provider_id ) else { continue ; };
2025-09-20 15:32:14 +02:00
if content . is_empty () {
warn! ( "Series info content is empty, skipping series with provider id: {provider_id}" );
continue ;
}
2025-12-22 17:25:11 +01:00
match serde_json ::from_str ::< SeriesStreamProperties > ( & content ) {
2024-12-24 15:00:05 +01:00
Ok ( series_content ) => {
2024-12-30 10:27:25 +01:00
let ( group , series_name ) = {
2025-03-11 14:56:10 +01:00
let header = & pli . header ;
2025-12-17 09:03:07 +01:00
( header . group . clone (), if header . name . is_empty () { header . title . clone () } else { header . name . clone () })
2024-12-30 10:27:25 +01:00
};
2025-12-22 17:25:11 +01:00
if let Some ( mut series ) = parse_xtream_series_info ( & series_content , & group , & series_name , input ) {
for pli_episode in & mut series {
let Some ( provider_id ) = & pli_episode . header . get_provider_id () else { continue ; };
match write_series_episode_record_to_wal_file ( & mut wal_writer , * provider_id , pli . header . additional_properties . as_ref ()). await {
Ok ( written_bytes ) => {
write_counter += written_bytes ;
// periodic flush to bound BufWriter memory
if write_counter >= IO_BUFFER_SIZE {
write_counter = 0 ;
if let Err ( err ) = wal_writer . flush (). await {
errors . push ( notify_err! ( format! ( "Failed periodic flush of wal content writer {err} " )));
2025-12-11 20:18:31 +01:00
}
}
2025-12-11 14:32:04 +01:00
}
2025-12-22 17:25:11 +01:00
Err ( err ) => { errors . push ( info_err! ( format! ( "Failed to write to series episode wal file: {err} " ))) }
2025-12-11 14:32:04 +01:00
}
2024-12-23 11:30:33 +01:00
}
2025-12-22 17:25:11 +01:00
group_series . extend ( series . into_iter ());
2024-12-23 11:30:33 +01:00
}
}
2024-12-24 15:00:05 +01:00
Err ( err ) => errors . push ( info_err! ( format! ( "Failed to parse JSON: {err} " ))),
2024-12-23 11:30:33 +01:00
}
}
if ! group_series . is_empty () {
result . push ( PlaylistGroup {
id : plg . id ,
title : plg . title . clone (),
channels : group_series ,
xtream_cluster : XtreamCluster ::Series ,
});
}
}
2025-12-17 09:03:07 +01:00
handle_error! ( wal_writer . flush (). await , | err | errors . push ( notify_err! ( format! ( "Failed to resolve series episodes, could not write to wal file {err} " ))));
handle_error! ( wal_writer . get_ref (). sync_all (). await , | err | errors . push ( notify_err! ( format! ( "Failed to sync series info to wal file {err} " ))));
2025-12-11 14:32:04 +01:00
2024-12-24 15:00:05 +01:00
drop ( wal_writer );
2024-12-25 12:02:00 +01:00
drop ( wal_file );
2025-06-24 01:06:25 +02:00
handle_error! ( xtream_update_input_series_episodes_record_from_wal_file ( app_config , input , & wal_path ). await ,
2024-12-24 15:00:05 +01:00
| err | errors . push ( err ));
2024-12-23 11:30:33 +01:00
result
}
2024-12-22 10:16:23 +01:00
2025-06-24 01:06:25 +02:00
pub async fn playlist_resolve_series ( cfg : & 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 );
if ! resolve_series { return ; }
2025-06-24 01:06:25 +02:00
if ! playlist_resolve_series_info ( cfg , client , errors , processed_fpl , resolve_delay ). await { return ; }
2024-12-29 13:40:50 +01:00
let series_playlist = process_series_info ( cfg , provider_fpl , errors ). await ;
2024-12-26 12:11:28 +01:00
if series_playlist . is_empty () { return ; }
2024-12-23 11:30:33 +01:00
// original content saved into original list
for plg in & series_playlist {
provider_fpl . update_playlist ( plg );
}
// run processing pipe over new items
2024-12-29 13:40:50 +01:00
let mut new_playlist = series_playlist ;
2024-12-23 11:30:33 +01:00
for f in pipe {
2024-12-29 13:40:50 +01:00
if let Some ( v ) = f ( & mut new_playlist , target ) {
new_playlist = v ;
2024-12-22 10:16:23 +01:00
}
}
2024-12-23 11:30:33 +01:00
// assign new items to the new playlist
2024-12-29 13:40:50 +01:00
for plg in & new_playlist {
2024-12-23 11:30:33 +01:00
processed_fpl . update_playlist ( plg );
}
2024-12-19 13:02:10 +01:00
}