use std::{ collections::BTreeSet, fs::{self, File, OpenOptions, ReadDir}, io::{self, Read, Write}, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, }; use serde::Deserialize; use toml_edit::{Array, ArrayOfTables, DocumentMut, Table, value}; use crate::youtube::{Video, valid_video_id, validate_video}; use tubular::miruro::{Identity, SourceDocument}; const MAX_STATE_BYTES: u64 = 8 * 1024 * 1024; const MAX_LINE: usize = 16 * 1024; const STREAM_BYTES: usize = 16 * 1024; const MAX_STREAMS: usize = 4096; const MAX_SOURCE_BYTES: usize = 1024 * 1024; const MAX_IDENTITY_BYTES: u64 = 7 + 512 + 1 + 10; static NEXT_NAME: AtomicU64 = AtomicU64::new(0); fn invalid(message: impl Into) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, message.into()) } struct StoredFile { path: PathBuf, original: Option>, } impl StoredFile { fn open(path: PathBuf) -> io::Result<(Self, DocumentMut)> { let original = read_bytes(&path, MAX_STATE_BYTES)?; let document = match &original { Some(bytes) => std::str::from_utf8(bytes) .map_err(|e| invalid(format!("{} is not UTF-8: {e}", path.display())))? .parse::() .map_err(|e| { invalid(format!( "Invalid {}: {e}; repair this file before retrying", path.display() )) })?, None => DocumentMut::new(), }; if original.is_some() && document.get("version").and_then(|v| v.as_integer()) != Some(1) { return Err(invalid(format!("{} requires version = 1", path.display()))); } Ok((Self { path, original }, document)) } fn save(&mut self, document: DocumentMut) -> io::Result<()> { let contents = document.to_string().into_bytes(); atomic_write(&self.path, self.original.as_deref(), &contents)?; self.original = Some(contents); Ok(()) } } #[derive(Clone, Copy)] pub enum CollectionKind { WatchLater, Saved, Watching, } #[derive(Clone, Copy)] pub enum WatchStatus { Watching, Watched, } struct Collection { file: StoredFile, videos: Vec