Aggiunge replay YouTube temporaneo, pull registrazioni dai CPX e snapshot ingest in admin.
Così overflow Hetzner e VOD YouTube restano in archivio dopo lo spegnimento del nodo, e la colonna ingest non si svuota. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+86
-99
@@ -2,156 +2,143 @@
|
||||
|
||||
## Panoramica
|
||||
|
||||
Al termine di ogni diretta Premium, MediaMTX registra i segmenti. Sidekiq unisce i segmenti, genera thumbnail, carica su **Garage (S3-compatible)** e attiva i servizi di notifica, statistiche e (opzionale) republicazione YouTube.
|
||||
Al termine di ogni diretta Premium, MediaMTX registra i segmenti. Sidekiq unisce i segmenti, genera thumbnail e carica su **Garage (S3-compatible)**.
|
||||
|
||||
- **Live MatchLiveTV (HLS)**: copia **permanente** (policy `retained`) con retention del piano.
|
||||
- **Live YouTube**: copia **temporanea** di sicurezza (policy `temporary`, default 48h), poi player YouTube in archivio. **Niente** re-upload automatico del MP4 su YouTube.
|
||||
|
||||
## Piani e retention
|
||||
|
||||
| Piano | Registrazione | Retention | Download MP4 | YouTube VOD |
|
||||
|-------|---------------|-----------|--------------|-------------|
|
||||
| Piano | Registrazione | Retention archivio MLTV | Download MP4 | YouTube |
|
||||
|-------|---------------|-------------------------|--------------|---------|
|
||||
| Free | No | — | No | No |
|
||||
| Premium Light | Sì | 30 giorni | Sì | No |
|
||||
| Premium Full | Sì | 90 giorni | Sì | Sì (auto se diretta YouTube) |
|
||||
| Premium Light | Sì | 30 giorni (`expires_at`) | Sì (solo se file MLTV) | No |
|
||||
| Premium Full | Sì | 90 giorni (`expires_at`) se HLS; **nessuna scadenza archivio** se YouTube (`expires_at` nil) | Sì solo con file Garage | Live → VOD nativo; verify collega `youtube_video_id` |
|
||||
|
||||
## Storage policy (`Recordings::StoragePolicy`)
|
||||
|
||||
```text
|
||||
if youtube_destination? → temporary
|
||||
elsif can_create_recordings? → retained
|
||||
else → none
|
||||
```
|
||||
|
||||
| Policy | Prefisso storage | `expires_at` (archivio) | `temp_expires_at` |
|
||||
|--------|------------------|-------------------------|-------------------|
|
||||
| `temporary` | `temporary_replays/...` | `nil` | now + `YOUTUBE_TEMP_REPLAY_RETENTION_HOURS` (24–72, default 48) |
|
||||
| `retained` | `teams/...` | piano `recording_days` | `nil` |
|
||||
| `none` | — | — | — |
|
||||
|
||||
## Archivio logico vs storage fisico
|
||||
|
||||
Dopo cleanup della copia temp, la partita **resta** in archivio MatchLiveTV. Cambia solo la sorgente:
|
||||
|
||||
| `replay_source` | Player | File Garage permanente |
|
||||
|-----------------|--------|------------------------|
|
||||
| `youtube` | embed / link YouTube | no (MP4 temp già eliminato) |
|
||||
| `matchlivetv` | player MP4 proprietario | sì |
|
||||
| `none` | non playable | — |
|
||||
|
||||
`available_in_archive?` = `ready` e (YouTube URL/`youtube_video_id` **oppure** `storage_key`).
|
||||
|
||||
Clear/purge temporary: **solo** oggetti S3 video; mai soft-delete; mai azzerare `youtube_video_id` / metadata / score. Thumbnail preferibilmente conservata.
|
||||
|
||||
## Flusso YouTube
|
||||
|
||||
```text
|
||||
Stop → StoragePolicy(temporary) → Finalize (expires_at nil, temp_expires_at)
|
||||
→ Upload temporary_replays/... → ready
|
||||
→ VerifyYoutubeReplayJob (backoff)
|
||||
→ ok: youtube_video_id + youtube_verified_at → ClearTemporaryMediaJob
|
||||
→ fail fino a max attempts: aspetta safety net
|
||||
→ PurgeTemporaryMediaJob (cron): a temp_expires_at elimina file + log se non verificato
|
||||
```
|
||||
|
||||
## Funzionalità implementate
|
||||
|
||||
### 1. Registrazione automatica
|
||||
Pipeline: `Sessions::Stop` → `FinalizeSession` → `UploadJob` → storage.
|
||||
Pipeline: `Sessions::Stop` → `FinalizeSession` → `UploadJob` → storage → `PostProcessJob`.
|
||||
|
||||
MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso). In pausa o con sola slate `alwaysAvailable` la registrazione è disattivata, così il replay non contiene minuti di schermo «Trasmissione in pausa».
|
||||
MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso).
|
||||
|
||||
### 2. Retention e purge
|
||||
`Recordings::PurgeExpiredJob` / `rails recordings:purge_expired`
|
||||
- Retained: `Recordings::PurgeExpiredJob` / `rails recordings:purge_expired` (soft-delete + S3 + YouTube se collegato)
|
||||
- Temporary: `rails recordings:purge_temporary` (solo storage)
|
||||
|
||||
### 3. Archivio Replay
|
||||
- Web: `/clubs/:id/replays` (gestione società)
|
||||
- Pubblico: `/replay` con filtro società/squadra
|
||||
- App mobile: **nessuna** UI replay (gestione solo da sito web)
|
||||
- Stessa UX listing per YouTube e MLTV (differisce solo il player)
|
||||
|
||||
### 4. Riproduzione
|
||||
- Player MP4: `/replay/:id` + stream `/replay/:id/stream`
|
||||
- Delivery: Rails verifica accesso → redirect 302 a `/media/...` (edge → Garage). Puma non proxya il file.
|
||||
- YouTube: embed su `/replay/:id`
|
||||
- MLTV: MP4 via `/replay/:id/stream` → redirect `/media/...` → Garage
|
||||
- Contatore visualizzazioni su ogni play (`view_count`)
|
||||
|
||||
### 5. Visibilità
|
||||
- **Pubblico** (`public`): compare in `/replay`, indicizzabile, YouTube `public`
|
||||
- **Privato** (`unlisted`): non in catalogo pubblico, accessibile solo con link diretto, `noindex`, YouTube `unlisted`
|
||||
|
||||
Modificabile dall’archivio società (`/clubs/:id/replays`). Il cambio privacy sincronizza automaticamente il VOD YouTube se presente (`Recordings::SyncYoutubePrivacyJob`).
|
||||
Pubblico / privato (`unlisted`) come prima. Sync privacy YouTube se `youtube_video_id` presente.
|
||||
|
||||
### 6. Eliminazione anticipata
|
||||
`Recordings::Delete` — elimina video YouTube collegato, oggetti S3/Garage (MP4 + thumbnail) e soft-delete DB.
|
||||
`Recordings::Delete` manuale — resta distruttiva sul record (distinta dal cleanup temporary automatico).
|
||||
|
||||
Stesso comportamento alla scadenza retention (`PurgeExpiredJob` alle 03:00).
|
||||
### 7–8. KPI, email, thumbnail, download, views — invariati dove applicabili.
|
||||
|
||||
### 7. Dashboard KPI
|
||||
Replay disponibili, in scadenza (7 gg), spazio occupato, **visualizzazioni totali**.
|
||||
|
||||
### 8. Miglioramenti Premium
|
||||
|
||||
| Feature | Descrizione |
|
||||
|---------|-------------|
|
||||
| **Email replay pronto** | A owner e membri società quando status → `ready` |
|
||||
| **Email scadenza** | 7 giorni prima di `expires_at` (`rails recordings:expiry_warnings`) |
|
||||
| **Thumbnail** | Frame ffmpeg, URL `/replay/:id/thumbnail` |
|
||||
| **Download MP4** | Premium, link presigned 15 min o `/replay/:id/download` |
|
||||
| **Statistiche views** | `view_count` su ogni replay |
|
||||
| **metadata JSONB** | `source_platform`, `auto_publish_youtube`, `ai: {}` per estensioni |
|
||||
| **YouTube VOD** | Premium Full: upload automatico se diretta era su YouTube; manuale da archivio |
|
||||
|
||||
## Storage
|
||||
## Config ENV
|
||||
|
||||
```env
|
||||
YOUTUBE_TEMP_REPLAY_RETENTION_HOURS=48
|
||||
YOUTUBE_REPLAY_VERIFY_MAX_ATTEMPTS=12
|
||||
YOUTUBE_REPLAY_VERIFY_BASE_INTERVAL_SECS=300
|
||||
YOUTUBE_REPLAY_VERIFY_GRACE_SECS=120
|
||||
|
||||
REPLAY_STORAGE_ENDPOINT=http://garage:3900
|
||||
REPLAY_STORAGE_BUCKET=matchlivetv-replays
|
||||
REPLAY_STORAGE_ACCESS_KEY_ID=...
|
||||
REPLAY_STORAGE_SECRET_ACCESS_KEY=...
|
||||
REPLAY_DOWNLOAD_URL_TTL_SECONDS=900
|
||||
...
|
||||
```
|
||||
|
||||
Valori letti da `MatchLiveTv` (`config/initializers/match_live_tv.rb`).
|
||||
|
||||
## Storage Garage
|
||||
|
||||
Senza endpoint S3: storage locale `/recordings/replays/`.
|
||||
|
||||
### Dev locale con Garage
|
||||
|
||||
```bash
|
||||
cd infra
|
||||
docker compose up -d # include garage
|
||||
bash scripts/setup_garage_replays.sh # layout, bucket, chiave → .env
|
||||
docker compose up -d rails sidekiq
|
||||
|
||||
# Verifica pipeline (segmento finto → Garage)
|
||||
docker compose exec -T rails bundle exec rails replay:e2e_garage
|
||||
```
|
||||
|
||||
### Produzione (`/opt/matchlivetv`)
|
||||
|
||||
```bash
|
||||
cd infra
|
||||
bash scripts/setup_garage_production.sh # garage.prod.toml, bucket, chiavi → .env
|
||||
# Rails/Sidekiq usano REPLAY_STORAGE_ENDPOINT=http://garage:3900 (rete Docker)
|
||||
```
|
||||
|
||||
**Credenziali in `.env`:** non compilarle a mano in `.env.example`. Lo script scrive in `infra/.env`:
|
||||
- `REPLAY_STORAGE_ACCESS_KEY_ID` = Key ID Garage (es. `GK...`)
|
||||
- `REPLAY_STORAGE_SECRET_ACCESS_KEY` = Secret mostrato una sola volta alla creazione chiave
|
||||
### Dev / produzione Garage
|
||||
Vedi script `infra/scripts/setup_garage_*.sh` come in precedenza.
|
||||
|
||||
## Cron (produzione)
|
||||
|
||||
Installazione sul server:
|
||||
|
||||
```bash
|
||||
cd /opt/matchlivetv/infra
|
||||
bash scripts/install_production_cron.sh
|
||||
```
|
||||
|
||||
Job attivi (utente `eminux`):
|
||||
|
||||
| Orario | Task |
|
||||
|--------|------|
|
||||
| 03:00 | `recordings:purge_expired` — elimina replay scaduti (DB + Garage) |
|
||||
| 03:00 | `recordings:purge_expired` — replay retained scaduti |
|
||||
| 03:30 | `recordings:purge_temporary` — copie temp YouTube scadute |
|
||||
| 08:00 | `recordings:expiry_warnings` — email avviso scadenza (7 gg) |
|
||||
|
||||
Log: `${MATCHLIVETV_VIDEOS_ROOT}/log/cron-replay.log` (es. `/media/videos/matchlivetv/log/cron-replay.log`)
|
||||
|
||||
### Capacità Garage
|
||||
|
||||
All’installazione, `setup_garage_production.sh` assegna ~**(disco libero − 20 GB)** al nodo.
|
||||
Per ridimensionare dopo:
|
||||
### Audit duplicati storici
|
||||
|
||||
```bash
|
||||
bash scripts/garage_set_capacity.sh 85G # oppure senza argomento: calcolo automatico
|
||||
rails recordings:audit_youtube_duplicates # dry-run di default
|
||||
```
|
||||
|
||||
## Piattaforme live e replay
|
||||
Nessun cleanup automatico dei vecchi MP4 YouTube già in Garage.
|
||||
|
||||
| Origine diretta | Copia Garage/S3 | Player sito `/replay/:id` | YouTube VOD |
|
||||
|-----------------|-----------------|----------------------------|-------------|
|
||||
| `matchlivetv` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Manuale (Premium Full) |
|
||||
| `youtube` | Sì (Premium) | MP4 via redirect `/media/` → Garage | Auto se Premium Full + diretta YouTube |
|
||||
## API
|
||||
|
||||
Se il file S3 non è disponibile ma esiste `youtube_video_id`, la pagina replay mostra embed YouTube.
|
||||
- `GET /teams/:id/recordings` — include `replay_source`, `youtube_watch_url`, `playback_url` (null se youtube-only), `download_enabled` solo con `storage_key`
|
||||
- `GET /recordings/:id/download` — richiede file Garage
|
||||
- `POST /recordings/:id/publish_youtube` — solo MatchLiveTV-only con file (non per live già YouTube)
|
||||
|
||||
## Retention e abbonamento
|
||||
## Colonne DB (migration)
|
||||
|
||||
- `expires_at` viene impostato alla fine diretta: **30 giorni** (Premium Light), **90 giorni** (Premium Full)
|
||||
- `expires_at` **non cambia** al cambio piano successivo
|
||||
- **Nuove registrazioni** richiedono abbonamento attivo (`can_create_recordings?`)
|
||||
- **Archivio esistente** resta accessibile fino a `expires_at` anche se l’abbonamento scade (`can_access_recordings?`)
|
||||
- `storage_policy` (default `retained`)
|
||||
- `temp_expires_at`, `youtube_verified_at`, `local_media_purged_at`
|
||||
|
||||
## API mobile
|
||||
## Rischi
|
||||
|
||||
- `GET /teams/:id/recordings` — lista con thumbnail, views, download_enabled
|
||||
- `GET /recordings/:id/download` — URL download temporaneo
|
||||
- `POST /recordings/:id/publish_youtube` — coda republicazione YouTube
|
||||
|
||||
## Estensioni future (metadata.ai)
|
||||
|
||||
Campo `metadata["ai"]` predisposto per:
|
||||
- highlight automatici
|
||||
- trascrizioni
|
||||
- articoli generati
|
||||
- clip / Shorts
|
||||
|
||||
Esempio aggiornamento via API:
|
||||
```json
|
||||
{ "recording": { "metadata": { "ai": { "transcript_status": "queued" } } } } }
|
||||
```
|
||||
- VOD YouTube in ritardo oltre retention temp → safety net elimina file; archivio resta se id collegato; altrimenti anomaly log.
|
||||
- Thumb: se manca storage, poster YouTube da metadata / default.
|
||||
- Record storici restano `storage_policy=retained`.
|
||||
|
||||
Reference in New Issue
Block a user