Aggiunge modulo Replay, Garage dev e YouTube a livello società.

Pipeline registrazione/upload con storage S3, archivio web e app, proxy replay
per il browser, OAuth YouTube sulla pagina club (Premium Full) e footer legale.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-03 07:53:11 +02:00
parent a87cda156b
commit 1f273f849d
87 changed files with 2952 additions and 195 deletions

View File

@@ -26,3 +26,24 @@ STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID=
# Legacy (equivale a YEARLY se le righe sopra sono vuote)
STRIPE_PREMIUM_LIGHT_PRICE_ID=
STRIPE_PREMIUM_FULL_PRICE_ID=
# --- Replay storage (dopo ogni diretta Premium) ---
# Dev locale con Docker: Garage parte con `docker compose up` (servizio garage).
# Bootstrap una tantum: bash infra/scripts/setup_garage_replays.sh → aggiorna .env con le chiavi.
# MODALITÀ A — solo filesystem: lascia REPLAY_STORAGE_ENDPOINT vuoto (volume /recordings/replays/).
# MODALITÀ B — Garage/S3: compila tutte le righe sotto (endpoint rete Docker: http://garage:3900).
REPLAY_STORAGE_ENDPOINT=http://garage:3900
REPLAY_STORAGE_BUCKET=matchlivetv-replays
REPLAY_STORAGE_REGION=garage
# Credenziali S3: NON le inventi a mano. Le genera Garage con lo script di bootstrap:
# cd infra && bash scripts/setup_garage_replays.sh
# Lo script crea la chiave "replay-uploader" e scrive Key ID + Secret in infra/.env (non in questo file).
# ACCESS_KEY_ID = "Key ID" mostrato da Garage (es. GK6619e25d53883a7bc3f75755)
# SECRET_ACCESS_KEY = "Secret key" mostrata UNA SOLA VOLTA alla creazione della chiave
REPLAY_STORAGE_ACCESS_KEY_ID=
REPLAY_STORAGE_SECRET_ACCESS_KEY=
# true = obbligatorio per Garage/MinIO (path-style). Non cambiare se usi Garage.
REPLAY_STORAGE_FORCE_PATH_STYLE=true
# Opzionale: durata link firmati per play/download (secondi)
# REPLAY_PRESIGNED_URL_TTL_SECONDS=7200
# REPLAY_DOWNLOAD_URL_TTL_SECONDS=900

View File

@@ -94,6 +94,13 @@ services:
STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID: ${STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID:-${STRIPE_PREMIUM_FULL_PRICE_ID:-}}
STRIPE_PREMIUM_LIGHT_PRICE_ID: ${STRIPE_PREMIUM_LIGHT_PRICE_ID:-}
STRIPE_PREMIUM_FULL_PRICE_ID: ${STRIPE_PREMIUM_FULL_PRICE_ID:-}
RECORDINGS_PATH: /recordings
REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-}
REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays}
REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage}
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
ports:
- "3000:3000" # Solo LAN — NPM proxy HTTPS
depends_on:
@@ -133,9 +140,18 @@ services:
YOUTUBE_PLATFORM_REFRESH_TOKEN: ${YOUTUBE_PLATFORM_REFRESH_TOKEN:-}
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
RECORDINGS_PATH: /recordings
REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-}
REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays}
REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage}
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
depends_on:
rails:
condition: service_healthy
volumes:
- recordings:/recordings
volumes:
postgres_data:

View File

@@ -81,6 +81,13 @@ services:
STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID: ${STRIPE_PREMIUM_FULL_YEARLY_PRICE_ID:-${STRIPE_PREMIUM_FULL_PRICE_ID:-}}
STRIPE_PREMIUM_LIGHT_PRICE_ID: ${STRIPE_PREMIUM_LIGHT_PRICE_ID:-}
STRIPE_PREMIUM_FULL_PRICE_ID: ${STRIPE_PREMIUM_FULL_PRICE_ID:-}
RECORDINGS_PATH: /recordings
REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-http://garage:3900}
REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays}
REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage}
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
ports:
- "3000:3000"
depends_on:
@@ -90,6 +97,8 @@ services:
condition: service_healthy
mediamtx:
condition: service_started
garage:
condition: service_started
volumes:
- ../backend:/app
- recordings:/recordings
@@ -119,13 +128,33 @@ services:
JWT_SECRET: ${JWT_SECRET:-matchlivetv_jwt_dev_secret}
YOUTUBE_CLIENT_ID: ${YOUTUBE_CLIENT_ID:-}
YOUTUBE_CLIENT_SECRET: ${YOUTUBE_CLIENT_SECRET:-}
RECORDINGS_PATH: /recordings
REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-http://garage:3900}
REPLAY_STORAGE_BUCKET: ${REPLAY_STORAGE_BUCKET:-matchlivetv-replays}
REPLAY_STORAGE_REGION: ${REPLAY_STORAGE_REGION:-garage}
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
depends_on:
rails:
condition: service_healthy
garage:
condition: service_started
volumes:
- ../backend:/app
- recordings:/recordings
- bundle_cache:/usr/local/bundle
garage:
image: dxflrs/garage:v1.0.1
ports:
- "3900:3900"
volumes:
- garage_meta:/var/lib/garage/meta
- garage_data:/var/lib/garage/data
- ./garage/garage.toml:/etc/garage.toml:ro
command: ["/garage", "-c", "/etc/garage.toml", "server"]
nginx:
image: nginx:alpine
ports:
@@ -140,3 +169,5 @@ volumes:
redis_data:
recordings:
bundle_cache:
garage_meta:
garage_data:

26
infra/garage/garage.toml Normal file
View File

@@ -0,0 +1,26 @@
# Garage single-node dev config for Match Live TV replay storage
# Docs: https://garagehq.deuxfleurs.fr/documentation/quick-start/
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "sqlite"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "garage:3901"
# 32 byte hex (64 caratteri) — solo dev locale
rpc_secret = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
[s3_api]
api_bind_addr = "[::]:3900"
s3_region = "garage"
root_domain = ".s3.garage.localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage.localhost"
[admin]
api_bind_addr = "[::]:3903"
admin_token = "dev-garage-admin-token-change-in-production"

View File

@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# Bootstrap Garage (S3) per replay Match Live TV — locale o produzione
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
GARAGE_CONTAINER="${GARAGE_CONTAINER:-infra-garage-1}"
BUCKET="${REPLAY_STORAGE_BUCKET:-matchlivetv-replays}"
KEY_NAME="${REPLAY_STORAGE_KEY_NAME:-replay-uploader}"
CREDS_FILE="${ROOT}/garage/dev-credentials.env"
ENV_FILE="${ROOT}/.env"
gexec() {
docker exec "$GARAGE_CONTAINER" /garage "$@"
}
wait_garage() {
echo "Attendo Garage..."
for _ in $(seq 1 30); do
if docker exec "$GARAGE_CONTAINER" /garage status >/dev/null 2>&1; then
return 0
fi
sleep 1
done
echo "Garage non risponde. Avvia: cd $ROOT && docker compose up -d garage"
exit 1
}
ensure_layout() {
local node_id
node_id="$(gexec status 2>/dev/null | awk '/^[0-9a-f]{16,}/{print $1; exit}')"
if [ -z "$node_id" ]; then
echo "Impossibile leggere node id da garage status"
gexec status || true
exit 1
fi
echo "Node id: $node_id"
gexec layout assign -z dc1 -c 1G "$node_id" 2>/dev/null || true
gexec layout apply --version 1 2>/dev/null || gexec layout apply 2>/dev/null || true
}
ensure_key() {
if gexec key info "$KEY_NAME" >/dev/null 2>&1; then
echo "Chiave $KEY_NAME già presente"
return 0
fi
echo "Creazione chiave $KEY_NAME..."
local out
out="$(gexec key create "$KEY_NAME" 2>&1)"
echo "$out"
KEY_SECRET="$(echo "$out" | awk -F': ' '/Secret key/{print $2; exit}' | tr -d ' ')"
if [ -z "$KEY_SECRET" ]; then
KEY_SECRET="$(echo "$out" | grep -i secret | awk '{print $NF}' | head -1)"
fi
export KEY_SECRET
}
ensure_bucket() {
echo "Bucket $BUCKET..."
gexec bucket create "$BUCKET" 2>/dev/null || true
gexec bucket allow \
--read --write \
--owner \
"$BUCKET" \
--key "$KEY_NAME" 2>/dev/null || \
gexec bucket allow --read --write "$BUCKET" --key "$KEY_NAME" 2>/dev/null || true
}
write_credentials() {
local key_id
key_id="$(gexec key info "$KEY_NAME" 2>/dev/null | awk -F': ' '/Key ID|key id/{print $2; exit}' | tr -d ' ')"
if [ -z "$key_id" ]; then
key_id="$(gexec key info "$KEY_NAME" 2>/dev/null | grep -i 'key' | head -1 | awk '{print $NF}')"
fi
mkdir -p "${ROOT}/garage"
cat > "$CREDS_FILE" <<EOF
# Generato da setup_garage_replays.sh — non committare
REPLAY_STORAGE_ENDPOINT=http://garage:3900
REPLAY_STORAGE_BUCKET=${BUCKET}
REPLAY_STORAGE_REGION=garage
REPLAY_STORAGE_ACCESS_KEY_ID=${key_id}
REPLAY_STORAGE_SECRET_ACCESS_KEY=${KEY_SECRET:-}
REPLAY_STORAGE_FORCE_PATH_STYLE=true
EOF
echo ""
echo "=== Credenziali Garage (replay) ==="
gexec key info "$KEY_NAME" || true
echo "File: $CREDS_FILE"
}
merge_into_env() {
local key_id secret
key_id="$(grep '^REPLAY_STORAGE_ACCESS_KEY_ID=' "$CREDS_FILE" | cut -d= -f2-)"
secret="$(grep '^REPLAY_STORAGE_SECRET_ACCESS_KEY=' "$CREDS_FILE" | cut -d= -f2-)"
if [ ! -f "$ENV_FILE" ]; then
cp "${ROOT}/.env.example" "$ENV_FILE" 2>/dev/null || touch "$ENV_FILE"
fi
if grep -q '^REPLAY_STORAGE_ENDPOINT=' "$ENV_FILE" 2>/dev/null; then
sed -i 's|^REPLAY_STORAGE_ENDPOINT=.*|REPLAY_STORAGE_ENDPOINT=http://garage:3900|' "$ENV_FILE"
sed -i "s|^REPLAY_STORAGE_BUCKET=.*|REPLAY_STORAGE_BUCKET=${BUCKET}|" "$ENV_FILE"
sed -i 's|^REPLAY_STORAGE_REGION=.*|REPLAY_STORAGE_REGION=garage|' "$ENV_FILE"
sed -i "s|^REPLAY_STORAGE_ACCESS_KEY_ID=.*|REPLAY_STORAGE_ACCESS_KEY_ID=${key_id}|" "$ENV_FILE"
sed -i 's|^REPLAY_STORAGE_FORCE_PATH_STYLE=.*|REPLAY_STORAGE_FORCE_PATH_STYLE=true|' "$ENV_FILE"
if [ -n "$secret" ]; then
if grep -q '^REPLAY_STORAGE_SECRET_ACCESS_KEY=' "$ENV_FILE" 2>/dev/null; then
sed -i "s|^REPLAY_STORAGE_SECRET_ACCESS_KEY=.*|REPLAY_STORAGE_SECRET_ACCESS_KEY=${secret}|" "$ENV_FILE"
else
echo "REPLAY_STORAGE_SECRET_ACCESS_KEY=${secret}" >> "$ENV_FILE"
fi
fi
else
cat >> "$ENV_FILE" <<EOF
# Replay storage — Garage (generato da setup_garage_replays.sh)
REPLAY_STORAGE_ENDPOINT=http://garage:3900
REPLAY_STORAGE_BUCKET=${BUCKET}
REPLAY_STORAGE_REGION=garage
REPLAY_STORAGE_ACCESS_KEY_ID=${key_id}
REPLAY_STORAGE_SECRET_ACCESS_KEY=${secret}
REPLAY_STORAGE_FORCE_PATH_STYLE=true
EOF
fi
echo "Aggiornato $ENV_FILE (Garage replay storage)."
}
main() {
cd "$ROOT"
if ! docker ps --format '{{.Names}}' | grep -q "^${GARAGE_CONTAINER}$"; then
echo "Avvio container garage..."
docker compose up -d garage
fi
wait_garage
ensure_layout
ensure_key
ensure_bucket
write_credentials
merge_into_env
echo ""
echo "Riavvia app: docker compose up -d rails sidekiq"
}
main "$@"