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>
145 lines
5.4 KiB
Markdown
145 lines
5.4 KiB
Markdown
# Modulo Replay — Match Live TV
|
||
|
||
## Panoramica
|
||
|
||
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 archivio MLTV | Download MP4 | YouTube |
|
||
|-------|---------------|-------------------------|--------------|---------|
|
||
| Free | No | — | No | No |
|
||
| 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 → `PostProcessJob`.
|
||
|
||
MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso).
|
||
|
||
### 2. Retention e purge
|
||
- 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
|
||
- Stessa UX listing per YouTube e MLTV (differisce solo il player)
|
||
|
||
### 4. Riproduzione
|
||
- YouTube: embed su `/replay/:id`
|
||
- MLTV: MP4 via `/replay/:id/stream` → redirect `/media/...` → Garage
|
||
- Contatore visualizzazioni su ogni play (`view_count`)
|
||
|
||
### 5. Visibilità
|
||
Pubblico / privato (`unlisted`) come prima. Sync privacy YouTube se `youtube_video_id` presente.
|
||
|
||
### 6. Eliminazione anticipata
|
||
`Recordings::Delete` manuale — resta distruttiva sul record (distinta dal cleanup temporary automatico).
|
||
|
||
### 7–8. KPI, email, thumbnail, download, views — invariati dove applicabili.
|
||
|
||
## 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
|
||
...
|
||
```
|
||
|
||
Valori letti da `MatchLiveTv` (`config/initializers/match_live_tv.rb`).
|
||
|
||
## Storage Garage
|
||
|
||
Senza endpoint S3: storage locale `/recordings/replays/`.
|
||
|
||
### Dev / produzione Garage
|
||
Vedi script `infra/scripts/setup_garage_*.sh` come in precedenza.
|
||
|
||
## Cron (produzione)
|
||
|
||
```bash
|
||
cd /opt/matchlivetv/infra
|
||
bash scripts/install_production_cron.sh
|
||
```
|
||
|
||
| Orario | Task |
|
||
|--------|------|
|
||
| 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) |
|
||
|
||
### Audit duplicati storici
|
||
|
||
```bash
|
||
rails recordings:audit_youtube_duplicates # dry-run di default
|
||
```
|
||
|
||
Nessun cleanup automatico dei vecchi MP4 YouTube già in Garage.
|
||
|
||
## API
|
||
|
||
- `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)
|
||
|
||
## Colonne DB (migration)
|
||
|
||
- `storage_policy` (default `retained`)
|
||
- `temp_expires_at`, `youtube_verified_at`, `local_media_purged_at`
|
||
|
||
## Rischi
|
||
|
||
- 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`.
|