docs(readme): add Surprise Me video recommendation feature documentation

- Introduce "Surprise Me" as a new video recommendation system for personal video discovery
- Add detailed quick links including guides, summaries, design, architecture, and implementation examples
- Reorganize documentation overview to include Surprise Me above Library Cluster feature
- Update documentation statistics reflecting increased total documents and size
- Update current project focus including Surprise Me as top priority
- Add new documentation entry for Surprise Me on recent changes list
- Bump documentation version to 1.1 and update last modified date accordingly

docs(archive): update Docker image tag for build and push commands

- Change Docker image tag from version 1.3 to 1.4 in deployment instructions
This commit is contained in:
tigeren 2025-10-12 12:05:59 +00:00
parent a31973ea46
commit dd3eb91fbe
8 changed files with 2932 additions and 12 deletions

View File

@ -6,7 +6,20 @@ Welcome to the NextAV documentation repository. This directory contains comprehe
## 📚 Documentation Overview
### 🆕 Latest: Library Cluster Feature
### 🆕 Latest: Surprise Me - Video Recommendation Feature
The **Surprise Me** feature is a lightweight video recommendation system that helps you rediscover videos in your personal collection through smart algorithms.
**Start Here**: 📑 [Surprise Me Documentation Index](SURPRISE_ME_INDEX.md)
**Quick Links**:
- ⚡ [Quick Start Guide](SURPRISE_ME_QUICKSTART.md) - **Fastest way to implement**
- 📋 [Quick Summary](SURPRISE_ME_SUMMARY.md) - Overview and key concepts
- 📖 [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md) - Full specification
- 🏗️ [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md) - Visual system design
- 💻 [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) - Code snippets
### Library Cluster Feature
The **Library Cluster** feature allows you to group multiple libraries that contain similar content for better organization and unified access.
@ -25,7 +38,16 @@ The **Library Cluster** feature allows you to group multiple libraries that cont
### Features & Enhancements
#### Library Cluster Feature (NEW)
#### Surprise Me - Video Recommendation (NEW)
Lightweight recommendation system for personal video discovery.
- 📑 [Documentation Index](SURPRISE_ME_INDEX.md) - **Complete navigation hub**
- ⚡ [Quick Start Guide](SURPRISE_ME_QUICKSTART.md) - **Start here for implementation**
- 📋 [Quick Summary](SURPRISE_ME_SUMMARY.md) - Overview and concepts
- 📖 [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md) - Full specification
- 🏗️ [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md) - Visual system design
- 💻 [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) - Code snippets
#### Library Cluster Feature
Group and organize libraries by content category.
- [Index](LIBRARY_CLUSTER_INDEX.md)
- [Summary](LIBRARY_CLUSTER_SUMMARY.md)
@ -173,6 +195,7 @@ Older documentation and historical records are stored in the [`archive/`](archiv
| Topic | Key Documents |
|-------|--------------|
| **Video Discovery** | Surprise Me Summary, Surprise Me Design |
| **Library Management** | Library Cluster Feature docs |
| **Video Playback** | ArtPlayer Enhancement, Format Compatibility |
| **Database** | Migration Guide, Migration README |
@ -192,10 +215,10 @@ Older documentation and historical records are stored in the [`archive/`](archiv
## 📊 Documentation Statistics
- **Total Documents**: 17+ active documents
- **Total Size**: ~250 KB
- **Total Documents**: 23+ active documents
- **Total Size**: ~550 KB
- **Categories**: 5 main categories
- **Latest Update**: 2025-10-11
- **Latest Update**: 2025-10-12
---
@ -223,9 +246,10 @@ Please create an issue or update the documentation directly.
## 🎯 Current Focus
The project is currently focused on:
1. **Library Cluster Feature** - New feature for library organization
2. **Video Playback Optimization** - Improving compatibility and performance
3. **Database Migration** - Ongoing schema improvements
1. **Surprise Me Feature** - Video recommendation system for personal discovery
2. **Library Cluster Feature** - Library organization and grouping
3. **Video Playback Optimization** - Improving compatibility and performance
4. **Database Migration** - Ongoing schema improvements
---
@ -233,6 +257,7 @@ The project is currently focused on:
| Date | Document | Change |
|------|----------|--------|
| 2025-10-12 | Surprise Me docs | Initial documentation for recommendation feature |
| 2025-10-11 | Library Cluster docs | Initial documentation package created |
| [Previous] | Various | See individual documents for history |
@ -246,6 +271,6 @@ The project is currently focused on:
---
**Documentation Version**: 1.0
**Last Updated**: 2025-10-11
**Documentation Version**: 1.1
**Last Updated**: 2025-10-12
**Maintainer**: Development Team

View File

@ -0,0 +1,385 @@
# Surprise Me - Architecture Diagrams
## System Architecture Overview
```mermaid
graph TB
User[User] --> Sidebar[Sidebar Navigation]
Sidebar --> SurpriseMePage[Surprise Me Page]
SurpriseMePage --> AlgorithmSelector[Algorithm Selector]
SurpriseMePage --> RecommendationGrid[Recommendation Grid]
SurpriseMePage --> RefreshButton[Refresh Button]
AlgorithmSelector --> API[/api/videos/recommendations]
RefreshButton --> API
API --> RecService[Recommendation Service]
RecService --> SmartMix[Smart Mix Algorithm]
RecService --> WeightedRandom[Weighted Random]
RecService --> ForgottenGems[Forgotten Gems]
RecService --> SimilarFavorites[Similar to Favorites]
RecService --> TimeBased[Time-Based]
RecService --> PureRandom[Pure Random]
RecService --> UnwatchedFirst[Unwatched First]
SmartMix --> Database[(SQLite Database)]
WeightedRandom --> Database
ForgottenGems --> Database
SimilarFavorites --> Database
TimeBased --> Database
PureRandom --> Database
UnwatchedFirst --> Database
Database --> MediaTable[media table]
Database --> AccessTable[media_access table]
Database --> BookmarksTable[bookmarks table]
Database --> StarsTable[stars table]
RecommendationGrid --> VideoCard[Video Cards]
VideoCard --> VideoPlayer[Unified Video Player]
VideoCard --> AccessTracking[Track Access API]
AccessTracking --> Database
```
## Data Flow Diagram
```mermaid
sequenceDiagram
participant User
participant SurpriseMePage
participant API
participant RecService
participant Database
participant VideoPlayer
User->>SurpriseMePage: Click "Surprise Me" in sidebar
SurpriseMePage->>API: GET /api/videos/recommendations?algorithm=smart_mix
API->>RecService: getRecommendations('smart_mix', 20)
RecService->>Database: Query media table
RecService->>Database: Query media_access table
RecService->>Database: Query bookmarks & stars
Database-->>RecService: Return matching videos
RecService->>RecService: Apply algorithm logic
RecService->>RecService: Score and rank videos
RecService-->>API: Return top 20 videos
API-->>SurpriseMePage: JSON response with videos
SurpriseMePage->>User: Display 20 recommended videos
User->>VideoCard: Click video to watch
VideoCard->>VideoPlayer: Open player with video
VideoCard->>API: POST /api/media-access/:id {type: "view"}
API->>Database: INSERT into media_access
Database-->>API: Success
User->>VideoPlayer: Play video
VideoPlayer->>API: POST /api/media-access/:id {type: "play"}
API->>Database: INSERT into media_access
```
## Database Schema Diagram
```mermaid
erDiagram
MEDIA ||--o{ MEDIA_ACCESS : tracks
MEDIA ||--o{ BOOKMARKS : has
MEDIA ||--o{ STARS : has
MEDIA }o--|| LIBRARIES : belongs_to
MEDIA {
int id PK
int library_id FK
string path
string type
string title
int size
string thumbnail
int bookmark_count
int star_count
float avg_rating
datetime created_at
}
MEDIA_ACCESS {
int id PK
int media_id FK
string access_type
datetime created_at
}
BOOKMARKS {
int id PK
int media_id FK
datetime created_at
}
STARS {
int id PK
int media_id FK
int rating
datetime created_at
}
LIBRARIES {
int id PK
string path
}
```
## Algorithm Decision Flow
```mermaid
graph TD
Start[User Opens Surprise Me] --> CheckAlgorithm{Algorithm Selected?}
CheckAlgorithm -->|Default| SmartMix[Smart Mix Algorithm]
CheckAlgorithm -->|User Selected| SelectedAlg[Selected Algorithm]
SmartMix --> DistributeLoad[Distribute Load]
DistributeLoad --> WR[30% Weighted Random]
DistributeLoad --> FG[25% Forgotten Gems]
DistributeLoad --> SF[20% Similar Favorites]
DistributeLoad --> UF[15% Unwatched First]
DistributeLoad --> TB[10% Time-Based]
WR --> Combine[Combine & Shuffle]
FG --> Combine
SF --> Combine
UF --> Combine
TB --> Combine
SelectedAlg --> SingleAlg[Run Single Algorithm]
SingleAlg --> Combine
Combine --> Dedupe[Remove Duplicates]
Dedupe --> LimitResults[Limit to 20 videos]
LimitResults --> Display[Display to User]
```
## Weighted Random Scoring Flow
```mermaid
graph LR
Video[Video] --> BaseScore[Base Score: Random 0.5-1.0]
BaseScore --> CheckAccess{Ever Accessed?}
CheckAccess -->|Yes| CalcRecency[Calculate Days Since View]
CheckAccess -->|No| MaxRecency[Recency Multiplier = 1.0]
CalcRecency --> RecencyMult[Recency Multiplier:<br/>min 1.0, days/90]
RecencyMult --> ApplyRecency[Score × Recency]
MaxRecency --> ApplyRecency
ApplyRecency --> CheckRating{Has Rating?}
CheckRating -->|Yes| RatingMult[Rating Multiplier:<br/>1.0 + rating/10]
CheckRating -->|No| NoRating[Rating Multiplier = 1.0]
RatingMult --> ApplyRating[Score × Rating]
NoRating --> ApplyRating
ApplyRating --> CheckDiversity{From Underrepresented<br/>Library?}
CheckDiversity -->|Yes| DiversityBonus[Diversity Bonus:<br/>Score × 1.2]
CheckDiversity -->|No| NoDiversity[No Bonus]
DiversityBonus --> FinalScore[Final Score]
NoDiversity --> FinalScore
FinalScore --> Rank[Rank Against Others]
```
## UI Component Hierarchy
```mermaid
graph TD
App[App Layout] --> Sidebar[Sidebar]
App --> MainArea[Main Content Area]
Sidebar --> SurpriseMenuItem[🎲 Surprise Me Item]
SurpriseMenuItem --> SurpriseMePage[Surprise Me Page]
MainArea --> SurpriseMePage
SurpriseMePage --> Header[Page Header]
SurpriseMePage --> Controls[Control Panel]
SurpriseMePage --> Grid[Recommendation Grid]
Header --> Title[Title & Description]
Header --> Stats[Statistics]
Controls --> AlgoDropdown[Algorithm Dropdown]
Controls --> RefreshBtn[Refresh Button]
Controls --> LimitSelector[Limit Selector]
Grid --> VirtualGrid[Virtualized Grid]
VirtualGrid --> VideoCard1[Video Card 1]
VirtualGrid --> VideoCard2[Video Card 2]
VirtualGrid --> VideoCardN[Video Card N]
VideoCard1 --> Thumbnail[Thumbnail]
VideoCard1 --> Info[Video Info]
VideoCard1 --> Badge[Recommendation Badge]
VideoCard1 --> Actions[Actions]
Actions --> PlayBtn[Play Button]
Actions --> BookmarkBtn[Bookmark]
Actions --> RatingStars[Star Rating]
```
## State Management Flow
```mermaid
stateDiagram-v2
[*] --> Initial
Initial --> Loading: User clicks Surprise Me
Loading --> Loaded: Recommendations received
Loading --> Error: API Error
Error --> Loading: User retries
Loaded --> Refreshing: User clicks Refresh
Loaded --> ChangingAlgorithm: User changes algorithm
Refreshing --> Loaded: New recommendations received
Refreshing --> Error: API Error
ChangingAlgorithm --> Loading: Fetching with new algorithm
Loaded --> PlayingVideo: User clicks video
PlayingVideo --> Loaded: User closes player
PlayingVideo --> TrackingAccess: Video opened
TrackingAccess --> Loaded: Access tracked
```
## Recommendation Algorithm Comparison
```mermaid
graph LR
subgraph "Algorithm Characteristics"
WR[Weighted Random<br/>Balanced & Fair]
FG[Forgotten Gems<br/>Personal History]
SF[Similar Favorites<br/>Folder-Based]
TB[Time-Based<br/>Temporal Patterns]
PR[Pure Random<br/>Complete Surprise]
UF[Unwatched First<br/>New Content]
SM[Smart Mix<br/>Best of All]
end
subgraph "User Goals"
Variety[Want Variety]
Rediscover[Rediscover Old]
Similar[Similar to Favorites]
New[Explore New]
Surprise[Complete Surprise]
end
Variety --> WR
Variety --> SM
Rediscover --> FG
Similar --> SF
New --> UF
New --> TB
Surprise --> PR
```
## Access Tracking Integration
```mermaid
graph TB
subgraph "User Actions"
OpenPlayer[Open Video Player]
StartPlayback[Start Playback]
BookmarkVideo[Bookmark Video]
RateVideo[Rate Video]
end
subgraph "Tracking System"
TrackView[Track: view]
TrackPlay[Track: play]
TrackBookmark[Track: bookmark]
TrackRate[Track: rate]
end
subgraph "Database"
MediaAccessTable[media_access table]
end
subgraph "Recommendation Engine"
UseInAlgorithms[Use in Future Recommendations]
end
OpenPlayer --> TrackView
StartPlayback --> TrackPlay
BookmarkVideo --> TrackBookmark
RateVideo --> TrackRate
TrackView --> MediaAccessTable
TrackPlay --> MediaAccessTable
TrackBookmark --> MediaAccessTable
TrackRate --> MediaAccessTable
MediaAccessTable --> UseInAlgorithms
UseInAlgorithms --> RecencyPenalty[Apply Recency Penalty]
UseInAlgorithms --> IdentifyFavorites[Identify Favorite Patterns]
UseInAlgorithms --> FindUnwatched[Find Unwatched Videos]
```
## Performance Optimization Strategy
```mermaid
graph TB
Request[Recommendation Request] --> CheckCache{Cache Hit?}
CheckCache -->|Yes| ReturnCached[Return Cached Results]
CheckCache -->|No| QueryDB[Query Database]
QueryDB --> UseIndexes[Use Optimized Indexes]
UseIndexes --> idx1[idx_media_access_created_at]
UseIndexes --> idx2[idx_media_type_created_at]
UseIndexes --> idx3[idx_media_bookmark_count]
idx1 --> ExecuteQuery[Execute Query]
idx2 --> ExecuteQuery
idx3 --> ExecuteQuery
ExecuteQuery --> LimitResults[LIMIT 20]
LimitResults --> ScoreRank[Score & Rank]
ScoreRank --> CacheResults[Cache for 5 minutes]
CacheResults --> ReturnResults[Return Results]
ReturnCached --> ReturnResults
```
---
## Legend
### Diagram Types
- **Architecture Overview**: High-level system components
- **Data Flow**: Request/response sequences
- **Database Schema**: Entity relationships
- **Algorithm Flow**: Decision trees and logic
- **Component Hierarchy**: UI structure
- **State Management**: Application states
- **Performance**: Optimization strategies
### Color Coding
- **Blue**: User-facing components
- **Green**: Backend services
- **Yellow**: Database operations
- **Purple**: Algorithm logic
- **Orange**: Tracking/analytics
---
**Related Documentation**:
- [Complete Design Document](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- [Quick Summary](SURPRISE_ME_SUMMARY.md)
- [Main Docs Index](README.md)

View File

@ -0,0 +1,912 @@
# Surprise Me - Implementation Code Examples
This document provides code examples and snippets for implementing the Surprise Me feature.
---
## Table of Contents
1. [Database Migration](#database-migration)
2. [Recommendation Service](#recommendation-service)
3. [API Routes](#api-routes)
4. [UI Components](#ui-components)
5. [Utility Functions](#utility-functions)
---
## Database Migration
### Add media_access Table
```typescript
// src/db/index.ts - Add to initialization
db.exec(`
CREATE TABLE IF NOT EXISTS 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
);
`);
// Create indexes
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_media_id ON media_access(media_id);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_created_at ON media_access(created_at);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_type_created_at ON media_access(access_type, created_at);`);
```
### Helper Functions
```typescript
// src/db/index.ts
export function trackMediaAccess(
mediaId: number,
accessType: 'view' | 'play' | 'bookmark' | 'rate'
): void {
const db = getDatabase();
db.prepare(`
INSERT INTO media_access (media_id, access_type)
VALUES (?, ?)
`).run(mediaId, accessType);
}
export function getLastAccess(mediaId: number): Date | null {
const db = getDatabase();
const result = db.prepare(`
SELECT MAX(created_at) as last_access
FROM media_access
WHERE media_id = ? AND access_type IN ('view', 'play')
`).get(mediaId) as { last_access: string | null };
return result?.last_access ? new Date(result.last_access) : null;
}
```
---
## Recommendation Service
### Base Service Structure
```typescript
// src/lib/recommendation-service.ts
import { getDatabase } from '@/db';
export type RecommendationAlgorithm =
| 'smart_mix'
| 'weighted_random'
| 'forgotten_gems'
| 'similar_to_favorites'
| 'time_based'
| 'pure_random'
| 'unwatched_first';
export interface RecommendedVideo {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
type: string;
bookmark_count: number;
star_count: number;
avg_rating: number;
created_at: string;
recommendation_reason: string;
recommendation_score: number;
}
export interface RecommendationOptions {
algorithm?: RecommendationAlgorithm;
limit?: number;
excludeRecentDays?: number;
}
export class RecommendationService {
private db: any;
constructor() {
this.db = getDatabase();
}
async getRecommendations(options: RecommendationOptions): Promise<RecommendedVideo[]> {
const {
algorithm = 'smart_mix',
limit = 20,
excludeRecentDays = 30
} = options;
switch (algorithm) {
case 'smart_mix':
return this.getSmartMixRecommendations(limit, excludeRecentDays);
case 'weighted_random':
return this.getWeightedRandomRecommendations(limit, excludeRecentDays);
case 'forgotten_gems':
return this.getForgottenGemsRecommendations(limit, excludeRecentDays);
case 'similar_to_favorites':
return this.getSimilarToFavoritesRecommendations(limit, excludeRecentDays);
case 'time_based':
return this.getTimeBasedRecommendations(limit, excludeRecentDays);
case 'pure_random':
return this.getPureRandomRecommendations(limit);
case 'unwatched_first':
return this.getUnwatchedFirstRecommendations(limit);
default:
return this.getSmartMixRecommendations(limit, excludeRecentDays);
}
}
// Algorithm implementations...
}
```
### Weighted Random Algorithm
```typescript
private getWeightedRandomRecommendations(
limit: number,
excludeRecentDays: number
): RecommendedVideo[] {
const query = `
SELECT
m.*,
ABS(RANDOM() % 100) / 100.0 as random_factor,
COALESCE(
JULIANDAY('now') - JULIANDAY(ma.last_access),
JULIANDAY('now') - JULIANDAY(m.created_at)
) as days_since_view,
COALESCE(m.avg_rating, 0) as rating,
'Smart random selection' as recommendation_reason
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', '-' || ? || ' days')
)
ORDER BY (
random_factor *
MIN(1.0, days_since_view / 90.0) *
(1.0 + rating / 10.0)
) DESC
LIMIT ?
`;
const results = this.db.prepare(query).all(excludeRecentDays, limit);
return results.map((row: any, index: number) => ({
...row,
recommendation_score: 1.0 - (index / limit)
}));
}
```
### Forgotten Gems Algorithm
```typescript
private getForgottenGemsRecommendations(
limit: number,
excludeRecentDays: number
): RecommendedVideo[] {
const query = `
SELECT DISTINCT m.*,
CASE
WHEN b.id IS NOT NULL THEN 'You bookmarked this video'
WHEN m.avg_rating >= 4 THEN 'You rated this ' || m.avg_rating || ' stars'
ELSE 'From a folder you liked'
END as recommendation_reason,
(
COALESCE(m.avg_rating, 0) * 0.7 +
CASE WHEN b.id IS NOT NULL THEN 3 ELSE 0 END
) / 8.0 as recommendation_score
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
)
AND (
ma.last_access IS NULL
OR ma.last_access < datetime('now', '-' || ? || ' days')
)
ORDER BY recommendation_score DESC, RANDOM()
LIMIT ?
`;
return this.db.prepare(query).all(excludeRecentDays, limit);
}
```
### Unwatched First Algorithm
```typescript
private getUnwatchedFirstRecommendations(limit: number): RecommendedVideo[] {
const query = `
SELECT m.*,
'Never watched before' as recommendation_reason,
1.0 as recommendation_score
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 ?
`;
return this.db.prepare(query).all(limit);
}
```
### Smart Mix Algorithm
```typescript
private async getSmartMixRecommendations(
limit: number,
excludeRecentDays: number
): Promise<RecommendedVideo[]> {
// Calculate distribution
const distribution = {
weightedRandom: Math.floor(limit * 0.30),
forgottenGems: Math.floor(limit * 0.25),
similarToFavorites: Math.floor(limit * 0.20),
unwatchedFirst: Math.floor(limit * 0.15),
timeBased: Math.floor(limit * 0.10)
};
// Collect from each algorithm
const recommendations: RecommendedVideo[] = [];
recommendations.push(
...this.getWeightedRandomRecommendations(
distribution.weightedRandom,
excludeRecentDays
)
);
recommendations.push(
...this.getForgottenGemsRecommendations(
distribution.forgottenGems,
excludeRecentDays
)
);
recommendations.push(
...this.getSimilarToFavoritesRecommendations(
distribution.similarToFavorites,
excludeRecentDays
)
);
recommendations.push(
...this.getUnwatchedFirstRecommendations(
distribution.unwatchedFirst
)
);
recommendations.push(
...this.getTimeBasedRecommendations(
distribution.timeBased,
excludeRecentDays
)
);
// Shuffle and deduplicate
return this.shuffleAndDeduplicate(recommendations).slice(0, limit);
}
private shuffleAndDeduplicate(videos: RecommendedVideo[]): RecommendedVideo[] {
// Remove duplicates by ID
const uniqueMap = new Map<number, RecommendedVideo>();
videos.forEach(video => {
if (!uniqueMap.has(video.id)) {
uniqueMap.set(video.id, video);
}
});
// Convert to array and shuffle
const unique = Array.from(uniqueMap.values());
for (let i = unique.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[unique[i], unique[j]] = [unique[j], unique[i]];
}
return unique;
}
```
---
## API Routes
### Recommendations Endpoint
```typescript
// src/app/api/videos/recommendations/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { RecommendationService } from '@/lib/recommendation-service';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const algorithm = searchParams.get('algorithm') || 'smart_mix';
const limit = parseInt(searchParams.get('limit') || '20');
const excludeRecentDays = parseInt(searchParams.get('exclude_recent_days') || '30');
// Validate parameters
if (limit < 1 || limit > 100) {
return NextResponse.json(
{ error: 'Limit must be between 1 and 100' },
{ status: 400 }
);
}
const service = new RecommendationService();
const recommendations = await service.getRecommendations({
algorithm: algorithm as any,
limit,
excludeRecentDays
});
return NextResponse.json({
recommendations,
algorithm,
total_videos: recommendations.length,
total_recommended: recommendations.length
});
} catch (error: any) {
console.error('Recommendation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to get recommendations' },
{ status: 500 }
);
}
}
```
### Media Access Tracking Endpoint
```typescript
// src/app/api/media-access/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { trackMediaAccess } from '@/db';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const mediaId = parseInt(id);
if (isNaN(mediaId)) {
return NextResponse.json(
{ error: 'Invalid media ID' },
{ status: 400 }
);
}
const body = await request.json();
const { accessType } = body;
if (!['view', 'play', 'bookmark', 'rate'].includes(accessType)) {
return NextResponse.json(
{ error: 'Invalid access type' },
{ status: 400 }
);
}
trackMediaAccess(mediaId, accessType);
return NextResponse.json({
success: true,
mediaId,
accessType
});
} catch (error: any) {
console.error('Access tracking error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to track access' },
{ status: 500 }
);
}
}
```
---
## UI Components
### Surprise Me Page
```typescript
// src/app/surprise-me/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { RefreshCw, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import InfiniteVirtualGrid from '@/components/infinite-virtual-grid';
import UnifiedVideoPlayer from '@/components/unified-video-player';
type Algorithm =
| 'smart_mix'
| 'weighted_random'
| 'forgotten_gems'
| 'similar_to_favorites'
| 'time_based'
| 'pure_random'
| 'unwatched_first';
const ALGORITHMS: { value: Algorithm; label: string; description: string }[] = [
{
value: 'smart_mix',
label: 'Smart Mix',
description: 'Balanced mix of all recommendation types'
},
{
value: 'weighted_random',
label: 'Weighted Random',
description: 'Smart randomization with variety'
},
{
value: 'forgotten_gems',
label: 'Forgotten Gems',
description: 'Bookmarked or highly-rated, but not watched recently'
},
{
value: 'similar_to_favorites',
label: 'Similar to Favorites',
description: 'From folders with your top-rated videos'
},
{
value: 'time_based',
label: 'Time-Based',
description: 'Based on when videos were added'
},
{
value: 'pure_random',
label: 'Pure Random',
description: 'Complete serendipity'
},
{
value: 'unwatched_first',
label: 'Unwatched First',
description: 'Videos you\'ve never seen'
}
];
export default function SurpriseMePage() {
const [algorithm, setAlgorithm] = useState<Algorithm>('smart_mix');
const [recommendations, setRecommendations] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<any>(null);
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
const fetchRecommendations = async () => {
setIsLoading(true);
try {
const response = await fetch(
`/api/videos/recommendations?algorithm=${algorithm}&limit=20`
);
const data = await response.json();
setRecommendations(data.recommendations || []);
} catch (error) {
console.error('Failed to fetch recommendations:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchRecommendations();
}, [algorithm]);
const handleVideoClick = async (video: any) => {
// Track view
try {
await fetch(`/api/media-access/${video.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessType: 'view' })
});
} catch (error) {
console.error('Failed to track access:', error);
}
setSelectedVideo(video);
setIsPlayerOpen(true);
};
const handleClosePlayer = () => {
setIsPlayerOpen(false);
setSelectedVideo(null);
};
const handleRefresh = () => {
fetchRecommendations();
};
return (
<div className="min-h-screen bg-zinc-950">
{/* Header */}
<div className="bg-gradient-to-r from-purple-900/20 to-pink-900/20 border-b border-zinc-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-purple-600 to-pink-600 rounded-2xl flex items-center justify-center shadow-lg">
<Sparkles className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">Surprise Me</h1>
<p className="text-zinc-400">Discover videos from your collection</p>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap items-center gap-4">
<select
value={algorithm}
onChange={(e) => setAlgorithm(e.target.value as Algorithm)}
className="px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-white"
>
{ALGORITHMS.map(algo => (
<option key={algo.value} value={algo.value}>
{algo.label}
</option>
))}
</select>
<Button
onClick={handleRefresh}
disabled={isLoading}
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<div className="text-sm text-zinc-400">
{ALGORITHMS.find(a => a.value === algorithm)?.description}
</div>
</div>
</div>
</div>
{/* Recommendations Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{isLoading ? (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600 mx-auto mb-4"></div>
<p className="text-zinc-400">Loading recommendations...</p>
</div>
</div>
) : recommendations.length === 0 ? (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Sparkles className="h-16 w-16 text-zinc-700 mx-auto mb-4" />
<p className="text-zinc-400">No recommendations available</p>
<p className="text-sm text-zinc-500 mt-2">Try a different algorithm</p>
</div>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{recommendations.map((video) => (
<VideoRecommendationCard
key={video.id}
video={video}
onClick={() => handleVideoClick(video)}
/>
))}
</div>
)}
</div>
{/* Video Player */}
{selectedVideo && (
<UnifiedVideoPlayer
video={selectedVideo}
isOpen={isPlayerOpen}
onClose={handleClosePlayer}
playerType="modal"
useArtPlayer={true}
/>
)}
</div>
);
}
```
### Video Recommendation Card Component
```typescript
// Component within the same file or separate
interface VideoRecommendationCardProps {
video: any;
onClick: () => void;
}
function VideoRecommendationCard({ video, onClick }: VideoRecommendationCardProps) {
const getBadgeColor = (reason: string) => {
if (reason.includes('bookmark')) return 'bg-blue-600';
if (reason.includes('rated')) return 'bg-yellow-600';
if (reason.includes('Never')) return 'bg-teal-600';
if (reason.includes('folder')) return 'bg-orange-600';
return 'bg-purple-600';
};
return (
<div
onClick={onClick}
className="group cursor-pointer bg-zinc-900 rounded-lg overflow-hidden border border-zinc-800 hover:border-purple-600 transition-all"
>
{/* Thumbnail */}
<div className="aspect-video bg-zinc-800 relative overflow-hidden">
{video.thumbnail ? (
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Sparkles className="h-8 w-8 text-zinc-600" />
</div>
)}
{/* Recommendation Badge */}
{video.recommendation_reason && (
<div className={`absolute top-2 left-2 px-2 py-1 rounded text-xs font-medium text-white ${getBadgeColor(video.recommendation_reason)}`}>
{video.recommendation_reason}
</div>
)}
</div>
{/* Info */}
<div className="p-3">
<h3 className="text-sm font-medium text-white truncate">
{video.title || video.path.split('/').pop()}
</h3>
<div className="flex items-center gap-2 mt-2">
{video.avg_rating > 0 && (
<div className="flex items-center gap-1">
<span className="text-yellow-500"></span>
<span className="text-xs text-zinc-400">{video.avg_rating.toFixed(1)}</span>
</div>
)}
{video.bookmark_count > 0 && (
<span className="text-xs text-blue-400">🔖</span>
)}
</div>
</div>
</div>
);
}
```
### Sidebar Integration
```typescript
// src/components/sidebar.tsx - Add this item
import { Sparkles } from 'lucide-react';
// ... inside the sidebar component
<Link
href="/surprise-me"
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg transition-colors",
pathname === "/surprise-me"
? "bg-purple-600 text-white"
: "text-zinc-400 hover:text-white hover:bg-zinc-800"
)}
>
<Sparkles className="h-5 w-5" />
{!isCollapsed && <span>Surprise Me</span>}
</Link>
```
---
## Utility Functions
### Format File Size
```typescript
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
```
### Calculate Days Since
```typescript
export function daysSince(date: string | Date): number {
const then = new Date(date);
const now = new Date();
const diffTime = Math.abs(now.getTime() - then.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
```
### Get Recommendation Badge
```typescript
export function getRecommendationBadge(reason: string): {
icon: string;
color: string;
label: string;
} {
const badges: Record<string, any> = {
bookmark: { icon: '🔖', color: 'blue', label: 'Bookmarked' },
rated: { icon: '⭐', color: 'yellow', label: 'Rated' },
never: { icon: '🎯', color: 'teal', label: 'Unwatched' },
folder: { icon: '📁', color: 'orange', label: 'From Favorites' },
random: { icon: '🎲', color: 'purple', label: 'Random' },
new: { icon: '✨', color: 'green', label: 'New' }
};
for (const [key, value] of Object.entries(badges)) {
if (reason.toLowerCase().includes(key)) {
return value;
}
}
return { icon: '🎲', color: 'gray', label: 'Recommended' };
}
```
---
## Testing Examples
### Unit Test for Recommendation Service
```typescript
// __tests__/recommendation-service.test.ts
import { RecommendationService } from '@/lib/recommendation-service';
describe('RecommendationService', () => {
let service: RecommendationService;
beforeEach(() => {
service = new RecommendationService();
});
test('should return recommendations', async () => {
const recommendations = await service.getRecommendations({
algorithm: 'smart_mix',
limit: 10
});
expect(recommendations).toBeInstanceOf(Array);
expect(recommendations.length).toBeLessThanOrEqual(10);
});
test('should return unwatched videos first', async () => {
const recommendations = await service.getRecommendations({
algorithm: 'unwatched_first',
limit: 5
});
recommendations.forEach(video => {
expect(video.recommendation_reason).toBe('Never watched before');
});
});
test('should exclude recently viewed videos', async () => {
const recommendations = await service.getRecommendations({
algorithm: 'weighted_random',
limit: 20,
excludeRecentDays: 7
});
// Verify none are from the last 7 days
// (would need access tracking test data)
});
});
```
### API Route Test
```typescript
// __tests__/api/recommendations.test.ts
import { GET } from '@/app/api/videos/recommendations/route';
import { NextRequest } from 'next/server';
describe('Recommendations API', () => {
test('should return recommendations with default algorithm', async () => {
const request = new NextRequest('http://localhost/api/videos/recommendations');
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.algorithm).toBe('smart_mix');
expect(data.recommendations).toBeInstanceOf(Array);
});
test('should respect limit parameter', async () => {
const request = new NextRequest('http://localhost/api/videos/recommendations?limit=5');
const response = await GET(request);
const data = await response.json();
expect(data.recommendations.length).toBeLessThanOrEqual(5);
});
});
```
---
## Configuration Examples
### Environment Variables
```bash
# .env.local
# Recommendation settings
RECOMMENDATION_DEFAULT_ALGORITHM=smart_mix
RECOMMENDATION_DEFAULT_LIMIT=20
RECOMMENDATION_CACHE_TTL=300
RECOMMENDATION_EXCLUDE_RECENT_DAYS=30
```
### Feature Flags
```typescript
// src/lib/feature-flags.ts
export const FEATURE_FLAGS = {
surpriseMe: {
enabled: true,
algorithms: {
smartMix: true,
weightedRandom: true,
forgottenGems: true,
similarToFavorites: true,
timeBased: true,
pureRandom: true,
unwatchedFirst: true
},
caching: true,
accessTracking: true
}
};
```
---
**Related Documentation**:
- [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
- [Quick Summary](SURPRISE_ME_SUMMARY.md)

235
docs/SURPRISE_ME_INDEX.md Normal file
View File

@ -0,0 +1,235 @@
# Surprise Me Feature - Documentation Index
Welcome to the Surprise Me feature documentation! This page helps you navigate all the documentation for this feature.
---
## 🎯 What You're Looking For
### I want to understand the feature quickly
👉 **[Quick Summary](SURPRISE_ME_SUMMARY.md)** - 5-minute read with key concepts and benefits
### I want to implement the feature
👉 **[Quick Start Guide](SURPRISE_ME_QUICKSTART.md)** - Step-by-step implementation (2-3 hours)
### I need the complete design specification
👉 **[Complete Design Document](SURPRISE_ME_RECOMMENDATION_DESIGN.md)** - Full technical specification
### I want to see code examples
👉 **[Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)** - Ready-to-use code snippets
### I need visual diagrams
👉 **[Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)** - System architecture and flows
---
## 📚 All Documentation
| Document | Purpose | Audience | Time to Read |
|----------|---------|----------|--------------|
| **[Quick Summary](SURPRISE_ME_SUMMARY.md)** | Feature overview | Everyone | 5 min |
| **[Quick Start Guide](SURPRISE_ME_QUICKSTART.md)** | Implementation steps | Developers | 10 min |
| **[Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)** | Full specification | Developers, Architects | 30 min |
| **[Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)** | Visual system design | Developers, Architects | 15 min |
| **[Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)** | Code snippets | Developers | 20 min |
---
## 🎓 Learning Path
### For Product Owners / Managers
1. Read [Quick Summary](SURPRISE_ME_SUMMARY.md)
2. Review "Key Concept" and "Benefits" sections
3. Check "Success Metrics" section
4. Done! ✅
### For Developers (New to Project)
1. Read [Quick Summary](SURPRISE_ME_SUMMARY.md)
2. Study [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
3. Read [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
4. Follow [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
5. Reference [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) as needed
### For Developers (Ready to Code)
1. Skim [Quick Summary](SURPRISE_ME_SUMMARY.md)
2. Jump to [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
3. Use [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) for reference
4. Refer to [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md) for details
### For System Architects
1. Read [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
2. Study [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
3. Review database schema and API design
4. Evaluate scalability and performance considerations
### For QA Engineers
1. Read [Quick Summary](SURPRISE_ME_SUMMARY.md)
2. Review "User Experience Flow" in [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
3. Use "Testing Checklist" in [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
4. Reference [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) for test cases
---
## 🔍 Quick Reference
### Key Concepts
- **7 Recommendation Algorithms**: Smart Mix, Weighted Random, Forgotten Gems, Similar to Favorites, Time-Based, Pure Random, Unwatched First
- **Lightweight Design**: No ML, just smart SQL queries
- **Personal Focus**: Single-user video rediscovery
- **Sidebar Navigation**: Standalone page, not a tab
### Technical Stack
- **Frontend**: Next.js, React, TailwindCSS
- **Backend**: Next.js API Routes
- **Database**: SQLite with new `media_access` table
- **Service Layer**: RecommendationService class
### Main Components
- `/app/surprise-me/page.tsx` - Main page
- `/app/api/videos/recommendations/route.ts` - API endpoint
- `/app/api/media-access/[id]/route.ts` - Access tracking
- `/lib/recommendation-service.ts` - Algorithm logic
### Database Schema
```sql
CREATE TABLE media_access (
id INTEGER PRIMARY KEY,
media_id INTEGER,
access_type TEXT CHECK (access_type IN ('view', 'play', 'bookmark', 'rate')),
created_at DATETIME
);
```
---
## 📖 Document Details
### [Quick Summary](SURPRISE_ME_SUMMARY.md)
- **Lines**: ~220
- **Topics**: Overview, algorithms, benefits, scenarios
- **Best for**: Understanding the "why" and "what"
### [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
- **Lines**: ~570
- **Topics**: Step-by-step implementation, testing, troubleshooting
- **Best for**: Actually building the feature
### [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- **Lines**: ~580
- **Topics**: Architecture, algorithms, database, API, UI, performance
- **Best for**: Deep technical understanding
### [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
- **Lines**: ~390
- **Topics**: Mermaid diagrams for system, data flow, algorithms
- **Best for**: Visual learners and system design
### [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)
- **Lines**: ~910
- **Topics**: Complete code examples, tests, utilities
- **Best for**: Copy-paste ready code
---
## 🎯 Implementation Phases
### Phase 1: MVP (Week 1)
- Database schema ✅ [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
- Basic algorithms (3) ✅ [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)
- Simple UI ✅ [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
- Sidebar integration ✅ [Quick Start Guide](SURPRISE_ME_QUICKSTART.md)
### Phase 2: Enhanced (Week 2)
- Advanced algorithms (4 more) → [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- Smart Mix → [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)
- Polished UI → [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
### Phase 3: Optimized (Week 3)
- Caching → [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- Performance tuning → [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- Analytics → [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
---
## 🤔 Frequently Asked Questions
### Where do I start?
**Answer**: If you're implementing, go to [Quick Start Guide](SURPRISE_ME_QUICKSTART.md). If you're learning, start with [Quick Summary](SURPRISE_ME_SUMMARY.md).
### How long does implementation take?
**Answer**: Basic MVP takes 2-3 hours. Full feature with all algorithms takes 1-2 weeks.
### Do I need machine learning?
**Answer**: No! The design specifically avoids ML for simplicity and lightweight operation.
### Will this work with 100,000 videos?
**Answer**: Yes, with proper database indexes (included in design). See performance section in [Complete Design](SURPRISE_ME_RECOMMENDATION_DESIGN.md).
### Can I customize the algorithms?
**Answer**: Absolutely! See [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md) for how to add new algorithms.
### Is this production-ready?
**Answer**: The design is production-ready. Implementation quality depends on following the guides.
---
## 🔗 Related Features
This feature integrates with:
- **Video Player**: Uses existing UnifiedVideoPlayer component
- **Bookmarks**: Leverages bookmark data for Forgotten Gems algorithm
- **Star Ratings**: Uses rating data for recommendations
- **Media Database**: Extends existing media table with access tracking
---
## 📝 Contributing to Documentation
Found an issue or want to improve the docs?
1. Check which document needs updating using this index
2. Follow the style of existing documentation
3. Update the relevant document
4. Update this index if you add new content
5. Update the main [docs README](README.md)
---
## 🆘 Need Help?
### Can't find something?
- Use browser's find function (Ctrl/Cmd + F) to search this page
- Check the [main docs README](README.md)
- Search within individual documents
### Found a bug in documentation?
- Create an issue describing the problem
- Reference the specific document and section
- Suggest a fix if possible
### Want to suggest improvements?
- Share your ideas in project discussions
- Submit a pull request with improvements
- Update this index to reflect changes
---
## 📊 Version Information
| Document | Version | Last Updated | Status |
|----------|---------|--------------|--------|
| Quick Summary | 1.0 | 2025-10-12 | ✅ Complete |
| Quick Start Guide | 1.0 | 2025-10-12 | ✅ Complete |
| Complete Design | 1.0 | 2025-10-12 | ✅ Complete |
| Architecture Diagrams | 1.0 | 2025-10-12 | ✅ Complete |
| Implementation Examples | 1.0 | 2025-10-12 | ✅ Complete |
| This Index | 1.0 | 2025-10-12 | ✅ Complete |
---
**Next Steps**:
1. Choose your learning path above
2. Start reading the recommended documents
3. Begin implementation using the Quick Start Guide
**Happy Coding! 🎉**

View File

@ -0,0 +1,566 @@
# Surprise Me - Quick Start Implementation Guide
This guide will help you implement the Surprise Me feature step by step.
---
## 📋 Pre-Implementation Checklist
Before starting, ensure you have:
- ✅ Next.js project set up
- ✅ SQLite database configured
- ✅ Existing video browsing functionality
- ✅ Understanding of the codebase structure
---
## 🚀 Implementation Steps
### Phase 1: Database Setup (15 minutes)
#### Step 1.1: Add Media Access Table
Edit `/src/db/index.ts` and add the new table in the `initializeDatabase()` function:
```typescript
// Add after the library_cluster_mapping table creation
db.exec(`
CREATE TABLE IF NOT EXISTS 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
);
`);
```
#### Step 1.2: Add Indexes
```typescript
// Add with other indexes
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_media_id ON media_access(media_id);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_created_at ON media_access(created_at);`);
db.exec(`CREATE INDEX IF NOT EXISTS idx_media_access_type_created_at ON media_access(access_type, created_at);`);
```
#### Step 1.3: Add Helper Functions
At the end of `/src/db/index.ts`:
```typescript
export function trackMediaAccess(mediaId: number, accessType: 'view' | 'play' | 'bookmark' | 'rate'): void {
const db = getDatabase();
db.prepare(`
INSERT INTO media_access (media_id, access_type)
VALUES (?, ?)
`).run(mediaId, accessType);
}
```
**Test**: Run your app and check that the database initializes without errors.
---
### Phase 2: Recommendation Service (30 minutes)
#### Step 2.1: Create Recommendation Service File
Create `/src/lib/recommendation-service.ts`:
```typescript
import { getDatabase } from '@/db';
export type RecommendationAlgorithm =
| 'smart_mix'
| 'weighted_random'
| 'forgotten_gems'
| 'unwatched_first'
| 'pure_random';
export interface RecommendedVideo {
id: number;
title: string;
path: string;
size: number;
thumbnail: string;
type: string;
bookmark_count: number;
star_count: number;
avg_rating: number;
created_at: string;
recommendation_reason: string;
recommendation_score: number;
}
export class RecommendationService {
private db: any;
constructor() {
this.db = getDatabase();
}
async getRecommendations(
algorithm: RecommendationAlgorithm = 'smart_mix',
limit: number = 20
): Promise<RecommendedVideo[]> {
switch (algorithm) {
case 'unwatched_first':
return this.getUnwatchedFirst(limit);
case 'pure_random':
return this.getPureRandom(limit);
case 'weighted_random':
return this.getWeightedRandom(limit);
default:
return this.getUnwatchedFirst(limit); // Start simple
}
}
private getUnwatchedFirst(limit: number): RecommendedVideo[] {
const query = `
SELECT m.*,
'Never watched before' as recommendation_reason,
1.0 as recommendation_score
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 ?
`;
return this.db.prepare(query).all(limit);
}
private getPureRandom(limit: number): RecommendedVideo[] {
const query = `
SELECT m.*,
'Random selection' as recommendation_reason,
1.0 as recommendation_score
FROM media m
WHERE m.type = 'video'
ORDER BY RANDOM()
LIMIT ?
`;
return this.db.prepare(query).all(limit);
}
private getWeightedRandom(limit: number): RecommendedVideo[] {
const query = `
SELECT m.*,
'Smart random selection' as recommendation_reason,
1.0 as recommendation_score
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 (ma.id IS NULL OR ma.created_at < datetime('now', '-30 days'))
ORDER BY RANDOM()
LIMIT ?
`;
return this.db.prepare(query).all(limit);
}
}
```
**Test**: Import and instantiate the service in a test file to verify it compiles.
---
### Phase 3: API Endpoints (20 minutes)
#### Step 3.1: Create Recommendations API
Create `/src/app/api/videos/recommendations/route.ts`:
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { RecommendationService, RecommendationAlgorithm } from '@/lib/recommendation-service';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const algorithm = (searchParams.get('algorithm') || 'unwatched_first') as RecommendationAlgorithm;
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);
const service = new RecommendationService();
const recommendations = await service.getRecommendations(algorithm, limit);
return NextResponse.json({
recommendations,
algorithm,
total_videos: recommendations.length,
total_recommended: recommendations.length
});
} catch (error: any) {
console.error('Recommendation error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to get recommendations' },
{ status: 500 }
);
}
}
```
**Test**: Visit `http://localhost:3000/api/videos/recommendations` in your browser.
#### Step 3.2: Create Media Access Tracking API
Create `/src/app/api/media-access/[id]/route.ts`:
```typescript
import { NextRequest, NextResponse } from 'next/server';
import { trackMediaAccess } from '@/db';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const mediaId = parseInt(id);
if (isNaN(mediaId)) {
return NextResponse.json(
{ error: 'Invalid media ID' },
{ status: 400 }
);
}
const body = await request.json();
const { accessType } = body;
if (!['view', 'play', 'bookmark', 'rate'].includes(accessType)) {
return NextResponse.json(
{ error: 'Invalid access type' },
{ status: 400 }
);
}
trackMediaAccess(mediaId, accessType);
return NextResponse.json({
success: true,
mediaId,
accessType
});
} catch (error: any) {
console.error('Access tracking error:', error);
return NextResponse.json(
{ error: error.message || 'Failed to track access' },
{ status: 500 }
);
}
}
```
**Test**: Use Postman or curl to POST to the endpoint.
---
### Phase 4: Sidebar Integration (10 minutes)
#### Step 4.1: Add Surprise Me to Sidebar
Edit `/src/components/sidebar.tsx`:
1. Import the icon:
```typescript
import { Sparkles } from 'lucide-react';
```
2. Add the menu item (after Bookmarks, before Clusters):
```typescript
<Link
href="/surprise-me"
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg transition-colors",
pathname === "/surprise-me"
? "bg-gradient-to-r from-purple-600 to-pink-600 text-white"
: "text-zinc-400 hover:text-white hover:bg-zinc-800"
)}
>
<Sparkles className="h-5 w-5 flex-shrink-0" />
{!isCollapsed && <span>Surprise Me</span>}
</Link>
```
**Test**: Check that the sidebar shows the new item and it's clickable.
---
### Phase 5: Surprise Me Page (45 minutes)
#### Step 5.1: Create the Page
Create `/src/app/surprise-me/page.tsx`:
```typescript
'use client';
import { useState, useEffect } from 'react';
import { RefreshCw, Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import UnifiedVideoPlayer from '@/components/unified-video-player';
type Algorithm = 'unwatched_first' | 'pure_random' | 'weighted_random';
const ALGORITHMS = [
{ value: 'unwatched_first', label: 'Unwatched First', description: 'Videos you\'ve never seen' },
{ value: 'weighted_random', label: 'Weighted Random', description: 'Smart randomization' },
{ value: 'pure_random', label: 'Pure Random', description: 'Complete surprise' }
] as const;
export default function SurpriseMePage() {
const [algorithm, setAlgorithm] = useState<Algorithm>('unwatched_first');
const [recommendations, setRecommendations] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedVideo, setSelectedVideo] = useState<any>(null);
const [isPlayerOpen, setIsPlayerOpen] = useState(false);
const fetchRecommendations = async () => {
setIsLoading(true);
try {
const response = await fetch(
`/api/videos/recommendations?algorithm=${algorithm}&limit=20`
);
const data = await response.json();
setRecommendations(data.recommendations || []);
} catch (error) {
console.error('Failed to fetch recommendations:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchRecommendations();
}, [algorithm]);
const handleVideoClick = async (video: any) => {
try {
await fetch(`/api/media-access/${video.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accessType: 'view' })
});
} catch (error) {
console.error('Failed to track access:', error);
}
setSelectedVideo(video);
setIsPlayerOpen(true);
};
return (
<div className="min-h-screen bg-zinc-950">
{/* Header */}
<div className="bg-gradient-to-r from-purple-900/20 to-pink-900/20 border-b border-zinc-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="flex items-center gap-4 mb-4">
<div className="w-12 h-12 bg-gradient-to-br from-purple-600 to-pink-600 rounded-2xl flex items-center justify-center shadow-lg">
<Sparkles className="h-6 w-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white">Surprise Me</h1>
<p className="text-zinc-400">Discover videos from your collection</p>
</div>
</div>
{/* Controls */}
<div className="flex flex-wrap items-center gap-4">
<select
value={algorithm}
onChange={(e) => setAlgorithm(e.target.value as Algorithm)}
className="px-4 py-2 bg-zinc-900 border border-zinc-700 rounded-lg text-white"
>
{ALGORITHMS.map(algo => (
<option key={algo.value} value={algo.value}>
{algo.label}
</option>
))}
</select>
<Button
onClick={fetchRecommendations}
disabled={isLoading}
className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700"
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<div className="text-sm text-zinc-400">
{ALGORITHMS.find(a => a.value === algorithm)?.description}
</div>
</div>
</div>
</div>
{/* Recommendations Grid */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{isLoading ? (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600 mx-auto mb-4"></div>
<p className="text-zinc-400">Loading recommendations...</p>
</div>
</div>
) : recommendations.length === 0 ? (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<Sparkles className="h-16 w-16 text-zinc-700 mx-auto mb-4" />
<p className="text-zinc-400">No recommendations available</p>
<p className="text-sm text-zinc-500 mt-2">Try a different algorithm</p>
</div>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
{recommendations.map((video) => (
<div
key={video.id}
onClick={() => handleVideoClick(video)}
className="group cursor-pointer bg-zinc-900 rounded-lg overflow-hidden border border-zinc-800 hover:border-purple-600 transition-all"
>
<div className="aspect-video bg-zinc-800 relative overflow-hidden">
{video.thumbnail ? (
<img
src={video.thumbnail}
alt={video.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<Sparkles className="h-8 w-8 text-zinc-600" />
</div>
)}
{video.recommendation_reason && (
<div className="absolute top-2 left-2 px-2 py-1 rounded text-xs font-medium text-white bg-purple-600">
{video.recommendation_reason}
</div>
)}
</div>
<div className="p-3">
<h3 className="text-sm font-medium text-white truncate">
{video.title || video.path.split('/').pop()}
</h3>
<div className="flex items-center gap-2 mt-2">
{video.avg_rating > 0 && (
<span className="text-xs text-yellow-500">⭐ {video.avg_rating.toFixed(1)}</span>
)}
{video.bookmark_count > 0 && (
<span className="text-xs text-blue-400">🔖</span>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
{/* Video Player */}
{selectedVideo && (
<UnifiedVideoPlayer
video={selectedVideo}
isOpen={isPlayerOpen}
onClose={() => {
setIsPlayerOpen(false);
setSelectedVideo(null);
}}
playerType="modal"
useArtPlayer={true}
/>
)}
</div>
);
}
```
**Test**: Navigate to `/surprise-me` and verify the page loads.
---
## ✅ Testing Checklist
### Manual Testing
- [ ] Database table created successfully
- [ ] Sidebar shows "Surprise Me" item
- [ ] Clicking sidebar item navigates to `/surprise-me`
- [ ] Page loads without errors
- [ ] Algorithm dropdown works
- [ ] Refresh button works
- [ ] Video cards display correctly
- [ ] Clicking video opens player
- [ ] Player closes correctly
- [ ] Different algorithms show different results
- [ ] Access tracking works (check database)
### API Testing
```bash
# Test recommendations endpoint
curl http://localhost:3000/api/videos/recommendations
# Test with algorithm
curl http://localhost:3000/api/videos/recommendations?algorithm=pure_random&limit=10
# Test access tracking
curl -X POST http://localhost:3000/api/media-access/1 \
-H "Content-Type: application/json" \
-d '{"accessType": "view"}'
```
---
## 🐛 Common Issues & Solutions
### Issue: No recommendations showing
**Solution**: Check if you have videos in the database. Run:
```sql
SELECT COUNT(*) FROM media WHERE type = 'video';
```
### Issue: Database error on startup
**Solution**: Delete `data/media.db` and restart to recreate schema.
### Issue: API returns 500 error
**Solution**: Check server logs for detailed error messages.
### Issue: Sidebar item not showing
**Solution**: Verify you imported `Sparkles` icon and added the Link component correctly.
---
## 🎯 Next Steps
After basic implementation:
1. Add more algorithms (Forgotten Gems, Similar to Favorites)
2. Improve UI with animations and transitions
3. Add statistics (total unwatched, most recommended, etc.)
4. Implement caching for better performance
5. Add user preferences for default algorithm
---
## 📚 Additional Resources
- [Complete Design Document](SURPRISE_ME_RECOMMENDATION_DESIGN.md)
- [Architecture Diagrams](SURPRISE_ME_ARCHITECTURE_DIAGRAM.md)
- [Implementation Examples](SURPRISE_ME_IMPLEMENTATION_EXAMPLES.md)
- [Quick Summary](SURPRISE_ME_SUMMARY.md)
---
**Estimated Total Time**: 2-3 hours for basic implementation
**Difficulty**: Intermediate
**Prerequisites**:
- Next.js knowledge
- SQLite/SQL basics
- React hooks understanding
- TypeScript familiarity

View File

@ -0,0 +1,580 @@
# 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

217
docs/SURPRISE_ME_SUMMARY.md Normal file
View File

@ -0,0 +1,217 @@
# Surprise Me Feature - Quick Summary
## What is "Surprise Me"?
A lightweight video recommendation system for personal video collections that helps users **rediscover** content they might have forgotten about or haven't watched yet.
## Key Concept
Unlike YouTube's complex ML-based recommendations, "Surprise Me" focuses on:
- ✨ **Rediscovery**: Surface videos you've forgotten
- 🎲 **Variety**: Mix content from different libraries
- 🎯 **Simplicity**: No complex ML, just smart queries
- 📊 **Personal**: Uses YOUR ratings, bookmarks, and viewing patterns
## UI Location
**Sidebar Item** (NOT a tab):
```
Sidebar Navigation:
├── Home
├── Settings
├── Videos
├── Photos
├── Texts
├── Bookmarks
├── 🎲 Surprise Me ← NEW standalone page at /surprise-me
├── Clusters
└── Folder Viewer
```
When clicked → navigates to dedicated `/surprise-me` page
## Recommendation Algorithms
### 1. **Smart Mix** (Default)
Balanced combination of all algorithms:
- 30% Weighted Random
- 25% Forgotten Gems
- 20% Similar to Favorites
- 15% Unwatched First
- 10% Time-Based
### 2. **Weighted Random**
Smart randomization considering:
- How long since last viewed (prefer older)
- Video ratings (slight boost for higher rated)
- Library diversity (ensure variety)
### 3. **Forgotten Gems**
Videos you showed interest in but haven't watched recently:
- Bookmarked videos (30+ days ago)
- Highly-rated videos (4+ stars, 60+ days)
- Videos from bookmarked folders
### 4. **Similar to Favorites**
Find videos from same folders as your 4+ star videos
### 5. **Time-Based**
- Recently added but unwatched
- Seasonal (same time last year)
- Archive discoveries (old, never watched)
### 6. **Pure Random**
Complete serendipity - totally random selection
### 7. **Unwatched First**
Prioritize videos never accessed before
## Technical Architecture
### New Database Table
```sql
media_access (
media_id,
access_type: 'view' | 'play' | 'bookmark' | 'rate',
created_at
)
```
Tracks when users interact with videos for better recommendations.
### New API Endpoints
**GET `/api/videos/recommendations`**
- Query params: `algorithm`, `limit`, `exclude_recent_days`
- Returns: Array of recommended videos with reasons
**POST `/api/media-access/:id`**
- Tracks user interactions for recommendation improvement
### New Components
1. **`/src/app/surprise-me/page.tsx`** - Main page
2. **`/src/lib/recommendation-service.ts`** - Algorithm logic
3. **Modified `sidebar.tsx`** - Add Surprise Me item
## User Flow
1. User clicks "🎲 Surprise Me" in sidebar
2. Page loads with 20 recommended videos (default: Smart Mix)
3. Each video card shows:
- Thumbnail
- Title, path, size
- Rating/bookmark status
- **Recommendation reason** badge (e.g., "From your favorite library")
4. User can:
- Click video to watch
- Click "Refresh" for new recommendations
- Change algorithm via dropdown
- Bookmark/rate directly from cards
## Implementation Phases
### Phase 1: Foundation ✅
- [x] Design documentation
- [ ] Database migration
- [ ] Basic service structure
### Phase 2: Core (Week 1)
- [ ] 3 basic algorithms (Weighted Random, Unwatched, Pure Random)
- [ ] API endpoint
- [ ] Basic UI page
### Phase 3: Advanced (Week 2)
- [ ] 4 advanced algorithms
- [ ] Smart Mix
- [ ] Polished UI
### Phase 4: Integration (Week 3)
- [ ] Auto-track video views
- [ ] Recommendation badges
- [ ] Performance optimization
## Benefits
### For Users
- 🎯 **Discover new content** in their own library
- ⏰ **Save time** deciding what to watch
- 💎 **Rediscover forgotten favorites**
- 🎲 **Serendipitous discoveries**
- 📚 **Explore entire collection** systematically
### Technical
- 🚀 **Lightweight** - no ML infrastructure needed
- ⚡ **Fast** - simple SQL queries with indexes
- 📈 **Scalable** - works with 1K to 100K+ videos
- 🔧 **Maintainable** - pure SQL, no black boxes
- 🎨 **Extensible** - easy to add new algorithms
## Example Scenarios
### Scenario: Large Movie Collection
**Problem**: User has 5,000 movies but always watches the same 50
**Solution**:
- Unwatched First: Shows 100s of movies never opened
- Forgotten Gems: Surfaces 4-star movies from 2 years ago
- Similar to Favorites: Finds movies in same genre folders
### Scenario: New Content Added
**Problem**: User adds 200 new videos but forgets about them
**Solution**:
- Time-Based: "Recently added" shows last 7 days
- Smart Mix: Includes 30% new content automatically
### Scenario: Bored, Want Something Different
**Problem**: User wants to watch something but doesn't know what
**Solution**:
- Pure Random: Complete surprise from entire collection
- Weighted Random: Random but slightly favors good stuff
## Why This Design?
### Lightweight Approach
- ✅ No complex ML training
- ✅ No external dependencies
- ✅ Works offline
- ✅ Instant results
- ✅ Easy to understand and debug
### Personal-Focused
- ✅ Designed for single user
- ✅ Leverages existing data (bookmarks, ratings)
- ✅ Respects user's organizational structure
- ✅ No privacy concerns (all local)
### Practical
- ✅ Solves real problem (content discovery)
- ✅ Easy to implement incrementally
- ✅ Low maintenance overhead
- ✅ Clear success metrics
## Success Metrics
- **80%** of users try the feature
- **30%** click-through rate on recommendations
- **50%** of recommendations are unwatched videos
- **High** diversity across libraries/folders
## Future Enhancements
- 🎵 Playlist generation from recommendations
- 📊 Statistics dashboard
- 🎯 "Similar videos" on player page
- 📅 "Video of the Day" feature
- 🔍 Advanced filters (by library, rating, size)
- 💾 Save recommendation sets
- 🚫 "Not interested" exclusions
---
**See Full Design**: [SURPRISE_ME_RECOMMENDATION_DESIGN.md](./SURPRISE_ME_RECOMMENDATION_DESIGN.md)
**Status**: 📝 Design Complete → Ready for Implementation
**Next Step**: Database schema migration

View File

@ -254,7 +254,7 @@ For issues and feature requests, please check:
## Build/Push Docker image to private repo
Usage:
# Build & push to private registry
docker build -t 192.168.2.212:3000/tigeren/nextav:1.3 .
docker push 192.168.2.212:3000/tigeren/nextav:1.3
docker build -t 192.168.2.212:3000/tigeren/nextav:1.4 .
docker push 192.168.2.212:3000/tigeren/nextav:1.4
docker login 192.168.2.212:3000