581 lines
19 KiB
Markdown
581 lines
19 KiB
Markdown
# Surprise Me - Video Recommendation Feature Design
|
||
|
||
## Overview
|
||
|
||
The "Surprise Me" feature is a lightweight video recommendation system designed for personal video management. Unlike enterprise systems like YouTube, this focuses on **rediscovery and variety** for a single user's personal collection, helping users find interesting videos they might have forgotten about or haven't watched recently.
|
||
|
||
## Design Philosophy
|
||
|
||
### Core Principles
|
||
1. **Discovery Over Prediction**: Help users rediscover their own collection rather than predict preferences
|
||
2. **Lightweight**: No machine learning infrastructure required
|
||
3. **Variety-Focused**: Ensure diverse recommendations across different libraries/folders
|
||
4. **Personal**: Leverage user's own bookmarks, ratings, and viewing patterns
|
||
5. **Serendipity**: Balance between smart recommendations and pleasant surprises
|
||
|
||
### Key Difference from YouTube-Style Recommendations
|
||
- **No collaborative filtering**: Single user system
|
||
- **No external data**: Works entirely with user's personal library
|
||
- **Focus on forgotten content**: Help surface older or unwatched videos
|
||
- **Simplicity**: Easy to understand and implement
|
||
|
||
## Architecture
|
||
|
||
### UI Integration
|
||
|
||
#### Sidebar Navigation
|
||
```
|
||
├── Home
|
||
├── Settings
|
||
├── Videos
|
||
├── Photos
|
||
├── Texts
|
||
├── Bookmarks
|
||
├── 🎲 Surprise Me ← NEW ITEM
|
||
├── Clusters
|
||
│ ├── Cluster 1
|
||
│ └── Cluster 2
|
||
└── Folder Viewer
|
||
├── Library 1
|
||
└── Library 2
|
||
```
|
||
|
||
**Important**: "Surprise Me" is a **standalone sidebar item**, NOT a tab within the Videos page.
|
||
|
||
#### Page Layout (`/surprise-me`)
|
||
When users click "Surprise Me" in the sidebar, they navigate to a dedicated page showing:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 🎲 Surprise Me │
|
||
│ │
|
||
│ Discover videos from your collection │
|
||
│ │
|
||
│ [🔄 Refresh Recommendations] [⚙️ Algorithm: Smart Mix]│
|
||
└─────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Recommended for You │
|
||
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||
│ │Video │ │Video │ │Video │ │Video │ │Video │ │
|
||
│ │ #1 │ │ #2 │ │ #3 │ │ #4 │ │ #5 │ │
|
||
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
|
||
│ │
|
||
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||
│ │Video │ │Video │ │Video │ │Video │ │Video │ │
|
||
│ │ #6 │ │ #7 │ │ #8 │ │ #9 │ │ #10 │ │
|
||
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Database Schema Extensions
|
||
|
||
#### New Table: `media_access`
|
||
Tracks user interactions with media for better recommendations.
|
||
|
||
```sql
|
||
CREATE TABLE media_access (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
media_id INTEGER NOT NULL,
|
||
access_type TEXT NOT NULL CHECK (access_type IN ('view', 'play', 'bookmark', 'rate')),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (media_id) REFERENCES media(id) ON DELETE CASCADE
|
||
);
|
||
|
||
-- Indexes for performance
|
||
CREATE INDEX idx_media_access_media_id ON media_access(media_id);
|
||
CREATE INDEX idx_media_access_created_at ON media_access(created_at);
|
||
CREATE INDEX idx_media_access_type_created_at ON media_access(access_type, created_at);
|
||
```
|
||
|
||
**Usage**:
|
||
- `view`: When video modal/player is opened
|
||
- `play`: When video playback actually starts
|
||
- `bookmark`: When video is bookmarked
|
||
- `rate`: When video is rated
|
||
|
||
### API Endpoints
|
||
|
||
#### GET `/api/videos/recommendations`
|
||
|
||
**Query Parameters**:
|
||
- `algorithm` (optional): Recommendation algorithm to use
|
||
- `smart_mix` (default): Balanced mix of all algorithms
|
||
- `weighted_random`: Smart randomization with recency weighting
|
||
- `forgotten_gems`: Bookmarked/rated but not recently viewed
|
||
- `similar_to_favorites`: From same folders as highly-rated videos
|
||
- `time_based`: Based on video creation/addition time
|
||
- `pure_random`: Complete randomization
|
||
- `unwatched_first`: Prioritize never-accessed videos
|
||
- `limit` (optional, default: 20): Number of recommendations
|
||
- `exclude_recent_days` (optional, default: 30): Exclude videos viewed in last N days
|
||
|
||
**Response**:
|
||
```json
|
||
{
|
||
"recommendations": [
|
||
{
|
||
"id": 123,
|
||
"title": "Movie Title",
|
||
"path": "/mnt/videos/movie.mp4",
|
||
"size": 1073741824,
|
||
"thumbnail": "/api/thumbnails/...",
|
||
"type": "video",
|
||
"bookmark_count": 0,
|
||
"star_count": 1,
|
||
"avg_rating": 4.5,
|
||
"created_at": "2024-01-15T10:30:00Z",
|
||
"recommendation_reason": "From your favorite library",
|
||
"recommendation_score": 0.85
|
||
}
|
||
],
|
||
"algorithm": "smart_mix",
|
||
"total_videos": 1500,
|
||
"total_recommended": 20
|
||
}
|
||
```
|
||
|
||
#### POST `/api/media-access/:id`
|
||
|
||
Track media access for recommendation improvement.
|
||
|
||
**Body**:
|
||
```json
|
||
{
|
||
"accessType": "view" | "play" | "bookmark" | "rate"
|
||
}
|
||
```
|
||
|
||
**Response**:
|
||
```json
|
||
{
|
||
"success": true,
|
||
"mediaId": 123
|
||
}
|
||
```
|
||
|
||
## Recommendation Algorithms
|
||
|
||
### 1. Smart Mix (Default)
|
||
Combines multiple strategies for balanced recommendations:
|
||
- 30% Weighted Random Discovery
|
||
- 25% Forgotten Gems
|
||
- 20% Similar to Favorites
|
||
- 15% Unwatched First
|
||
- 10% Time-Based Variety
|
||
|
||
**Implementation**:
|
||
```typescript
|
||
async function getSmartMixRecommendations(limit: number) {
|
||
const recommendations = [];
|
||
|
||
// Calculate distribution
|
||
const distribution = {
|
||
weightedRandom: Math.floor(limit * 0.3),
|
||
forgottenGems: Math.floor(limit * 0.25),
|
||
similarToFavorites: Math.floor(limit * 0.2),
|
||
unwatchedFirst: Math.floor(limit * 0.15),
|
||
timeBased: Math.floor(limit * 0.1)
|
||
};
|
||
|
||
// Get from each algorithm
|
||
recommendations.push(...await getWeightedRandom(distribution.weightedRandom));
|
||
recommendations.push(...await getForgottenGems(distribution.forgottenGems));
|
||
recommendations.push(...await getSimilarToFavorites(distribution.similarToFavorites));
|
||
recommendations.push(...await getUnwatchedFirst(distribution.unwatchedFirst));
|
||
recommendations.push(...await getTimeBased(distribution.timeBased));
|
||
|
||
// Shuffle and deduplicate
|
||
return shuffleAndDeduplicate(recommendations).slice(0, limit);
|
||
}
|
||
```
|
||
|
||
### 2. Weighted Random Discovery
|
||
Smart randomization that considers:
|
||
- **Recency penalty**: Videos viewed recently get lower weight
|
||
- **Rating boost**: Higher-rated videos get slight preference
|
||
- **Library diversity**: Ensure videos from different sources
|
||
|
||
**Scoring Formula**:
|
||
```
|
||
score = base_score × recency_multiplier × rating_multiplier × diversity_bonus
|
||
|
||
where:
|
||
base_score = random(0.5, 1.0)
|
||
recency_multiplier = min(1.0, days_since_last_view / 90)
|
||
rating_multiplier = 1.0 + (avg_rating / 10)
|
||
diversity_bonus = 1.2 if from underrepresented library
|
||
```
|
||
|
||
**SQL Query**:
|
||
```sql
|
||
SELECT
|
||
m.*,
|
||
RANDOM() as random_factor,
|
||
JULIANDAY('now') - JULIANDAY(COALESCE(ma.last_access, m.created_at)) as days_since_view,
|
||
COALESCE(m.avg_rating, 0) as rating
|
||
FROM media m
|
||
LEFT JOIN (
|
||
SELECT media_id, MAX(created_at) as last_access
|
||
FROM media_access
|
||
WHERE access_type IN ('view', 'play')
|
||
GROUP BY media_id
|
||
) ma ON m.id = ma.media_id
|
||
WHERE m.type = 'video'
|
||
AND (ma.last_access IS NULL OR ma.last_access < datetime('now', '-30 days'))
|
||
ORDER BY (
|
||
random_factor *
|
||
MIN(1.0, (JULIANDAY('now') - JULIANDAY(COALESCE(ma.last_access, m.created_at))) / 90.0) *
|
||
(1.0 + COALESCE(m.avg_rating, 0) / 10.0)
|
||
) DESC
|
||
LIMIT ?
|
||
```
|
||
|
||
### 3. Forgotten Gems
|
||
Surface videos that you've shown interest in but haven't watched recently:
|
||
- Bookmarked videos not viewed in 30+ days
|
||
- Highly-rated videos (4+ stars) not viewed in 60+ days
|
||
- Videos from folders you've bookmarked
|
||
|
||
**Target Criteria**:
|
||
- Has bookmark OR rating >= 4
|
||
- Not accessed in last 30-60 days
|
||
- Sorted by rating and bookmark combination
|
||
|
||
**SQL Query**:
|
||
```sql
|
||
SELECT DISTINCT m.*,
|
||
CASE
|
||
WHEN b.id IS NOT NULL THEN 'You bookmarked this'
|
||
WHEN m.avg_rating >= 4 THEN 'You rated this highly'
|
||
ELSE 'From a favorite folder'
|
||
END as recommendation_reason
|
||
FROM media m
|
||
LEFT JOIN bookmarks b ON m.id = b.media_id
|
||
LEFT JOIN (
|
||
SELECT media_id, MAX(created_at) as last_access
|
||
FROM media_access
|
||
WHERE access_type IN ('view', 'play')
|
||
GROUP BY media_id
|
||
) ma ON m.id = ma.media_id
|
||
WHERE m.type = 'video'
|
||
AND (
|
||
b.id IS NOT NULL OR
|
||
m.avg_rating >= 4 OR
|
||
m.path LIKE (SELECT folder_path || '%' FROM folder_bookmarks LIMIT 1)
|
||
)
|
||
AND (ma.last_access IS NULL OR ma.last_access < datetime('now', '-30 days'))
|
||
ORDER BY
|
||
(COALESCE(m.avg_rating, 0) * 0.7 +
|
||
CASE WHEN b.id IS NOT NULL THEN 3 ELSE 0 END) DESC,
|
||
RANDOM()
|
||
LIMIT ?
|
||
```
|
||
|
||
### 4. Similar to Favorites
|
||
Find videos from the same location as your highly-rated content:
|
||
- Same parent folder as 4+ star videos
|
||
- Same library as frequently accessed content
|
||
- Similar file size/characteristics
|
||
|
||
**Logic**:
|
||
1. Find user's top-rated videos (4+ stars)
|
||
2. Extract their parent folders
|
||
3. Find other videos in those folders
|
||
4. Exclude already-watched videos
|
||
|
||
**SQL Query**:
|
||
```sql
|
||
WITH favorite_folders AS (
|
||
SELECT DISTINCT
|
||
SUBSTR(path, 1, LENGTH(path) - LENGTH(SUBSTR(path, INSTR(path, '/', -1) + 1))) as folder_path
|
||
FROM media
|
||
WHERE type = 'video' AND avg_rating >= 4
|
||
LIMIT 10
|
||
)
|
||
SELECT m.*,
|
||
'From a folder with your favorites' as recommendation_reason
|
||
FROM media m
|
||
LEFT JOIN media_access ma ON m.id = ma.media_id AND ma.access_type IN ('view', 'play')
|
||
WHERE m.type = 'video'
|
||
AND EXISTS (
|
||
SELECT 1 FROM favorite_folders ff
|
||
WHERE m.path LIKE ff.folder_path || '%'
|
||
)
|
||
AND m.avg_rating < 4 -- Not already a favorite
|
||
AND (ma.id IS NULL OR ma.created_at < datetime('now', '-60 days'))
|
||
ORDER BY RANDOM()
|
||
LIMIT ?
|
||
```
|
||
|
||
### 5. Time-Based Recommendations
|
||
Recommendations based on temporal patterns:
|
||
- **Seasonal**: Videos added around the same time last year
|
||
- **New discoveries**: Recently added to library but not yet viewed
|
||
- **Old but gold**: Videos added long ago that you might have missed
|
||
|
||
**Categories**:
|
||
```typescript
|
||
type TimeBasedCategory =
|
||
| 'recently_added' // Added in last 7 days, not viewed
|
||
| 'one_month_old' // Added ~30 days ago
|
||
| 'seasonal' // Added ~365 days ago
|
||
| 'archive_discovery' // Added >1 year ago, never viewed
|
||
```
|
||
|
||
### 6. Pure Random
|
||
Complete randomization with minimal filters:
|
||
- Only filter: video type and exclude corrupted files
|
||
- Pure serendipity mode
|
||
- No bias towards ratings or access patterns
|
||
|
||
### 7. Unwatched First
|
||
Prioritize videos that have never been accessed:
|
||
- Never appeared in `media_access` table
|
||
- Sorted by date added (newest first)
|
||
- Ensures new content gets discovered
|
||
|
||
**SQL Query**:
|
||
```sql
|
||
SELECT m.*,
|
||
'Never watched before' as recommendation_reason
|
||
FROM media m
|
||
LEFT JOIN media_access ma ON m.id = ma.media_id
|
||
WHERE m.type = 'video'
|
||
AND ma.id IS NULL
|
||
ORDER BY m.created_at DESC
|
||
LIMIT ?
|
||
```
|
||
|
||
## Implementation Plan
|
||
|
||
### Phase 1: Foundation (Priority: P0)
|
||
- [x] Design documentation
|
||
- [ ] Database schema migration (add `media_access` table)
|
||
- [ ] Media access tracking utility functions
|
||
- [ ] Basic recommendation service structure
|
||
|
||
### Phase 2: Core Algorithms (Priority: P0)
|
||
- [ ] Implement Weighted Random algorithm
|
||
- [ ] Implement Unwatched First algorithm
|
||
- [ ] Implement Pure Random algorithm
|
||
- [ ] Create API endpoint `/api/videos/recommendations`
|
||
- [ ] Create media access tracking endpoint
|
||
|
||
### Phase 3: Advanced Algorithms (Priority: P1)
|
||
- [ ] Implement Forgotten Gems algorithm
|
||
- [ ] Implement Similar to Favorites algorithm
|
||
- [ ] Implement Time-Based algorithm
|
||
- [ ] Implement Smart Mix algorithm
|
||
|
||
### Phase 4: UI Components (Priority: P0)
|
||
- [ ] Create `SurpriseMePage` component (`/src/app/surprise-me/page.tsx`)
|
||
- [ ] Add "Surprise Me" item to sidebar navigation
|
||
- [ ] Implement recommendation grid display
|
||
- [ ] Add algorithm selector dropdown
|
||
- [ ] Add refresh button functionality
|
||
|
||
### Phase 5: Integration & Polish (Priority: P1)
|
||
- [ ] Track video views automatically when player opens
|
||
- [ ] Add recommendation reasons/badges to video cards
|
||
- [ ] Implement smooth transitions and loading states
|
||
- [ ] Add empty state when no recommendations available
|
||
- [ ] Performance testing with large libraries
|
||
|
||
### Phase 6: Advanced Features (Priority: P2)
|
||
- [ ] Save preferred algorithm in user preferences
|
||
- [ ] "Not interested" button to exclude videos
|
||
- [ ] Recommendation history tracking
|
||
- [ ] Export/share recommendations
|
||
- [ ] Statistics dashboard (most recommended, never recommended, etc.)
|
||
|
||
## File Structure
|
||
|
||
```
|
||
/root/workspace/nextav/
|
||
├── src/
|
||
│ ├── app/
|
||
│ │ ├── api/
|
||
│ │ │ ├── media-access/
|
||
│ │ │ │ └── [id]/
|
||
│ │ │ │ └── route.ts ← NEW: Track media access
|
||
│ │ │ └── videos/
|
||
│ │ │ └── recommendations/
|
||
│ │ │ └── route.ts ← NEW: Get recommendations
|
||
│ │ └── surprise-me/
|
||
│ │ └── page.tsx ← NEW: Surprise Me page
|
||
│ ├── components/
|
||
│ │ ├── recommendation-grid.tsx ← NEW: Display recommendations
|
||
│ │ └── sidebar.tsx ← MODIFY: Add Surprise Me item
|
||
│ ├── db/
|
||
│ │ └── index.ts ← MODIFY: Add media_access table
|
||
│ └── lib/
|
||
│ └── recommendation-service.ts ← NEW: Recommendation algorithms
|
||
└── docs/
|
||
└── SURPRISE_ME_RECOMMENDATION_DESIGN.md ← This file
|
||
```
|
||
|
||
## User Experience Flow
|
||
|
||
### Scenario 1: First Time User
|
||
1. User clicks "🎲 Surprise Me" in sidebar
|
||
2. Page shows loading state
|
||
3. Default "Smart Mix" algorithm runs
|
||
4. 20 diverse videos displayed
|
||
5. User can click any video to watch
|
||
6. User can click "Refresh" for new recommendations
|
||
|
||
### Scenario 2: Regular User
|
||
1. User navigates to Surprise Me
|
||
2. System recognizes viewing patterns
|
||
3. Shows mix of:
|
||
- Unwatched videos from favorite libraries
|
||
- Old favorites not seen recently
|
||
- Random discoveries for variety
|
||
4. Each card shows reason: "From your favorite library" or "Never watched"
|
||
|
||
### Scenario 3: Algorithm Exploration
|
||
1. User selects "Forgotten Gems" from dropdown
|
||
2. System shows only bookmarked/highly-rated unwatched content
|
||
3. User finds an old favorite they forgot about
|
||
4. User watches and enjoys rediscovery
|
||
|
||
## Performance Considerations
|
||
|
||
### Database Optimization
|
||
- All queries use indexed columns (`media_id`, `created_at`, `type`)
|
||
- Limit queries to reasonable batch sizes (20-50 videos)
|
||
- Use `RANDOM()` efficiently with proper `ORDER BY`
|
||
- Cache recommendation results for 5-10 minutes
|
||
|
||
### Caching Strategy
|
||
```typescript
|
||
// Cache recommendations for short period
|
||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||
const recommendationCache = new Map<string, {
|
||
data: Video[],
|
||
timestamp: number
|
||
}>();
|
||
```
|
||
|
||
### Scalability
|
||
- Works efficiently with 1,000 to 100,000+ videos
|
||
- Lazy loading for recommendation display
|
||
- Pagination for large result sets
|
||
- Background precomputation for complex algorithms (future enhancement)
|
||
|
||
## Configuration Options
|
||
|
||
### User Preferences (Future)
|
||
```typescript
|
||
interface RecommendationPreferences {
|
||
defaultAlgorithm: RecommendationAlgorithm;
|
||
excludeRecentDays: number; // Default: 30
|
||
recommendationLimit: number; // Default: 20
|
||
enableAutoRefresh: boolean; // Auto-refresh on page visit
|
||
preferredLibraries: number[]; // Boost these libraries
|
||
excludedFolders: string[]; // Never recommend from these
|
||
}
|
||
```
|
||
|
||
### System Settings
|
||
```typescript
|
||
const RECOMMENDATION_CONFIG = {
|
||
MAX_RECOMMENDATIONS: 100,
|
||
DEFAULT_LIMIT: 20,
|
||
MIN_RATING_FOR_FAVORITES: 4,
|
||
RECENT_VIEW_THRESHOLD_DAYS: 30,
|
||
DIVERSITY_BOOST_FACTOR: 1.2,
|
||
CACHE_TTL_SECONDS: 300
|
||
};
|
||
```
|
||
|
||
## Testing Strategy
|
||
|
||
### Unit Tests
|
||
- Test each algorithm independently
|
||
- Verify SQL query correctness
|
||
- Test edge cases (empty library, all videos watched, etc.)
|
||
|
||
### Integration Tests
|
||
- Test API endpoint responses
|
||
- Verify media access tracking
|
||
- Test algorithm switching
|
||
|
||
### User Acceptance Testing
|
||
- Test with small library (<100 videos)
|
||
- Test with medium library (100-1000 videos)
|
||
- Test with large library (>10,000 videos)
|
||
- Verify recommendations are diverse and interesting
|
||
|
||
## Future Enhancements
|
||
|
||
### Phase 2 Features (Post-MVP)
|
||
1. **Playlist Generation**: Create playlists from recommendations
|
||
2. **Mood-Based**: Filter by video characteristics (length, genre from filename)
|
||
3. **Social**: Share recommendation sets with other users
|
||
4. **Learning**: Track which recommendations user clicks to improve algorithms
|
||
5. **Filters**: By library, rating range, size, date added
|
||
6. **Smart Scheduling**: "Video of the Day" feature
|
||
7. **Collections**: "Similar to this video" on video player
|
||
8. **Statistics**: Dashboard showing recommendation effectiveness
|
||
|
||
### Advanced Algorithms (Future)
|
||
1. **Content-Based Filtering**: Use filename patterns, folder structure
|
||
2. **Collaborative Patterns**: If multiple users, find correlation
|
||
3. **Temporal Patterns**: Learn user's viewing time preferences
|
||
4. **Sequence Learning**: Detect video series/collections
|
||
5. **Quality Scoring**: Prefer higher quality encodes
|
||
|
||
## Success Metrics
|
||
|
||
### Key Performance Indicators
|
||
- **Engagement**: % of recommendations clicked
|
||
- **Discovery Rate**: # of previously unwatched videos discovered
|
||
- **Diversity Score**: # of different libraries in recommendations
|
||
- **User Satisfaction**: Time spent on Surprise Me page
|
||
- **Rediscovery**: % of forgotten favorites surfaced
|
||
|
||
### Target Goals
|
||
- 80%+ of users try Surprise Me feature
|
||
- 30%+ click-through rate on recommendations
|
||
- 50%+ of recommendations are from unwatched videos
|
||
- 70%+ user satisfaction with variety
|
||
|
||
## Appendix
|
||
|
||
### Recommendation Reason Badges
|
||
Visual indicators showing why a video was recommended:
|
||
|
||
| Badge | Meaning | Color |
|
||
|-------|---------|-------|
|
||
| 🌟 Favorite | From folder with 4+ star videos | Gold |
|
||
| 🔖 Bookmarked | You bookmarked this | Blue |
|
||
| ✨ New | Added recently, never watched | Green |
|
||
| 🕰️ Forgotten Gem | Rated highly but not watched in 60+ days | Purple |
|
||
| 🎲 Random | Pure random selection | Gray |
|
||
| 📁 Same Folder | From folder with your favorites | Orange |
|
||
| 🎯 Unwatched | Never accessed before | Teal |
|
||
|
||
### Example UI Component Props
|
||
```typescript
|
||
interface SurpriseMePageProps {
|
||
initialAlgorithm?: RecommendationAlgorithm;
|
||
initialLimit?: number;
|
||
}
|
||
|
||
interface RecommendationCardProps {
|
||
video: Video;
|
||
reason: string;
|
||
score: number;
|
||
onVideoClick: (video: Video) => void;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**Document Version**: 1.0
|
||
**Last Updated**: 2025-10-12
|
||
**Status**: Design Phase
|
||
**Next Steps**: Begin Phase 1 implementation
|