19 KiB
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
- Discovery Over Prediction: Help users rediscover their own collection rather than predict preferences
- Lightweight: No machine learning infrastructure required
- Variety-Focused: Ensure diverse recommendations across different libraries/folders
- Personal: Leverage user's own bookmarks, ratings, and viewing patterns
- 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.
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 openedplay: When video playback actually startsbookmark: When video is bookmarkedrate: When video is rated
API Endpoints
GET /api/videos/recommendations
Query Parameters:
algorithm(optional): Recommendation algorithm to usesmart_mix(default): Balanced mix of all algorithmsweighted_random: Smart randomization with recency weightingforgotten_gems: Bookmarked/rated but not recently viewedsimilar_to_favorites: From same folders as highly-rated videostime_based: Based on video creation/addition timepure_random: Complete randomizationunwatched_first: Prioritize never-accessed videos
limit(optional, default: 20): Number of recommendationsexclude_recent_days(optional, default: 30): Exclude videos viewed in last N days
Response:
{
"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:
{
"accessType": "view" | "play" | "bookmark" | "rate"
}
Response:
{
"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:
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:
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:
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:
- Find user's top-rated videos (4+ stars)
- Extract their parent folders
- Find other videos in those folders
- Exclude already-watched videos
SQL Query:
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:
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_accesstable - Sorted by date added (newest first)
- Ensures new content gets discovered
SQL Query:
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)
- Design documentation
- Database schema migration (add
media_accesstable) - 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
SurpriseMePagecomponent (/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
- User clicks "🎲 Surprise Me" in sidebar
- Page shows loading state
- Default "Smart Mix" algorithm runs
- 20 diverse videos displayed
- User can click any video to watch
- User can click "Refresh" for new recommendations
Scenario 2: Regular User
- User navigates to Surprise Me
- System recognizes viewing patterns
- Shows mix of:
- Unwatched videos from favorite libraries
- Old favorites not seen recently
- Random discoveries for variety
- Each card shows reason: "From your favorite library" or "Never watched"
Scenario 3: Algorithm Exploration
- User selects "Forgotten Gems" from dropdown
- System shows only bookmarked/highly-rated unwatched content
- User finds an old favorite they forgot about
- 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 properORDER BY - Cache recommendation results for 5-10 minutes
Caching Strategy
// 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)
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
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)
- Playlist Generation: Create playlists from recommendations
- Mood-Based: Filter by video characteristics (length, genre from filename)
- Social: Share recommendation sets with other users
- Learning: Track which recommendations user clicks to improve algorithms
- Filters: By library, rating range, size, date added
- Smart Scheduling: "Video of the Day" feature
- Collections: "Similar to this video" on video player
- Statistics: Dashboard showing recommendation effectiveness
Advanced Algorithms (Future)
- Content-Based Filtering: Use filename patterns, folder structure
- Collaborative Patterns: If multiple users, find correlation
- Temporal Patterns: Learn user's viewing time preferences
- Sequence Learning: Detect video series/collections
- 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
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