use std::{ collections::VecDeque, ffi::OsStr, io::{self, Read}, path::Path, process::{Child, Command, Stdio}, sync::mpsc::{self, Receiver, TryRecvError}, thread, }; use serde::{Deserialize, Serialize}; pub const PAGE_SIZE: usize = 60; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FeedRequest { pub channel: usize, pub start: usize, pub count: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct FeedProgress { pub request: FeedRequest, /// Raw rows, including unavailable, invalid and duplicate entries. pub count: usize, } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct FeedCursor { // None before planning; an exhausted channel has no next offset. positions: Option>>, rotation: usize, } impl FeedCursor { /// Planning is repeatable until a complete successful page is accepted. pub fn plan(&mut self, channels: usize) -> io::Result> { let positions = self .positions .get_or_insert_with(|| vec![Some(1); channels]); if positions.len() != channels { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Feed channel membership changed without resetting its cursor", )); } self.planned().map(|(requests, _)| requests) } fn planned(&self) -> io::Result<(Vec, usize)> { let Some(positions) = &self.positions else { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Feed is not planned", )); }; let active = positions .iter() .filter(|position| position.is_some()) .count(); if active == 0 { return Ok((Vec::new(), self.rotation)); } let selected = active.min(PAGE_SIZE); let mut requests = Vec::with_capacity(selected); let mut next_rotation = self.rotation; for step in 0..positions.len() { let channel = (self.rotation + step) % positions.len(); let Some(start) = positions[channel] else { continue; }; let index = requests.len(); let count = PAGE_SIZE / selected + usize::from(index < PAGE_SIZE % selected); range_end(start, count)?; requests.push(FeedRequest { channel, start, count, }); if index == (PAGE_SIZE - 1) % selected { next_rotation = (channel + 1) % positions.len(); } if requests.len() == selected { break; } } requests.sort_unstable_by_key(|request| (request.start, request.count, request.channel)); Ok((requests, next_rotation)) } pub fn accept(&mut self, progress: &[FeedProgress]) -> Result<(), String> { let (requests, rotation) = self.planned().map_err(|error| error.to_string())?; if progress.len() != requests.len() { return Err("Feed progress did not match the planned channels".into()); } // Preflight every request and offset before changing any channel or the rotation. for (entry, request) in progress.iter().zip(&requests) { if entry.request != *request || entry.count > request.count { return Err("Feed progress did not match the planned request".into()); } if entry.count == request.count && request.start.checked_add(request.count).is_none() { return Err("Feed channel offset overflow".into()); } } let positions = self.positions.as_mut().unwrap(); for entry in progress { positions[entry.request.channel] = if entry.count < entry.request.count { None } else { Some(entry.request.start + entry.request.count) }; } self.rotation = rotation; Ok(()) } /// Unrequested channels remain active until their own response proves exhaustion. pub fn has_more(&self) -> bool { self.positions .as_ref() .is_some_and(|positions| positions.iter().any(Option::is_some)) } } pub struct Page { pub videos: Vec