2026-02-18 08:18:28 +01:00
|
|
|
use parking_lot::Mutex;
|
2026-01-22 13:30:59 +01:00
|
|
|
use std::sync::Arc;
|
2025-12-25 12:33:28 +01:00
|
|
|
use shared::error::{TuliproxError};
|
2026-02-14 17:52:35 +01:00
|
|
|
use crate::model::{AppConfig, ConfigInput, ConfigInputFlags};
|
2026-02-12 15:06:57 +01:00
|
|
|
use shared::model::{LiveStreamProperties, StreamProperties, XtreamCluster, XtreamPlaylistItem};
|
|
|
|
|
use crate::repository::{get_input_storage_path, persist_input_live_info, BPlusTreeQuery, xtream_get_file_path};
|
|
|
|
|
use crate::utils::{debug_if_enabled};
|
2026-02-26 14:41:02 +01:00
|
|
|
use crate::utils::ffmpeg::{ProbeFailureKind, ProbeUrlOutcome};
|
2026-02-12 15:06:57 +01:00
|
|
|
use log::{debug, warn};
|
|
|
|
|
use crate::processing::parser::xtream::create_xtream_url;
|
2026-02-25 19:30:19 +01:00
|
|
|
use crate::api::model::{ActiveProviderManager, ProviderHandle, ProviderIdType};
|
2024-12-19 13:02:10 +01:00
|
|
|
|
2026-02-12 15:06:57 +01:00
|
|
|
/// Updates metadata for a single Live stream (primarily probing)
|
2026-02-18 08:18:28 +01:00
|
|
|
#[allow(clippy::too_many_lines)]
|
2026-02-12 15:06:57 +01:00
|
|
|
pub async fn update_live_stream_metadata(
|
2026-01-22 13:30:59 +01:00
|
|
|
app_config: &Arc<AppConfig>,
|
2026-01-20 18:51:46 +01:00
|
|
|
input: &ConfigInput,
|
2026-02-12 15:06:57 +01:00
|
|
|
id: ProviderIdType,
|
|
|
|
|
save: bool,
|
2026-02-18 08:18:28 +01:00
|
|
|
db_query: Option<Arc<Mutex<BPlusTreeQuery<u32, XtreamPlaylistItem>>>>,
|
2026-02-25 19:30:19 +01:00
|
|
|
_active_handle: Option<&ProviderHandle>,
|
|
|
|
|
_active_provider: &Arc<ActiveProviderManager>,
|
2026-02-12 15:06:57 +01:00
|
|
|
) -> Result<Option<LiveStreamProperties>, TuliproxError> {
|
2026-03-06 17:29:42 +01:00
|
|
|
let storage_dir = &app_config.config.load().storage_dir;
|
|
|
|
|
let storage_path = get_input_storage_path(&input.name, storage_dir).await
|
2026-02-12 15:06:57 +01:00
|
|
|
.map_err(|e| shared::error::info_err!("Storage path error: {e}"))?;
|
|
|
|
|
|
|
|
|
|
// Try to load existing info first to preserve data
|
|
|
|
|
let mut props: Option<LiveStreamProperties> = None;
|
|
|
|
|
let mut existing_item: Option<XtreamPlaylistItem> = None;
|
|
|
|
|
|
|
|
|
|
let stream_id_opt = if let ProviderIdType::Id(vid) = id { Some(vid) } else { None };
|
|
|
|
|
|
|
|
|
|
if let Some(stream_id) = stream_id_opt {
|
|
|
|
|
if let Some(query) = db_query {
|
2026-02-18 08:18:28 +01:00
|
|
|
let query = Arc::clone(&query);
|
|
|
|
|
let item = match tokio::task::spawn_blocking(move || {
|
|
|
|
|
let mut guard = query.lock();
|
|
|
|
|
guard.query_zero_copy(&stream_id).ok().flatten()
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(item) => item,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("Failed to query Live metadata from disk for {stream_id}: {err}");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(item) = item {
|
2026-02-12 15:06:57 +01:00
|
|
|
existing_item = Some(item.clone());
|
|
|
|
|
if let Some(StreamProperties::Live(p)) = item.additional_properties.as_ref() {
|
2026-02-18 08:18:28 +01:00
|
|
|
props = Some(*p.clone());
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2024-12-10 23:16:04 +01:00
|
|
|
}
|
2026-02-12 15:06:57 +01:00
|
|
|
} else {
|
|
|
|
|
let xtream_path = xtream_get_file_path(&storage_path, XtreamCluster::Live);
|
|
|
|
|
if xtream_path.exists() {
|
2026-02-18 08:18:28 +01:00
|
|
|
let file_lock = app_config.file_locks.read_lock(&xtream_path).await;
|
|
|
|
|
let xtream_path = xtream_path.clone();
|
|
|
|
|
let item = match tokio::task::spawn_blocking(move || {
|
|
|
|
|
let _guard = file_lock;
|
|
|
|
|
let mut query = BPlusTreeQuery::<u32, XtreamPlaylistItem>::try_new(&xtream_path)?;
|
|
|
|
|
query.query_zero_copy(&stream_id)
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(Ok(item)) => item,
|
|
|
|
|
Ok(Err(err)) => {
|
|
|
|
|
warn!("Failed to query Live metadata from disk for {stream_id}: {err}");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("Failed to query Live metadata from disk for {stream_id}: {err}");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(item) = item {
|
|
|
|
|
existing_item = Some(item.clone());
|
|
|
|
|
if let Some(StreamProperties::Live(p)) = item.additional_properties.as_ref() {
|
|
|
|
|
props = Some(*p.clone());
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize props if missing
|
|
|
|
|
let mut properties = if let Some(p) = props {
|
|
|
|
|
p
|
|
|
|
|
} else {
|
|
|
|
|
LiveStreamProperties {
|
|
|
|
|
stream_id: stream_id_opt.unwrap_or(0),
|
|
|
|
|
// If item exists but no props, try to recover name
|
|
|
|
|
name: existing_item.as_ref().map_or_else(|| "".into(), |i| i.name.clone()),
|
|
|
|
|
..LiveStreamProperties::default()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 1. Create dummy properties to generate URL for probing
|
|
|
|
|
// We construct a temporary property object just for URL generation to ensure we match the stream config
|
|
|
|
|
// (prefix/extension) correctly, even if we modify `properties` later.
|
|
|
|
|
let temp_stream_prop = StreamProperties::Live(Box::new(properties.clone()));
|
|
|
|
|
|
|
|
|
|
let input_url = input.url.as_str();
|
|
|
|
|
let username = input.username.as_deref().unwrap_or("");
|
|
|
|
|
let password = input.password.as_deref().unwrap_or("");
|
2026-02-14 17:52:35 +01:00
|
|
|
let use_prefix = input.has_flag(ConfigInputFlags::XtreamLiveStreamUsePrefix);
|
|
|
|
|
let no_ext = input.has_flag(ConfigInputFlags::XtreamLiveStreamWithoutExtension);
|
2026-02-12 15:06:57 +01:00
|
|
|
|
|
|
|
|
// We generate the URL to probe directly on the provider
|
|
|
|
|
let stream_url = create_xtream_url(
|
|
|
|
|
XtreamCluster::Live,
|
|
|
|
|
input_url, username, password,
|
|
|
|
|
&temp_stream_prop,
|
|
|
|
|
use_prefix, no_ext
|
|
|
|
|
);
|
|
|
|
|
let config = app_config.config.load();
|
2026-02-25 19:30:19 +01:00
|
|
|
let metadata_update = config.metadata_update.clone().unwrap_or_default();
|
2026-02-26 14:41:02 +01:00
|
|
|
let ffprobe_timeout = metadata_update.ffprobe.timeout.unwrap_or(60);
|
2026-02-12 15:06:57 +01:00
|
|
|
let user_agent = config.default_user_agent.clone();
|
2026-02-26 14:41:02 +01:00
|
|
|
let analyze_duration = metadata_update.ffprobe.live_analyze_duration_micros;
|
|
|
|
|
let probe_size = metadata_update.ffprobe.live_probe_size_bytes;
|
2026-02-12 15:06:57 +01:00
|
|
|
|
|
|
|
|
let display_id = stream_id_opt.map_or_else(|| "StringID".to_string(), |v| v.to_string());
|
|
|
|
|
debug!("Probing Live Stream ID {} for input {}", display_id, input.name);
|
|
|
|
|
|
|
|
|
|
// Update last_probed_timestamp BEFORE probing to ensure we record the attempt even if it crashes/panics (unlikely but safe)
|
|
|
|
|
// Actually, update it before persisting.
|
|
|
|
|
let now = chrono::Utc::now().timestamp();
|
|
|
|
|
properties.last_probed_timestamp = Some(now);
|
|
|
|
|
|
|
|
|
|
let mut success = false;
|
2026-02-26 14:41:02 +01:00
|
|
|
let mut not_found = false;
|
|
|
|
|
match crate::utils::ffmpeg::probe_url(
|
2026-02-12 15:06:57 +01:00
|
|
|
&stream_url,
|
|
|
|
|
user_agent.as_deref(),
|
|
|
|
|
analyze_duration,
|
|
|
|
|
probe_size,
|
|
|
|
|
ffprobe_timeout,
|
2026-02-26 20:36:01 +01:00
|
|
|
config.proxy.as_ref(),
|
2026-02-26 14:41:02 +01:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio) => {
|
|
|
|
|
// 3. Update properties on success
|
|
|
|
|
if let Some(v) = raw_video {
|
|
|
|
|
properties.video = Some(v.to_string().into());
|
|
|
|
|
}
|
|
|
|
|
if let Some(a) = raw_audio {
|
|
|
|
|
properties.audio = Some(a.to_string().into());
|
|
|
|
|
}
|
|
|
|
|
properties.last_success_timestamp = Some(now);
|
|
|
|
|
success = true;
|
|
|
|
|
|
|
|
|
|
debug_if_enabled!("Successfully probed Live Stream ID {}", display_id);
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2026-02-26 14:41:02 +01:00
|
|
|
ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => {
|
|
|
|
|
warn!("Live stream probe target returned 404 for ID {} (Input: {})", display_id, input.name);
|
|
|
|
|
not_found = true;
|
|
|
|
|
}
|
|
|
|
|
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
|
|
|
|
|
warn!("Probe failed for Live Stream ID {} (Input: {})", display_id, input.name);
|
|
|
|
|
// We still persist the updated last_probed_timestamp so we don't retry immediately
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
2026-03-05 00:04:53 +01:00
|
|
|
ProbeUrlOutcome::Failed(ProbeFailureKind::Cancelled) => {
|
|
|
|
|
warn!("Probe cancelled for Live Stream ID {} (Input: {})", display_id, input.name);
|
|
|
|
|
// We still persist the updated last_probed_timestamp so we don't retry immediately
|
|
|
|
|
}
|
2026-02-12 15:06:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Persist
|
|
|
|
|
if save {
|
|
|
|
|
if let Some(stream_id) = stream_id_opt {
|
|
|
|
|
persist_input_live_info(app_config, &storage_path, XtreamCluster::Live, &input.name, stream_id, &properties)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| shared::error::info_err!("Persist error: {e}"))?;
|
|
|
|
|
}
|
2024-12-10 23:16:04 +01:00
|
|
|
}
|
2026-02-12 15:06:57 +01:00
|
|
|
|
|
|
|
|
if !success {
|
2026-02-26 14:41:02 +01:00
|
|
|
if not_found {
|
|
|
|
|
return Err(shared::error::info_err!("Probe failed with 404 Not Found for stream {display_id}"));
|
|
|
|
|
}
|
2026-02-12 15:06:57 +01:00
|
|
|
// Return error to propagate failure up to task manager/logs
|
|
|
|
|
return Err(shared::error::info_err!("Probe failed for stream {display_id}"));
|
2024-12-10 23:16:04 +01:00
|
|
|
}
|
2026-02-12 15:06:57 +01:00
|
|
|
|
|
|
|
|
Ok(Some(properties))
|
2024-12-10 23:16:04 +01:00
|
|
|
}
|