2025-12-25 12:33:28 +01:00
use crate ::model ::{ AppConfig , Config , ConfigFavourites , ConfigInput , ConfigRename };
2025-05-04 10:59:08 +02:00
use crate ::utils ::epg ;
use crate ::utils ::m3u ;
use crate ::utils ::xtream ;
2023-10-27 19:03:29 +02:00
use std ::collections ::{ HashMap , HashSet };
2025-01-06 09:17:05 +01:00
use std ::path ::PathBuf ;
2025-04-30 17:26:07 +02:00
use std ::sync ::Arc ;
use tokio ::sync ::Mutex ;
2025-12-16 09:54:59 +01:00
use tokio ::task ::JoinSet ;
2023-10-19 18:56:08 +02:00
2025-12-16 09:54:59 +01:00
use crate ::api ::model ::{ EventManager , EventMessage , PlaylistStorageState , UpdateGuard };
2025-10-22 11:33:49 +02:00
use crate ::messaging ::send_message_json ;
2025-06-24 01:06:25 +02:00
use crate ::model ::Epg ;
use crate ::model ::FetchedPlaylist ;
2025-10-22 11:33:49 +02:00
use crate ::model ::Mapping ;
use crate ::model ::{ ConfigTarget , ProcessTargets };
2025-05-04 10:59:08 +02:00
use crate ::model ::{ InputStats , PlaylistStats , SourceStats , TargetStats };
2025-06-24 01:06:25 +02:00
use crate ::processing ::parser ::xmltv ::flatten_tvguide ;
2023-10-13 18:13:16 +02:00
use crate ::processing ::playlist_watch ::process_group_watch ;
2025-06-24 01:06:25 +02:00
use crate ::processing ::processor ::epg ::process_playlist_epg ;
2025-12-22 17:25:11 +01:00
use crate ::processing ::processor ::library ;
2025-06-24 01:06:25 +02:00
use crate ::processing ::processor ::sort ::sort_playlist ;
2025-06-16 17:53:03 +02:00
use crate ::processing ::processor ::trakt ::process_trakt_categories_for_target ;
2025-06-24 01:06:25 +02:00
use crate ::processing ::processor ::xtream_series ::playlist_resolve_series ;
use crate ::processing ::processor ::xtream_vod ::playlist_resolve_vod ;
2025-12-25 12:33:28 +01:00
use crate ::repository ::playlist_repository ::{ persist_input_playlist , persist_playlist };
2025-06-24 01:06:25 +02:00
use crate ::utils ::StepMeasure ;
2025-12-16 09:54:59 +01:00
use crate ::utils ::{ debug_if_enabled , trace_if_enabled };
2025-04-30 17:26:07 +02:00
use deunicode ::deunicode ;
2025-10-28 20:21:36 +01:00
use futures ::StreamExt ;
2025-04-30 17:26:07 +02:00
use log ::{ debug , error , info , log_enabled , trace , warn , Level };
2025-08-13 23:07:21 +02:00
use shared ::error ::{ get_errors_notify_message , notify_err , TuliproxError };
2025-10-20 15:55:12 +02:00
use shared ::foundation ::filter ::{ get_field_value , set_field_value , Filter , ValueAccessor , ValueProvider };
2025-10-23 18:26:25 +02:00
use shared ::model ::{ CounterModifier , FieldGetAccessor , FieldSetAccessor , InputType , ItemField , MsgKind , PlaylistEntry , PlaylistGroup , PlaylistItem , PlaylistUpdateState , ProcessingOrder , UUIDType , XtreamCluster };
2025-11-30 18:35:01 +01:00
use shared ::utils ::{ default_as_default , hash_bytes };
2025-06-24 01:06:25 +02:00
use std ::time ::Instant ;
2025-04-03 18:03:59 +03:00
2025-10-20 15:55:12 +02:00
fn is_valid ( pli : & PlaylistItem , filter : & Filter ) -> bool {
2025-03-11 14:56:10 +01:00
let provider = ValueProvider { pli };
2025-10-20 15:55:12 +02:00
filter . filter ( & provider )
2024-05-04 20:30:20 +02:00
}
2021-10-15 16:59:20 +02:00
2024-05-10 12:01:48 +02:00
#[allow(clippy::unnecessary_wraps)]
2025-10-20 15:55:12 +02:00
pub fn apply_filter_to_playlist ( playlist : & mut [ PlaylistGroup ], filter : & Filter ) -> Option < Vec < PlaylistGroup >> {
2023-10-28 22:20:57 +02:00
debug! ( "Filtering {} groups" , playlist . len ());
2025-01-09 13:31:05 +01:00
let mut new_playlist = Vec ::with_capacity ( 128 );
2024-05-10 12:01:48 +02:00
for pg in playlist . iter_mut () {
2024-05-04 20:30:20 +02:00
let channels = pg . channels . iter ()
2025-10-20 15:55:12 +02:00
. filter ( |& pli | is_valid ( pli , filter )). cloned (). collect ::< Vec < PlaylistItem >> ();
2024-11-03 14:52:56 +01:00
trace! ( "Filtered group {} has now {}/{} items" , pg . title , channels . len (), pg . channels . len ());
2023-05-03 17:46:39 +02:00
if ! channels . is_empty () {
2023-01-13 15:56:03 +01:00
new_playlist . push ( PlaylistGroup {
2023-10-06 19:24:06 +02:00
id : pg . id ,
2023-01-13 15:56:03 +01:00
title : pg . title . clone (),
channels ,
2024-09-10 20:05:29 +02:00
xtream_cluster : pg . xtream_cluster ,
2023-01-13 15:56:03 +01:00
});
}
2024-05-10 12:01:48 +02:00
}
2023-01-13 15:56:03 +01:00
Some ( new_playlist )
}
2025-10-23 18:26:25 +02:00
pub fn apply_favourites_to_playlist (
2025-12-18 21:51:54 +01:00
_playlist : & mut [ PlaylistGroup ],
_favourites_cfg : Option <& [ ConfigFavourites ] > ,
2025-10-23 18:26:25 +02:00
) {
2025-12-20 16:33:59 +01:00
// TODO implement favourites
2025-10-24 18:36:13 +02:00
// if let Some(favourites) = favourites_cfg {
// let mut fav_groups: HashMap<String, Vec<PlaylistItem>> = HashMap::new();
//
// for pg in playlist.iter_mut() {
// for pli in &pg.channels {
// for fav in favourites {
// if is_valid(pli, &fav.filter) {
// let mut channel = pli.clone();
// channel.header.copy = true;
// channel.header.group.clone_from(&fav.group);
// channel.header.gen_uuid();
// fav_groups
// .entry(fav.group.clone())
// .or_default()
// .push(channel);
// }
// }
// }
// }
//
// for (group_name, channels) in fav_groups {
// if !channels.is_empty() {
// let xtream_cluster = channels[0].header.xtream_cluster;
// playlist.push(PlaylistGroup {
// id: 0,
// title: group_name,
// channels,
// xtream_cluster,
// });
// }
// }
// }
2025-10-23 18:26:25 +02:00
}
2025-10-20 15:55:12 +02:00
fn filter_playlist ( playlist : & mut [ PlaylistGroup ], target : & ConfigTarget ) -> Option < Vec < PlaylistGroup >> {
2025-10-23 18:26:25 +02:00
if let Some ( mut filtered_playlist ) = apply_filter_to_playlist ( playlist , & target . filter ) {
2025-12-18 21:51:54 +01:00
apply_favourites_to_playlist ( & mut filtered_playlist , target . favourites . as_deref ());
2025-10-23 18:26:25 +02:00
Some ( filtered_playlist )
} else {
None
}
2025-10-20 15:55:12 +02:00
}
2024-05-05 19:15:54 +02:00
2025-05-04 10:59:08 +02:00
fn assign_channel_no_playlist ( new_playlist : & mut [ PlaylistGroup ]) {
2025-04-12 18:20:46 +02:00
let assigned_chnos : HashSet < u32 > = new_playlist . iter (). flat_map ( | g | & g . channels )
2025-12-22 17:25:11 +01:00
. filter ( | c | ! c . header . chno == 0 )
. map ( | c | c . header . chno )
. collect ();
2025-01-29 17:33:06 +01:00
let mut chno = 1 ;
for group in new_playlist {
2025-03-11 14:56:10 +01:00
for chan in & mut group . channels {
2025-12-22 17:25:11 +01:00
if chan . header . chno == 0 {
2025-04-12 18:20:46 +02:00
while assigned_chnos . contains ( & chno ) {
chno += 1 ;
}
2025-12-22 17:25:11 +01:00
chan . header . chno = chno ;
2025-04-12 18:20:46 +02:00
chno += 1 ;
}
2025-01-29 17:33:06 +01:00
}
}
}
2025-03-11 14:56:10 +01:00
fn exec_rename ( pli : & mut PlaylistItem , rename : Option <& Vec < ConfigRename >> ) {
2023-05-03 17:46:39 +02:00
if let Some ( renames ) = rename {
if ! renames . is_empty () {
let result = pli ;
for r in renames {
2025-05-26 13:57:29 +02:00
let value = get_field_value ( result , r . field );
2025-06-24 01:06:25 +02:00
let cap = r . pattern . replace_all ( value . as_str (), & r . new_name );
2025-05-12 21:38:02 +02:00
if log_enabled! ( log ::Level ::Debug ) && * value != cap {
2025-12-12 14:35:00 +01:00
trace_if_enabled! ( "Renamed {}={value} to {cap}" , & r . field );
2025-01-01 10:59:51 +01:00
}
2023-05-03 17:46:39 +02:00
let value = cap . into_owned ();
2025-05-26 13:57:29 +02:00
set_field_value ( result , r . field , value );
2023-01-13 15:56:03 +01:00
}
}
}
}
2023-10-08 19:54:59 +02:00
fn rename_playlist ( playlist : & mut [ PlaylistGroup ], target : & ConfigTarget ) -> Option < Vec < PlaylistGroup >> {
2023-01-13 15:56:03 +01:00
match & target . rename {
Some ( renames ) => {
2023-05-03 17:46:39 +02:00
if ! renames . is_empty () {
2025-01-09 13:31:05 +01:00
let mut new_playlist : Vec < PlaylistGroup > = Vec ::with_capacity ( playlist . len ());
2023-01-13 15:56:03 +01:00
for g in playlist {
let mut grp = g . clone ();
2023-01-12 15:59:40 +01:00
for r in renames {
2024-11-04 18:46:56 +01:00
if matches! ( r . field , ItemField ::Group ) {
2025-06-24 01:06:25 +02:00
let cap = r . pattern . replace_all ( & grp . title , & r . new_name );
2025-12-12 14:35:00 +01:00
trace_if_enabled! ( "Renamed group {} to {cap} for {}" , & grp . title , target . name );
2025-03-11 14:56:10 +01:00
grp . title = cap . into_owned ();
2023-01-12 15:59:40 +01:00
}
}
2023-01-13 15:56:03 +01:00
2024-12-02 17:24:06 +01:00
grp . channels . iter_mut (). for_each ( | pli | exec_rename ( pli , target . rename . as_ref ()));
2023-01-13 15:56:03 +01:00
new_playlist . push ( grp );
2023-01-12 15:59:40 +01:00
}
2023-02-13 12:19:58 +01:00
return Some ( new_playlist );
2023-01-12 15:59:40 +01:00
}
2023-01-13 15:56:03 +01:00
None
2023-01-12 15:59:40 +01:00
}
2023-01-13 15:56:03 +01:00
_ => None
2023-01-12 15:59:40 +01:00
}
}
2025-11-30 18:35:01 +01:00
fn create_alias_uuid ( base_uuid : & UUIDType , mapping_id : & str ) -> UUIDType {
let mut data = Vec ::with_capacity ( base_uuid . len () + mapping_id . len ());
2025-12-25 12:33:28 +01:00
data . extend_from_slice ( base_uuid . as_ref ());
2025-11-30 18:35:01 +01:00
data . extend_from_slice ( mapping_id . as_bytes ());
hash_bytes ( & data )
}
fn map_channel ( mut channel : PlaylistItem , mapping : & Mapping ) -> ( PlaylistItem , bool ) {
let mut matched = false ;
2025-05-14 18:23:22 +02:00
if let Some ( mapper ) = & mapping . mapper {
if ! mapper . is_empty () {
let header = & channel . header ;
2025-08-26 13:53:11 +02:00
let channel_name = if mapping . match_as_ascii { deunicode ( & header . name ) } else { header . name . clone () };
2025-05-14 18:23:22 +02:00
if mapping . match_as_ascii && log_enabled! ( Level ::Trace ) { trace! ( "Decoded {} for matching to {}" , & header . name , & channel_name ); }
let ref_chan = & mut channel ;
2025-06-12 16:20:15 +02:00
let templates = mapping . templates . as_ref ();
2025-05-14 18:23:22 +02:00
for m in mapper {
2025-05-22 13:34:45 +02:00
if let Some ( script ) = m . t_script . as_ref () {
if let Some ( filter ) = & m . t_filter {
2025-06-12 17:27:59 +02:00
let provider = ValueProvider { pli : ref_chan };
2025-05-22 13:34:45 +02:00
if filter . filter ( & provider ) {
2025-11-30 18:35:01 +01:00
matched = true ;
2025-06-12 17:27:59 +02:00
let mut accessor = ValueAccessor { pli : ref_chan };
2025-06-12 16:20:15 +02:00
script . eval ( & mut accessor , templates );
2025-05-14 18:23:22 +02:00
}
}
2022-04-05 12:23:30 +02:00
}
2025-04-10 00:06:48 +02:00
}
2023-01-12 15:59:40 +01:00
}
}
2025-11-30 18:35:01 +01:00
( channel , matched )
}
fn map_channel_with_aliases ( channel : PlaylistItem , mapping : & Mapping ) -> Vec < PlaylistItem > {
if mapping . create_alias {
let original = channel . clone ();
let ( mut mapped_channel , matched ) = map_channel ( channel , mapping );
if matched {
mapped_channel . header . uuid = create_alias_uuid ( original . header . get_uuid (), & mapping . id );
vec! [ original , mapped_channel ]
} else {
vec! [ mapped_channel ]
}
} else {
let ( mapped_channel , _ ) = map_channel ( channel , mapping );
vec! [ mapped_channel ]
}
2023-01-12 15:59:40 +01:00
}
2023-10-08 19:54:59 +02:00
fn map_playlist ( playlist : & mut [ PlaylistGroup ], target : & ConfigTarget ) -> Option < Vec < PlaylistGroup >> {
2025-06-24 01:06:25 +02:00
if let Some ( mappings ) = target . mapping . load (). as_ref () {
2023-05-03 17:46:39 +02:00
let new_playlist : Vec < PlaylistGroup > = playlist . iter (). map ( | playlist_group | {
2023-02-14 18:18:19 +01:00
let mut grp = playlist_group . clone ();
2025-05-14 19:11:43 +02:00
mappings . iter (). filter ( |& mapping | mapping . mapper . as_ref (). is_some_and ( | v | ! v . is_empty ()))
2025-05-14 18:23:22 +02:00
. for_each ( | mapping |
2025-11-30 18:35:01 +01:00
grp . channels = grp . channels . drain ( .. ). flat_map ( | chan | map_channel_with_aliases ( chan , mapping )). collect ());
2023-05-03 17:46:39 +02:00
grp
}). collect ();
2023-04-26 14:30:43 +02:00
// if the group names are changed, restructure channels to the right groups
// we use
2025-01-09 13:31:05 +01:00
let mut new_groups : Vec < PlaylistGroup > = Vec ::with_capacity ( 128 );
2024-03-28 16:27:39 +01:00
let mut grp_id : u32 = 0 ;
2023-04-26 14:30:43 +02:00
for playlist_group in new_playlist {
for channel in & playlist_group . channels {
2025-03-11 14:56:10 +01:00
let cluster = & channel . header . xtream_cluster ;
let title = & channel . header . group ;
2024-05-10 12:01:48 +02:00
if let Some ( grp ) = new_groups . iter_mut (). find ( | x | * x . title == ** title ) {
grp . channels . push ( channel . clone ());
} else {
grp_id += 1 ;
new_groups . push ( PlaylistGroup {
id : grp_id ,
2025-08-26 13:53:11 +02:00
title : title . clone (),
2024-05-10 12:01:48 +02:00
channels : vec ! [ channel . clone ()],
2024-09-10 20:05:29 +02:00
xtream_cluster : * cluster ,
2024-05-10 12:01:48 +02:00
});
2023-04-26 14:30:43 +02:00
}
}
}
Some ( new_groups )
2023-01-12 17:15:50 +01:00
} else {
None
2022-04-05 12:23:30 +02:00
}
}
2025-03-11 14:56:10 +01:00
fn map_playlist_counter ( target : & ConfigTarget , playlist : & mut [ PlaylistGroup ]) {
2025-06-24 01:06:25 +02:00
if let Some ( guard ) = &* target . mapping . load () {
let mappings = guard . as_ref ();
for mapping in mappings {
2024-09-11 19:41:50 +02:00
if let Some ( counter_list ) = & mapping . t_counter {
for counter in counter_list {
2025-03-11 14:56:10 +01:00
for plg in & mut * playlist {
for channel in & mut plg . channels {
let provider = ValueProvider { pli : channel };
2025-05-22 13:34:45 +02:00
if counter . filter . filter ( & provider ) {
2025-11-30 13:12:59 +01:00
let cntval = counter . value . fetch_add ( 1 , core ::sync ::atomic ::Ordering ::AcqRel );
2025-04-28 08:30:48 +02:00
let padded_cntval = if counter . padding > 0 {
format! ( " {:0width$} " , cntval , width = counter . padding as usize )
} else {
2024-09-11 19:41:50 +02:00
cntval . to_string ()
2025-04-28 08:30:48 +02:00
};
let new_value = if counter . modifier == CounterModifier ::Assign {
padded_cntval
2024-09-11 19:41:50 +02:00
} else {
2025-03-11 14:56:10 +01:00
let value = channel . header . get_field ( & counter . field ). map_or_else ( String ::new , | field_value | field_value . to_string ());
2024-09-11 19:41:50 +02:00
if counter . modifier == CounterModifier ::Suffix {
2025-04-28 08:30:48 +02:00
format! ( " {value}{}{padded_cntval} " , counter . concat )
2024-10-25 00:39:22 +02:00
} else {
2025-04-28 08:30:48 +02:00
format! ( " {padded_cntval}{}{value} " , counter . concat )
2024-09-11 19:41:50 +02:00
}
};
2025-03-11 14:56:10 +01:00
channel . header . set_field ( & counter . field , new_value . as_str ());
2024-09-11 19:41:50 +02:00
}
}
}
}
}
}
}
}
2023-10-12 11:08:37 +02:00
// If no input is enabled but the user set the target as command line argument,
// we force the input to be enabled.
// If there are enabled input, then only these are used.
2025-05-23 18:17:47 +02:00
fn is_input_enabled ( input : & ConfigInput , user_targets : & ProcessTargets ) -> bool {
2025-02-14 17:51:33 +01:00
let input_enabled = input . enabled ;
let input_id = input . id ;
2025-05-23 18:17:47 +02:00
( ! user_targets . enabled && input_enabled ) || user_targets . has_input ( input_id )
2023-10-12 11:08:37 +02:00
}
2023-12-08 19:08:04 +01:00
fn is_target_enabled ( target : & ConfigTarget , user_targets : & ProcessTargets ) -> bool {
( ! user_targets . enabled && target . enabled ) || ( user_targets . enabled && user_targets . has_target ( target . id ))
}
2025-12-16 16:23:15 +01:00
async fn playlist_download_from_input ( client : & reqwest ::Client , app_config : & Arc < AppConfig > , input : & Arc < ConfigInput > ) -> ( Vec < PlaylistGroup > , Vec < TuliproxError > ) {
let config = &* app_config . config . load ();
2025-08-13 13:40:41 +02:00
match input . input_type {
2025-12-25 12:33:28 +01:00
InputType ::M3u => m3u ::download_m3u_playlist ( client , config , input ). await ,
InputType ::Xtream => xtream ::download_xtream_playlist ( config , client , input ). await ,
2025-12-16 16:23:15 +01:00
InputType ::M3uBatch | InputType ::XtreamBatch => ( vec! [], vec! []),
2025-12-25 12:33:28 +01:00
InputType ::Library => library ::download_library_playlist ( client , app_config , input ). await ,
2025-08-13 13:40:41 +02:00
}
}
2025-12-16 16:23:15 +01:00
async fn process_source ( client : & reqwest ::Client , app_config : Arc < AppConfig > , source_idx : usize ,
2025-10-16 21:19:47 +02:00
user_targets : Arc < ProcessTargets > , event_manager : Option < Arc < EventManager >> ,
2025-10-22 11:33:49 +02:00
playlist_state : Option <& Arc < PlaylistStorageState >> ,
) -> ( Vec < InputStats > , Vec < TargetStats > , Vec < TuliproxError > ) {
2025-12-16 16:23:15 +01:00
let sources = app_config . sources . load ();
2023-10-13 13:59:22 +02:00
let mut errors = vec! [];
2025-01-27 15:58:57 +01:00
let mut input_stats = HashMap ::< String , InputStats > ::new ();
2025-01-05 13:29:28 +01:00
let mut target_stats = Vec ::< TargetStats > ::new ();
2025-07-22 11:32:46 +02:00
if let Some ( source ) = sources . get_source_at ( source_idx ) {
let mut source_playlists = Vec ::with_capacity ( 128 );
// Download the sources
let mut source_downloaded = false ;
for input in & source . inputs {
if is_input_enabled ( input , & user_targets ) {
source_downloaded = true ;
let start_time = Instant ::now ();
2025-12-25 12:33:28 +01:00
// Download playlist for input
let ( playlistgroups , mut error_list ) = {
let ( downloaded_playlist , mut download_err ) = playlist_download_from_input ( client , & app_config , input ). await ;
let ( playlist , error ) = persist_input_playlist ( & app_config , input , downloaded_playlist ). await ;
error! ( "Failed to persist input playlist {}" , input . name );
if let Some ( err ) = error {
download_err . push ( err );
}
( playlist , download_err )
};
// Download epg for input
2025-07-22 11:32:46 +02:00
let ( tvguide , mut tvguide_errors ) = if error_list . is_empty () {
2025-12-16 16:23:15 +01:00
let working_dir = & app_config . config . load (). working_dir ;
2025-12-11 09:53:49 +01:00
epg ::get_xmltv ( client , input , working_dir ). await
2025-07-22 11:32:46 +02:00
} else {
( None , vec! [])
};
2025-12-25 12:33:28 +01:00
2025-07-22 11:32:46 +02:00
errors . append ( & mut error_list );
errors . append ( & mut tvguide_errors );
let group_count = playlistgroups . len ();
2025-12-13 11:32:55 +01:00
let channel_count = playlistgroups . iter (). map ( | group | group . channels . len ()). sum ();
2025-07-22 11:32:46 +02:00
let input_name = & input . name ;
if playlistgroups . is_empty () {
info! ( "Source is empty {input_name}" );
errors . push ( notify_err! ( format! ( "Source is empty {input_name} " )));
} else {
source_playlists . push (
FetchedPlaylist {
input ,
playlistgroups ,
epg : tvguide ,
}
);
}
let elapsed = start_time . elapsed (). as_secs ();
2025-08-26 13:53:11 +02:00
input_stats . insert ( input_name . clone (), create_input_stat ( group_count , channel_count , error_list . len (),
2025-10-22 11:33:49 +02:00
input . input_type , input_name , elapsed ));
2022-03-24 14:08:25 +01:00
}
}
2025-07-22 11:32:46 +02:00
if source_downloaded {
if source_playlists . is_empty () {
debug! ( "Source at index {source_idx} is empty" );
2025-07-22 17:37:56 +02:00
errors . push ( notify_err! ( format! ( "Source at index {source_idx} is empty: {} " , source . inputs . iter (). map ( | i | i . name . as_str ()). collect ::< Vec < _ >> (). join ( ", " ))));
2025-07-22 11:32:46 +02:00
} else {
debug_if_enabled! ( "Source has {} groups" , source_playlists . iter (). map ( | fpl | fpl . playlistgroups . len ()). sum ::< usize > ());
2025-08-04 14:18:46 +02:00
let event_manager_clone = event_manager . clone ();
2025-07-22 11:32:46 +02:00
for target in & source . targets {
2025-08-04 14:18:46 +02:00
let event_manager_clone = event_manager_clone . clone ();
2025-07-22 11:32:46 +02:00
if is_target_enabled ( target , & user_targets ) {
2025-12-16 16:23:15 +01:00
match process_playlist_for_target ( & app_config , client , & mut source_playlists , target , & mut input_stats , & mut errors , event_manager_clone , playlist_state ). await {
2025-07-22 11:32:46 +02:00
Ok (()) => {
target_stats . push ( TargetStats ::success ( & target . name ));
}
Err ( mut err ) => {
target_stats . push ( TargetStats ::failure ( & target . name ));
errors . append ( & mut err );
}
2025-05-23 18:17:47 +02:00
}
2025-01-05 13:29:28 +01:00
}
2023-10-12 11:08:37 +02:00
}
}
2023-12-08 19:08:04 +01:00
}
2023-10-12 11:08:37 +02:00
}
2025-01-05 13:29:28 +01:00
( input_stats . into_values (). collect (), target_stats , errors )
2023-02-13 12:19:58 +01:00
}
2024-11-03 14:52:56 +01:00
fn create_input_stat ( group_count : usize , channel_count : usize , error_count : usize , input_type : InputType , input_name : & str , secs_took : u64 ) -> InputStats {
2024-10-25 00:39:22 +02:00
InputStats {
name : input_name . to_string (),
2024-10-30 11:14:55 +01:00
input_type ,
error_count ,
2024-10-25 00:39:22 +02:00
raw_stats : PlaylistStats {
group_count ,
channel_count ,
},
processed_stats : PlaylistStats {
group_count : 0 ,
channel_count : 0 ,
},
2024-11-04 18:46:56 +01:00
secs_took ,
2024-10-25 00:39:22 +02:00
}
}
2025-12-11 09:53:49 +01:00
async fn process_sources ( client : & reqwest ::Client , config : & Arc < AppConfig > , user_targets : Arc < ProcessTargets > ,
2025-10-22 11:33:49 +02:00
event_manager : Option < Arc < EventManager >> , playlist_state : Option <& Arc < PlaylistStorageState >> ,
2025-10-16 21:19:47 +02:00
) -> ( Vec < SourceStats > , Vec < TuliproxError > ) {
2025-11-19 14:13:54 -06:00
let mut async_tasks = JoinSet ::new ();
2025-06-24 01:06:25 +02:00
let sources = config . sources . load ();
2025-11-20 13:00:01 +01:00
let process_parallel = config . config . load (). process_parallel && sources . sources . len () > 1 ;
2024-03-26 16:07:31 +01:00
if process_parallel && log_enabled! ( Level ::Debug ) {
2025-11-20 13:00:01 +01:00
debug! ( "Parallel processing enabled" );
2023-11-03 18:05:13 +01:00
}
2025-05-13 21:50:08 +02:00
let errors = Arc ::new ( Mutex ::< Vec < TuliproxError >> ::new ( vec! []));
2025-01-05 13:29:28 +01:00
let stats = Arc ::new ( Mutex ::< Vec < SourceStats >> ::new ( vec! []));
2025-12-16 16:23:15 +01:00
for ( index , source ) in sources . sources . iter (). enumerate () {
if ! source . should_process_for_user_targets ( & user_targets ) {
continue ;
}
2025-01-06 09:17:05 +01:00
// We're using the file lock this way on purpose
let source_lock_path = PathBuf ::from ( format! ( "source_ {index} " ));
2025-03-11 14:56:10 +01:00
let Ok ( update_lock ) = config . file_locks . try_write_lock ( & source_lock_path ). await else {
2025-01-06 09:17:05 +01:00
warn! ( "The update operation for the source at index {index} was skipped because an update is already in progress." );
continue ;
};
2023-10-13 13:59:22 +02:00
let shared_errors = errors . clone ();
2023-10-19 18:56:08 +02:00
let shared_stats = stats . clone ();
let cfg = config . clone ();
let usr_trgts = user_targets . clone ();
2025-08-04 14:18:46 +02:00
let event_manager = event_manager . clone ();
2023-02-13 12:19:58 +01:00
if process_parallel {
2025-12-11 09:53:49 +01:00
let http_client = client . clone ();
2025-10-16 21:19:47 +02:00
let playlist_state = playlist_state . cloned ();
2025-11-19 14:13:54 -06:00
async_tasks . spawn ( async move {
2025-11-20 13:40:04 +01:00
// Hold the per-source lock for the full duration of this update.
let current_update_lock = update_lock ;
2025-11-19 14:13:54 -06:00
let ( input_stats , target_stats , mut res_errors ) =
2025-12-11 09:53:49 +01:00
process_source ( & http_client , cfg , index , usr_trgts , event_manager , playlist_state . as_ref ()). await ;
2025-11-19 14:13:54 -06:00
shared_errors . lock (). await . append ( & mut res_errors );
if let Some ( process_stats ) = SourceStats ::try_new ( input_stats , target_stats ) {
shared_stats . lock (). await . push ( process_stats );
2025-07-22 11:32:46 +02:00
}
2025-11-20 13:40:04 +01:00
drop ( current_update_lock );
2025-11-19 14:13:54 -06:00
});
2023-02-13 12:19:58 +01:00
} else {
2025-10-16 21:19:47 +02:00
let ( input_stats , target_stats , mut res_errors ) =
2025-12-11 09:53:49 +01:00
process_source ( client , cfg , index , usr_trgts , event_manager , playlist_state ). await ;
2025-03-11 14:56:10 +01:00
shared_errors . lock (). await . append ( & mut res_errors );
2025-10-22 11:33:49 +02:00
if let Some ( process_stats ) = SourceStats ::try_new ( input_stats , target_stats ) {
2025-10-22 11:04:23 +02:00
shared_stats . lock (). await . push ( process_stats );
}
2025-11-20 13:40:04 +01:00
drop ( update_lock );
2023-02-13 12:19:58 +01:00
}
}
2025-11-19 14:13:54 -06:00
while let Some ( result ) = async_tasks . join_next (). await {
if let Err ( err ) = result {
error! ( "Playlist processing task failed: {err:?}" );
}
2022-03-24 14:08:25 +01:00
}
2025-07-22 11:32:46 +02:00
if let ( Ok ( s ), Ok ( e )) = ( Arc ::try_unwrap ( stats ), Arc ::try_unwrap ( errors )) {
( s . into_inner (), e . into_inner ())
} else {
( vec! [], vec! [])
}
2022-03-24 14:08:25 +01:00
}
2023-09-29 16:03:20 +02:00
2024-05-04 20:30:20 +02:00
pub type ProcessingPipe = Vec < fn ( playlist : & mut [ PlaylistGroup ], target : & ConfigTarget ) -> Option < Vec < PlaylistGroup >>> ;
2023-09-29 16:03:20 +02:00
2023-10-13 13:59:22 +02:00
fn get_processing_pipe ( target : & ConfigTarget ) -> ProcessingPipe {
match & target . processing_order {
ProcessingOrder ::Frm => vec! [ filter_playlist , rename_playlist , map_playlist ],
ProcessingOrder ::Fmr => vec! [ filter_playlist , map_playlist , rename_playlist ],
ProcessingOrder ::Rfm => vec! [ rename_playlist , filter_playlist , map_playlist ],
ProcessingOrder ::Rmf => vec! [ rename_playlist , map_playlist , filter_playlist ],
ProcessingOrder ::Mfr => vec! [ map_playlist , filter_playlist , rename_playlist ],
ProcessingOrder ::Mrf => vec! [ map_playlist , rename_playlist , filter_playlist ]
}
}
2025-01-06 10:11:14 +01:00
fn duplicate_hash ( item : & PlaylistItem ) -> UUIDType {
2025-01-29 17:33:06 +01:00
item . get_uuid ()
2025-01-06 10:11:14 +01:00
}
2024-09-10 20:05:29 +02:00
2025-01-06 10:11:14 +01:00
fn execute_pipe < 'a > ( target : & ConfigTarget , pipe : & ProcessingPipe , fpl : & FetchedPlaylist < 'a > , duplicates : & mut HashSet < UUIDType > ) -> FetchedPlaylist < 'a > {
2024-09-10 20:05:29 +02:00
let mut new_fpl = FetchedPlaylist {
input : fpl . input ,
playlistgroups : fpl . playlistgroups . clone (), // we need to clone, because of multiple target definitions, we cant change the initial playlist.
epg : fpl . epg . clone (),
};
2025-01-06 10:11:14 +01:00
if target . options . as_ref (). is_some_and ( | opt | opt . remove_duplicates ) {
for group in & mut new_fpl . playlistgroups {
// `HashSet::insert` returns true for first insert, otherweise false
group . channels . retain ( | item | duplicates . insert ( duplicate_hash ( item )));
}
}
2024-09-10 20:05:29 +02:00
for f in pipe {
if let Some ( groups ) = f ( & mut new_fpl . playlistgroups , target ) {
new_fpl . playlistgroups = groups ;
}
}
new_fpl
}
// This method is needed, because of duplicate group names in different inputs.
// We merge the same group names considering cluster together.
2024-11-04 18:46:56 +01:00
fn flatten_groups ( playlistgroups : Vec < PlaylistGroup > ) -> Vec < PlaylistGroup > {
2024-09-11 12:13:05 +02:00
let mut sort_order : Vec < PlaylistGroup > = vec! [];
let mut idx : usize = 0 ;
2025-03-11 14:56:10 +01:00
let mut group_map : HashMap < ( String , XtreamCluster ), usize > = HashMap ::new ();
2024-11-04 18:46:56 +01:00
for group in playlistgroups {
2025-08-26 13:53:11 +02:00
let key = ( group . title . clone (), group . xtream_cluster );
2024-09-10 20:05:29 +02:00
match group_map . entry ( key ) {
2024-09-11 12:13:05 +02:00
std ::collections ::hash_map ::Entry ::Vacant ( v ) => {
v . insert ( idx );
idx += 1 ;
sort_order . push ( group );
}
2024-09-10 20:05:29 +02:00
std ::collections ::hash_map ::Entry ::Occupied ( o ) => {
2025-07-22 11:32:46 +02:00
if let Some ( pl_group ) = sort_order . get_mut ( * o . get ()) {
pl_group . channels . extend ( group . channels );
}
2024-09-10 20:05:29 +02:00
}
2025-04-10 00:06:48 +02:00
}
2024-11-04 18:46:56 +01:00
}
2024-09-11 12:13:05 +02:00
sort_order
2024-09-10 20:05:29 +02:00
}
2025-10-16 21:19:47 +02:00
#[allow(clippy::too_many_arguments)]
2025-12-25 12:33:28 +01:00
async fn process_playlist_for_target ( app_config : & Arc < AppConfig > ,
2025-12-11 09:53:49 +01:00
client : & reqwest ::Client ,
2025-01-09 15:58:38 +01:00
playlists : & mut [ FetchedPlaylist < '_ > ],
2025-01-06 10:11:14 +01:00
target : & ConfigTarget ,
2025-01-27 15:58:57 +01:00
stats : & mut HashMap < String , InputStats > ,
2025-08-04 14:18:46 +02:00
errors : & mut Vec < TuliproxError > ,
2025-10-16 21:19:47 +02:00
event_manager : Option < Arc < EventManager >> ,
2025-10-22 11:33:49 +02:00
playlist_state : Option <& Arc < PlaylistStorageState >> ,
2025-10-16 21:19:47 +02:00
) -> Result < (), Vec < TuliproxError >> {
2023-10-13 13:59:22 +02:00
let pipe = get_processing_pipe ( target );
2024-12-09 19:26:39 +01:00
debug_if_enabled! ( "Processing order is {}" , & target . processing_order );
2023-09-29 16:03:20 +02:00
2025-01-06 10:11:14 +01:00
let mut duplicates : HashSet < UUIDType > = HashSet ::new ();
2024-12-10 23:16:04 +01:00
let mut processed_fetched_playlists : Vec < FetchedPlaylist > = vec! [];
2025-04-23 20:21:36 +02:00
debug! ( "Executing processing pipes" );
2025-08-04 14:18:46 +02:00
let broadcast_step = {
let event_manager = event_manager . clone ();
move | context : & str , msg : & str | {
if let Some ( events ) = & event_manager {
events . send_event ( EventMessage ::PlaylistUpdateProgress ( context . to_owned (), msg . to_owned ()));
}
}
};
2025-04-23 20:21:36 +02:00
2025-08-04 14:18:46 +02:00
let mut step = StepMeasure ::new ( & target . name , broadcast_step );
2024-12-10 23:16:04 +01:00
for provider_fpl in playlists . iter_mut () {
2025-01-06 10:11:14 +01:00
let mut processed_fpl = execute_pipe ( target , & pipe , provider_fpl , & mut duplicates );
2025-12-11 09:53:49 +01:00
playlist_resolve_series ( app_config , client , target , errors , & pipe , provider_fpl , & mut processed_fpl ). await ;
playlist_resolve_vod ( app_config , client , target , errors , & mut processed_fpl ). await ;
2023-10-19 18:56:08 +02:00
// stats
2025-01-27 15:58:57 +01:00
let input_stats = stats . get_mut ( & processed_fpl . input . name );
2023-10-19 18:56:08 +02:00
if let Some ( stat ) = input_stats {
2024-12-10 23:16:04 +01:00
stat . processed_stats . group_count = processed_fpl . playlistgroups . len ();
stat . processed_stats . channel_count = processed_fpl . playlistgroups . iter ()
2023-10-19 18:56:08 +02:00
. map ( | group | group . channels . len ())
. sum ();
}
2024-12-10 23:16:04 +01:00
processed_fetched_playlists . push ( processed_fpl );
2023-12-08 19:08:04 +01:00
}
2025-08-04 14:18:46 +02:00
step . tick ( "filter rename map" );
2025-10-01 14:40:44 +02:00
let ( new_epg , mut new_playlist ) = process_epg ( & mut processed_fetched_playlists ). await ;
2025-08-04 14:18:46 +02:00
step . tick ( "epg" );
2025-04-15 18:38:18 +02:00
if new_playlist . is_empty () {
2025-08-04 14:18:46 +02:00
step . stop ( "" );
2025-04-15 18:38:18 +02:00
info! ( "Playlist is empty: {}" , & target . name );
Ok (())
} else {
2025-06-17 13:06:52 +02:00
// Process Trakt categories
2025-12-11 09:53:49 +01:00
if trakt_playlist ( client , target , errors , & mut new_playlist ). await {
2025-09-23 16:56:56 +02:00
step . tick ( "trakt categories" );
}
2025-06-17 13:06:52 +02:00
2025-04-15 18:38:18 +02:00
let mut flat_new_playlist = flatten_groups ( new_playlist );
2025-08-04 14:18:46 +02:00
step . tick ( "playlist merge" );
2025-06-17 13:06:52 +02:00
2025-10-22 11:33:49 +02:00
if sort_playlist ( target , & mut flat_new_playlist ) {
2025-09-23 16:56:56 +02:00
step . tick ( "playlist sort" );
}
2025-05-04 10:59:08 +02:00
assign_channel_no_playlist ( & mut flat_new_playlist );
2025-08-04 14:18:46 +02:00
step . tick ( "assigning channel numbers" );
2025-04-15 18:38:18 +02:00
map_playlist_counter ( target , & mut flat_new_playlist );
2025-08-04 14:18:46 +02:00
step . tick ( "assigning channel counter" );
2025-06-16 11:49:17 +02:00
2025-06-24 01:06:25 +02:00
let config = app_config . config . load ();
2025-12-11 09:53:49 +01:00
if process_watch ( & config , client , target , & flat_new_playlist ). await {
2025-09-23 16:56:56 +02:00
step . tick ( "group watches" );
}
2025-10-16 21:19:47 +02:00
let result = persist_playlist ( app_config , & mut flat_new_playlist , flatten_tvguide ( & new_epg ). as_ref (), target , playlist_state ). await ;
2025-08-04 14:18:46 +02:00
step . stop ( "Persisting playlists" );
2025-04-23 20:21:36 +02:00
result
2025-04-15 18:38:18 +02:00
}
}
2025-12-11 09:53:49 +01:00
async fn trakt_playlist ( client : & reqwest ::Client , target : & ConfigTarget , errors : & mut Vec < TuliproxError > , playlist : & mut Vec < PlaylistGroup > ) -> bool {
match process_trakt_categories_for_target ( client , playlist , target ). await {
2025-09-23 16:56:56 +02:00
Ok ( Some ( trakt_categories )) => {
2025-06-16 17:53:03 +02:00
if ! trakt_categories . is_empty () {
info! ( "Adding {} Trakt categories to playlist" , trakt_categories . len ());
2025-06-17 13:06:52 +02:00
playlist . extend ( trakt_categories );
2025-06-16 17:53:03 +02:00
}
}
2025-09-23 16:56:56 +02:00
Ok ( None ) => {
return false ;
}
2025-06-16 17:53:03 +02:00
Err ( trakt_errors ) => {
warn! ( "Trakt processing failed with {} errors" , trakt_errors . len ());
errors . extend ( trakt_errors );
}
}
2025-09-23 16:56:56 +02:00
true
2025-06-16 17:53:03 +02:00
}
2025-10-01 14:40:44 +02:00
async fn process_epg ( processed_fetched_playlists : & mut Vec < FetchedPlaylist < '_ >> ) -> ( Vec < Epg > , Vec < PlaylistGroup > ) {
2023-10-13 15:56:00 +02:00
let mut new_playlist = vec! [];
2023-10-27 19:03:29 +02:00
let mut new_epg = vec! [];
2024-05-05 19:15:54 +02:00
2024-11-02 16:44:38 +01:00
// each fetched playlist can have its own epgl url.
// we need to process each input epg.
2025-04-15 18:38:18 +02:00
for fp in processed_fetched_playlists {
2025-10-01 14:40:44 +02:00
process_playlist_epg ( fp , & mut new_epg ). await ;
2025-04-06 16:50:43 +02:00
new_playlist . append ( & mut fp . playlistgroups );
2024-11-04 18:46:56 +01:00
}
2025-04-15 18:38:18 +02:00
( new_epg , new_playlist )
2024-09-10 20:05:29 +02:00
}
2023-10-13 18:13:16 +02:00
2025-12-11 09:53:49 +01:00
async fn process_watch ( cfg : & Config , client : & reqwest ::Client , target : & ConfigTarget , new_playlist : & [ PlaylistGroup ]) -> bool {
2025-10-22 11:33:49 +02:00
if let Some ( watches ) = & target . watch {
2024-09-10 20:05:29 +02:00
if default_as_default (). eq_ignore_ascii_case ( & target . name ) {
2025-10-28 20:30:04 +01:00
error! ( "can't watch a target with no unique name" );
2025-10-28 19:52:46 +01:00
return false ;
}
2025-10-28 20:21:36 +01:00
futures ::stream ::iter (
new_playlist
. iter ()
. filter ( | pl | watches . iter (). any ( | r | r . is_match ( & pl . title )))
. map ( | pl | process_group_watch ( client , cfg , & target . name , pl ))
). for_each_concurrent ( 16 , | f | f ). await ;
2025-09-23 16:56:56 +02:00
true
} else {
false
2023-10-06 19:24:06 +02:00
}
2023-09-29 16:03:20 +02:00
}
2023-10-19 18:56:08 +02:00
2025-12-16 09:54:59 +01:00
pub async fn exec_processing ( client : & reqwest ::Client , app_config : Arc < AppConfig > , targets : Arc < ProcessTargets > ,
event_manager : Option < Arc < EventManager >> , playlist_state : Option < Arc < PlaylistStorageState >> ,
update_guard : Option < UpdateGuard > ) {
let _guard = if let Some ( guard ) = update_guard {
if let Some ( permit ) = guard . try_playlist () {
Some ( permit )
} else {
warn! ( "Playlist update already in progress; update skipped." );
if let Some ( events ) = event_manager . as_ref () {
events . send_event ( EventMessage ::PlaylistUpdate ( PlaylistUpdateState ::Failure ));
}
return ;
}
} else {
None
};
2025-08-04 14:18:46 +02:00
let event_manager_clone = event_manager . clone ();
2025-12-16 09:54:59 +01:00
let start_time = Instant ::now ();
2025-12-11 09:53:49 +01:00
let ( stats , errors ) = process_sources ( client , & app_config , targets . clone (), event_manager_clone , playlist_state . as_ref ()). await ;
2024-12-13 17:51:47 +01:00
// log errors
2024-12-28 20:01:28 +01:00
for err in & errors {
error! ( "{}" , err . message );
}
2025-08-03 10:24:23 +02:00
let config = app_config . config . load ();
2025-06-24 01:06:25 +02:00
let messaging = config . messaging . as_ref ();
2025-10-23 18:26:25 +02:00
if ! stats . is_empty () {
match serde_json ::to_value ( & stats ) {
Ok ( val ) => {
match serde_json ::to_string ( & serde_json ::Value ::Object (
serde_json ::map ::Map ::from_iter ([( "stats" . to_string (), val )]))) {
Ok ( stats_msg ) => {
// print stats
info! ( "{stats_msg}" );
// send stats
2025-12-11 09:53:49 +01:00
send_message_json ( client , MsgKind ::Stats , messaging , stats_msg . as_str ()). await ;
2025-10-23 18:26:25 +02:00
}
Err ( err ) => error! ( "Failed to serialize playlist stats {err}" ),
2025-10-22 11:33:49 +02:00
}
}
2025-10-23 18:26:25 +02:00
Err ( err ) => error! ( "Failed to serialize playlist stats {err}" )
2025-10-22 11:33:49 +02:00
}
2024-12-13 17:51:47 +01:00
}
2025-10-22 11:33:49 +02:00
2023-10-20 08:23:25 +02:00
// send errors
2023-10-19 18:56:08 +02:00
if let Some ( message ) = get_errors_notify_message! ( errors , 255 ) {
2025-08-03 10:24:23 +02:00
if let Some ( events ) = event_manager {
events . send_event ( EventMessage ::PlaylistUpdate ( PlaylistUpdateState ::Failure ));
}
2024-12-25 14:12:30 +01:00
if let Ok ( error_msg ) = serde_json ::to_string ( & serde_json ::Value ::Object ( serde_json ::map ::Map ::from_iter ([( "errors" . to_string (), serde_json ::Value ::String ( message ))]))) {
2025-12-11 09:53:49 +01:00
send_message_json ( client , MsgKind ::Error , messaging , error_msg . as_str ()). await ;
2024-12-25 14:12:30 +01:00
}
2025-08-03 10:24:23 +02:00
} else if let Some ( events ) = event_manager {
events . send_event ( EventMessage ::PlaylistUpdate ( PlaylistUpdateState ::Success ));
2023-10-19 18:56:08 +02:00
}
2024-12-25 14:12:30 +01:00
let elapsed = start_time . elapsed (). as_secs ();
2025-05-12 21:38:02 +02:00
info! ( "🌷 Update process finished! Took {elapsed} secs." );
2025-02-01 21:50:13 -06:00
}
2025-04-11 20:25:09 +02:00
2025-08-13 13:40:41 +02:00
// #[cfg(test)]
// mod tests {
2025-10-22 11:33:49 +02:00
// #[test]
// fn test_jaro_winkeler() {
// let data = [("yessport5", "heyessport5gold"), ("yessport5", "heyesport5gold")];
//
// data.iter().for_each(|(first, second)|
// println!("jaro_winkler {} = {} => {}", first, second, strsim::jaro_winkler(first, second)));
// // println!("jaro {}", strsim::jaro(data.0, data.1));
// // println!("levenhstein {}", strsim::levenshtein(data.0, data.1));
// // println!("damerau_levenshtein {:?}", strsim::damerau_levenshtein(data.0, data.1));
// // println!("osa distance {:?}", strsim::osa_distance(data.0, data.1));
// // println!("sorensen dice {:?}", strsim::sorensen_dice(data.0, data.1));
// }
2025-04-30 17:16:53 +02:00
2025-11-19 14:13:54 -06:00
// }