mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-04 15:02:16 +02:00
- Fixed Sort
- Added cluster to favourites - Added Regex cache to avoid multiple compilations of same regex
This commit is contained in:
@@ -40,7 +40,7 @@ use shared::foundation::filter::{get_field_value, set_field_value, Filter, Value
|
||||
use shared::model::xtream_const::XTREAM_CLUSTER;
|
||||
use shared::model::{CounterModifier, FieldGetAccessor, FieldSetAccessor, InputType, ItemField, MsgKind,
|
||||
PlaylistGroup, PlaylistItem, PlaylistItemType, PlaylistUpdateState,
|
||||
ProcessingOrder, UUIDType};
|
||||
ProcessingOrder, UUIDType, XtreamCluster};
|
||||
use shared::utils::{create_alias_uuid, default_as_default, StringInterner};
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -50,13 +50,14 @@ fn is_valid(pli: &PlaylistItem, filter: &Filter, match_as_ascii: bool) -> bool {
|
||||
}
|
||||
|
||||
pub fn apply_filter_to_source(source: &mut dyn PlaylistSource, filter: &Filter) -> Option<Vec<PlaylistGroup>> {
|
||||
let mut groups: IndexMap<Arc<str>, PlaylistGroup> = IndexMap::new();
|
||||
let mut groups: IndexMap<CategoryKey, PlaylistGroup> = IndexMap::new();
|
||||
for pli in source.into_items() {
|
||||
if is_valid(&pli, filter, false) {
|
||||
let group_title = pli.header.group.clone();
|
||||
let cluster = pli.header.xtream_cluster;
|
||||
let cat_id = pli.header.category_id;
|
||||
groups.entry(group_title.clone())
|
||||
let key = (cluster, group_title.clone());
|
||||
groups.entry(key)
|
||||
.or_insert_with(|| PlaylistGroup {
|
||||
id: cat_id,
|
||||
title: group_title,
|
||||
@@ -130,7 +131,7 @@ fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec<ConfigRename>>, inter
|
||||
fn rename_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, interner: &mut StringInterner) -> Option<Vec<PlaylistGroup>> {
|
||||
match &target.rename {
|
||||
Some(renames) if !renames.is_empty() => {
|
||||
let mut groups: IndexMap<Arc<str>, PlaylistGroup> = IndexMap::new();
|
||||
let mut groups: IndexMap<(XtreamCluster, Arc<str>), PlaylistGroup> = IndexMap::new();
|
||||
for mut pli in source.into_items() {
|
||||
// Handle group rename first if it's in the renames
|
||||
for r in renames {
|
||||
@@ -146,7 +147,7 @@ fn rename_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, inter
|
||||
let group_title = pli.header.group.clone();
|
||||
let cluster = pli.header.xtream_cluster;
|
||||
let cat_id = pli.header.category_id;
|
||||
groups.entry(group_title.clone())
|
||||
groups.entry((cluster, group_title.clone()))
|
||||
.or_insert_with(|| PlaylistGroup {
|
||||
id: cat_id,
|
||||
title: group_title,
|
||||
@@ -205,12 +206,12 @@ fn map_playlist(source: &mut dyn PlaylistSource, target: &ConfigTarget, _interne
|
||||
Box::new(iter.flat_map(move |chan| map_channel_and_flatten(chan, mapping)))
|
||||
as Box<dyn Iterator<Item=PlaylistItem>>
|
||||
});
|
||||
let mut next_groups: IndexMap<Arc<str>, PlaylistGroup> = IndexMap::new();
|
||||
let mut next_groups: IndexMap<(XtreamCluster, Arc<str>), PlaylistGroup> = IndexMap::new();
|
||||
let mut grp_id: u32 = 0;
|
||||
for channel in mapped_iter {
|
||||
let group_title = channel.header.group.clone();
|
||||
let cluster = channel.header.xtream_cluster;
|
||||
next_groups.entry(group_title.clone())
|
||||
next_groups.entry((cluster, group_title.clone()))
|
||||
.or_insert_with(|| {
|
||||
grp_id += 1;
|
||||
PlaylistGroup {
|
||||
@@ -774,7 +775,7 @@ async fn process_playlist_for_target(ctx: &PlaylistProcessingContext,
|
||||
|
||||
pub fn process_favourites(playlist: &mut Vec<PlaylistGroup>, favourites_cfg: Option<&[ConfigFavourites]>) {
|
||||
if let Some(favourites) = favourites_cfg {
|
||||
let mut fav_groups: IndexMap<Arc<str>, Vec<PlaylistItem>> = IndexMap::new();
|
||||
let mut fav_groups: IndexMap<CategoryKey, Vec<PlaylistItem>> = IndexMap::new();
|
||||
for pg in playlist.iter() {
|
||||
for pli in &pg.channels {
|
||||
// series episodes can't be included in favourites
|
||||
@@ -782,13 +783,13 @@ pub fn process_favourites(playlist: &mut Vec<PlaylistGroup>, favourites_cfg: Opt
|
||||
continue;
|
||||
}
|
||||
for fav in favourites {
|
||||
if is_valid(pli, &fav.filter, fav.match_as_ascii) {
|
||||
if pli.header.xtream_cluster == fav.cluster && is_valid(pli, &fav.filter, fav.match_as_ascii) {
|
||||
let mut channel = pli.clone();
|
||||
channel.header.group.clone_from(&fav.group);
|
||||
// Update UUID to be an alias of the original
|
||||
channel.header.uuid = create_alias_uuid(&pli.header.uuid, &fav.group);
|
||||
fav_groups
|
||||
.entry(fav.group.clone())
|
||||
.entry((fav.cluster, fav.group.clone()))
|
||||
.or_default()
|
||||
.push(channel);
|
||||
}
|
||||
@@ -796,9 +797,9 @@ pub fn process_favourites(playlist: &mut Vec<PlaylistGroup>, favourites_cfg: Opt
|
||||
}
|
||||
}
|
||||
|
||||
for (group_name, channels) in fav_groups {
|
||||
for (fav_group, channels) in fav_groups {
|
||||
if !channels.is_empty() {
|
||||
let xtream_cluster = channels[0].header.xtream_cluster;
|
||||
let (xtream_cluster, group_name) = fav_group;
|
||||
playlist.push(PlaylistGroup {
|
||||
id: 0,
|
||||
title: group_name,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::model::{ConfigSortRule, ConfigTarget};
|
||||
use shared::foundation::filter::ValueProvider;
|
||||
use shared::model::{PlaylistGroup, SortOrder, SortTarget};
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn direction(order: SortOrder, ordering: Ordering) -> Ordering {
|
||||
match (order, ordering) {
|
||||
@@ -12,7 +13,7 @@ fn direction(order: SortOrder, ordering: Ordering) -> Ordering {
|
||||
}
|
||||
|
||||
fn playlist_comparator(
|
||||
sequence: Option<&Vec<regex::Regex>>,
|
||||
sequence: Option<&Vec<Arc<regex::Regex>>>,
|
||||
order: SortOrder,
|
||||
value_a: &str,
|
||||
value_b: &str,
|
||||
@@ -77,24 +78,11 @@ fn playlist_comparator(
|
||||
}
|
||||
}
|
||||
|
||||
let o = value_a.cmp(value_b);
|
||||
match order {
|
||||
SortOrder::Asc => o,
|
||||
SortOrder::Desc => o.reverse(),
|
||||
SortOrder::None => Ordering::Equal,
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
(Some(_), None) => direction(order, Ordering::Less),
|
||||
(None, Some(_)) => direction(order, Ordering::Greater),
|
||||
(None, None) => {
|
||||
// NP match → fallback
|
||||
let o = value_a.cmp(value_b);
|
||||
match order {
|
||||
SortOrder::Asc => o,
|
||||
SortOrder::Desc => o.reverse(),
|
||||
SortOrder::None => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
(None, None) => Ordering::Equal,
|
||||
}
|
||||
} else {
|
||||
// No Regex-Sequence defined → fallback
|
||||
@@ -123,65 +111,64 @@ pub(in crate::processing::processor) fn sort_playlist(
|
||||
true
|
||||
}
|
||||
|
||||
fn sort_groups(groups: &mut [PlaylistGroup], rules: &[ConfigSortRule], match_as_ascii: bool) {
|
||||
fn sort_groups(
|
||||
groups: &mut [PlaylistGroup],
|
||||
rules: &[ConfigSortRule],
|
||||
match_as_ascii: bool,
|
||||
) {
|
||||
let group_rules: Vec<_> = rules
|
||||
.iter()
|
||||
.filter(|r| matches!(r.target, SortTarget::Group))
|
||||
.filter(|r| r.order != SortOrder::None)
|
||||
.collect();
|
||||
|
||||
if group_rules.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for rule in &group_rules {
|
||||
if rule.order == SortOrder::None {
|
||||
continue;
|
||||
}
|
||||
groups.sort_by(|a_grp, b_grp| {
|
||||
let (a_chan, b_chan) = match (a_grp.channels.first(), b_grp.channels.first()) {
|
||||
(None, None) => return Ordering::Equal,
|
||||
groups.sort_by(|a_grp, b_grp| {
|
||||
let a_chan = a_grp.channels.first();
|
||||
let b_chan = b_grp.channels.first();
|
||||
|
||||
for rule in &group_rules {
|
||||
let (vp_a, vp_b) = match (a_chan, b_chan) {
|
||||
(Some(a), Some(b)) => (
|
||||
ValueProvider { pli: a, match_as_ascii },
|
||||
ValueProvider { pli: b, match_as_ascii },
|
||||
),
|
||||
(Some(_), None) => return direction(rule.order, Ordering::Less),
|
||||
(None, Some(_)) => return direction(rule.order, Ordering::Greater),
|
||||
(Some(a), Some(b)) => (a, b),
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
let provider_a = ValueProvider {
|
||||
pli: a_chan,
|
||||
match_as_ascii,
|
||||
};
|
||||
let provider_b = ValueProvider {
|
||||
pli: b_chan,
|
||||
match_as_ascii,
|
||||
};
|
||||
let fa = rule.filter.filter(&vp_a);
|
||||
let fb = rule.filter.filter(&vp_b);
|
||||
|
||||
match (
|
||||
rule.filter.filter(&provider_a),
|
||||
rule.filter.filter(&provider_b),
|
||||
) {
|
||||
// (false, false) => return Ordering::Equal,
|
||||
// (true, false) => return direction(rule.order, Ordering::Less),
|
||||
// (false, true) => return direction(rule.order, Ordering::Greater),
|
||||
(true, true) => { /* fallthrough */ }
|
||||
_ => return Ordering::Equal,
|
||||
match (fa, fb) {
|
||||
(false, false) => continue,
|
||||
(true, false) => return direction(rule.order, Ordering::Less),
|
||||
(false, true) => return direction(rule.order, Ordering::Greater),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let va = provider_a.get(rule.field.as_str());
|
||||
let vb = provider_b.get(rule.field.as_str());
|
||||
match (va, vb) {
|
||||
(None, None) => return Ordering::Equal,
|
||||
(Some(_), None) => return direction(rule.order, Ordering::Less),
|
||||
(None, Some(_)) => return direction(rule.order, Ordering::Greater),
|
||||
let va = vp_a.get(rule.field.as_str());
|
||||
let vb = vp_b.get(rule.field.as_str());
|
||||
let ord = match (va, vb) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(Some(_), None) => direction(rule.order, Ordering::Less),
|
||||
(None, Some(_)) => direction(rule.order, Ordering::Greater),
|
||||
(Some(va), Some(vb)) => {
|
||||
let ord = playlist_comparator(rule.sequence.as_ref(), rule.order, &va, &vb);
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
playlist_comparator(rule.sequence.as_ref(), rule.order, &va, &vb)
|
||||
}
|
||||
};
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
|
||||
fn sort_channels_in_groups(
|
||||
@@ -192,50 +179,48 @@ fn sort_channels_in_groups(
|
||||
let channel_rules: Vec<_> = rules
|
||||
.iter()
|
||||
.filter(|r| matches!(r.target, SortTarget::Channel))
|
||||
.filter(|r| r.order != SortOrder::None)
|
||||
.collect();
|
||||
|
||||
if channel_rules.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for rule in channel_rules {
|
||||
for group in &mut *groups {
|
||||
if rule.order == SortOrder::None {
|
||||
continue;
|
||||
}
|
||||
group.channels.sort_by(|a, b| {
|
||||
let provider_a = ValueProvider {
|
||||
pli: a,
|
||||
match_as_ascii,
|
||||
};
|
||||
let provider_b = ValueProvider {
|
||||
pli: b,
|
||||
match_as_ascii,
|
||||
};
|
||||
for group in groups {
|
||||
group.channels.sort_by(|a, b| {
|
||||
let vp_a = ValueProvider { pli: a, match_as_ascii };
|
||||
let vp_b = ValueProvider { pli: b, match_as_ascii };
|
||||
|
||||
match (
|
||||
rule.filter.filter(&provider_a),
|
||||
rule.filter.filter(&provider_b),
|
||||
) {
|
||||
// (false, false) => return Ordering::Equal,
|
||||
// (true, false) => return direction(rule.order, Ordering::Less),
|
||||
// (false, true) => return direction(rule.order, Ordering::Greater),
|
||||
(true, true) => { /* fallthrough */ }
|
||||
_ => return Ordering::Equal,
|
||||
for rule in &channel_rules {
|
||||
let fa = rule.filter.filter(&vp_a);
|
||||
let fb = rule.filter.filter(&vp_b);
|
||||
|
||||
match (fa, fb) {
|
||||
(false, false) => continue,
|
||||
(true, false) => return direction(rule.order, Ordering::Less),
|
||||
(false, true) => return direction(rule.order, Ordering::Greater),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let va = provider_a.get(rule.field.as_str());
|
||||
let vb = provider_b.get(rule.field.as_str());
|
||||
match (va, vb) {
|
||||
let va = vp_a.get(rule.field.as_str());
|
||||
let vb = vp_b.get(rule.field.as_str());
|
||||
|
||||
let ord = match (va, vb) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(Some(_), None) => direction(rule.order, Ordering::Less),
|
||||
(None, Some(_)) => direction(rule.order, Ordering::Greater),
|
||||
(Some(va), Some(vb)) => {
|
||||
playlist_comparator(rule.sequence.as_ref(), rule.order, &va, &vb)
|
||||
}
|
||||
};
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ordering::Equal
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +228,6 @@ fn sort_channels_in_groups(
|
||||
mod tests {
|
||||
use crate::model::ConfigSortRule;
|
||||
use crate::processing::processor::sort::playlist_comparator;
|
||||
use regex::Regex;
|
||||
use shared::foundation::filter::Filter;
|
||||
use shared::model::{ItemField, PlaylistItem, PlaylistItemHeader, SortOrder, SortTarget};
|
||||
use std::cmp::Ordering;
|
||||
@@ -280,9 +264,9 @@ mod tests {
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Asc,
|
||||
sequence: Some(vec![
|
||||
Regex::new(r"(?P<c1>.*?)\bUHD\b").unwrap(),
|
||||
Regex::new(r"(?P<c1>.*?)\bFHD\b").unwrap(),
|
||||
Regex::new(r"(?P<c1>.*?)\bHD\b").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"(?P<c1>.*?)\bUHD\b").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"(?P<c1>.*?)\bFHD\b").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"(?P<c1>.*?)\bHD\b").unwrap(),
|
||||
]),
|
||||
filter: Filter::default(),
|
||||
};
|
||||
@@ -368,14 +352,14 @@ mod tests {
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Asc,
|
||||
sequence: Some(vec").unwrap(),
|
||||
Regex::new(r"^US\| EAST.*?\[\bFHD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| EAST.*?\[\bHD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| EAST.*?\[\bSD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| WEST.*?\[\bUHD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| WEST.*?\[\bFHD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| WEST.*?\[\bHD\b\](?P<c1>.*)").unwrap(),
|
||||
Regex::new(r"^US\| WEST.*?\[\bSD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| EAST.*?\[\bUHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| EAST.*?\[\bFHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| EAST.*?\[\bHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| EAST.*?\[\bSD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| WEST.*?\[\bUHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| WEST.*?\[\bFHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| WEST.*?\[\bHD\b\](?P<c1>.*)").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^US\| WEST.*?\[\bSD\b\](?P<c1>.*)").unwrap(),
|
||||
]),
|
||||
filter: Filter::default(),
|
||||
};
|
||||
@@ -429,4 +413,5 @@ mod tests {
|
||||
|
||||
assert_eq!(expected, sorted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use shared::error::TuliproxError;
|
||||
use shared::utils::StringInterner;
|
||||
use shared::model::{FieldGetAccessor, FieldSetAccessor, PlaylistGroup, PlaylistItem, TraktContentType, XtreamCluster};
|
||||
use shared::utils::CONSTANTS;
|
||||
use indexmap::IndexMap;
|
||||
use std::borrow::Cow;
|
||||
use strsim::normalized_levenshtein;
|
||||
|
||||
@@ -130,10 +131,10 @@ fn create_category_from_matches<'a>(
|
||||
matches: Vec<TraktMatchResult<'a>>,
|
||||
list_config: &'a TraktListConfig,
|
||||
interner: &mut StringInterner,
|
||||
) -> Option<PlaylistGroup> {
|
||||
if matches.is_empty() { return None; }
|
||||
) -> Vec<PlaylistGroup> {
|
||||
if matches.is_empty() { return vec![]; }
|
||||
|
||||
let mut matched_items = Vec::new();
|
||||
let mut matched_items_by_cluster: IndexMap<XtreamCluster, Vec<PlaylistItem>> = IndexMap::new();
|
||||
|
||||
let mut sorted_matches = matches;
|
||||
sorted_matches.sort_by(|a, b| {
|
||||
@@ -150,11 +151,7 @@ fn create_category_from_matches<'a>(
|
||||
|
||||
for match_result in sorted_matches {
|
||||
let mut modified_item = match_result.playlist_item.clone();
|
||||
// Use the (possibly numbered) title from the match result (which now contains the original playlist title)
|
||||
with!(mut modified_item.header => header {
|
||||
// Synchronize name with title so both fields show the same value
|
||||
// header.title.clone_from(&match_result.trakt_item.title.to_string());
|
||||
// header.name.clone_from(&match_result.trakt_item.title.to_string());
|
||||
let title = header.get_field("caption").unwrap_or_else(|| Cow::Borrowed(&header.title));
|
||||
if extract_quality(&title).is_none() {
|
||||
if let Some(quality) = extract_quality(&header.group) {
|
||||
@@ -168,28 +165,18 @@ fn create_category_from_matches<'a>(
|
||||
}
|
||||
header.group = interner.intern(group_title);
|
||||
header.gen_uuid();
|
||||
matched_items_by_cluster.entry(header.xtream_cluster).or_default().push(modified_item);
|
||||
});
|
||||
matched_items.push(modified_item);
|
||||
}
|
||||
|
||||
if matched_items.is_empty() { return None; }
|
||||
|
||||
|
||||
let cluster = match list_config.content_type {
|
||||
TraktContentType::Vod => XtreamCluster::Video,
|
||||
TraktContentType::Series => XtreamCluster::Series,
|
||||
TraktContentType::Both => {
|
||||
matched_items.first()
|
||||
.map_or(XtreamCluster::Video, |item| item.header.xtream_cluster)
|
||||
matched_items_by_cluster.into_iter().map(|(cluster, channels)| {
|
||||
PlaylistGroup {
|
||||
id: 0,
|
||||
title: interner.intern(group_title),
|
||||
channels,
|
||||
xtream_cluster: cluster,
|
||||
}
|
||||
};
|
||||
|
||||
Some(PlaylistGroup {
|
||||
id: 0,
|
||||
title: interner.intern(group_title),
|
||||
channels: matched_items,
|
||||
xtream_cluster: cluster,
|
||||
})
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn match_trakt_items_with_playlist<'a>(
|
||||
@@ -197,7 +184,7 @@ fn match_trakt_items_with_playlist<'a>(
|
||||
playlist: &'a [PlaylistGroup],
|
||||
list_config: &'a TraktListConfig,
|
||||
interner: &mut StringInterner,
|
||||
) -> Option<PlaylistGroup> {
|
||||
) -> Vec<PlaylistGroup> {
|
||||
let trakt_match_items: Vec<TraktMatchItem<'a>> = trakt_items
|
||||
.iter()
|
||||
.filter(|item| should_include_item(item, list_config.content_type))
|
||||
@@ -256,7 +243,8 @@ impl TraktCategoriesProcessor {
|
||||
Ok(trakt_items) => {
|
||||
debug!("Processing Trakt list {cache_key} with {} items", trakt_items.len());
|
||||
|
||||
if let Some(category) = match_trakt_items_with_playlist(&trakt_items, playlist, list_config, &mut interner) {
|
||||
let categories = match_trakt_items_with_playlist(&trakt_items, playlist, list_config, &mut interner);
|
||||
for category in categories {
|
||||
if !category.channels.is_empty() {
|
||||
total_matches += category.channels.len();
|
||||
let category_len = category.channels.len();
|
||||
|
||||
Reference in New Issue
Block a user