34d05b7a49
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)
316 lines
8.6 KiB
Markdown
316 lines
8.6 KiB
Markdown
# Smart Features API Documentation
|
|
|
|
## Overview
|
|
|
|
MusicFS Week 9 introduces three intelligent features:
|
|
1. **Smart Collections** - Dynamic playlists based on queries, time ranges, and listening patterns
|
|
2. **Artwork Extraction & Caching** - Extract and serve album art in multiple sizes
|
|
3. **Predictive Prefetching** - Learn listening patterns to preload likely-next tracks
|
|
|
|
---
|
|
|
|
## Smart Collections
|
|
|
|
### CollectionStore
|
|
|
|
Manages persistent smart collections using SQLite.
|
|
|
|
```rust
|
|
pub struct CollectionStore {
|
|
db: rusqlite::Connection,
|
|
}
|
|
|
|
pub struct Collection {
|
|
pub id: i64,
|
|
pub name: String,
|
|
pub query: CollectionQuery,
|
|
pub created_at: SystemTime,
|
|
pub updated_at: SystemTime,
|
|
}
|
|
```
|
|
|
|
### CollectionQuery Types
|
|
|
|
| Query Type | Description | Example |
|
|
|------------|-------------|---------|
|
|
| `Match(String)` | tantivy search query | `"artist:Metallica"` |
|
|
| `DateRange { start, end }` | Files added within range | Last 30 days |
|
|
| `RecentlyAdded(days)` | Files added in last N days | `RecentlyAdded(7)` |
|
|
| `RecentlyPlayed(days)` | Files played in last N days | `RecentlyPlayed(30)` |
|
|
| `MostPlayed(limit)` | Top N most played tracks | `MostPlayed(100)` |
|
|
| `Genre(String)` | All tracks matching genre | `"Progressive Rock"` |
|
|
| `Compound(Vec)` | AND combination of queries | Multiple conditions |
|
|
|
|
### API
|
|
|
|
```rust
|
|
impl CollectionStore {
|
|
fn create(&self, name: &str, query: CollectionQuery) -> Result<i64, CollectionError>;
|
|
fn get(&self, id: i64) -> Result<Option<Collection>, CollectionError>;
|
|
fn list(&self) -> Result<Vec<Collection>, CollectionError>;
|
|
fn update(&self, id: i64, name: &str, query: CollectionQuery) -> Result<(), CollectionError>;
|
|
fn delete(&self, id: i64) -> Result<(), CollectionError>;
|
|
fn evaluate(&self, id: i64, index: &SearchIndex, patterns: &PatternStore) -> Result<Vec<FileId>, CollectionError>;
|
|
}
|
|
```
|
|
|
|
### FUSE Integration (Planned)
|
|
|
|
Collections will appear as virtual directories under `/.collections/`:
|
|
|
|
```bash
|
|
$ ls /mnt/musicfs/.collections/
|
|
Recent Additions/
|
|
Most Played/
|
|
80s Metal/
|
|
|
|
$ ls /mnt/musicfs/.collections/Most\ Played/
|
|
001. Track1.flac -> /mnt/musicfs/Artist/Album/Track1.flac
|
|
002. Track2.flac -> /mnt/musicfs/Artist/Album/Track2.flac
|
|
```
|
|
|
|
---
|
|
|
|
## Artwork Extraction & Caching
|
|
|
|
### ArtworkExtractor
|
|
|
|
Extracts embedded artwork from audio files.
|
|
|
|
```rust
|
|
pub struct Artwork {
|
|
pub data: Vec<u8>,
|
|
pub mime_type: String,
|
|
pub art_type: ArtType,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
pub enum ArtType {
|
|
Front,
|
|
Back,
|
|
Other,
|
|
}
|
|
|
|
pub enum ArtSize {
|
|
Thumbnail, // 150x150 max
|
|
Medium, // 300x300 max
|
|
Full, // Original size
|
|
}
|
|
```
|
|
|
|
### API
|
|
|
|
```rust
|
|
impl ArtworkExtractor {
|
|
fn extract(&self, path: &Path) -> Result<Vec<Artwork>, ArtworkError>;
|
|
fn extract_first(&self, path: &Path) -> Result<Option<Artwork>, ArtworkError>;
|
|
fn resize(data: &[u8], size: ArtSize) -> Result<Vec<u8>, ArtworkError>;
|
|
}
|
|
```
|
|
|
|
### ArtworkCache
|
|
|
|
Caches artwork in CAS (Content-Addressable Storage).
|
|
|
|
```rust
|
|
impl ArtworkCache {
|
|
async fn store(&self, file_id: i64, artwork: &Artwork) -> Result<ChunkHash, ArtworkError>;
|
|
async fn get(&self, file_id: i64, art_type: &str, size: ArtSize) -> Result<Option<Vec<u8>>, ArtworkError>;
|
|
async fn has(&self, file_id: i64, art_type: &str) -> Result<bool, ArtworkError>;
|
|
}
|
|
```
|
|
|
|
### Size Specifications
|
|
|
|
| Size | Max Dimension | Use Case |
|
|
|------|---------------|----------|
|
|
| Thumbnail | 150px | List views, grids |
|
|
| Medium | 300px | Detail panels |
|
|
| Full | Original | High-res display |
|
|
|
|
### Caching Strategy
|
|
|
|
1. Original artwork stored in CAS with content hash
|
|
2. SQLite maps `(file_id, art_type)` → `chunk_hash`
|
|
3. Resizing performed on-demand, not cached (saves storage)
|
|
4. Max input size: 10MB (reject larger images)
|
|
|
|
---
|
|
|
|
## Predictive Prefetching
|
|
|
|
### Access Patterns (PatternStore)
|
|
|
|
Tracks file access history to predict next tracks.
|
|
|
|
```rust
|
|
pub struct AccessPattern {
|
|
pub file_id: FileId,
|
|
pub timestamp: SystemTime,
|
|
pub context: AccessContext,
|
|
pub hour_of_day: u8,
|
|
}
|
|
|
|
pub struct AccessContext {
|
|
pub album_id: Option<i64>,
|
|
pub track_number: Option<u32>,
|
|
pub artist: Option<String>,
|
|
}
|
|
```
|
|
|
|
### Pattern Learning
|
|
|
|
| Pattern Type | Description | Use Case |
|
|
|--------------|-------------|----------|
|
|
| Sequential | A → B → C transitions | Album playback |
|
|
| Time-based | Hour-of-day preferences | Morning playlist |
|
|
| Frequency | Most played tracks | Popular content |
|
|
|
|
### API
|
|
|
|
```rust
|
|
impl PatternStore {
|
|
fn record(&self, file_id: FileId, context: AccessContext) -> Result<(), PatternError>;
|
|
fn predict_next(&self, current: FileId, limit: usize) -> Vec<FileId>;
|
|
fn predict_for_time(&self, hour: u8, limit: usize) -> Vec<FileId>;
|
|
fn recently_played(&self, days: u32) -> Result<Vec<FileId>, PatternError>;
|
|
fn most_played(&self, limit: u32) -> Result<Vec<FileId>, PatternError>;
|
|
}
|
|
```
|
|
|
|
### PrefetchEngine
|
|
|
|
Background engine that listens for file access events and prefetches predicted content.
|
|
|
|
```rust
|
|
pub struct PrefetchConfig {
|
|
pub lookahead: usize, // How many tracks to prefetch (default: 3)
|
|
pub max_concurrent: usize, // Concurrent prefetch limit (default: 2)
|
|
pub cooldown: Duration, // Delay between prefetch bursts (default: 100ms)
|
|
pub enabled: bool, // Master switch
|
|
}
|
|
```
|
|
|
|
### Architecture
|
|
|
|
```
|
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
│ EventBus │────▶│ PrefetchEngine │────▶│ ContentFetcher │
|
|
│ (FileAccessed) │ │ (predictions) │ │ (CAS storage) │
|
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────┐
|
|
│ PatternStore │
|
|
│ (SQLite DB) │
|
|
└─────────────────┘
|
|
```
|
|
|
|
### FUSE Interface
|
|
|
|
Virtual directory `/.prefetch/` exposes prefetch status and hints:
|
|
|
|
```bash
|
|
$ cat /mnt/musicfs/.prefetch/status
|
|
MusicFS Prefetch Status
|
|
=======================
|
|
running: true
|
|
in_flight: 2
|
|
most_played: [42, 57, 103, 89, 12]
|
|
|
|
$ ls /mnt/musicfs/.prefetch/
|
|
status
|
|
hint_0042
|
|
hint_0057
|
|
hint_0103
|
|
|
|
$ cat /mnt/musicfs/.prefetch/hint_0042
|
|
57
|
|
103
|
|
89
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Targets
|
|
|
|
| Feature | Metric | Target |
|
|
|---------|--------|--------|
|
|
| Collection evaluation | Latency | <100ms for 100k files |
|
|
| Artwork extraction | Throughput | >10 files/sec |
|
|
| Artwork resize | Latency | <50ms per image |
|
|
| Pattern prediction | Latency | <10ms |
|
|
| Prefetch hit rate | Accuracy | >70% for sequential play |
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### CollectionError
|
|
|
|
| Error | Description |
|
|
|-------|-------------|
|
|
| `Database(rusqlite::Error)` | SQLite operation failed |
|
|
| `NotFound` | Collection ID doesn't exist |
|
|
| `InvalidQuery` | Query failed to serialize |
|
|
| `Search(SearchError)` | tantivy query failed |
|
|
| `Pattern(PatternError)` | Pattern lookup failed |
|
|
|
|
### ArtworkError
|
|
|
|
| Error | Description |
|
|
|-------|-------------|
|
|
| `Database(rusqlite::Error)` | Cache DB operation failed |
|
|
| `Cas(CasError)` | CAS storage operation failed |
|
|
| `InvalidHash` | Stored hash is malformed |
|
|
| `NotFound` | Artwork not in cache |
|
|
| `ImageTooLarge(usize)` | Input exceeds 10MB limit |
|
|
| `InvalidImage` | Cannot decode image data |
|
|
| `ResizeFailed` | Image resize operation failed |
|
|
|
|
### PatternError
|
|
|
|
| Error | Description |
|
|
|-------|-------------|
|
|
| `Database(rusqlite::Error)` | SQLite operation failed |
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Default Settings
|
|
|
|
```toml
|
|
[prefetch]
|
|
enabled = true
|
|
lookahead = 3
|
|
max_concurrent = 2
|
|
cooldown_ms = 100
|
|
|
|
[artwork]
|
|
max_input_size_mb = 10
|
|
thumbnail_size = 150
|
|
medium_size = 300
|
|
|
|
[patterns]
|
|
max_history_days = 30
|
|
```
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
| Test | Type | Validates |
|
|
|------|------|-----------|
|
|
| `test_collection_crud` | Unit | Create, read, update, delete |
|
|
| `test_collection_evaluate_match` | Unit | Match query evaluation |
|
|
| `test_collection_persistence` | Unit | Collections survive restart |
|
|
| `test_artwork_extract_flac` | Unit | FLAC artwork extraction |
|
|
| `test_artwork_cache_store_get` | Unit | Cache round-trip |
|
|
| `test_artwork_resize` | Unit | Resize produces valid output |
|
|
| `test_pattern_prediction` | Unit | Sequential pattern learning |
|
|
| `test_pattern_persistence` | Unit | Patterns survive restart |
|
|
| `test_prefetch_config_defaults` | Unit | Default config values |
|
|
| `test_prefetch_ops_*` | Unit | FUSE PrefetchOps integration |
|