mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 06:42:20 +02:00
@@ -4,7 +4,7 @@ use shared::model::{PlaylistGroup, SortOrder, SortTarget};
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn direction(order: SortOrder, ordering: Ordering) -> Ordering {
|
||||
fn apply_sort_order(order: SortOrder, ordering: Ordering) -> Ordering {
|
||||
match (order, ordering) {
|
||||
(SortOrder::None, _) | (_, Ordering::Equal) => Ordering::Equal,
|
||||
(SortOrder::Asc, o) => o,
|
||||
@@ -12,6 +12,220 @@ fn direction(order: SortOrder, ordering: Ordering) -> Ordering {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_capture_group_rank(name: &str) -> Option<u32> {
|
||||
let suffix = name.strip_prefix('c')?;
|
||||
if suffix.is_empty() || !suffix.bytes().all(|c| c.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
suffix.parse::<u32>().ok()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SequencePattern {
|
||||
regex: Arc<regex::Regex>,
|
||||
ordered_capture_names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SequencePlan {
|
||||
patterns: Vec<SequencePattern>,
|
||||
}
|
||||
|
||||
impl SequencePlan {
|
||||
fn new(sequence: &[Arc<regex::Regex>]) -> Self {
|
||||
let patterns = sequence
|
||||
.iter()
|
||||
.map(|regex| {
|
||||
let mut ordered_capture_names: Vec<(u32, String)> = regex
|
||||
.capture_names()
|
||||
.flatten()
|
||||
.filter_map(|name| parse_capture_group_rank(name).map(|rank| (rank, name.to_owned())))
|
||||
.collect();
|
||||
ordered_capture_names.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
|
||||
|
||||
SequencePattern {
|
||||
regex: Arc::clone(regex),
|
||||
ordered_capture_names: ordered_capture_names.into_iter().map(|(_, name)| name).collect(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self { patterns }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SequenceMatch {
|
||||
Matched { sequence_idx: usize, captures: Vec<Option<Arc<str>>> },
|
||||
Unmatched,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PreparedRule<'a> {
|
||||
rule: &'a ConfigSortRule,
|
||||
sequence_plan: Option<SequencePlan>,
|
||||
}
|
||||
|
||||
impl<'a> PreparedRule<'a> {
|
||||
fn new(rule: &'a ConfigSortRule) -> Self {
|
||||
let sequence_plan = rule.sequence.as_ref().map(|sequence| SequencePlan::new(sequence));
|
||||
Self { rule, sequence_plan }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RuleCacheEntry {
|
||||
filter_pass: bool,
|
||||
value: Option<Arc<str>>,
|
||||
sequence_match: Option<SequenceMatch>,
|
||||
}
|
||||
|
||||
fn evaluate_sequence(plan: &SequencePlan, value: &str) -> SequenceMatch {
|
||||
for (sequence_idx, pattern) in plan.patterns.iter().enumerate() {
|
||||
if let Some(captures) = pattern.regex.captures(value) {
|
||||
let ordered_captures = pattern
|
||||
.ordered_capture_names
|
||||
.iter()
|
||||
.map(|name| captures.name(name).map(|capture| Arc::<str>::from(capture.as_str())))
|
||||
.collect();
|
||||
return SequenceMatch::Matched { sequence_idx, captures: ordered_captures };
|
||||
}
|
||||
}
|
||||
SequenceMatch::Unmatched
|
||||
}
|
||||
|
||||
fn compare_sequence_match(a: &SequenceMatch, b: &SequenceMatch, order: SortOrder) -> Ordering {
|
||||
match (a, b) {
|
||||
(
|
||||
SequenceMatch::Matched {
|
||||
sequence_idx: idx_a,
|
||||
captures: captures_a,
|
||||
},
|
||||
SequenceMatch::Matched {
|
||||
sequence_idx: idx_b,
|
||||
captures: captures_b,
|
||||
},
|
||||
) => {
|
||||
// Sequence index is an explicit priority list and is never reversed by DESC.
|
||||
let idx_ord = idx_a.cmp(idx_b);
|
||||
if idx_ord != Ordering::Equal {
|
||||
return idx_ord;
|
||||
}
|
||||
|
||||
let capture_count = captures_a.len().max(captures_b.len());
|
||||
for index in 0..capture_count {
|
||||
let ord = match (captures_a.get(index), captures_b.get(index)) {
|
||||
(Some(Some(v1)), Some(Some(v2))) => v1.cmp(v2),
|
||||
(Some(Some(_)), Some(None) | None) => Ordering::Greater,
|
||||
(Some(None) | None, Some(Some(_))) => Ordering::Less,
|
||||
_ => Ordering::Equal,
|
||||
};
|
||||
if ord != Ordering::Equal {
|
||||
return apply_sort_order(order, ord);
|
||||
}
|
||||
}
|
||||
Ordering::Equal
|
||||
}
|
||||
(SequenceMatch::Matched { .. }, SequenceMatch::Unmatched) => Ordering::Less,
|
||||
(SequenceMatch::Unmatched, SequenceMatch::Matched { .. }) => Ordering::Greater,
|
||||
(SequenceMatch::Unmatched, SequenceMatch::Unmatched) => Ordering::Equal,
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_rule_entries(rule: &PreparedRule, left: &RuleCacheEntry, right: &RuleCacheEntry) -> Ordering {
|
||||
match (left.filter_pass, right.filter_pass) {
|
||||
(false, false) => return Ordering::Equal,
|
||||
(true, false) => return Ordering::Less,
|
||||
(false, true) => return Ordering::Greater,
|
||||
(true, true) => {}
|
||||
}
|
||||
|
||||
match (&left.value, &right.value) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(Some(value_left), Some(value_right)) => {
|
||||
if rule.sequence_plan.is_some() {
|
||||
match (&left.sequence_match, &right.sequence_match) {
|
||||
(Some(seq_left), Some(seq_right)) => compare_sequence_match(seq_left, seq_right, rule.rule.order),
|
||||
_ => Ordering::Equal,
|
||||
}
|
||||
} else {
|
||||
apply_sort_order(rule.rule.order, value_left.cmp(value_right))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_rule_cache_entry(
|
||||
rule: &PreparedRule,
|
||||
item: Option<&shared::model::PlaylistItem>,
|
||||
match_as_ascii: bool,
|
||||
) -> RuleCacheEntry {
|
||||
let Some(item) = item else {
|
||||
return RuleCacheEntry {
|
||||
filter_pass: false,
|
||||
value: None,
|
||||
sequence_match: None,
|
||||
};
|
||||
};
|
||||
|
||||
let provider = ValueProvider { pli: item, match_as_ascii };
|
||||
let filter_pass = rule.rule.filter.filter(&provider);
|
||||
if !filter_pass {
|
||||
return RuleCacheEntry {
|
||||
filter_pass: false,
|
||||
value: None,
|
||||
sequence_match: None,
|
||||
};
|
||||
}
|
||||
|
||||
let value = provider.get(rule.rule.field.as_str());
|
||||
let sequence_match = match (&rule.sequence_plan, value.as_deref()) {
|
||||
(Some(sequence_plan), Some(value)) => Some(evaluate_sequence(sequence_plan, value)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
RuleCacheEntry {
|
||||
filter_pass: true,
|
||||
value,
|
||||
sequence_match,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_group_rule_cache(rule: &PreparedRule, groups: &[PlaylistGroup], match_as_ascii: bool) -> Vec<RuleCacheEntry> {
|
||||
groups
|
||||
.iter()
|
||||
.map(|group| build_rule_cache_entry(rule, group.channels.first(), match_as_ascii))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_channel_rule_cache(
|
||||
rule: &PreparedRule,
|
||||
channels: &[shared::model::PlaylistItem],
|
||||
match_as_ascii: bool,
|
||||
) -> Vec<RuleCacheEntry> {
|
||||
channels
|
||||
.iter()
|
||||
.map(|channel| build_rule_cache_entry(rule, Some(channel), match_as_ascii))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn reorder_by_indices<T>(items: &mut Vec<T>, indices: &[usize]) {
|
||||
debug_assert_eq!(items.len(), indices.len());
|
||||
|
||||
let mut original: Vec<Option<T>> = std::mem::take(items).into_iter().map(Some).collect();
|
||||
items.reserve(indices.len());
|
||||
|
||||
for &idx in indices {
|
||||
let item = original[idx]
|
||||
.take()
|
||||
.expect("index permutation must contain each source position exactly once");
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn playlist_comparator(
|
||||
sequence: Option<&Vec<Arc<regex::Regex>>>,
|
||||
order: SortOrder,
|
||||
@@ -21,80 +235,14 @@ fn playlist_comparator(
|
||||
if matches!(order, SortOrder::None) {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if let Some(regex_list) = sequence {
|
||||
let mut match_a = None;
|
||||
let mut match_b = None;
|
||||
|
||||
for (i, regex) in regex_list.iter().enumerate() {
|
||||
if match_a.is_none() {
|
||||
if let Some(caps) = regex.captures(value_a) {
|
||||
match_a = Some((i, caps));
|
||||
}
|
||||
}
|
||||
if match_b.is_none() {
|
||||
if let Some(caps) = regex.captures(value_b) {
|
||||
match_b = Some((i, caps));
|
||||
}
|
||||
}
|
||||
|
||||
// If both matches found → break
|
||||
if match_a.is_some() && match_b.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match (match_a, match_b) {
|
||||
(Some((idx_a, caps_a)), Some((idx_b, caps_b))) => {
|
||||
if idx_a != idx_b {
|
||||
return match order {
|
||||
SortOrder::Asc => idx_a.cmp(&idx_b),
|
||||
SortOrder::Desc => idx_b.cmp(&idx_a),
|
||||
SortOrder::None => Ordering::Equal,
|
||||
};
|
||||
}
|
||||
|
||||
// Same regex → sort by captures (c1, c2, …)
|
||||
let mut named: Vec<_> = regex_list[idx_a]
|
||||
.capture_names()
|
||||
.flatten()
|
||||
.filter(|name| name.starts_with('c'))
|
||||
.collect();
|
||||
|
||||
named.sort_by_key(|name| name[1..].parse::<u32>().unwrap_or(0));
|
||||
|
||||
for name in named {
|
||||
let va = caps_a.name(name).map(|m| m.as_str());
|
||||
let vb = caps_b.name(name).map(|m| m.as_str());
|
||||
let o = match (va, vb) {
|
||||
(Some(a), Some(b)) => a.cmp(b),
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
_ => Ordering::Equal,
|
||||
};
|
||||
|
||||
if o != Ordering::Equal {
|
||||
return 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) => Ordering::Equal,
|
||||
}
|
||||
if let Some(sequence) = sequence {
|
||||
let plan = SequencePlan::new(sequence);
|
||||
let left = evaluate_sequence(&plan, value_a);
|
||||
let right = evaluate_sequence(&plan, value_b);
|
||||
compare_sequence_match(&left, &right, order)
|
||||
} else {
|
||||
// No Regex-Sequence defined → fallback
|
||||
let o = value_a.cmp(value_b);
|
||||
match order {
|
||||
SortOrder::Asc => o,
|
||||
SortOrder::Desc => o.reverse(),
|
||||
SortOrder::None => Ordering::Equal,
|
||||
}
|
||||
apply_sort_order(order, value_a.cmp(value_b))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,9 +256,37 @@ macro_rules! sort_groups_by_source_order {
|
||||
};
|
||||
}
|
||||
|
||||
fn compare_cached_rule_entries(
|
||||
rules: &[PreparedRule<'_>],
|
||||
rule_caches: &[Vec<RuleCacheEntry>],
|
||||
left_idx: usize,
|
||||
right_idx: usize,
|
||||
) -> Ordering {
|
||||
for (rule, cache) in rules.iter().zip(rule_caches.iter()) {
|
||||
let ord = compare_rule_entries(rule, &cache[left_idx], &cache[right_idx]);
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply raw-value fallback only after all rule-level comparisons are exhausted.
|
||||
for (rule, cache) in rules.iter().zip(rule_caches.iter()) {
|
||||
let left = &cache[left_idx];
|
||||
let right = &cache[right_idx];
|
||||
if let (Some(va), Some(vb)) = (&left.value, &right.value) {
|
||||
let fallback = apply_sort_order(rule.rule.order, va.cmp(vb));
|
||||
if fallback != Ordering::Equal {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ordering::Equal
|
||||
}
|
||||
|
||||
pub(in crate::processing::processor) fn sort_playlist(
|
||||
target: &ConfigTarget,
|
||||
playlist: &mut [PlaylistGroup],
|
||||
playlist: &mut Vec<PlaylistGroup>,
|
||||
) -> bool {
|
||||
let Some(sort) = &target.sort else {
|
||||
for group in &mut *playlist {
|
||||
@@ -122,14 +298,14 @@ pub(in crate::processing::processor) fn sort_playlist(
|
||||
|
||||
let rules = &sort.rules;
|
||||
let match_as_ascii = sort.match_as_ascii;
|
||||
sort_channels_in_groups(playlist, rules, match_as_ascii);
|
||||
sort_channels_in_groups(playlist.as_mut_slice(), rules, match_as_ascii);
|
||||
sort_groups(playlist, rules, match_as_ascii);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn sort_groups(
|
||||
groups: &mut [PlaylistGroup],
|
||||
groups: &mut Vec<PlaylistGroup>,
|
||||
rules: &[ConfigSortRule],
|
||||
match_as_ascii: bool,
|
||||
) {
|
||||
@@ -137,6 +313,7 @@ fn sort_groups(
|
||||
.iter()
|
||||
.filter(|r| matches!(r.target, SortTarget::Group))
|
||||
.filter(|r| r.order != SortOrder::None)
|
||||
.map(PreparedRule::new)
|
||||
.collect();
|
||||
|
||||
if group_rules.is_empty() {
|
||||
@@ -144,51 +321,30 @@ fn sort_groups(
|
||||
return;
|
||||
}
|
||||
|
||||
groups.sort_by(|a_grp, b_grp| {
|
||||
let a_chan = a_grp.channels.first();
|
||||
let b_chan = b_grp.channels.first();
|
||||
let rule_caches: Vec<Vec<RuleCacheEntry>> = group_rules
|
||||
.iter()
|
||||
.map(|rule| build_group_rule_cache(rule, groups.as_slice(), match_as_ascii))
|
||||
.collect();
|
||||
|
||||
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),
|
||||
(None, None) => continue,
|
||||
};
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
let mut group_indices: Vec<usize> = (0..groups.len()).collect();
|
||||
group_indices.sort_by(|left_idx, right_idx| {
|
||||
let ord = compare_cached_rule_entries(&group_rules, &rule_caches, *left_idx, *right_idx);
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
|
||||
let order1 = a_chan.as_ref().map_or(u32::MAX, |c| c.header.source_ordinal);
|
||||
let order2 = b_chan.as_ref().map_or(u32::MAX, |c| c.header.source_ordinal);
|
||||
let order1 = groups[*left_idx]
|
||||
.channels
|
||||
.first()
|
||||
.map_or(u32::MAX, |c| c.header.source_ordinal);
|
||||
let order2 = groups[*right_idx]
|
||||
.channels
|
||||
.first()
|
||||
.map_or(u32::MAX, |c| c.header.source_ordinal);
|
||||
order1.cmp(&order2)
|
||||
});
|
||||
|
||||
reorder_by_indices(groups, &group_indices);
|
||||
}
|
||||
|
||||
fn sort_channels_in_groups(
|
||||
@@ -200,6 +356,7 @@ fn sort_channels_in_groups(
|
||||
.iter()
|
||||
.filter(|r| matches!(r.target, SortTarget::Channel))
|
||||
.filter(|r| r.order != SortOrder::None)
|
||||
.map(PreparedRule::new)
|
||||
.collect();
|
||||
|
||||
if channel_rules.is_empty() {
|
||||
@@ -212,64 +369,51 @@ fn sort_channels_in_groups(
|
||||
}
|
||||
|
||||
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 };
|
||||
let rule_caches: Vec<Vec<RuleCacheEntry>> = channel_rules
|
||||
.iter()
|
||||
.map(|rule| build_channel_rule_cache(rule, &group.channels, match_as_ascii))
|
||||
.collect();
|
||||
|
||||
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 = 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 {
|
||||
if let (Some(va), Some(vb)) = (&va, &vb) {
|
||||
let fallback = direction(rule.order, va.cmp(vb));
|
||||
if fallback != Ordering::Equal {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
let mut channel_indices: Vec<usize> = (0..group.channels.len()).collect();
|
||||
channel_indices.sort_by(|left_idx, right_idx| {
|
||||
let ord = compare_cached_rule_entries(&channel_rules, &rule_caches, *left_idx, *right_idx);
|
||||
if ord != Ordering::Equal {
|
||||
return ord;
|
||||
}
|
||||
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
group.channels[*left_idx]
|
||||
.header
|
||||
.source_ordinal
|
||||
.cmp(&group.channels[*right_idx].header.source_ordinal)
|
||||
});
|
||||
|
||||
reorder_by_indices(&mut group.channels, &channel_indices);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
compare_rule_entries, playlist_comparator, sort_channels_in_groups, sort_groups, PreparedRule, RuleCacheEntry,
|
||||
};
|
||||
use crate::model::ConfigSortRule;
|
||||
use crate::processing::processor::sort::playlist_comparator;
|
||||
use shared::foundation::Filter;
|
||||
use shared::model::{ItemField, PlaylistItem, PlaylistItemHeader, SortOrder, SortTarget};
|
||||
use shared::model::{ItemField, PlaylistGroup, PlaylistItem, PlaylistItemHeader, SortOrder, SortTarget, XtreamCluster};
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn make_group(id: u32, title: &str, channels: Vec<PlaylistItem>) -> PlaylistGroup {
|
||||
PlaylistGroup {
|
||||
id,
|
||||
title: title.into(),
|
||||
channels,
|
||||
xtream_cluster: XtreamCluster::Live,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort() {
|
||||
let mut channels: Vec<PlaylistItem> = vec![
|
||||
let channels: Vec<PlaylistItem> = vec![
|
||||
("D", "HD"),
|
||||
("A", "FHD"),
|
||||
("Z", "HD"),
|
||||
@@ -306,19 +450,8 @@ mod tests {
|
||||
filter: Filter::default(),
|
||||
};
|
||||
|
||||
channels.sort_by(|a, b| {
|
||||
let va = &a.header.title;
|
||||
let vb = &b.header.title;
|
||||
|
||||
let ord =
|
||||
playlist_comparator(channel_sort.sequence.as_ref(), channel_sort.order, va, vb);
|
||||
|
||||
if ord == Ordering::Equal {
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
} else {
|
||||
ord
|
||||
}
|
||||
});
|
||||
let mut groups = vec![make_group(1, "G1", channels)];
|
||||
sort_channels_in_groups(groups.as_mut_slice(), &[channel_sort], false);
|
||||
|
||||
let expected = vec![
|
||||
"Chanel K [UHD]",
|
||||
@@ -335,7 +468,11 @@ mod tests {
|
||||
"Chanel T [SD]",
|
||||
].into_iter().map(Into::into).collect::<Vec<Arc<str>>>();
|
||||
|
||||
let sorted = channels
|
||||
let sorted = groups
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("group should exist")
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|pli| pli.header.title)
|
||||
.collect::<Vec<_>>();
|
||||
@@ -345,7 +482,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_sort2() {
|
||||
let mut channels: Vec<PlaylistItem> = vec![
|
||||
let channels: Vec<PlaylistItem> = vec![
|
||||
"US| EAST [FHD] abc",
|
||||
"US| EAST [FHD] def",
|
||||
"US| EAST [FHD] ghi",
|
||||
@@ -399,20 +536,8 @@ mod tests {
|
||||
filter: Filter::default(),
|
||||
};
|
||||
|
||||
channels.sort_by(|a, b| {
|
||||
let ord = playlist_comparator(
|
||||
channel_sort.sequence.as_ref(),
|
||||
channel_sort.order,
|
||||
&a.header.title,
|
||||
&b.header.title,
|
||||
);
|
||||
|
||||
if ord == Ordering::Equal {
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
} else {
|
||||
ord
|
||||
}
|
||||
});
|
||||
let mut groups = vec![make_group(1, "G1", channels)];
|
||||
sort_channels_in_groups(groups.as_mut_slice(), &[channel_sort], false);
|
||||
|
||||
let expected = vec![
|
||||
"US| EAST [UHD] m",
|
||||
@@ -441,11 +566,188 @@ mod tests {
|
||||
"US| West f",
|
||||
].into_iter().map(Into::into).collect::<Vec<Arc<str>>>();
|
||||
|
||||
let sorted = channels
|
||||
let sorted = groups
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("group should exist")
|
||||
.channels
|
||||
.into_iter()
|
||||
.map(|pli| pli.header.title)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(expected, sorted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sequence_priority_is_not_reversed_for_desc() {
|
||||
let mut channels: Vec<PlaylistItem> = vec!["A-1", "B-9", "A-7", "B-2"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, title)| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: title.to_string().into(),
|
||||
source_ordinal: u32::try_from(i).unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
let sequence = vec![
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^A-(?P<c1>\d+)$").unwrap(),
|
||||
shared::model::REGEX_CACHE.get_or_compile(r"^B-(?P<c1>\d+)$").unwrap(),
|
||||
];
|
||||
|
||||
channels.sort_by(|a, b| {
|
||||
let ord = playlist_comparator(
|
||||
Some(&sequence),
|
||||
SortOrder::Desc,
|
||||
&a.header.title,
|
||||
&b.header.title,
|
||||
);
|
||||
|
||||
if ord == Ordering::Equal {
|
||||
a.header.source_ordinal.cmp(&b.header.source_ordinal)
|
||||
} else {
|
||||
ord
|
||||
}
|
||||
});
|
||||
|
||||
let sorted = channels
|
||||
.into_iter()
|
||||
.map(|pli| pli.header.title)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = vec!["A-7", "A-1", "B-9", "B-2"]
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Arc<str>>>();
|
||||
|
||||
assert_eq!(expected, sorted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_rule_entries_desc_filter_mismatch() {
|
||||
let rule = ConfigSortRule {
|
||||
target: SortTarget::Channel,
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Desc,
|
||||
sequence: Some(vec![shared::model::REGEX_CACHE.get_or_compile(r"^A-(?P<c1>\d+)$").unwrap()]),
|
||||
filter: Filter::default(),
|
||||
};
|
||||
|
||||
let prepared = PreparedRule::new(&rule);
|
||||
let pass_with_value = RuleCacheEntry {
|
||||
filter_pass: true,
|
||||
value: Some(Arc::from("A-2")),
|
||||
sequence_match: None,
|
||||
};
|
||||
let pass_without_value = RuleCacheEntry {
|
||||
filter_pass: true,
|
||||
value: None,
|
||||
sequence_match: None,
|
||||
};
|
||||
let fail_filter = RuleCacheEntry {
|
||||
filter_pass: false,
|
||||
value: None,
|
||||
sequence_match: None,
|
||||
};
|
||||
|
||||
assert_eq!(compare_rule_entries(&prepared, &pass_with_value, &fail_filter), Ordering::Less);
|
||||
assert_eq!(compare_rule_entries(&prepared, &fail_filter, &pass_with_value), Ordering::Greater);
|
||||
assert_eq!(compare_rule_entries(&prepared, &pass_with_value, &pass_without_value), Ordering::Less);
|
||||
assert_eq!(compare_rule_entries(&prepared, &pass_without_value, &pass_with_value), Ordering::Greater);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_channels_does_not_short_circuit_later_rules_with_raw_value_fallback() {
|
||||
let channels: Vec<PlaylistItem> = vec!["A-2-x", "A-1-y"]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, title)| PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: title.to_string().into(),
|
||||
source_ordinal: u32::try_from(i).unwrap(),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
let first_rule = ConfigSortRule {
|
||||
target: SortTarget::Channel,
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Asc,
|
||||
// Both values match the same sequence item and produce equal sequence priority.
|
||||
sequence: Some(vec![shared::model::REGEX_CACHE.get_or_compile(r"^A-\d+-.$").unwrap()]),
|
||||
filter: Filter::default(),
|
||||
};
|
||||
let second_rule = ConfigSortRule {
|
||||
target: SortTarget::Channel,
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Desc,
|
||||
sequence: None,
|
||||
filter: Filter::default(),
|
||||
};
|
||||
|
||||
let mut groups = vec![make_group(1, "G1", channels)];
|
||||
sort_channels_in_groups(groups.as_mut_slice(), &[first_rule, second_rule], false);
|
||||
let sorted = groups[0]
|
||||
.channels
|
||||
.iter()
|
||||
.map(|pli| pli.header.title.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected = vec!["A-2-x", "A-1-y"]
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Arc<str>>>();
|
||||
assert_eq!(expected, sorted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sort_groups_uses_raw_value_fallback_before_source_ordinal() {
|
||||
let group_a = make_group(
|
||||
1,
|
||||
"A",
|
||||
vec![PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: Arc::from("A-2-x"),
|
||||
source_ordinal: 0,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
);
|
||||
let group_b = make_group(
|
||||
2,
|
||||
"B",
|
||||
vec![PlaylistItem {
|
||||
header: PlaylistItemHeader {
|
||||
title: Arc::from("A-1-y"),
|
||||
source_ordinal: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
);
|
||||
|
||||
let group_rule = ConfigSortRule {
|
||||
target: SortTarget::Group,
|
||||
field: ItemField::Caption,
|
||||
order: SortOrder::Asc,
|
||||
// Both groups match the same sequence item and produce equal sequence priority.
|
||||
sequence: Some(vec![shared::model::REGEX_CACHE.get_or_compile(r"^A-\d+-.$").unwrap()]),
|
||||
filter: Filter::default(),
|
||||
};
|
||||
|
||||
let mut groups = vec![group_a, group_b];
|
||||
sort_groups(&mut groups, &[group_rule], false);
|
||||
|
||||
let sorted = groups
|
||||
.iter()
|
||||
.map(|group| group.channels[0].header.title.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let expected = vec!["A-1-y", "A-2-x"]
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Arc<str>>>();
|
||||
assert_eq!(expected, sorted);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user