fix: fill_weighted sorts by recency via playback history
This commit is contained in:
@@ -4,7 +4,9 @@ use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::SeedableRng;
|
||||
|
||||
use crate::models::MediaItem;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::models::{MediaItem, PlaybackRecord};
|
||||
use crate::value_objects::{FillStrategy, MediaItemId};
|
||||
|
||||
pub(super) fn fill_block<'a>(
|
||||
@@ -14,6 +16,7 @@ pub(super) fn fill_block<'a>(
|
||||
strategy: &FillStrategy,
|
||||
last_item_id: Option<&MediaItemId>,
|
||||
loop_on_finish: bool,
|
||||
history: &[PlaybackRecord],
|
||||
) -> Vec<&'a MediaItem> {
|
||||
match strategy {
|
||||
FillStrategy::BestFit => fill_best_fit(pool, target_secs),
|
||||
@@ -38,7 +41,7 @@ pub(super) fn fill_block<'a>(
|
||||
fill_alternating(candidates, pool, target_secs)
|
||||
}
|
||||
FillStrategy::Weighted => {
|
||||
fill_weighted(candidates, pool, target_secs)
|
||||
fill_weighted(pool, target_secs, history)
|
||||
}
|
||||
FillStrategy::Marathon => {
|
||||
fill_marathon(candidates, pool, target_secs, loop_on_finish)
|
||||
@@ -201,60 +204,39 @@ pub(super) fn fill_alternating<'a>(
|
||||
}
|
||||
|
||||
pub(super) fn fill_weighted<'a>(
|
||||
candidates: &'a [MediaItem],
|
||||
pool: &'a [MediaItem],
|
||||
target_secs: u32,
|
||||
history: &[PlaybackRecord],
|
||||
) -> Vec<&'a MediaItem> {
|
||||
if pool.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let pool_ids: HashSet<&MediaItemId> = pool.iter().map(|i| i.id()).collect();
|
||||
let candidate_ids: HashSet<&MediaItemId> = candidates.iter().map(|i| i.id()).collect();
|
||||
|
||||
let mut fresh: Vec<&MediaItem> = pool
|
||||
let last_played: HashMap<&MediaItemId, i64> = history
|
||||
.iter()
|
||||
.filter(|i| !candidate_ids.contains(i.id()) || pool_ids.contains(i.id()))
|
||||
.collect();
|
||||
|
||||
let all_in_pool: Vec<&MediaItem> = pool.iter().collect();
|
||||
.fold(HashMap::new(), |mut acc, r| {
|
||||
let ts = r.played_at().timestamp();
|
||||
acc.entry(r.item_id())
|
||||
.and_modify(|prev| *prev = (*prev).max(ts))
|
||||
.or_insert(ts);
|
||||
acc
|
||||
});
|
||||
|
||||
let mut items: Vec<&MediaItem> = pool.iter().collect();
|
||||
let mut rng = StdRng::from_entropy();
|
||||
fresh.shuffle(&mut rng);
|
||||
items.shuffle(&mut rng);
|
||||
items.sort_by_key(|i| last_played.get(i.id()).copied().unwrap_or(i64::MIN));
|
||||
|
||||
let mut remaining = target_secs;
|
||||
let mut result = Vec::new();
|
||||
let mut used: HashSet<&MediaItemId> = HashSet::new();
|
||||
|
||||
for item in &fresh {
|
||||
for item in items {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
if used.contains(item.id()) {
|
||||
continue;
|
||||
}
|
||||
if item.duration_secs() <= remaining {
|
||||
remaining -= item.duration_secs();
|
||||
used.insert(item.id());
|
||||
result.push(*item);
|
||||
}
|
||||
}
|
||||
|
||||
if remaining > 0 {
|
||||
let mut rest: Vec<&MediaItem> = all_in_pool
|
||||
.iter()
|
||||
.filter(|i| !used.contains(i.id()))
|
||||
.copied()
|
||||
.collect();
|
||||
rest.shuffle(&mut rng);
|
||||
for item in rest {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
if item.duration_secs() <= remaining {
|
||||
remaining -= item.duration_secs();
|
||||
result.push(item);
|
||||
}
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -744,6 +744,7 @@ impl ScheduleEngineService {
|
||||
params.strategy,
|
||||
rotation.last_item_id,
|
||||
params.loop_on_finish,
|
||||
rotation.history,
|
||||
);
|
||||
|
||||
let mut slots = Vec::new();
|
||||
|
||||
@@ -64,7 +64,7 @@ fn sequential_includes_oversize_first_episode() {
|
||||
fn random_fill_respects_budget() {
|
||||
let pool = vec![item("a", 100), item("b", 100), item("c", 100)];
|
||||
let candidates = pool.clone();
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Random, None, true);
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Random, None, true, &[]);
|
||||
let total: u32 = result.iter().map(|i| i.duration_secs()).sum();
|
||||
assert!(total <= 200);
|
||||
}
|
||||
@@ -118,7 +118,7 @@ fn alternating_empty_pool() {
|
||||
fn weighted_respects_budget() {
|
||||
let pool = vec![item("a", 100), item("b", 100), item("c", 100)];
|
||||
let candidates = pool.clone();
|
||||
let result = fill_weighted(&candidates, &pool, 200);
|
||||
let result = fill_weighted(&pool, 200, &[]);
|
||||
let total: u32 = result.iter().map(|i| i.duration_secs()).sum();
|
||||
assert!(total <= 200);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ fn weighted_respects_budget() {
|
||||
fn weighted_no_duplicates() {
|
||||
let pool = vec![item("a", 50), item("b", 50), item("c", 50)];
|
||||
let candidates = pool.clone();
|
||||
let result = fill_weighted(&candidates, &pool, 150);
|
||||
let result = fill_weighted(&pool, 150, &[]);
|
||||
let ids: Vec<&str> = result.iter().map(|i| i.id().value()).collect();
|
||||
let unique: HashSet<&str> = ids.iter().copied().collect();
|
||||
assert_eq!(ids.len(), unique.len());
|
||||
@@ -137,7 +137,7 @@ fn weighted_no_duplicates() {
|
||||
fn weighted_empty_pool() {
|
||||
let candidates: Vec<MediaItem> = vec![];
|
||||
let pool: Vec<MediaItem> = vec![];
|
||||
let result = fill_weighted(&candidates, &pool, 300);
|
||||
let result = fill_weighted(&pool, 300, &[]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ fn marathon_oversize_single_item() {
|
||||
fn fill_block_dispatches_alternating() {
|
||||
let candidates = vec![item("a", 100), item("b", 100)];
|
||||
let pool = candidates.clone();
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Alternating, None, true);
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Alternating, None, true, &[]);
|
||||
let total: u32 = result.iter().map(|i| i.duration_secs()).sum();
|
||||
assert!(total <= 200);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ fn fill_block_dispatches_alternating() {
|
||||
fn fill_block_dispatches_weighted() {
|
||||
let candidates = vec![item("a", 100), item("b", 100)];
|
||||
let pool = candidates.clone();
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Weighted, None, true);
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Weighted, None, true, &[]);
|
||||
let total: u32 = result.iter().map(|i| i.duration_secs()).sum();
|
||||
assert!(total <= 200);
|
||||
}
|
||||
@@ -210,6 +210,6 @@ fn fill_block_dispatches_weighted() {
|
||||
fn fill_block_dispatches_marathon() {
|
||||
let candidates = vec![item("a", 100), item("b", 100)];
|
||||
let pool = candidates.clone();
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Marathon, None, true);
|
||||
let result = fill_block(&candidates, &pool, 200, &FillStrategy::Marathon, None, true, &[]);
|
||||
assert_eq!(result[0].id().value(), "a");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user