Staged filter and safe yaml update (#846)

New Features

    Added separate processing and persistence filters, with empty/non-empty checks and expanded filter fields.
    Added persistence-stage filter controls in target settings.
    Added clear_invalid_epg_ids, preserving playlist entries while clearing unresolved EPG IDs; the legacy setting remains supported.
    Added safer, atomic source configuration updates that preserve comments and formatting.

Bug Fixes

    Null-like Xtream and Stalker values are now handled correctly.
    Duplicate credential warnings provide clearer details.
This commit is contained in:
euzu
2026-08-30 21:08:30 +02:00
committed by GitHub
parent e7b5fb2748
commit 5ecfd68148
94 changed files with 5167 additions and 2129 deletions
+14 -44
View File
@@ -17,12 +17,12 @@ use shared::{
error::TuliproxError,
foundation::ValueProvider,
model::{
EventSink, MediaQuality, PlaylistEntry, PlaylistItem, PlaylistItemType, StreamProperties,
VideoStreamDetailProperties, VideoStreamProperties, XtreamCluster, XtreamPlaylistItem, XtreamVideoInfo,
EventSink, MediaQuality, PlaylistItem, PlaylistItemType, StreamProperties, VideoStreamDetailProperties,
VideoStreamProperties, XtreamCluster, XtreamPlaylistItem, XtreamVideoInfo,
},
};
use std::{
collections::{HashMap, HashSet},
collections::HashSet,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -96,47 +96,17 @@ pub async fn playlist_resolve_vod<E: EventSink + Clone + 'static, M: MetadataUpd
}
fn sync_resolved_vod_properties(provider_fpl: &mut FetchedPlaylist<'_>, processed_fpl: &mut FetchedPlaylist<'_>) {
let mut resolved_vod_by_provider_id: HashMap<u32, VideoStreamProperties> = HashMap::new();
for pli in processed_fpl.items() {
if pli.header.xtream_cluster != XtreamCluster::Video || pli.header.item_type != PlaylistItemType::Video {
continue;
}
let Some(provider_id) = pli.get_provider_id() else {
continue;
};
if provider_id == 0 {
continue;
}
if let Some(StreamProperties::Video(properties)) = pli.header.additional_properties.as_ref() {
resolved_vod_by_provider_id.entry(provider_id).or_insert_with(|| properties.as_ref().clone());
}
}
if resolved_vod_by_provider_id.is_empty() {
return;
}
for source_pli in provider_fpl.items_mut() {
if source_pli.header.xtream_cluster != XtreamCluster::Video
|| source_pli.header.item_type != PlaylistItemType::Video
{
continue;
}
let Some(provider_id) = source_pli.get_provider_id() else {
continue;
};
if provider_id == 0 {
continue;
}
if let Some(resolved) = resolved_vod_by_provider_id.get(&provider_id) {
source_pli.header.additional_properties = Some(StreamProperties::Video(Box::new(resolved.clone())));
}
}
crate::processor::xtream::sync_resolved_xtream_properties(
provider_fpl,
processed_fpl,
XtreamCluster::Video,
PlaylistItemType::Video,
|props| match props {
StreamProperties::Video(v) => Some(v.as_ref()),
_ => None,
},
StreamProperties::Video,
);
}
#[allow(clippy::too_many_lines, clippy::too_many_arguments)]