# Piano Architetturale Backend Streaming Video ## Guida Pratica per il Team - Progetto da 1 Settimana --- ## 🎯 Obiettivo Costruire un backend stile Netflix/Jellyfin in ~40-50 ore. È fattibile concentrandosi sul **MVP (Minimum Viable Product)** ed evitando di reinventare ciò che .NET già fornisce. --- ## 📚 Stack Tecnologico | Componente | Tecnologia | Perché | |------------|-----------|---------| | **API** | .NET 10 Minimal APIs | Veloci, poco codice ripetitivo | | **Database** | SQL Server 2022 | Funziona su Linux (Docker) e Azure | | **ORM** | Entity Framework Core 10 | Gestione database automatica | | **Background Jobs** | `BackgroundService` | Per operazioni pesanti senza bloccare le API | | **Video Engine** | FFmpeg CLI | Estrazione metadati e anteprime | **Comando Docker per SQL Server:** ```bash docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=TuaPassword123!" \ -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest ``` --- ## 🗂️ Schema Database ### Opzione Consigliata: Tabella Unica Semplificata Per una settimana di lavoro, **una tabella principale** è più che sufficiente: #### **Tabella: Movies** | Colonna | Tipo | Scopo | |---------|------|-------| | `Id` | `Guid` (PK) | Identificatore unico | | `TmdbId` | `Int` | ID da TMDB API (evita duplicati) | | `Title` | `NVarChar(255)` | Titolo del film | | `Overview` | `NVarChar(MAX)` | Trama | | `ReleaseDate` | `Date` | Data uscita | | `RuntimeMinutes` | `Int` | Durata | | `PosterPath` | `VarChar(500)` | URL copertina | | `BackdropPath` | `VarChar(500)` | Immagine sfondo | | `RelativePath` | `NVarChar(1000)` | Posizione fisica file | | `FileSignature` | `VarChar(64)` | **Hash primi 5MB + dimensione** | | `FileSizeBytes` | `BigInt` | Dimensione file | | `Width` / `Height` | `Int` | Risoluzione (es. 1920x1080) | | `VideoCodec` | `VarChar(20)` | Codec video (h264, hevc) | | `GenresJson` | `NVarChar(MAX)` | Array JSON generi | | `CastJson` | `NVarChar(MAX)` | Array JSON attori (top 5) | | `DateAdded` | `DateTimeOffset` | Quando aggiunto | | `LastScanned` | `DateTimeOffset` | Ultima verifica file | **💡 Suggerimento:** Non create tabelle separate per attori o generi. Usate JSON per la settimana 1. SQL Server 2022 può interrogare JSON nativamente. #### **Tabella: Users** | Colonna | Tipo | Scopo | |---------|------|-------| | `Id` | `Guid` (PK) | Identificatore utente | | `Username` | `NVarChar(50)` | Username unico | | `PasswordHash` | `VarChar(MAX)` | Password criptata (BCrypt) | | `CreatedAt` | `DateTimeOffset` | Data registrazione | #### **Tabella: WatchProgress** | Colonna | Tipo | Scopo | |---------|------|-------| | `UserId` | `Guid` (FK, PK composita) | Riferimento utente | | `MovieId` | `Guid` (FK, PK composita) | Riferimento film | | `LastPositionSeconds` | `BigInt` | Punto di interruzione (in secondi) | | `IsFinished` | `Bit` | True se visto al 90%+ | | `LastUpdated` | `DateTimeOffset` | Ultimo aggiornamento | --- ## 🔌 API Endpoints - Guida Dettagliata ### **Autenticazione** (`/api/auth`) #### `POST /api/auth/register` **Cosa fa:** Crea nuovo utente **Riceve:** ```json { "username": "mario.rossi", "password": "Password123!" } ``` **Restituisce:** ```json { "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "message": "Utente creato con successo" } ``` **Logica interna:** 1. Valida username e password 2. Cripta password con BCrypt 3. Salva nel database 4. Restituisce ID utente --- #### `POST /api/auth/login` **Cosa fa:** Autentica utente e fornisce token di accesso **Riceve:** ```json { "username": "mario.rossi", "password": "Password123!" } ``` **Restituisce:** ```json { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refreshToken": "dGhpc2lzYXJlZnJlc2h0b2tlbg==", "expiresIn": 3600 } ``` **Logica interna:** 1. Verifica username esiste 2. Confronta password hash 3. Genera JWT con claims (userId, username) 4. Genera refresh token (validità 7 giorni) 5. Salva refresh token nel database **Importante:** Il client deve allegare il token a ogni richiesta successiva: ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` --- #### `POST /api/auth/refresh` **Cosa fa:** Rinnova token scaduto senza richiedere login **Riceve:** ```json { "refreshToken": "dGhpc2lzYXJlZnJlc2h0b2tlbg==" } ``` **Restituisce:** Nuovo `accessToken` e `refreshToken` **Perché serve:** Le sessioni di streaming durano ore. Il token di accesso scade dopo 1 ora per sicurezza, ma l'utente non deve rifare login ogni ora. --- ### **Catalogo Film** (`/api/movies`) #### `GET /api/movies` **Cosa fa:** Restituisce lista film paginata e filtrabile **Parametri query:** - `page` (int): Numero pagina (default: 1) - `pageSize` (int): Film per pagina (default: 20, max: 100) - `genre` (string): Filtra per genere ("action", "scifi") - `sort` (string): Ordinamento ("newest", "oldest", "title", "rating") - `q` (string): Ricerca testuale nel titolo **Esempio richiesta:** ``` GET /api/movies?page=1&pageSize=20&genre=action&sort=newest ``` **Restituisce:** ```json { "totalCount": 156, "page": 1, "pageSize": 20, "totalPages": 8, "movies": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "The Matrix", "releaseDate": "1999-03-31", "posterPath": "/posters/matrix.jpg", "duration": 136, "rating": 8.7 } ] } ``` **Logica interna:** 1. Query database con filtri 2. Applica ordinamento 3. Pagina risultati 4. Restituisce solo campi essenziali (non tutta la metadata) --- #### `GET /api/movies/{id}` **Cosa fa:** Restituisce tutti i dettagli di un film specifico **Restituisce:** ```json { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "title": "The Matrix", "overview": "Un hacker scopre la verità sulla realtà...", "releaseDate": "1999-03-31", "runtimeMinutes": 136, "posterPath": "/posters/matrix.jpg", "backdropPath": "/backdrops/matrix.jpg", "rating": 8.7, "resolution": "1920x1080", "codec": "h264", "fileSize": 2147483648, "genres": ["Action", "Sci-Fi"], "cast": [ {"name": "Keanu Reeves", "character": "Neo"}, {"name": "Laurence Fishburne", "character": "Morpheus"} ], "hasSubtitles": true, "audioLanguages": ["en", "it"] } ``` **Logica interna:** 1. Query database per ID 2. Deserializza JSON (genres, cast) 3. Restituisce oggetto completo per UI dettaglio film --- ### **Streaming Video** (`/api/stream`) #### `GET /api/stream/{movieId}` **Cosa fa:** Serve il file video con supporto per ricerca temporale (seek) **Il cuore del sistema.** Questo endpoint è speciale perché: **Come funziona:** 1. Client video player invia header `Range: bytes=0-1024000` 2. Backend legge solo quella porzione del file 3. Risponde con status `206 Partial Content` 4. Include header `Content-Range: bytes 0-1024000/2147483648` **Perché è importante:** - Permette di saltare avanti/indietro nel video - Non carica l'intero file in memoria - Risparmia banda (invia solo ciò che serve) **.NET 10 gestisce tutto automaticamente** se configurato correttamente con `Results.File()` e `EnableRangeProcessing: true`. **Flusso completo:** 1. Verifica autenticazione (token JWT) 2. Verifica permessi utente per quel film 3. Trova percorso fisico file dal database 4. Verifica file esiste su disco 5. Restituisce stream con supporto Range **Headers importanti da impostare:** - `Accept-Ranges: bytes` - `Content-Type: video/mp4` (o mkv, avi) - `Content-Length: dimensione-file` --- ### **Cronologia Visione** (`/api/progress`) #### `POST /api/progress/{movieId}` **Cosa fa:** Salva punto di interruzione per ripresa successiva **Riceve:** ```json { "positionSeconds": 4523, "isFinished": false } ``` **Logica:** - Client invia aggiornamento ogni **10 secondi** durante riproduzione - Se `positionSeconds / runtimeMinutes > 0.9` → imposta `isFinished = true` - Aggiorna tabella `WatchProgress` (UPSERT: update se esiste, insert altrimenti) --- #### `GET /api/progress/continue-watching` **Cosa fa:** Restituisce film iniziati ma non finiti **Restituisce:** ```json [ { "movieId": "3fa85f64...", "title": "The Matrix", "posterPath": "/posters/matrix.jpg", "progressPercent": 67, "lastWatched": "2025-02-10T20:30:00Z" } ] ``` **Query SQL logica:** ```sql SELECT * FROM WatchProgress WHERE UserId = @UserId AND IsFinished = 0 AND LastPositionSeconds > 300 -- almeno 5 minuti ORDER BY LastUpdated DESC LIMIT 10 ``` --- ### **Amministrazione** (`/api/admin`) #### `POST /api/admin/scan` **Cosa fa:** Avvia scansione libreria per nuovi film **Restituisce subito:** ```json { "scanId": "scan_20250210_143022", "status": "in_progress", "statusUrl": "/api/admin/scan/scan_20250210_143022/status" } ``` **Risposta HTTP:** `202 Accepted` (operazione asincrona iniziata) **Perché non blocca:** La scansione può richiedere minuti/ore. Il servizio background la gestisce, l'API resta reattiva. --- #### `GET /api/admin/scan/{scanId}/status` **Cosa fa:** Controlla progresso scansione **Restituisce:** ```json { "scanId": "scan_20250210_143022", "status": "in_progress", "filesScanned": 145, "filesTotal": 832, "newMoviesFound": 3, "orphanedFiles": 1, "startedAt": "2025-02-10T14:30:22Z" } ``` --- ## 🔍 Sistema di Tracciamento File (Problema Rinominazioni) ### Il Problema Se rinominate `Matrix(1999).mkv` → `The Matrix (1999).mkv` o spostate il file in un'altra cartella, un sistema ingenuo: - Pensa che il vecchio file sia stato eliminato - Crea un nuovo record database - **Perde tutta la cronologia di visione dell'utente** ### La Soluzione: File Signature **Cos'è:** Un'impronta digitale del file calcolata da: 1. Hash dei **primi 5 MB** del file (algoritmo veloce: xxHash o BLAKE3) 2. Dimensione totale file in bytes **Perché funziona:** - Calcolo rapidissimo (< 100ms anche per file da 50GB) - La signature rimane identica anche rinominando o spostando - Due file diversi hanno signature diverse (collisioni impossibili) ### Flusso Scanner Intelligente ``` 1. Scanner Background trova file: /SciFi/Matrix Reloaded.mkv 2. Calcola signature: "a1b2c3d4e5f6..."|4294967296 3. Query database: - Esiste già questo percorso? → Aggiorna LastScanned - NO percorso, MA signature esiste? → FILE SPOSTATO/RINOMINATO → Aggiorna solo campo RelativePath → Mantiene ID, metadata, cronologia utente - Signature nuova? → FILE NUOVO → Crea record → Acoda job FFmpeg per estrazione metadata 4. Se percorso vecchio database non esiste su disco: → Marca come "Orphaned" (non cancellare subito) → Attendi alcuni giorni (forse è su disco esterno scollegato) ``` ### Tabella Stati File | Scenario | Azione Database | |----------|-----------------| | File nuovo | INSERT record | | File rinominato/spostato | UPDATE `RelativePath` | | File eliminato | Marca `IsOrphaned=true` | | File "orfano" riappare | UPDATE `IsOrphaned=false` | --- ## 🎬 Integrazione FFmpeg FFmpeg è un programma da riga di comando che fa "magia" con i video. Il vostro backend lo esegue come processo esterno. ### Utilizzo 1: Estrazione Metadata **Quando:** File nuovo trovato dallo scanner **Output include:** - Durata (secondi) - Risoluzione (1920x1080, 3840x2160) - Codec video (h264, hevc, av1) - Codec audio (aac, ac3, dts) - Bitrate - Frame rate - Tracce sottotitoli **Salvate queste info nel database** per mostrare badge UI tipo "4K", "HDR", "Dolby 5.1". --- ### Utilizzo 2: Generazione Anteprime (Thumbnails) **Quando:** File nuovo, per popolare griglia poster **Tecnica "Double-SS"** (seek veloce + seek preciso): ```bash ffmpeg -ss 00:15:00 -i "/movies/Matrix.mkv" \ -ss 00:00:01 -frames:v 1 \ -q:v 2 \ "/wwwroot/thumbs/3fa85f64.../thumb_15m.jpg" ``` **Parametri:** - Primo `-ss`: Salta rapidamente a 15 minuti (non decodifica tutto) - `-i`: File input - Secondo `-ss`: Raffinamento preciso (+1 secondo) - `-frames:v 1`: Estrai 1 frame - `-q:v 2`: Qualità JPEG alta **Generate anteprime a:** - 1 minuto, 5 min, 15 min, 30 min, 60 min - Salvate in `/wwwroot/thumbs/{movieId}/` - Servite come file statici (velocissimo, senza query database) --- ### Utilizzo 3: Sprite Sheets per Scrubbing **Come Netflix:** Quando passate il mouse sulla barra progresso, vedete miniature. **Comando:** ```bash ffmpeg -i "/movies/Matrix.mkv" \ -vf "fps=1/10,scale=160:90,tile=10x10" \ "/wwwroot/sprites/3fa85f64.../sprite.jpg" ``` **Risultato:** Un'immagine con griglia 10x10 (100 anteprime, una ogni 10 secondi) **Servite anche file WebVTT** che mappa tempo → coordinate nell'immagine: ``` 00:00:10.000 --> 00:00:20.000 /sprites/3fa85f64.../sprite.jpg#xywh=0,0,160,90 00:00:20.000 --> 00:00:30.000 /sprites/3fa85f64.../sprite.jpg#xywh=160,0,160,90 ``` --- ### Utilizzo 4: Estrazione Sottotitoli **Logica:** 1. Elenca stream sottotitoli con `ffprobe` 2. Estrai ogni traccia come file `.vtt` separato 3. Salva info nella tabella `Subtitles` (opzionale, o JSON in `Movies`) 4. Client richiede `GET /api/movies/{id}/subtitles/ita` --- ## ⚡ Funzionalità Extra (Se Avete Tempo) ### 🎉 Priorità Alta (2-4 ore ciascuna) #### 1. Sprite Sheets per Video Scrubbing **Impatto:** Esperienza Netflix-level **Tempo:** 3 ore **Come:** Vedi sezione FFmpeg sopra #### 2. Estrazione Sottotitoli **Impatto:** Essenziale per contenuti internazionali **Tempo:** 2 ore **Bonus:** Molti MKV già li hanno incorporati #### 3. Continue Watching Row **Impatto:** UX chiave **Tempo:** 1 ora **Query:** `WatchProgress` dove `IsFinished = false` --- ### 🚀 Priorità Media (4-8 ore ciascuna) #### 4. Watch Parties (Visione Sincronizzata) **Impatto:** Feature social unica **Tempo:** 6-8 ore **Tech:** Server-Sent Events (SSE) in .NET 10 **Come funziona:** 1. User A crea "stanza": `POST /api/rooms` 2. Users B, C si uniscono: `GET /api/rooms/{id}/events` (SSE endpoint) 3. User A preme play → Server invia evento a tutti 4. Tutti i client si sincronizzano allo stesso timestamp **Codice concettuale:** ```csharp app.MapGet("/api/rooms/{id}/events", async (string id, HttpContext ctx) => { ctx.Response.Headers["Content-Type"] = "text/event-stream"; // Stream eventi play/pause/seek a tutti i client connessi }); ``` #### 5. Raccomandazioni Basiche **Impatto:** Aumenta engagement **Tempo:** 4 ore **Algoritmo semplice:** ```sql -- Trova film simili per genere a quelli visti dall'utente SELECT TOP 10 m.* FROM Movies m WHERE m.GenresJson LIKE '%Action%' -- Genere preferito utente AND m.Id NOT IN (SELECT MovieId FROM WatchProgress WHERE UserId = @UserId) ORDER BY m.Rating DESC ``` --- ### 🎨 Priorità Bassa (se rimane tempo) #### Parental Controls Campo `ContentRating` in `Movies`, filtro basato su profilo utente --- ## ⚠️ Cosa NON Fare (Trappole Temporali) ### ❌ Transcodifica Real-Time (HLS/DASH) **Perché NO:** Richiede 15-20 ore solo per questo - Segmentazione video complessa - Accelerazione hardware (NVENC, QuickSync) - Gestione playlist .m3u8 - Profili qualità multipli (240p, 480p, 720p, 1080p, 4K) **Soluzione:** **Direct Play Only** - Assumete che client possano riprodurre h264/h265 - Documentate formati supportati - Eventualmente implementate in v2.0 --- ## 🛠️ Best Practices Tecniche ### Sicurezza ```csharp // Sempre validare JWT su endpoint protetti [Authorize] app.MapGet("/api/movies", async (HttpContext ctx) => { ... }); // Mai esporre percorsi file assoluti al client // ❌ BAD: "C:/Movies/Matrix.mkv" // ✅ GOOD: "/api/stream/3fa85f64..." ``` ### Performance ```csharp // Indici database essenziali CREATE INDEX IX_Movies_Title ON Movies(Title); CREATE INDEX IX_Movies_DateAdded ON Movies(DateAdded DESC); CREATE INDEX IX_WatchProgress_UserId ON WatchProgress(UserId); // Paginazione sempre con LIMIT/OFFSET SELECT * FROM Movies ORDER BY DateAdded DESC OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY; ``` ### Logging ```csharp // Serilog per tracciare tutto Log.Information("Scan started: {ScanId}", scanId); Log.Warning("File not found: {Path}", filePath); Log.Error(ex, "FFmpeg failed for {MovieId}", movieId); ``` ## Struttura Progetto ```text MyStream.Backend/ ├── Program.cs # Punto di ingresso, configurazione Minimal APIs e Middleware ├── appsettings.json # Connection String, API Key TMDB, JWT Secret │ ├── 📂 Core/ # Il "cuore" dei dati │ ├── Movie.cs # Entità combinata (Metadata + File Info) │ ├── User.cs # Entità Utente │ └── WatchProgress.cs # Entità per il "Continua a guardare" │ ├── 📂 Data/ # Accesso al database │ └── AppDbContext.cs # Configurazione EF Core 10 e Fluent API │ ├── 📂 Endpoints/ # Definizione delle rotte API (Raggruppate) │ ├── AuthEndpoints.cs # Login, Register, Refresh │ ├── MovieEndpoints.cs # Lista, Dettaglio, Ricerca │ ├── StreamEndpoints.cs # Logica di streaming (Range Processing) │ └── AdminEndpoints.cs # Trigger scansione e stato │ ├── 📂 Services/ # Logica di business e integrazioni │ ├── ScannerService.cs # Logica di scansione cartelle e File Signature │ ├── MetadataService.cs # Wrapper per TMDbLib (Recupero info film) │ ├── FFmpegService.cs # Wrapper per processi FFmpeg (Thumbnails, Probe) │ └── AuthService.cs # Generazione Token JWT e hashing password │ ├── 📂 Background/ # Processi asincroni │ └── LibraryWorker.cs # Il "motore" che gira in background per lo scan │ └── 📂 wwwroot/ # File statici serviti direttamente (Nginx o Kestrel) ├── 📂 posters/ # Copertine dei film scaricate └── 📂 thumbs/ # Anteprime generate da FFmpeg ``` --- ## 💡 Strategia di Implementazione (Day-by-Day) * **Giorno 1-2:** Setup Database, Entità e Autenticazione JWT. Creazione degli endpoint CRUD base per i film (senza file). * **Giorno 3:** Sviluppo dello `ScannerService` con logica Signature e integrazione `TMDbLib`. Dovreste essere in grado di vedere i film apparire nel DB puntando a una cartella. * **Giorno 4:** Integrazione FFmpeg. Generazione automatica di metadati e poster durante la scansione. * **Giorno 5:** Endpoint di Streaming e logica `WatchProgress` (salvataggio secondi visione). * **Giorno 6:** Pulizia, gestione errori (file non trovati, API TMDB offline) e testing dei casi limite (file spostati). * **Giorno 7:** Buffer per bug e ottimizzazione delle performance (aggiunta indici SQL). **Vuoi che ti scriva la struttura dettagliata di una delle classi specifiche (es. `Movie.cs` o `ScannerService.cs`) per darti un vantaggio sulla scrittura del codice?** --- ## 📚 Risorse Utili - **TMDB API:** https://developers.themoviedb.org/3 - **FFmpeg Docs:** https://ffmpeg.org/ffmpeg.html - **.NET Minimal APIs:** https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis - **JWT Auth:** https://jwt.io/introduction