2026-02-12 15:06:57 +01:00
|
|
|
mod playlist;
|
2026-07-20 14:58:06 +02:00
|
|
|
mod stalker;
|
|
|
|
|
mod stalker_refresh;
|
2025-02-05 20:01:49 +01:00
|
|
|
mod xtream;
|
2025-05-23 00:27:35 +02:00
|
|
|
// mod affix;
|
2026-08-21 17:50:12 +05:00
|
|
|
mod deduplicate;
|
2026-02-12 15:06:57 +01:00
|
|
|
mod epg;
|
2025-12-16 16:23:15 +01:00
|
|
|
mod library;
|
2026-03-15 08:32:18 +01:00
|
|
|
mod probe_handle_guard;
|
2026-08-29 15:32:08 +02:00
|
|
|
mod providers;
|
2026-03-15 08:32:18 +01:00
|
|
|
mod resolve_options;
|
2026-08-27 18:36:23 +05:00
|
|
|
mod sort;
|
|
|
|
|
mod stream_probe;
|
|
|
|
|
mod xtream_series;
|
|
|
|
|
mod xtream_vod;
|
|
|
|
|
pub use self::{
|
|
|
|
|
epg::*,
|
|
|
|
|
playlist::*,
|
|
|
|
|
probe_handle_guard::*,
|
2026-08-29 15:32:08 +02:00
|
|
|
providers::*,
|
2026-08-27 18:36:23 +05:00
|
|
|
resolve_options::*,
|
|
|
|
|
stalker::{download_stalker_playlist, re_resolve_stalker_url, StalkerCluster},
|
|
|
|
|
stalker_refresh::StalkerRefreshMode,
|
|
|
|
|
stream_probe::*,
|
|
|
|
|
xtream::*,
|
|
|
|
|
xtream_series::*,
|
|
|
|
|
xtream_vod::*,
|
2026-07-20 14:58:06 +02:00
|
|
|
};
|
2026-03-05 00:04:53 +01:00
|
|
|
use tokio_util::sync::CancellationToken;
|
2026-08-27 18:36:23 +05:00
|
|
|
use tuliprox_core::model::ProviderHandle;
|
2026-02-12 15:06:57 +01:00
|
|
|
|
2026-08-27 18:36:23 +05:00
|
|
|
pub const FOREGROUND_BATCH_SIZE: usize = 200;
|
|
|
|
|
pub const FOREGROUND_RETRY_BATCH_MAX_SIZE: usize = FOREGROUND_BATCH_SIZE * 4;
|
|
|
|
|
pub const FOREGROUND_MIN_RETRY_DELAY_SECS: u64 = 1;
|
2026-02-25 19:30:19 +01:00
|
|
|
|
2026-08-27 18:36:23 +05:00
|
|
|
pub fn select_cancel_token<'a>(
|
2026-03-05 00:04:53 +01:00
|
|
|
acquired_handle: Option<&'a ProviderHandle>,
|
|
|
|
|
active_handle: Option<&'a ProviderHandle>,
|
|
|
|
|
) -> Option<&'a CancellationToken> {
|
|
|
|
|
acquired_handle
|
|
|
|
|
.and_then(|h| h.cancel_token.as_ref())
|
|
|
|
|
.or_else(|| active_handle.and_then(|h| h.cancel_token.as_ref()))
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 09:03:07 +01:00
|
|
|
//
|
2026-02-12 15:06:57 +01:00
|
|
|
// fn get_resolve_<cluster>_options(target: &ConfigTarget, fpl: &FetchedPlaylist) -> bool
|
2025-12-17 09:03:07 +01:00
|
|
|
//
|
2025-02-05 20:01:49 +01:00
|
|
|
#[macro_export]
|
|
|
|
|
macro_rules! create_resolve_options_function_for_xtream_target {
|
|
|
|
|
($cluster:ident) => {
|
|
|
|
|
paste::paste! {
|
2026-08-27 18:36:23 +05:00
|
|
|
fn [<get_resolve_ $cluster _options>](target: &ConfigTarget, fpl: &FetchedPlaylist) -> $crate::processor::ResolveOptions {
|
2025-03-10 13:28:22 +01:00
|
|
|
match target.get_xtream_output() {
|
2026-02-14 17:52:35 +01:00
|
|
|
Some(_) => {
|
|
|
|
|
let input_options = fpl.input.options.as_ref();
|
|
|
|
|
let input_is_xtream = fpl.input.input_type.is_xtream();
|
|
|
|
|
|
|
|
|
|
let (
|
|
|
|
|
resolve_tmdb_missing,
|
|
|
|
|
input_resolve_enabled,
|
|
|
|
|
input_probe_enabled,
|
|
|
|
|
input_resolve_delay,
|
|
|
|
|
resolve_background
|
|
|
|
|
) = if let Some(options) = input_options {
|
|
|
|
|
(
|
2026-08-27 18:36:23 +05:00
|
|
|
options.has_flag(tuliprox_core::model::ConfigInputFlags::ResolveTmdb),
|
|
|
|
|
options.has_flag(tuliprox_core::model::ConfigInputFlags::[<Resolve $cluster:camel>]),
|
|
|
|
|
options.has_flag(tuliprox_core::model::ConfigInputFlags::[<Probe $cluster:camel>]),
|
2026-02-14 17:52:35 +01:00
|
|
|
options.resolve_delay,
|
2026-08-27 18:36:23 +05:00
|
|
|
options.has_flag(tuliprox_core::model::ConfigInputFlags::ResolveBackground)
|
2026-02-14 17:52:35 +01:00
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
2026-07-08 11:52:22 +02:00
|
|
|
shared::defaults::default_resolve_delay_secs(),
|
|
|
|
|
shared::defaults::default_resolve_background(),
|
2026-02-14 17:52:35 +01:00
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let resolve_enabled = input_resolve_enabled;
|
|
|
|
|
let probe_enabled = input_probe_enabled;
|
|
|
|
|
let resolve_delay = input_resolve_delay;
|
|
|
|
|
|
2026-08-27 18:36:23 +05:00
|
|
|
let mut flags = $crate::processor::ResolveOptionsFlagsSet::new();
|
2026-02-14 17:52:35 +01:00
|
|
|
if resolve_enabled && input_is_xtream {
|
2026-08-27 18:36:23 +05:00
|
|
|
flags.set($crate::processor::ResolveOptionsFlags::Resolve);
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
|
|
|
|
if resolve_tmdb_missing {
|
2026-08-27 18:36:23 +05:00
|
|
|
flags.set($crate::processor::ResolveOptionsFlags::TmdbMissing);
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2026-02-14 17:52:35 +01:00
|
|
|
if input_is_xtream && probe_enabled {
|
2026-08-27 18:36:23 +05:00
|
|
|
flags.set($crate::processor::ResolveOptionsFlags::Probe);
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2026-02-14 17:52:35 +01:00
|
|
|
if resolve_background {
|
2026-08-27 18:36:23 +05:00
|
|
|
flags.set($crate::processor::ResolveOptionsFlags::Background);
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2026-08-27 18:36:23 +05:00
|
|
|
$crate::processor::ResolveOptions {
|
2026-02-12 15:06:57 +01:00
|
|
|
flags,
|
2026-02-14 17:52:35 +01:00
|
|
|
resolve_delay,
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
|
|
|
|
},
|
2026-08-27 18:36:23 +05:00
|
|
|
None => $crate::processor::ResolveOptions::default(),
|
2025-03-10 13:28:22 +01:00
|
|
|
}
|
2025-02-05 20:01:49 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
use create_resolve_options_function_for_xtream_target;
|
2026-02-25 19:30:19 +01:00
|
|
|
|
|
|
|
|
/// Foreground retry helper that retries each queued item at most once.
|
|
|
|
|
///
|
|
|
|
|
/// `retry_delay_secs` is applied sequentially per item, so the total wall-clock
|
|
|
|
|
/// delay is roughly `retry_delay_secs * retry_item_count` plus network/DB time.
|
|
|
|
|
macro_rules! process_foreground_retry_once {
|
|
|
|
|
(
|
|
|
|
|
ctx: $ctx:expr,
|
|
|
|
|
fpl: $fpl:expr,
|
|
|
|
|
filter: $filter:expr,
|
|
|
|
|
retry_once_ids: $retry_once_ids:ident,
|
|
|
|
|
retry_delay_secs: $retry_delay_secs:expr,
|
|
|
|
|
xtream_path: $xtream_path:ident,
|
|
|
|
|
db_query_holder: $db_query_holder:ident,
|
|
|
|
|
db_lock_holder: $db_lock_holder:ident,
|
|
|
|
|
batch: $batch:ident,
|
|
|
|
|
batch_size: $batch_size:expr,
|
|
|
|
|
retry_batch_max_len: $retry_batch_max_len:expr,
|
|
|
|
|
processed_count: $processed_count:ident,
|
|
|
|
|
query_error_context: $query_error_context:expr,
|
|
|
|
|
reasons: |$pli_reasons:ident| $reasons_expr:expr,
|
|
|
|
|
update: |$active_provider:ident, $pli_update:ident, $provider_id:ident, $reasons:ident, $db_query_ref:ident| $update_expr:expr,
|
|
|
|
|
apply_properties: |$pli_apply:ident, $updated_props:ident| $apply_expr:expr,
|
|
|
|
|
persist: |$updates:ident| $persist_expr:expr,
|
|
|
|
|
on_persist_error: |$persist_err:ident| $on_persist_error_expr:expr,
|
|
|
|
|
on_retry_error: |$pli_error:ident, $retry_err:ident| $on_retry_error_expr:expr,
|
|
|
|
|
on_after_attempt: |$pli_after:ident, $retry_succeeded:ident| $on_after_attempt_expr:expr $(,)?
|
|
|
|
|
) => {
|
|
|
|
|
for __pli in $fpl.items_mut() {
|
|
|
|
|
if !($filter)(__pli) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let __provider_id = if let Ok(__uid) = __pli.header.id.parse::<u32>() {
|
2026-08-27 18:36:23 +05:00
|
|
|
tuliprox_core::model::ProviderIdType::Id(__uid)
|
2026-02-25 19:30:19 +01:00
|
|
|
} else {
|
2026-08-27 18:36:23 +05:00
|
|
|
tuliprox_core::model::ProviderIdType::from(&*__pli.header.id)
|
2026-02-25 19:30:19 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !$retry_once_ids.remove(&__provider_id) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut __retry_succeeded = false;
|
|
|
|
|
let __reasons = {
|
|
|
|
|
let $pli_reasons = &mut *__pli;
|
|
|
|
|
$reasons_expr
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if !__reasons.is_empty() {
|
|
|
|
|
if let Some(__active_provider) = $ctx.provider_manager.as_ref() {
|
|
|
|
|
// Do not hold a read lock over the retry delay window.
|
|
|
|
|
if $db_query_holder.is_some() {
|
|
|
|
|
$db_query_holder = None;
|
|
|
|
|
$db_lock_holder = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs($retry_delay_secs)).await;
|
|
|
|
|
|
|
|
|
|
if $db_query_holder.is_none() && $xtream_path.exists() {
|
|
|
|
|
let __file_lock = $ctx.config.file_locks.read_lock(&$xtream_path).await;
|
|
|
|
|
let __xtream_path = $xtream_path.clone();
|
|
|
|
|
let __query = match tokio::task::spawn_blocking(move || {
|
2026-08-27 18:36:23 +05:00
|
|
|
tuliprox_repository::BPlusTreeQuery::<u32, shared::model::XtreamPlaylistItem>::try_new(
|
2026-02-25 19:30:19 +01:00
|
|
|
&__xtream_path,
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(Ok(__query)) => Some((__query, __file_lock)),
|
|
|
|
|
Ok(Err(__err)) => {
|
|
|
|
|
log::error!("Failed to open BPlusTreeQuery for {}: {__err}", $query_error_context);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Err(__err) => {
|
|
|
|
|
log::error!("Failed to open BPlusTreeQuery for {}: {__err}", $query_error_context);
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some((__query, __guard)) = __query {
|
|
|
|
|
$db_query_holder = Some(std::sync::Arc::new(parking_lot::Mutex::new(__query)));
|
|
|
|
|
$db_lock_holder = Some(__guard);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let __db_query_ref = $db_query_holder.as_ref().map(std::sync::Arc::clone);
|
|
|
|
|
let __update_future = {
|
|
|
|
|
let $active_provider = __active_provider;
|
|
|
|
|
let $pli_update = &mut *__pli;
|
|
|
|
|
let $provider_id = __provider_id.clone();
|
|
|
|
|
let $reasons = &__reasons;
|
|
|
|
|
let $db_query_ref = __db_query_ref;
|
|
|
|
|
$update_expr
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match __update_future.await {
|
|
|
|
|
Ok(Some(__updated_props)) => {
|
|
|
|
|
{
|
|
|
|
|
let $pli_apply = &mut *__pli;
|
|
|
|
|
let $updated_props = &__updated_props;
|
|
|
|
|
$apply_expr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$batch.push((__provider_id.clone(), __updated_props));
|
|
|
|
|
|
|
|
|
|
if $batch.len() >= $batch_size {
|
|
|
|
|
$db_query_holder = None;
|
|
|
|
|
$db_lock_holder = None;
|
|
|
|
|
|
|
|
|
|
let __updates: Vec<(u32, _)> = $batch
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|(__id, __props)| {
|
|
|
|
|
// Foreground retry batches can include text provider IDs (e.g. M3U).
|
|
|
|
|
// Persist batch functions for these paths are keyed by numeric Xtream IDs,
|
|
|
|
|
// so text IDs are intentionally skipped here.
|
2026-08-27 18:36:23 +05:00
|
|
|
if let tuliprox_core::model::ProviderIdType::Id(__vid) = __id {
|
2026-02-25 19:30:19 +01:00
|
|
|
Some((*__vid, __props.clone()))
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
if __updates.is_empty() {
|
|
|
|
|
$batch.clear();
|
|
|
|
|
} else {
|
|
|
|
|
let __persist_future = {
|
|
|
|
|
let $updates = __updates;
|
|
|
|
|
$persist_expr
|
|
|
|
|
};
|
|
|
|
|
match __persist_future.await {
|
|
|
|
|
Ok(()) => $batch.clear(),
|
|
|
|
|
Err($persist_err) => {
|
|
|
|
|
$on_persist_error_expr;
|
|
|
|
|
if $batch.len() > $retry_batch_max_len {
|
|
|
|
|
let __drop_count = $batch.len().saturating_sub($retry_batch_max_len);
|
|
|
|
|
if __drop_count > 0 {
|
|
|
|
|
$batch.drain(0..__drop_count);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$processed_count += 1;
|
|
|
|
|
__retry_succeeded = true;
|
|
|
|
|
}
|
|
|
|
|
Ok(None) => {}
|
|
|
|
|
Err($retry_err) => {
|
|
|
|
|
let $pli_error = &*__pli;
|
|
|
|
|
$on_retry_error_expr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let $pli_after = &mut *__pli;
|
|
|
|
|
let $retry_succeeded = __retry_succeeded;
|
|
|
|
|
$on_after_attempt_expr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-08-27 18:36:23 +05:00
|
|
|
// `macro_rules!` without `#[macro_export]`: the re-export cannot be wider than
|
|
|
|
|
// the crate.
|
2026-02-25 19:30:19 +01:00
|
|
|
pub(crate) use process_foreground_retry_once;
|