Add Week 9 Smart Features: collections, artwork, predictive prefetch

Smart Collections (musicfs-search/src/collections.rs):
- CollectionStore with thread-safe Mutex<Connection>
- CollectionQuery enum: Match, DateRange, RecentlyAdded/Played, MostPlayed, Genre, Compound
- Builtin collections for Recently Added, 80s/90s Music

Artwork Extraction & Caching:
- ArtworkExtractor using symphonia Visual (musicfs-metadata)
- ArtworkCache with CAS storage + on-demand resize (musicfs-cache)
- ArtType: Front/Back/Other, ArtSize: Thumbnail/Medium/Full

Predictive Prefetching:
- PatternStore tracks access patterns with sequence prediction
- PrefetchEngine listens to FileAccessed events, prefetches predictions
- PrefetchOps exposes /.prefetch/ virtual directory with status/hints

Oracle review fixes applied:
- CollectionStore uses Mutex for thread safety
- FileAccessed event now includes file_id for canonical correlation
- JSON parse warnings in collection deserialization

130 tests pass (15 new tests added)
This commit is contained in:
Alexander
2026-05-13 07:21:28 +02:00
parent 3cb6dfcaf8
commit 34d05b7a49
18 changed files with 1933 additions and 0 deletions
@@ -8,3 +8,4 @@ musicfs-core = { path = "../musicfs-core" }
symphonia.workspace = true
thiserror.workspace = true
tracing.workspace = true
image.workspace = true
@@ -0,0 +1,116 @@
use image::ImageFormat;
use std::io::Cursor;
use symphonia::core::meta::Visual;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct Artwork {
pub art_type: ArtType,
pub mime_type: String,
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtType {
Front,
Back,
Other,
}
#[derive(Debug, Clone, Copy)]
pub enum ArtSize {
Thumbnail,
Medium,
Full,
}
impl ArtSize {
pub fn max_dimension(&self) -> Option<u32> {
match self {
ArtSize::Thumbnail => Some(150),
ArtSize::Medium => Some(300),
ArtSize::Full => None,
}
}
}
pub struct ArtworkExtractor;
impl ArtworkExtractor {
pub fn extract_from_visual(visual: &Visual) -> Option<Artwork> {
let data = visual.data.to_vec();
let img = image::load_from_memory(&data).ok()?;
let art_type = match visual.usage {
Some(symphonia::core::meta::StandardVisualKey::FrontCover) => ArtType::Front,
Some(symphonia::core::meta::StandardVisualKey::BackCover) => ArtType::Back,
_ => ArtType::Other,
};
let mime_type = if visual.media_type.is_empty() {
"image/jpeg".to_string()
} else {
visual.media_type.clone()
};
Some(Artwork {
art_type,
mime_type,
width: img.width(),
height: img.height(),
data,
})
}
pub fn resize(artwork: &Artwork, size: ArtSize) -> Option<Artwork> {
let max_dim = size.max_dimension()?;
if artwork.width <= max_dim && artwork.height <= max_dim {
return Some(artwork.clone());
}
let img = image::load_from_memory(&artwork.data).ok()?;
let resized = img.thumbnail(max_dim, max_dim);
let mut output = Vec::new();
let mut cursor = Cursor::new(&mut output);
resized.write_to(&mut cursor, ImageFormat::Jpeg).ok()?;
debug!(
"Resized artwork from {}x{} to {}x{}",
artwork.width,
artwork.height,
resized.width(),
resized.height()
);
Some(Artwork {
art_type: artwork.art_type,
mime_type: "image/jpeg".to_string(),
width: resized.width(),
height: resized.height(),
data: output,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_art_size_dimensions() {
assert_eq!(ArtSize::Thumbnail.max_dimension(), Some(150));
assert_eq!(ArtSize::Medium.max_dimension(), Some(300));
assert_eq!(ArtSize::Full.max_dimension(), None);
}
#[test]
fn test_art_type_equality() {
assert_eq!(ArtType::Front, ArtType::Front);
assert_ne!(ArtType::Front, ArtType::Back);
}
}
@@ -1,3 +1,5 @@
pub mod artwork;
mod parser;
pub use artwork::{ArtSize, ArtType, Artwork, ArtworkExtractor};
pub use parser::MetadataParser;