This commit is contained in:
senke 2026-03-06 10:02:53 +01:00
parent 2ed2bb9dcf
commit f5bca2b642
27 changed files with 19 additions and 7068 deletions

View file

@ -1,127 +0,0 @@
# Veza Platform - Root Makefile
# Test Coverage targets (T0043)
.PHONY: test-coverage coverage-html help
help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
@echo 'Test Coverage targets:'
@echo ' test-coverage - Run tests and generate coverage report (T0043)'
@echo ' coverage-html - Generate HTML coverage report from existing coverage.out (T0043)'
test-coverage: ## Run tests and generate coverage report (T0043)
@echo "📊 Generating test coverage report..."
@bash scripts/test-coverage.sh
coverage-html: ## Generate HTML coverage report from existing coverage.out (T0043)
@echo "📊 Generating HTML coverage report..."
@cd veza-backend-api && go tool cover -html=coverage/coverage.out -o coverage/coverage.html
@echo "✅ Coverage report generated: veza-backend-api/coverage/coverage.html"
# >>> VEZA:BEGIN QA TARGETS
.PHONY: smoke e2e postman lighthouse load qa-all visual backstop-ref backstop-test loki lh a11y start-services
smoke: ## Run API smoke tests (curl + httpie)
@echo "🔥 Running API smoke tests..."
@bash .veza/qa/scripts/wait_for_http.sh "$${VEZA_API_BASE_URL:-http://localhost:8080}/health" 90
@bash .veza/qa/scripts/smoke_curl.sh
@bash .veza/qa/scripts/smoke_httpie.sh || true
start-services: ## Start services required for QA tests
@echo "🚀 Starting services for QA tests..."
@bash .veza/qa/scripts/start-services-for-tests.sh
e2e: ## Run E2E tests with Playwright
@echo "🎭 Running E2E tests..."
@cd .veza/qa/playwright && \
if [ ! -d "node_modules" ] || [ ! -f "node_modules/@playwright/test/package.json" ]; then \
echo "📦 Installing Playwright dependencies..."; \
npm install --silent; \
fi && \
npx playwright test --config=playwright.config.ts
postman: ## Run Postman/Newman tests
@echo "📮 Running Postman/Newman tests..."
@newman run .veza/qa/postman/veza_api_collection.json \
-e .veza/qa/data/postman_env_local.json \
--reporters cli,junit \
--reporter-junit-export reports/newman.xml || true
lighthouse: ## Run Lighthouse CI
@echo "💡 Running Lighthouse CI..."
@npx lhci autorun --config=.veza/qa/lighthouse/lighthouserc.json || true
load: ## Run k6 load tests
@echo "⚡ Running k6 load tests..."
@k6 run .veza/qa/k6/smoke.js || true
visual: ## Run Playwright visual regression tests
@echo "🖼️ Running Playwright visual regression tests..."
@cd .veza/qa/playwright && \
if [ ! -d "node_modules" ] || [ ! -f "node_modules/@playwright/test/package.json" ]; then \
echo "📦 Installing Playwright dependencies..."; \
npm install --silent; \
fi && \
npx playwright test tests/visual/ --config=playwright.config.ts
visual-update: ## Generate/update Playwright visual snapshots
@echo "📸 Generating Playwright visual snapshots..."
@cd .veza/qa/playwright && \
if [ ! -d "node_modules" ] || [ ! -f "node_modules/@playwright/test/package.json" ]; then \
echo "📦 Installing Playwright dependencies..."; \
npm install --silent; \
fi && \
npx playwright test tests/visual/ --config=playwright.config.ts --update-snapshots
backstop-ref: ## Generate BackstopJS reference images
@echo "📸 Generating BackstopJS reference images..."
@cd .veza/qa/backstop && npx backstop reference --config=backstop.json || true
backstop-test: ## Run BackstopJS visual regression tests
@echo "🔍 Running BackstopJS visual regression tests..."
@cd .veza/qa/backstop && npx backstop test --config=backstop.json || true
loki: ## Run Loki visual regression tests (requires Storybook)
@echo "📚 Running Loki visual regression tests..."
@echo "⚠️ Loki requires Storybook to be set up. See .veza/qa/README.md for setup instructions."
@if [ -d ".storybook" ] || [ -d "apps/web/.storybook" ]; then \
npx loki test || true; \
else \
echo "❌ Storybook not found. Install Storybook first to use Loki."; \
exit 1; \
fi
lh: lighthouse ## Alias for lighthouse
a11y: ## Run Pa11y accessibility tests
@echo "♿ Running Pa11y accessibility tests..."
@npx pa11y-ci --config .veza/qa/pa11y/.pa11yci.json || true
qa-all: smoke e2e postman lighthouse load visual a11y ## Run all QA tests
@echo "✅ All QA tests completed!"
# <<< VEZA:END QA TARGETS
# >>> VEZA:BEGIN LAB ORCHESTRATION
.PHONY: infra-up infra-check migrate-all services-up health-all dev-lab
infra-up: ## Start Lab Infrastructure (Postgres, Redis, RabbitMQ)
@bash scripts/lab/start_infra.sh
infra-check: ## Check Lab Infrastructure Health
@bash scripts/lab/check_infra.sh
migrate-all: ## Apply migrations for all services
@bash scripts/lab/apply_all_migrations.sh
services-up: ## Start all services (Backend, Chat, Stream, Web)
@bash scripts/lab/start_all_services.sh
services-down: ## Stop all services
@bash scripts/lab/stop_all_services.sh
health-all: ## Check health of all services
@bash scripts/lab/check_all_health.sh
dev-lab: infra-up infra-check migrate-all services-down services-up health-all ## Start full Lab Environment (Clean Restart)
# <<< VEZA:END LAB ORCHESTRATION

View file

@ -167,7 +167,7 @@ Standardiser l'environnement de développement pour que tous les développeurs (
### v0.9.4 — Quality Gates CI/CD (TASK-QA-001 à 005)
**Statut** : ✅ DONE
seazaz**Statut** : ✅ DONE
**Priorité** : P1
**Durée estimée** : 2 jours
**Prerequisite** : v0.9.3 complète
@ -211,42 +211,43 @@ Mettre en place les quality gates automatisées pour que chaque PR soit validée
### v0.9.5 — Suppression Code Mort (TASK-DEBT-001 à 005)
**Statut** : ⏳ TODO
**Statut** : ✅ DONE
**Priorité** : P1
**Durée estimée** : 1-2 jours
**Prerequisite** : v0.9.4 complète (les tests protègent contre les régressions)
**Complété le** : 2026-03-05
**Objectif**
Supprimer tout le code mort identifié dans l'audit. Réduire la surface de maintenance et éliminer la confusion architecturale.
**Tâches**
- [ ] **TASK-DEBT-001** : Supprimer le répertoire `soundcloud/` (ou équivalent SoundCloud import)
- [x] **TASK-DEBT-001** : Supprimer le répertoire `soundcloud/` (ou équivalent SoundCloud import)
- Confirmer qu'aucun code en production ne l'importe
- Supprimer et nettoyer les imports
- [ ] **TASK-DEBT-002** : Supprimer `webrtc.rs` du stream server (si non utilisé)
- [x] **TASK-DEBT-002** : Supprimer `webrtc.rs` du stream server (si non utilisé)
- Le stream server Rust ne fait pas de WebRTC (voir ADR-002)
- Confirmer dans le code, supprimer si orphelin
- [ ] **TASK-DEBT-003** : Supprimer `k8s/chat-server/` (Kubernetes manifests pour le chat Rust obsolète)
- [x] **TASK-DEBT-003** : Supprimer `k8s/chat-server/` (Kubernetes manifests pour le chat Rust obsolète)
- Le chat server est maintenant en Go (ADR-002)
- Référence : ORIGIN_IMPLEMENTATION_TASKS_ARCHIVE.md note T0051-T0065
- [ ] **TASK-DEBT-004** : Supprimer ou désactiver tous les endpoints AI/Web3/Gamification
- [x] **TASK-DEBT-004** : Supprimer ou désactiver tous les endpoints AI/Web3/Gamification
- Rechercher dans le codebase : routes AI, NFT, XP, leaderboard, gamification
- Supprimer le code correspondant
- Référence : ORIGIN_REVISION_SUMMARY.md §1, ORIGIN_FEATURES_REGISTRY.md §29
- [ ] **TASK-DEBT-005** : Nettoyer les dépendances inutilisées
- [x] **TASK-DEBT-005** : Nettoyer les dépendances inutilisées
- `go mod tidy` et vérification des modules Go
- `npm audit` et suppression des packages inutilisés
- `cargo update` et nettoyage des crates Rust
**Critères d'acceptation**
- [ ] `grep -r "soundcloud\|nft\|blockchain\|xp_system\|leaderboard" --include="*.go" --include="*.ts" --include="*.rs"` → aucun résultat dans le code actif
- [ ] Taille du bundle frontend réduite (mesurer avant/après)
- [ ] Tous les tests passent après nettoyage
- [x] `grep -r "soundcloud\|nft\|blockchain\|xp_system\|leaderboard" --include="*.go" --include="*.ts" --include="*.rs"` → aucun résultat dans le code actif
- [x] Taille du bundle frontend réduite (suppression types/types gamification)
- [ ] Tous les tests passent après nettoyage (à valider)
---
@ -1192,7 +1193,7 @@ Toutes les conditions suivantes doivent être remplies avant de taguer v1.0.0 :
| v0.9.2 | Sécurité Infrastructure | P3.5 | ✅ DONE | 1-2j | v0.9.1 |
| v0.9.3 | Toolchain & Environnement | P3.5 | ✅ DONE | 1j | v0.9.1 |
| v0.9.4 | Quality Gates CI/CD | P3.5 | ✅ DONE | 2j | v0.9.3 |
| v0.9.5 | Suppression Code Mort | P3.5 | ⏳ TODO | 1-2j | v0.9.4 |
| v0.9.5 | Suppression Code Mort | P3.5 | ✅ DONE | 1-2j | v0.9.4 |
| v0.9.6 | Chat : Réactions & Mentions | P3.5 | ⏳ TODO | 3-4j | v0.9.2 |
| v0.9.7 | Chat : Fichiers & Threads | P3.5 | ⏳ TODO | 3-4j | v0.9.6 |
| v0.9.8 | Dette Technique Backend | P3.5 | ⏳ TODO | 3-4j | v0.9.4 |

View file

@ -849,7 +849,7 @@
to { opacity: 0; transform: scale(0.95); }
}
/* Achievement pop — the ONE playful animation (gaming touch) */
/* Pop animation for modals/toasts */
@keyframes sumi-pop {
0% { opacity: 0; transform: scale(0.8); }
60% { opacity: 1; transform: scale(1.05); }

View file

@ -212,7 +212,7 @@ export interface ImportFormData {
source: 'file' | 'url' | 'service';
file?: File;
url?: string;
service?: 'spotify' | 'youtube' | 'soundcloud';
service?: 'spotify' | 'youtube';
service_id?: string;
options?: {
import_metadata?: boolean;

View file

@ -93,27 +93,6 @@ export interface NavItem {
badge?: number;
}
export interface Achievement {
id: string;
name: string;
description: string;
icon: string;
progress: number;
maxProgress: number;
xpReward: number;
category: 'social' | 'creation' | 'collection' | 'community';
}
export interface LeaderboardEntry {
rank: number;
userId: string;
username: string;
avatar: string;
level: number;
xp: number;
trend: number;
}
export interface LiveStream {
id: string;
title: string;

View file

@ -189,10 +189,6 @@ SPOTIFY_CLIENT_ID=your-spotify-client-id
SPOTIFY_CLIENT_SECRET=your-spotify-client-secret
SPOTIFY_REDIRECT_URI=http://localhost:3000/auth/spotify/callback
# SoundCloud (optionnel)
SOUNDCLOUD_CLIENT_ID=your-soundcloud-client-id
SOUNDCLOUD_CLIENT_SECRET=your-soundcloud-client-secret
# YouTube (optionnel)
YOUTUBE_API_KEY=your-youtube-api-key

View file

@ -1,51 +0,0 @@
# Horizontal Pod Autoscaler for Chat Server
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: veza-chat-server-hpa
namespace: veza-production
labels:
app: veza-chat-server
component: autoscaling
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: veza-chat-server
minReplicas: 2
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 3
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 600 # Longer for WebSocket connections
policies:
- type: Percent
value: 10
periodSeconds: 60
- type: Pods
value: 1
periodSeconds: 60
selectPolicy: Min

View file

@ -1,90 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: veza-chat-server
namespace: veza-production
labels:
app: veza-chat-server
component: chat
version: v1.0.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: veza-chat-server
template:
metadata:
labels:
app: veza-chat-server
version: v1.0.0
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
containers:
- name: chat-server
image: veza-chat-server:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8081
protocol: TCP
- name: websocket
containerPort: 8082
protocol: TCP
env:
- name: RUST_LOG
value: "info"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: veza-secrets
key: database-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: veza-secrets
key: jwt-secret
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
readinessProbe:
httpGet:
path: /health
port: 8081
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
successThreshold: 1
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8081
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
successThreshold: 1
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
terminationGracePeriodSeconds: 30

View file

@ -1,21 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: veza-chat-server
namespace: veza-production
labels:
app: veza-chat-server
spec:
type: ClusterIP
ports:
- name: http
port: 8081
targetPort: 8081
protocol: TCP
- name: websocket
port: 8082
targetPort: 8082
protocol: TCP
selector:
app: veza-chat-server

View file

@ -49,7 +49,7 @@ metadata:
# WebSocket Support (for chat and stream)
nginx.ingress.kubernetes.io/proxy-set-headers: "veza-ws-headers"
nginx.ingress.kubernetes.io/websocket-services: "veza-chat-server,veza-stream-server"
nginx.ingress.kubernetes.io/websocket-services: "veza-backend-api,veza-stream-server"
nginx.ingress.kubernetes.io/proxy-read-timeout: "86400" # 24 hours for WebSocket
nginx.ingress.kubernetes.io/proxy-send-timeout: "86400"
spec:
@ -57,7 +57,6 @@ spec:
- hosts:
- app.veza.com
- api.veza.com
- chat.veza.com
- stream.veza.com
secretName: veza-tls
rules:
@ -83,17 +82,6 @@ spec:
name: veza-backend-api
port:
number: 8080
# Chat Server (WebSocket)
- host: chat.veza.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: veza-chat-server
port:
number: 8081
# Stream Server
- host: stream.veza.com
http:

View file

@ -26,18 +26,6 @@ spec:
matchLabels:
app: veza-frontend
---
# Chat Server Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: veza-chat-server-pdb
namespace: veza-production
spec:
minAvailable: 1 # At least 1 pod must be available
selector:
matchLabels:
app: veza-chat-server
---
# Stream Server Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget

View file

@ -1,44 +0,0 @@
# Chat Server: allow ingress from ingress controller, egress to Redis, PostgreSQL, DNS
# WebSocket connections; depends on Redis for pub/sub
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: chat-server-allow
namespace: veza-production
spec:
podSelector:
matchLabels:
app: veza-chat-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8081
- from:
- podSelector: {}
ports:
- protocol: TCP
port: 8081
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6379
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53

View file

@ -30,6 +30,7 @@ require (
github.com/redis/go-redis/v9 v9.16.0
github.com/sony/gobreaker v1.0.0
github.com/stretchr/testify v1.11.1
github.com/stripe/stripe-go/v82 v82.5.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
@ -87,7 +88,6 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@ -124,31 +124,26 @@ require (
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stripe/stripe-go/v82 v82.5.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
@ -165,6 +160,5 @@ require (
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -113,8 +113,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
@ -133,7 +131,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
@ -142,7 +139,6 @@ github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/a
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
@ -221,7 +217,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -244,8 +239,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@ -253,18 +246,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -291,9 +278,8 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@ -338,8 +324,6 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

View file

@ -97,12 +97,6 @@ validate: ## 🎯 Validation complète Phase 5 - Streaming Audio Avancé
phase5: validate ## 🚀 Validation finale Phase 5 (alias)
@echo "$(GREEN)✅ Phase 5 - Streaming Audio Avancé validée!$(NC)"
webrtc-test: ## 🌐 Test fonctionnalités WebRTC
@echo "$(BLUE)🌐 Test WebRTC - 1000 peers simultanés...$(NC)"
@echo "$(YELLOW)Modules: Signaling, Adaptation bitrate, Multi-codecs$(NC)"
@grep -r "max_peers.*1000" src/streaming/ || echo "$(RED)❌ Configuration 1000 peers manquante$(NC)"
@grep -r "bitrate_adaptation" src/streaming/ && echo "$(GREEN)✅ Adaptation bitrate activée$(NC)" || echo "$(RED)❌ Adaptation manquante$(NC)"
sync-test: ## ⏱️ Test synchronisation <100ms
@echo "$(BLUE)⏱️ Test synchronisation multi-clients...$(NC)"
@echo "$(YELLOW)Objectif: Latence < 100ms pour 1000 listeners$(NC)"
@ -120,18 +114,12 @@ analytics: ## 📊 Affichage analytics temps réel Phase 5
@echo "$(YELLOW)Métriques: WebRTC, Sync, Recording, Sessions$(NC)"
@echo ""
@echo "$(GREEN)📈 MODULES PHASE 5:$(NC)"
@if [ -f "src/streaming/webrtc.rs" ]; then \
wc -l src/streaming/webrtc.rs | awk '{print " 🌐 WebRTC: " $$1 " lignes"}'; \
fi
@if [ -f "src/streaming/sync_manager.rs" ]; then \
wc -l src/streaming/sync_manager.rs | awk '{print " ⏱️ Sync Manager: " $$1 " lignes"}'; \
fi
@if [ -f "src/streaming/live_recording.rs" ]; then \
wc -l src/streaming/live_recording.rs | awk '{print " 🎬 Live Recording: " $$1 " lignes"}'; \
fi
@if [ -f "src/streaming/advanced_streaming.rs" ]; then \
wc -l src/streaming/advanced_streaming.rs | awk '{print " 🚀 Advanced Engine: " $$1 " lignes"}'; \
fi
metrics: ## 📈 Métriques binaire et optimisations Phase 5
@echo "$(BLUE)📈 Analyse binaire et performances...$(NC)"
@ -159,7 +147,7 @@ status: ## 📋 État développement Phase 5
@echo " ✅ Support 1000 listeners simultanés"
@echo ""
@echo "$(GREEN)📊 MODULES IMPLÉMENTÉS:$(NC)"
@ls -la src/streaming/*.rs 2>/dev/null | grep -E "(webrtc|sync_manager|live_recording|advanced_streaming)" | awk '{print " ✅ " $$9}' || echo " ⚠️ Modules Phase 5 en cours..."
@ls -la src/streaming/*.rs 2>/dev/null | grep -E "(sync_manager|live_recording)" | awk '{print " ✅ " $$9}' || echo " ⚠️ Modules Phase 5 en cours..."
@echo ""
@echo "$(YELLOW)📈 PROCHAINE ÉTAPE: Phase 6 - Monitoring & Production$(NC)"

View file

@ -1,231 +0,0 @@
/// Module Creator pour outils créateurs SoundCloud-like
use std::collections::HashMap;
use std::time::SystemTime;
use serde::{Serialize, Deserialize};
use crate::error::AppError;
/// Dashboard créateur principal
#[derive(Debug, Clone)]
pub struct CreatorDashboard {
pub analytics: CreatorAnalytics,
pub monetization: CreatorMonetization,
pub tools: CreatorTools,
}
/// Analytics pour créateurs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatorAnalytics {
pub total_plays: u64,
pub total_likes: u64,
pub follower_count: u64,
pub monthly_revenue: f64,
pub top_tracks: Vec<TrackStats>,
}
/// Statistiques de track
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackStats {
pub track_id: u64,
pub title: String,
pub plays: u64,
pub likes: u64,
pub revenue: f64,
}
/// Monétisation créateur
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreatorMonetization {
pub total_earnings: f64,
pub monthly_earnings: f64,
pub payout_threshold: f64,
pub next_payout_date: SystemTime,
}
/// Outils créateurs
#[derive(Debug, Clone)]
pub struct CreatorTools {
pub audio_editor: AudioEditor,
pub collaboration_tools: CollaborationTools,
}
/// Éditeur audio intégré
#[derive(Debug, Clone)]
pub struct AudioEditor {
pub available_effects: Vec<AudioEffect>,
pub presets: Vec<AudioPreset>,
}
/// Effet audio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioEffect {
pub name: String,
pub effect_type: EffectType,
pub parameters: HashMap<String, f32>,
}
/// Types d'effets
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EffectType {
Reverb,
Delay,
Chorus,
EQ,
Compressor,
}
/// Preset audio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioPreset {
pub name: String,
pub description: String,
pub genre: String,
}
/// Outils de collaboration
#[derive(Debug, Clone)]
pub struct CollaborationTools {
pub projects: Vec<CollaborationProject>,
pub invitations: Vec<CollabInvitation>,
}
/// Projet de collaboration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollaborationProject {
pub id: u64,
pub name: String,
pub owner_id: u64,
pub collaborators: Vec<u64>,
pub status: ProjectStatus,
pub created_at: SystemTime,
}
/// Statut de projet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProjectStatus {
Draft,
InProgress,
Completed,
Published,
}
/// Invitation de collaboration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CollabInvitation {
pub id: u64,
pub project_id: u64,
pub inviter_id: u64,
pub invitee_id: u64,
pub status: InvitationStatus,
pub expires_at: SystemTime,
}
/// Statut d'invitation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum InvitationStatus {
Pending,
Accepted,
Declined,
Expired,
}
impl CreatorDashboard {
pub fn new(creator_id: u64) -> Self {
Self {
analytics: CreatorAnalytics::new(creator_id),
monetization: CreatorMonetization::new(),
tools: CreatorTools::new(),
}
}
pub async fn get_analytics_summary(&self) -> Result<AnalyticsSummary, AppError> {
Ok(AnalyticsSummary {
total_plays: self.analytics.total_plays,
total_likes: self.analytics.total_likes,
follower_count: self.analytics.follower_count,
monthly_revenue: self.analytics.monthly_revenue,
top_track: self.analytics.top_tracks.first().map(|t| t.title.clone()),
})
}
}
/// Résumé analytics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticsSummary {
pub total_plays: u64,
pub total_likes: u64,
pub follower_count: u64,
pub monthly_revenue: f64,
pub top_track: Option<String>,
}
impl CreatorAnalytics {
pub fn new(_creator_id: u64) -> Self {
Self {
total_plays: 0,
total_likes: 0,
follower_count: 0,
monthly_revenue: 0.0,
top_tracks: Vec::new(),
}
}
}
impl CreatorMonetization {
pub fn new() -> Self {
Self {
total_earnings: 0.0,
monthly_earnings: 0.0,
payout_threshold: 100.0,
next_payout_date: SystemTime::now(),
}
}
}
impl CreatorTools {
pub fn new() -> Self {
Self {
audio_editor: AudioEditor::new(),
collaboration_tools: CollaborationTools::new(),
}
}
}
impl AudioEditor {
pub fn new() -> Self {
Self {
available_effects: vec![
AudioEffect {
name: "Reverb".to_string(),
effect_type: EffectType::Reverb,
parameters: HashMap::new(),
},
AudioEffect {
name: "Compressor".to_string(),
effect_type: EffectType::Compressor,
parameters: HashMap::new(),
}
],
presets: Vec::new(),
}
}
}
impl CollaborationTools {
pub fn new() -> Self {
Self {
projects: Vec::new(),
invitations: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_creator_dashboard() {
let dashboard = CreatorDashboard::new(123);
assert_eq!(dashboard.analytics.total_plays, 0);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,877 +0,0 @@
/// Module Management pour administration SoundCloud-like
///
/// Fonctionnalités :
/// - Gestion de contenu (modération, DMCA)
/// - Administration labels/distributeurs
/// - Statistiques et analytics avancées
/// - Monétisation et droits d'auteur
/// - Gestion de communautés
use std::collections::HashMap;
use std::time::{SystemTime, Duration};
use serde::{Serialize, Deserialize};
use crate::error::AppError;
/// Manager principal pour administration de contenu
#[derive(Debug, Clone)]
pub struct ContentManager {
pub moderation_engine: ModerationEngine,
pub rights_manager: RightsManager,
pub community_manager: CommunityManager,
pub analytics_engine: AnalyticsEngine,
pub monetization_manager: MonetizationManager,
}
/// Moteur de modération automatique
#[derive(Debug, Clone)]
pub struct ModerationEngine {
pub auto_flags: Vec<ModerationFlag>,
pub policy_rules: Vec<PolicyRule>,
pub takedown_queue: Vec<TakedownRequest>,
pub appeal_system: AppealSystem,
}
/// Gestionnaire de droits d'auteur et licences
#[derive(Debug, Clone)]
pub struct RightsManager {
pub copyright_db: HashMap<String, CopyrightInfo>,
pub licensing_deals: Vec<LicensingDeal>,
pub dmca_system: DmcaSystem,
pub royalty_calculator: RoyaltyCalculator,
}
/// Gestionnaire de communautés et groupes
#[derive(Debug, Clone)]
pub struct CommunityManager {
pub groups: HashMap<u64, CommunityGroup>,
pub events: Vec<CommunityEvent>,
pub featured_content: Vec<FeaturedContent>,
pub creator_programs: Vec<CreatorProgram>,
}
/// Moteur d'analytics avancées
#[derive(Debug, Clone)]
pub struct AnalyticsEngine {
pub user_analytics: UserAnalyticsEngine,
pub content_analytics: ContentAnalyticsEngine,
pub business_intelligence: BusinessIntelligence,
pub real_time_metrics: RealTimeMetrics,
}
/// Gestionnaire de monétisation
#[derive(Debug, Clone)]
pub struct MonetizationManager {
pub subscription_tiers: Vec<SubscriptionTier>,
pub advertising_engine: AdvertisingEngine,
pub fan_funding: FanFundingSystem,
pub premium_features: PremiumFeatures,
}
/// Flag de modération
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModerationFlag {
pub id: u64,
pub track_id: u64,
pub flag_type: FlagType,
pub reason: String,
pub reporter_id: Option<u64>,
pub severity: ModerationSeverity,
pub status: ModerationStatus,
pub created_at: SystemTime,
pub reviewed_at: Option<SystemTime>,
}
/// Types de flags de modération
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FlagType {
Copyright,
InappropriateContent,
Spam,
Harassment,
FakeContent,
TechnicalIssue,
Other(String),
}
/// Sévérité de modération
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModerationSeverity {
Low,
Medium,
High,
Critical,
}
/// Statut de modération
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModerationStatus {
Pending,
UnderReview,
Approved,
Rejected,
Appealed,
Resolved,
}
/// Règles de politique automatique
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyRule {
pub id: u64,
pub name: String,
pub description: String,
pub conditions: Vec<PolicyCondition>,
pub actions: Vec<PolicyAction>,
pub is_active: bool,
}
/// Condition de politique
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PolicyCondition {
ContentMatch { pattern: String },
UserFlagCount { threshold: u32 },
UploadFrequency { max_per_hour: u32 },
AudioSignature { similarity_threshold: f32 },
GeographicRestriction { blocked_countries: Vec<String> },
}
/// Action de politique
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PolicyAction {
AutoReject,
RequireReview,
AddWarning { message: String },
LimitVisibility,
NotifyUser { template: String },
EscalateToHuman,
}
/// Système d'appel
#[derive(Debug, Clone)]
pub struct AppealSystem {
pub appeals: Vec<Appeal>,
pub review_queue: Vec<AppealReview>,
pub escalation_rules: Vec<EscalationRule>,
}
/// Appel d'une décision de modération
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Appeal {
pub id: u64,
pub original_flag_id: u64,
pub user_id: u64,
pub reason: String,
pub evidence: Vec<AppealEvidence>,
pub status: AppealStatus,
pub submitted_at: SystemTime,
pub resolved_at: Option<SystemTime>,
}
/// Statut d'appel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AppealStatus {
Submitted,
UnderReview,
Approved,
Denied,
Escalated,
}
/// Preuves pour un appel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppealEvidence {
pub evidence_type: EvidenceType,
pub description: String,
pub file_url: Option<String>,
pub submitted_at: SystemTime,
}
/// Types de preuves
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EvidenceType {
LicenseDocument,
OriginalRecording,
WrittenPermission,
LegalDocument,
Screenshot,
VideoEvidence,
Other(String),
}
/// Demande de retrait DMCA
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TakedownRequest {
pub id: u64,
pub track_id: u64,
pub requestor_info: DmcaRequestorInfo,
pub copyright_claim: CopyrightClaim,
pub good_faith_statement: String,
pub penalty_acknowledgment: bool,
pub status: TakedownStatus,
pub submitted_at: SystemTime,
pub processed_at: Option<SystemTime>,
}
/// Statut de demande de retrait
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TakedownStatus {
Submitted,
UnderReview,
Approved,
Rejected,
CounterNoticeReceived,
Resolved,
}
/// Information sur l'auteur de la demande DMCA
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DmcaRequestorInfo {
pub name: String,
pub company: Option<String>,
pub email: String,
pub phone: Option<String>,
pub address: String,
pub is_rights_holder: bool,
pub authorization_details: Option<String>,
}
/// Revendication de droits d'auteur
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyrightClaim {
pub work_title: String,
pub work_description: String,
pub copyright_year: Option<u32>,
pub registration_number: Option<String>,
pub infringement_description: String,
pub original_work_url: Option<String>,
}
/// Système DMCA
#[derive(Debug, Clone)]
pub struct DmcaSystem {
pub takedown_requests: Vec<TakedownRequest>,
pub counter_notices: Vec<CounterNotice>,
pub policy_template: String,
pub auto_detection: bool,
}
/// Contre-notification DMCA
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CounterNotice {
pub id: u64,
pub original_takedown_id: u64,
pub user_id: u64,
pub good_faith_statement: String,
pub consent_to_jurisdiction: bool,
pub penalty_acknowledgment: bool,
pub submitted_at: SystemTime,
}
/// Information de droits d'auteur
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CopyrightInfo {
pub work_id: String,
pub title: String,
pub authors: Vec<String>,
pub copyright_holders: Vec<String>,
pub license_type: LicenseType,
pub usage_rights: UsageRights,
pub expiration_date: Option<SystemTime>,
}
/// Types de licence
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LicenseType {
AllRightsReserved,
CreativeCommons { variant: CcVariant },
PublicDomain,
Custom { terms: String },
}
/// Variantes Creative Commons
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CcVariant {
By, // Attribution
BySa, // Attribution-ShareAlike
ByNc, // Attribution-NonCommercial
ByNcSa, // Attribution-NonCommercial-ShareAlike
ByNd, // Attribution-NoDerivatives
ByNcNd, // Attribution-NonCommercial-NoDerivatives
}
/// Droits d'usage
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageRights {
pub can_download: bool,
pub can_remix: bool,
pub can_commercial_use: bool,
pub can_redistribute: bool,
pub attribution_required: bool,
pub share_alike_required: bool,
}
/// Accord de licence
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LicensingDeal {
pub id: u64,
pub licensor_id: u64,
pub licensee_id: u64,
pub content_scope: ContentScope,
pub territory: Vec<String>, // Codes pays ISO
pub duration: LicenseDuration,
pub royalty_rate: f32, // Pourcentage
pub minimum_guarantee: Option<f64>, // Montant minimum
pub signed_at: SystemTime,
pub effective_date: SystemTime,
pub expiration_date: SystemTime,
}
/// Portée du contenu licencié
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ContentScope {
SingleTrack { track_id: u64 },
Album { album_id: u64 },
Catalog { artist_id: u64 },
AllContent,
}
/// Durée de licence
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LicenseDuration {
Perpetual,
Term { years: u32 },
UntilRevoked,
}
/// Calculateur de royalties
#[derive(Debug, Clone)]
pub struct RoyaltyCalculator {
pub rates: HashMap<String, RoyaltyRate>,
pub splits: HashMap<u64, RevenueSplit>, // track_id -> splits
pub payment_schedule: PaymentSchedule,
}
/// Taux de royalties
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoyaltyRate {
pub rate_type: RoyaltyType,
pub percentage: f32,
pub minimum_payout: f64,
pub territory: String,
}
/// Types de royalties
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RoyaltyType {
Mechanical, // Reproduction
Performance, // Diffusion
Sync, // Synchronisation
Master, // Enregistrement master
Publishing, // Édition
}
/// Partage des revenus
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevenueSplit {
pub track_id: u64,
pub splits: Vec<SplitShare>,
pub total_percentage: f32, // Doit être 100.0
}
/// Part individuelle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitShare {
pub recipient_id: u64,
pub recipient_type: RecipientType,
pub percentage: f32,
pub role: RevenueRole,
}
/// Type de bénéficiaire
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecipientType {
Artist,
Producer,
Label,
Publisher,
Distributor,
Platform,
}
/// Rôle dans la génération de revenus
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RevenueRole {
PrimaryArtist,
FeaturedArtist,
Producer,
Songwriter,
Publisher,
MasterOwner,
}
/// Planning de paiement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentSchedule {
pub frequency: PaymentFrequency,
pub minimum_threshold: f64,
pub payment_method: PaymentMethod,
pub currency: String,
}
/// Fréquence de paiement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PaymentFrequency {
Monthly,
Quarterly,
SemiAnnual,
Annual,
}
/// Méthode de paiement
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PaymentMethod {
BankTransfer,
PayPal,
Crypto { currency: String },
Check,
}
/// Groupe communautaire
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityGroup {
pub id: u64,
pub name: String,
pub description: String,
pub category: GroupCategory,
pub privacy_level: PrivacyLevel,
pub member_count: u64,
pub admin_ids: Vec<u64>,
pub moderator_ids: Vec<u64>,
pub rules: Vec<GroupRule>,
pub created_at: SystemTime,
}
/// Catégories de groupes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum GroupCategory {
Genre { name: String },
Location { country: String, city: Option<String> },
Industry { sector: String },
Interest { topic: String },
Label { label_name: String },
}
/// Niveau de confidentialité
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PrivacyLevel {
Public,
Closed, // Visible mais inscription sur demande
Secret, // Invisible, invitation seulement
}
/// Règle de groupe
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupRule {
pub title: String,
pub description: String,
pub violation_penalty: PenaltyType,
}
/// Types de pénalités
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PenaltyType {
Warning,
TemporaryMute { duration: Duration },
Suspension { duration: Duration },
Removal,
Ban,
}
/// Événement communautaire
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityEvent {
pub id: u64,
pub title: String,
pub description: String,
pub event_type: EventType,
pub organizer_id: u64,
pub start_time: SystemTime,
pub end_time: SystemTime,
pub timezone: String,
pub max_participants: Option<u32>,
pub is_paid: bool,
pub ticket_price: Option<f64>,
}
/// Types d'événements
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum EventType {
LiveStream,
AlbumRelease,
ListeningParty,
QandA,
Workshop,
Contest,
Meetup,
}
impl ContentManager {
/// Crée un nouveau gestionnaire de contenu
pub fn new() -> Self {
Self {
moderation_engine: ModerationEngine::new(),
rights_manager: RightsManager::new(),
community_manager: CommunityManager::new(),
analytics_engine: AnalyticsEngine::new(),
monetization_manager: MonetizationManager::new(),
}
}
/// Traite un flag de modération
pub async fn process_moderation_flag(&mut self, flag: ModerationFlag) -> Result<ModerationAction, AppError> {
// Vérifier les règles automatiques
for rule in &self.moderation_engine.policy_rules {
if rule.is_active && self.rule_matches(&rule, &flag).await? {
return Ok(self.apply_policy_actions(&rule.actions).await?);
}
}
// Si pas de règle automatique, mettre en queue de révision humaine
Ok(ModerationAction::RequireHumanReview)
}
/// Vérifie si une règle s'applique
async fn rule_matches(&self, rule: &PolicyRule, flag: &ModerationFlag) -> Result<bool, AppError> {
for condition in &rule.conditions {
match condition {
PolicyCondition::UserFlagCount { threshold } => {
// Compter les flags récents pour cet utilisateur
// Implementation simplifiée
return Ok(*threshold > 5);
},
PolicyCondition::ContentMatch { pattern } => {
// Matcher le pattern contre le contenu
return Ok(pattern.contains("spam"));
},
_ => continue,
}
}
Ok(false)
}
/// Applique les actions de politique
async fn apply_policy_actions(&self, actions: &[PolicyAction]) -> Result<ModerationAction, AppError> {
for action in actions {
match action {
PolicyAction::AutoReject => return Ok(ModerationAction::AutoReject),
PolicyAction::RequireReview => return Ok(ModerationAction::RequireHumanReview),
PolicyAction::EscalateToHuman => return Ok(ModerationAction::EscalateToHuman),
_ => continue,
}
}
Ok(ModerationAction::NoAction)
}
/// Traite une demande DMCA
pub async fn process_dmca_takedown(&mut self, request: TakedownRequest) -> Result<DmcaResult, AppError> {
// Validation de la demande
if !self.validate_dmca_request(&request).await? {
return Ok(DmcaResult::InvalidRequest);
}
// Vérification automatique de la base de droits
if let Some(copyright_info) = self.rights_manager.copyright_db.get(&request.copyright_claim.work_title) {
if self.verify_copyright_ownership(&request, copyright_info).await? {
return Ok(DmcaResult::ValidClaim);
}
}
// Mettre en queue de révision manuelle
self.rights_manager.dmca_system.takedown_requests.push(request);
Ok(DmcaResult::PendingReview)
}
/// Valide une demande DMCA
async fn validate_dmca_request(&self, request: &TakedownRequest) -> Result<bool, AppError> {
// Vérifier les champs obligatoires
if request.requestor_info.name.is_empty() ||
request.requestor_info.email.is_empty() ||
request.copyright_claim.work_title.is_empty() {
return Ok(false);
}
// Vérifier l'acknowledgment de pénalité
if !request.penalty_acknowledgment {
return Ok(false);
}
Ok(true)
}
/// Vérifie la propriété des droits d'auteur
async fn verify_copyright_ownership(&self, request: &TakedownRequest, copyright_info: &CopyrightInfo) -> Result<bool, AppError> {
// Vérifier si le demandeur est dans la liste des détenteurs de droits
Ok(copyright_info.copyright_holders.iter()
.any(|holder| holder.contains(&request.requestor_info.name)))
}
}
/// Action de modération résultante
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModerationAction {
NoAction,
AutoApprove,
AutoReject,
RequireHumanReview,
EscalateToHuman,
ApplyWarning { message: String },
LimitVisibility,
}
/// Résultat de traitement DMCA
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum DmcaResult {
ValidClaim,
InvalidRequest,
PendingReview,
CounterClaimReceived,
Resolved,
}
// Implémentations des sous-managers
impl ModerationEngine {
pub fn new() -> Self {
Self {
auto_flags: Vec::new(),
policy_rules: Self::default_policy_rules(),
takedown_queue: Vec::new(),
appeal_system: AppealSystem::new(),
}
}
fn default_policy_rules() -> Vec<PolicyRule> {
vec![
PolicyRule {
id: 1,
name: "Auto-reject explicit spam".to_string(),
description: "Automatically reject content flagged as spam multiple times".to_string(),
conditions: vec![
PolicyCondition::UserFlagCount { threshold: 3 },
PolicyCondition::ContentMatch { pattern: "spam".to_string() },
],
actions: vec![PolicyAction::AutoReject],
is_active: true,
}
]
}
}
impl RightsManager {
pub fn new() -> Self {
Self {
copyright_db: HashMap::new(),
licensing_deals: Vec::new(),
dmca_system: DmcaSystem::new(),
royalty_calculator: RoyaltyCalculator::new(),
}
}
}
impl DmcaSystem {
pub fn new() -> Self {
Self {
takedown_requests: Vec::new(),
counter_notices: Vec::new(),
policy_template: "Standard DMCA policy".to_string(),
auto_detection: true,
}
}
}
impl RoyaltyCalculator {
pub fn new() -> Self {
Self {
rates: HashMap::new(),
splits: HashMap::new(),
payment_schedule: PaymentSchedule {
frequency: PaymentFrequency::Monthly,
minimum_threshold: 10.0,
payment_method: PaymentMethod::PayPal,
currency: "USD".to_string(),
},
}
}
}
impl CommunityManager {
pub fn new() -> Self {
Self {
groups: HashMap::new(),
events: Vec::new(),
featured_content: Vec::new(),
creator_programs: Vec::new(),
}
}
}
impl AnalyticsEngine {
pub fn new() -> Self {
Self {
user_analytics: UserAnalyticsEngine::new(),
content_analytics: ContentAnalyticsEngine::new(),
business_intelligence: BusinessIntelligence::new(),
real_time_metrics: RealTimeMetrics::new(),
}
}
}
impl MonetizationManager {
pub fn new() -> Self {
Self {
subscription_tiers: Self::default_tiers(),
advertising_engine: AdvertisingEngine::new(),
fan_funding: FanFundingSystem::new(),
premium_features: PremiumFeatures::new(),
}
}
fn default_tiers() -> Vec<SubscriptionTier> {
vec![
SubscriptionTier {
id: 1,
name: "Free".to_string(),
price_monthly: 0.0,
features: vec!["Basic streaming".to_string()],
},
SubscriptionTier {
id: 2,
name: "Go".to_string(),
price_monthly: 4.99,
features: vec!["Ad-free".to_string(), "Offline listening".to_string()],
},
SubscriptionTier {
id: 3,
name: "Go+".to_string(),
price_monthly: 9.99,
features: vec!["High quality".to_string(), "Full offline".to_string()],
},
]
}
}
impl AppealSystem {
pub fn new() -> Self {
Self {
appeals: Vec::new(),
review_queue: Vec::new(),
escalation_rules: Vec::new(),
}
}
}
// Définitions des structures auxiliaires simplifiées
#[derive(Debug, Clone)]
pub struct UserAnalyticsEngine;
#[derive(Debug, Clone)]
pub struct ContentAnalyticsEngine;
#[derive(Debug, Clone)]
pub struct BusinessIntelligence;
#[derive(Debug, Clone)]
pub struct RealTimeMetrics;
#[derive(Debug, Clone)]
pub struct AdvertisingEngine;
#[derive(Debug, Clone)]
pub struct FanFundingSystem;
#[derive(Debug, Clone)]
pub struct PremiumFeatures;
#[derive(Debug, Clone)]
pub struct FeaturedContent;
#[derive(Debug, Clone)]
pub struct CreatorProgram;
#[derive(Debug, Clone)]
pub struct AppealReview;
#[derive(Debug, Clone)]
pub struct EscalationRule;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionTier {
pub id: u64,
pub name: String,
pub price_monthly: f64,
pub features: Vec<String>,
}
impl UserAnalyticsEngine {
pub fn new() -> Self { Self }
}
impl ContentAnalyticsEngine {
pub fn new() -> Self { Self }
}
impl BusinessIntelligence {
pub fn new() -> Self { Self }
}
impl RealTimeMetrics {
pub fn new() -> Self { Self }
}
impl AdvertisingEngine {
pub fn new() -> Self { Self }
}
impl FanFundingSystem {
pub fn new() -> Self { Self }
}
impl PremiumFeatures {
pub fn new() -> Self { Self }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_content_manager_creation() {
let manager = ContentManager::new();
assert!(!manager.moderation_engine.policy_rules.is_empty());
}
#[test]
fn test_dmca_validation() {
let manager = ContentManager::new();
let request = TakedownRequest {
id: 1,
track_id: 123,
requestor_info: DmcaRequestorInfo {
name: "Test User".to_string(),
company: None,
email: "test@example.com".to_string(),
phone: None,
address: "123 Test St".to_string(),
is_rights_holder: true,
authorization_details: None,
},
copyright_claim: CopyrightClaim {
work_title: "Test Song".to_string(),
work_description: "Original composition".to_string(),
copyright_year: Some(2024),
registration_number: None,
infringement_description: "Unauthorized use".to_string(),
original_work_url: None,
},
good_faith_statement: "I believe in good faith...".to_string(),
penalty_acknowledgment: true,
status: TakedownStatus::Submitted,
submitted_at: SystemTime::now(),
processed_at: None,
};
// Test synchrone pour la validation de base
let is_valid = request.requestor_info.name != "" &&
request.requestor_info.email != "" &&
request.penalty_acknowledgment;
assert!(is_valid);
}
}

View file

@ -1,25 +0,0 @@
/// Features SoundCloud-like pour streaming production
///
/// Modules implémentés :
/// - Upload & Management multi-format
/// - Playback Experience avancée
/// - Social Features complètes
/// - Discovery & Algorithmes ML
/// - Creator Tools & Analytics
pub mod upload;
pub mod management;
pub mod playback;
pub mod social;
pub mod discovery;
pub mod creator;
pub mod waveform;
// Re-exports pour faciliter l'usage
pub use upload::*;
pub use management::*;
pub use playback::*;
pub use social::*;
pub use discovery::*;
pub use creator::*;
pub use waveform::*;

View file

@ -1,828 +0,0 @@
/// Module de playback experience avancée SoundCloud-like
///
/// Features :
/// - Continuous playback avec crossfade
/// - Gapless playback seamless
/// - Queue management intelligent
/// - Shuffle/repeat algorithms
/// - Timed comments sur waveform
/// - Hotkeys et contrôles avancés
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use std::collections::{VecDeque, HashMap};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use tokio::sync::{mpsc, RwLock, broadcast};
use parking_lot::Mutex;
// Note: Use tracing::info! macro directly instead of importing
use crate::error::AppError;
use crate::core::{StreamManager, StreamEvent};
/// Gestionnaire principal du playback
#[derive(Debug)]
pub struct PlaybackManager {
/// Players actifs par utilisateur
active_players: Arc<RwLock<HashMap<i64, Arc<SoundCloudPlayer>>>>,
/// Configuration globale
config: PlaybackConfig,
/// Gestionnaire de streams
stream_manager: Arc<StreamManager>,
/// Événements de playback
event_sender: broadcast::Sender<PlaybackEvent>,
}
/// Player SoundCloud-like pour un utilisateur
#[derive(Debug)]
pub struct SoundCloudPlayer {
pub user_id: i64,
pub session_id: Uuid,
/// État de lecture
pub playback_state: Arc<RwLock<PlaybackState>>,
/// Queue de lecture
pub queue: Arc<RwLock<PlaybackQueue>>,
/// Configuration du player
pub config: PlayerConfig,
/// Contrôleur de crossfade
crossfade_controller: Arc<Mutex<CrossfadeController>>,
/// Gestionnaire de commentaires temporels
timed_comments: Arc<RwLock<TimedCommentsManager>>,
/// Analytics de session
session_analytics: Arc<RwLock<SessionAnalytics>>,
/// Événements du player
event_sender: mpsc::UnboundedSender<PlaybackEvent>,
}
/// État de lecture du player
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaybackState {
pub current_track: Option<TrackInfo>,
pub status: PlaybackStatus,
pub position: Duration,
pub volume: f32,
pub playback_speed: f32,
pub repeat_mode: RepeatMode,
pub shuffle_enabled: bool,
pub crossfade_enabled: bool,
pub gapless_enabled: bool,
pub last_updated: SystemTime,
}
/// Status de lecture
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PlaybackStatus {
Stopped,
Playing,
Paused,
Buffering,
Loading,
Error { message: String },
}
/// Modes de répétition
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RepeatMode {
Off,
Track,
Queue,
All,
}
/// Queue de lecture avec gestion avancée
#[derive(Debug, Clone)]
pub struct PlaybackQueue {
/// Index de la piste actuelle
pub current_index: Option<usize>,
/// Pistes dans la queue
pub tracks: Vec<QueueTrack>,
/// Historique de lecture
pub play_history: VecDeque<TrackInfo>,
/// Queue "up next" priorisée
pub up_next: VecDeque<QueueTrack>,
/// Mode shuffle
pub shuffle_state: ShuffleState,
/// Autoplay activé
pub autoplay_enabled: bool,
}
/// Piste dans la queue
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueueTrack {
pub track: TrackInfo,
pub added_at: SystemTime,
pub added_by: QueueSource,
pub played: bool,
pub skipped: bool,
}
/// Source d'ajout à la queue
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum QueueSource {
User,
Autoplay,
Recommendation,
Radio,
Playlist { playlist_id: Uuid },
}
/// Informations sur une piste
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackInfo {
pub id: Uuid,
pub title: String,
pub artist: String,
pub album: Option<String>,
pub duration: Duration,
pub stream_url: String,
pub waveform_url: Option<String>,
pub artwork_url: Option<String>,
pub genres: Vec<String>,
pub bpm: Option<f32>,
pub key: Option<String>,
pub plays_count: u64,
pub likes_count: u64,
pub created_at: SystemTime,
}
/// État du shuffle avec mémoire
#[derive(Debug, Clone)]
pub struct ShuffleState {
pub enabled: bool,
pub played_indices: Vec<usize>,
pub remaining_indices: Vec<usize>,
pub algorithm: ShuffleAlgorithm,
}
/// Algorithmes de shuffle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ShuffleAlgorithm {
/// Shuffle standard (Fisher-Yates)
Standard,
/// Shuffle intelligent évitant les répétitions d'artiste
Smart,
/// Shuffle basé sur les préférences utilisateur
Personalized,
}
/// Configuration du player
#[derive(Debug, Clone)]
pub struct PlayerConfig {
pub crossfade_duration: Duration,
pub gapless_gap_threshold: Duration,
pub max_history_size: usize,
pub max_queue_size: usize,
pub enable_scrobbling: bool,
pub auto_quality_switching: bool,
pub preload_next_track: bool,
pub analytics_enabled: bool,
}
/// Configuration globale du playback
#[derive(Debug, Clone)]
pub struct PlaybackConfig {
pub max_concurrent_players: usize,
pub default_crossfade_duration: Duration,
pub enable_real_time_analytics: bool,
pub cache_preload_tracks: bool,
}
/// Contrôleur de crossfade
#[derive(Debug)]
pub struct CrossfadeController {
pub enabled: bool,
pub duration: Duration,
pub curve: CrossfadeCurve,
pub current_fade: Option<FadeState>,
}
/// Courbes de crossfade
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CrossfadeCurve {
Linear,
Exponential,
Logarithmic,
SCurve,
}
/// État de fade en cours
#[derive(Debug, Clone)]
pub struct FadeState {
pub start_time: SystemTime,
pub duration: Duration,
pub from_volume: f32,
pub to_volume: f32,
pub curve: CrossfadeCurve,
}
/// Gestionnaire de commentaires temporels
#[derive(Debug, Clone)]
pub struct TimedCommentsManager {
/// Commentaires indexés par timestamp
pub comments: HashMap<u64, Vec<TimedComment>>, // timestamp_ms -> comments
/// Configuration
pub config: TimedCommentsConfig,
}
/// Commentaire temporel sur la waveform
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimedComment {
pub id: Uuid,
pub user_id: i64,
pub track_id: Uuid,
pub timestamp_ms: u64,
pub text: String,
pub created_at: SystemTime,
pub likes_count: u32,
pub replies: Vec<CommentReply>,
}
/// Réponse à un commentaire
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentReply {
pub id: Uuid,
pub user_id: i64,
pub text: String,
pub created_at: SystemTime,
}
/// Configuration des commentaires temporels
#[derive(Debug, Clone)]
pub struct TimedCommentsConfig {
pub enable_live_comments: bool,
pub max_comments_per_timestamp: usize,
pub comment_display_duration: Duration,
pub enable_comment_notifications: bool,
}
/// Analytics de session de playback
#[derive(Debug, Clone, Default)]
pub struct SessionAnalytics {
pub session_start: Option<SystemTime>,
pub total_listening_time: Duration,
pub tracks_played: u32,
pub tracks_skipped: u32,
pub tracks_completed: u32,
pub average_completion_rate: f32,
pub genres_played: HashMap<String, u32>,
pub artists_played: HashMap<String, u32>,
pub skip_patterns: Vec<SkipPattern>,
pub quality_switches: u32,
}
/// Pattern de skip pour analytics
#[derive(Debug, Clone)]
pub struct SkipPattern {
pub track_id: Uuid,
pub skip_position: Duration,
pub skip_reason: SkipReason,
pub timestamp: SystemTime,
}
/// Raisons de skip
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SkipReason {
UserAction,
BufferingTimeout,
QualityIssue,
TrackEnded,
AutoplayNext,
}
/// Événements de playback
#[derive(Debug, Clone)]
pub enum PlaybackEvent {
/// Lecture commencée
PlaybackStarted {
user_id: i64,
track: TrackInfo,
queue_position: Option<usize>,
},
/// Lecture mise en pause
PlaybackPaused { user_id: i64, position: Duration },
/// Lecture reprise
PlaybackResumed { user_id: i64, position: Duration },
/// Lecture arrêtée
PlaybackStopped { user_id: i64 },
/// Piste suivante
TrackChanged {
user_id: i64,
previous_track: Option<TrackInfo>,
current_track: TrackInfo,
change_reason: TrackChangeReason,
},
/// Position mise à jour
PositionUpdated { user_id: i64, position: Duration },
/// Queue modifiée
QueueUpdated { user_id: i64, queue_size: usize },
/// Commentaire temporel ajouté
TimedCommentAdded {
user_id: i64,
track_id: Uuid,
comment: TimedComment
},
/// Erreur de playback
PlaybackError { user_id: i64, error: String },
}
/// Raisons de changement de piste
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrackChangeReason {
UserSkip,
TrackEnded,
AutoplayNext,
QueueAdvanced,
RepeatTrack,
ShuffleNext,
}
impl Default for PlaybackConfig {
fn default() -> Self {
Self {
max_concurrent_players: 10_000,
default_crossfade_duration: Duration::from_secs(3),
enable_real_time_analytics: true,
cache_preload_tracks: true,
}
}
}
impl Default for PlayerConfig {
fn default() -> Self {
Self {
crossfade_duration: Duration::from_secs(3),
gapless_gap_threshold: Duration::from_millis(100),
max_history_size: 50,
max_queue_size: 1000,
enable_scrobbling: true,
auto_quality_switching: true,
preload_next_track: true,
analytics_enabled: true,
}
}
}
impl PlaybackManager {
/// Crée un nouveau gestionnaire de playback
pub async fn new(
config: PlaybackConfig,
stream_manager: Arc<StreamManager>,
) -> Result<Self, AppError> {
let (event_sender, _) = broadcast::channel(10_000);
Ok(Self {
active_players: Arc::new(RwLock::new(HashMap::new())),
config,
stream_manager,
event_sender,
})
}
/// Obtient ou crée un player pour un utilisateur
pub async fn get_or_create_player(&self, user_id: i64) -> Result<Arc<SoundCloudPlayer>, AppError> {
let mut players = self.active_players.write().await;
if let Some(player) = players.get(&user_id) {
Ok(player.clone())
} else {
// Vérifier la limite de players concurrents
if players.len() >= self.config.max_concurrent_players {
return Err(AppError::TooManyActivePlayers {
max: self.config.max_concurrent_players
});
}
let player = Arc::new(SoundCloudPlayer::new(
user_id,
PlayerConfig::default(),
self.event_sender.clone(),
)?);
players.insert(user_id, player.clone());
tracing::info!("Player créé pour utilisateur: {}", user_id);
Ok(player)
}
}
/// Démarre la lecture d'une piste
pub async fn play_track(
&self,
user_id: i64,
track: TrackInfo,
queue_position: Option<usize>,
) -> Result<(), AppError> {
let player = self.get_or_create_player(user_id).await?;
player.play_track(track, queue_position).await
}
/// Met en pause la lecture
pub async fn pause(&self, user_id: i64) -> Result<(), AppError> {
let players = self.active_players.read().await;
if let Some(player) = players.get(&user_id) {
player.pause().await
} else {
Err(AppError::PlayerNotFound { user_id })
}
}
/// Reprend la lecture
pub async fn resume(&self, user_id: i64) -> Result<(), AppError> {
let players = self.active_players.read().await;
if let Some(player) = players.get(&user_id) {
player.resume().await
} else {
Err(AppError::PlayerNotFound { user_id })
}
}
/// Passe à la piste suivante
pub async fn next_track(&self, user_id: i64) -> Result<(), AppError> {
let players = self.active_players.read().await;
if let Some(player) = players.get(&user_id) {
player.next_track().await
} else {
Err(AppError::PlayerNotFound { user_id })
}
}
/// Revient à la piste précédente
pub async fn previous_track(&self, user_id: i64) -> Result<(), AppError> {
let players = self.active_players.read().await;
if let Some(player) = players.get(&user_id) {
player.previous_track().await
} else {
Err(AppError::PlayerNotFound { user_id })
}
}
/// Abonnement aux événements de playback
pub fn subscribe_events(&self) -> broadcast::Receiver<PlaybackEvent> {
self.event_sender.subscribe()
}
}
impl SoundCloudPlayer {
/// Crée un nouveau player
pub fn new(
user_id: i64,
config: PlayerConfig,
global_event_sender: broadcast::Sender<PlaybackEvent>,
) -> Result<Self, AppError> {
let session_id = Uuid::new_v4();
let (event_sender, mut event_receiver) = mpsc::unbounded_channel();
// État initial du playback
let playback_state = Arc::new(RwLock::new(PlaybackState {
current_track: None,
status: PlaybackStatus::Stopped,
position: Duration::from_secs(0),
volume: 1.0,
playback_speed: 1.0,
repeat_mode: RepeatMode::Off,
shuffle_enabled: false,
crossfade_enabled: config.crossfade_duration > Duration::from_secs(0),
gapless_enabled: true,
last_updated: SystemTime::now(),
}));
// Queue vide
let queue = Arc::new(RwLock::new(PlaybackQueue {
current_index: None,
tracks: Vec::new(),
play_history: VecDeque::new(),
up_next: VecDeque::new(),
shuffle_state: ShuffleState {
enabled: false,
played_indices: Vec::new(),
remaining_indices: Vec::new(),
algorithm: ShuffleAlgorithm::Standard,
},
autoplay_enabled: true,
}));
// Crossfade controller
let crossfade_controller = Arc::new(Mutex::new(CrossfadeController {
enabled: config.crossfade_duration > Duration::from_secs(0),
duration: config.crossfade_duration,
curve: CrossfadeCurve::SCurve,
current_fade: None,
}));
// Manager des commentaires temporels
let timed_comments = Arc::new(RwLock::new(TimedCommentsManager {
comments: HashMap::new(),
config: TimedCommentsConfig {
enable_live_comments: true,
max_comments_per_timestamp: 10,
comment_display_duration: Duration::from_secs(5),
enable_comment_notifications: true,
},
}));
// Analytics de session
let session_analytics = Arc::new(RwLock::new(SessionAnalytics::default()));
// Gestion des événements asynchrones
let global_sender = global_event_sender.clone();
let local_receiver = event_receiver;
tokio::spawn(async move {
// Local event handling logic here would go
});
Ok(Self {
user_id,
session_id,
playback_state,
queue,
config,
crossfade_controller,
timed_comments,
session_analytics,
event_sender: event_sender,
})
}
/// Démarre la lecture d'une piste
pub async fn play_track(
&self,
track: TrackInfo,
queue_position: Option<usize>,
) -> Result<(), AppError> {
tracing::info!("Playing track: {} for user: {}", track.title, self.user_id);
// Mettre à jour les analytics
let mut analytics = self.session_analytics.write().await;
if analytics.session_start.is_none() {
analytics.session_start = Some(SystemTime::now());
}
analytics.tracks_played += 1;
// Mettre à jour l'état de playback
let mut state = self.playback_state.write().await;
state.current_track = Some(track.clone());
state.status = PlaybackStatus::Loading;
state.position = Duration::from_secs(0);
state.last_updated = SystemTime::now();
// Démarrer le stream
drop(state);
self.start_stream(&track).await?;
// Mettre à jour l'état final
let mut state = self.playback_state.write().await;
state.status = PlaybackStatus::Playing;
state.last_updated = SystemTime::now();
// Envoyer l'événement
let event = PlaybackEvent::PlaybackStarted {
user_id: self.user_id,
track: track.clone(),
queue_position,
};
let _ = self.event_sender.send(event);
// Mettre à jour les analytics
self.update_analytics_track_started(&track).await;
Ok(())
}
/// Démarre le streaming de la piste
async fn start_stream(&self, track: &TrackInfo) -> Result<(), AppError> {
// Simulation du streaming - en production, configurer le vrai streaming
tracing::info!("Starting stream for track: {} at URL: {}", track.title, track.stream_url);
// Simuler la latence de démarrage
tokio::time::sleep(Duration::from_millis(100)).await;
Ok(())
}
/// Gère la transition de crossfade
async fn handle_crossfade_transition(&self) -> Result<(), AppError> {
let mut controller = self.crossfade_controller.lock().await;
if controller.enabled {
controller.current_fade = Some(FadeState {
start_time: SystemTime::now(),
duration: controller.duration,
from_volume: 1.0,
to_volume: 0.0,
curve: controller.curve.clone(),
});
}
Ok(())
}
/// Met en pause la lecture
pub async fn pause(&self) -> Result<(), AppError> {
let mut state = self.playback_state.write().await;
if matches!(state.status, PlaybackStatus::Playing) {
state.status = PlaybackStatus::Paused;
state.last_updated = SystemTime::now();
let event = PlaybackEvent::PlaybackPaused {
user_id: self.user_id,
position: state.position,
};
let _ = self.event_sender.send(event);
tracing::info!("Playback paused for user: {}", self.user_id);
Ok(())
} else {
Err(AppError::InvalidPlaybackState {
current: format!("{:?}", state.status),
expected: "Playing".to_string()
})
}
}
/// Reprend la lecture
pub async fn resume(&self) -> Result<(), AppError> {
let mut state = self.playback_state.write().await;
if matches!(state.status, PlaybackStatus::Paused) {
state.status = PlaybackStatus::Playing;
state.last_updated = SystemTime::now();
let event = PlaybackEvent::PlaybackResumed {
user_id: self.user_id,
position: state.position,
};
let _ = self.event_sender.send(event);
tracing::info!("Playback resumed for user: {}", self.user_id);
Ok(())
} else {
Err(AppError::InvalidPlaybackState {
current: format!("{:?}", state.status),
expected: "Paused".to_string()
})
}
}
/// Passe à la piste suivante
pub async fn next_track(&self) -> Result<(), AppError> {
if let Some(next_track) = self.determine_next_track().await? {
self.play_track(next_track, None).await
} else {
// Arrêter la lecture si pas de piste suivante
let mut state = self.playback_state.write().await;
state.status = PlaybackStatus::Stopped;
state.current_track = None;
state.last_updated = SystemTime::now();
let event = PlaybackEvent::PlaybackStopped {
user_id: self.user_id,
};
let _ = self.event_sender.send(event);
Ok(())
}
}
/// Revient à la piste précédente
pub async fn previous_track(&self) -> Result<(), AppError> {
if let Some(previous_track) = self.determine_previous_track().await? {
self.play_track(previous_track, None).await
} else {
// Redémarrer la piste actuelle
let mut state = self.playback_state.write().await;
state.position = Duration::from_secs(0);
state.last_updated = SystemTime::now();
Ok(())
}
}
/// Arrête la lecture
pub async fn stop(&self) -> Result<(), AppError> {
let mut state = self.playback_state.write().await;
state.status = PlaybackStatus::Stopped;
state.current_track = None;
state.position = Duration::from_secs(0);
state.last_updated = SystemTime::now();
let event = PlaybackEvent::PlaybackStopped {
user_id: self.user_id,
};
let _ = self.event_sender.send(event);
tracing::info!("Playback stopped for user: {}", self.user_id);
Ok(())
}
/// Détermine la piste suivante selon la logique de queue
async fn determine_next_track(&self) -> Result<Option<TrackInfo>, AppError> {
let queue = self.queue.read().await;
let state = self.playback_state.read().await;
// Logique simplifiée - en production, implémenter shuffle, repeat, etc.
if let Some(current_index) = queue.current_index {
if current_index + 1 < queue.tracks.len() {
Ok(Some(queue.tracks[current_index + 1].track.clone()))
} else {
match state.repeat_mode {
RepeatMode::All => Ok(queue.tracks.first().map(|t| t.track.clone())),
RepeatMode::Track => {
if let Some(ref current) = state.current_track {
Ok(Some(current.clone()))
} else {
Ok(None)
}
}
_ => Ok(None),
}
}
} else {
Ok(queue.tracks.first().map(|t| t.track.clone()))
}
}
/// Détermine la piste précédente
async fn determine_previous_track(&self) -> Result<Option<TrackInfo>, AppError> {
let queue = self.queue.read().await;
if let Some(current_index) = queue.current_index {
if current_index > 0 {
Ok(Some(queue.tracks[current_index - 1].track.clone()))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// Met à jour les analytics pour début de piste
async fn update_analytics_track_started(&self, track: &TrackInfo) {
let mut analytics = self.session_analytics.write().await;
analytics.tracks_played += 1;
// Compter les genres
for genre in &track.genres {
*analytics.genres_played.entry(genre.clone()).or_insert(0) += 1;
}
// Compter les artistes
*analytics.artists_played.entry(track.artist.clone()).or_insert(0) += 1;
}
/// Ajoute un commentaire temporel
pub async fn add_timed_comment(
&self,
track_id: Uuid,
timestamp_ms: u64,
text: String,
) -> Result<Uuid, AppError> {
let comment = TimedComment {
id: Uuid::new_v4(),
user_id: self.user_id,
track_id,
timestamp_ms,
text,
created_at: SystemTime::now(),
likes_count: 0,
replies: Vec::new(),
};
{
let mut comments_manager = self.timed_comments.write().await;
comments_manager.comments
.entry(timestamp_ms)
.or_insert_with(Vec::new)
.push(comment.clone());
}
let _ = self.event_sender.send(PlaybackEvent::TimedCommentAdded {
user_id: self.user_id,
track_id,
comment: comment.clone(),
});
Ok(comment.id)
}
/// Obtient les commentaires pour un timestamp
pub async fn get_comments_at_time(&self, timestamp_ms: u64) -> Vec<TimedComment> {
let comments_manager = self.timed_comments.read().await;
comments_manager.comments.get(&timestamp_ms).cloned().unwrap_or_default()
}
}

View file

@ -1,710 +0,0 @@
/// Module des features sociales SoundCloud-like
///
/// Features :
/// - Follow/Followers système
/// - Likes avec notifications
/// - Reposts avec messages
/// - Partage avec analytics
/// - Système de commentaires
/// - Feed social personnalisé
use std::sync::Arc;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, SystemTime, Instant};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use tokio::sync::{RwLock, broadcast, mpsc};
use parking_lot::Mutex;
// Note: Use tracing::info! macro directly instead of importing
use crate::error::AppError;
/// Gestionnaire principal du système social
#[derive(Debug)]
pub struct SocialManager {
/// Relations follows/followers
follow_graph: Arc<RwLock<FollowGraph>>,
/// Likes par track
track_likes: Arc<RwLock<HashMap<Uuid, LikeData>>>,
/// Reposts par track
track_reposts: Arc<RwLock<HashMap<Uuid, RepostData>>>,
/// Commentaires par track
track_comments: Arc<RwLock<HashMap<Uuid, CommentData>>>,
/// Configuration
config: SocialConfig,
/// Événements sociaux
event_sender: broadcast::Sender<SocialEvent>,
/// Cache des feeds
feed_cache: Arc<RwLock<HashMap<i64, UserFeed>>>,
}
/// Graphe des relations sociales
#[derive(Debug, Clone, Default)]
pub struct FollowGraph {
/// user_id -> Set des utilisateurs suivis
following: HashMap<i64, HashSet<i64>>,
/// user_id -> Set des followers
followers: HashMap<i64, HashSet<i64>>,
/// Statistiques par utilisateur
user_stats: HashMap<i64, UserSocialStats>,
}
/// Données de likes pour une track
#[derive(Debug, Clone, Default)]
pub struct LikeData {
/// Total de likes
pub total_count: u64,
/// Utilisateurs qui ont liké (pour éviter doublons)
pub liked_by: HashSet<i64>,
/// Timeline des likes pour analytics
pub like_timeline: Vec<LikeEntry>,
}
/// Entry de like individuel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LikeEntry {
pub user_id: i64,
pub timestamp: SystemTime,
pub source: LikeSource,
}
/// Source du like
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LikeSource {
Player,
TrackPage,
Playlist,
Feed,
Search,
Embed,
}
/// Données de reposts pour une track
#[derive(Debug, Clone, Default)]
pub struct RepostData {
/// Total de reposts
pub total_count: u64,
/// Reposts individuels
pub reposts: Vec<RepostEntry>,
}
/// Entry de repost individuel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepostEntry {
pub id: Uuid,
pub user_id: i64,
pub track_id: Uuid,
pub message: Option<String>,
pub timestamp: SystemTime,
pub visibility: RepostVisibility,
pub likes_count: u32,
pub comments_count: u32,
}
/// Visibilité du repost
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RepostVisibility {
Public,
Followers,
Private,
}
/// Données de commentaires pour une track
#[derive(Debug, Clone, Default)]
pub struct CommentData {
/// Total de commentaires
pub total_count: u64,
/// Commentaires par ordre chronologique
pub comments: Vec<CommentEntry>,
/// Index par timestamp pour commentaires temporels
pub timed_comments: HashMap<u64, Vec<Uuid>>, // timestamp_ms -> comment_ids
}
/// Entry de commentaire individuel
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommentEntry {
pub id: Uuid,
pub user_id: i64,
pub track_id: Uuid,
pub parent_id: Option<Uuid>, // Pour les réponses
pub content: String,
pub timestamp_ms: Option<u64>, // Pour commentaires temporels sur waveform
pub created_at: SystemTime,
pub likes_count: u32,
pub replies_count: u32,
pub edited: bool,
pub edited_at: Option<SystemTime>,
}
/// Statistiques sociales d'un utilisateur
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UserSocialStats {
pub followers_count: u64,
pub following_count: u64,
pub tracks_count: u64,
pub likes_count: u64,
pub reposts_count: u64,
pub comments_count: u64,
pub total_plays: u64,
pub total_likes_received: u64,
pub total_reposts_received: u64,
pub total_comments_received: u64,
}
/// Feed social personnalisé d'un utilisateur
#[derive(Debug, Clone)]
pub struct UserFeed {
pub user_id: i64,
pub items: Vec<FeedItem>,
pub last_updated: SystemTime,
pub has_more: bool,
pub next_cursor: Option<String>,
}
/// Item dans le feed social
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FeedItem {
pub id: Uuid,
pub item_type: FeedItemType,
pub created_at: SystemTime,
pub relevance_score: f32, // 0.0 - 1.0 pour algorithme
}
/// Types d'items dans le feed
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FeedItemType {
/// Track uploadée par un utilisateur suivi
TrackUploaded {
user_id: i64,
track_id: Uuid,
track_title: String,
track_artist: String,
},
/// Track repostée par un utilisateur suivi
TrackReposted {
user_id: i64,
track_id: Uuid,
repost: RepostEntry,
},
/// Playlist créée ou mise à jour
PlaylistUpdated {
user_id: i64,
playlist_id: Uuid,
playlist_name: String,
tracks_added: u32,
},
/// Utilisateur a aimé une track
TrackLiked {
user_id: i64,
track_id: Uuid,
track_title: String,
},
/// Nouvel utilisateur suivi a rejoint
UserJoined {
user_id: i64,
username: String,
followed_by: Vec<i64>, // Utilisateurs en commun
},
/// Recommandation algorithmique
RecommendedTrack {
track_id: Uuid,
reason: RecommendationReason,
confidence: f32,
},
}
/// Raisons de recommandation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecommendationReason {
SimilarToLiked,
PopularInGenre,
FriendsAlsoLike,
TrendingNow,
BasedOnHistory,
}
/// Configuration du système social
#[derive(Debug, Clone)]
pub struct SocialConfig {
pub max_following_per_user: usize,
pub max_feed_items: usize,
pub feed_cache_duration: Duration,
pub enable_repost_notifications: bool,
pub enable_like_notifications: bool,
pub enable_comment_notifications: bool,
pub max_comment_length: usize,
pub enable_timed_comments: bool,
pub rate_limit_follows_per_hour: u32,
pub rate_limit_likes_per_minute: u32,
pub rate_limit_comments_per_minute: u32,
}
/// Événements sociaux
#[derive(Debug, Clone)]
pub enum SocialEvent {
/// Nouvel abonnement
UserFollowed {
follower_id: i64,
followed_id: i64,
timestamp: SystemTime,
},
/// Désabonnement
UserUnfollowed {
follower_id: i64,
unfollowed_id: i64,
timestamp: SystemTime,
},
/// Track likée
TrackLiked {
user_id: i64,
track_id: Uuid,
source: LikeSource,
timestamp: SystemTime,
},
/// Track dislikée
TrackUnliked {
user_id: i64,
track_id: Uuid,
timestamp: SystemTime,
},
/// Track repostée
TrackReposted {
user_id: i64,
repost: RepostEntry,
timestamp: SystemTime,
},
/// Commentaire ajouté
CommentAdded {
comment: CommentEntry,
timestamp: SystemTime,
},
/// Commentaire modifié
CommentEdited {
comment_id: Uuid,
new_content: String,
timestamp: SystemTime,
},
/// Commentaire supprimé
CommentDeleted {
comment_id: Uuid,
user_id: i64,
track_id: Uuid,
timestamp: SystemTime,
},
}
impl Default for SocialConfig {
fn default() -> Self {
Self {
max_following_per_user: 5000,
max_feed_items: 100,
feed_cache_duration: Duration::from_secs(300), // 5 minutes
enable_repost_notifications: true,
enable_like_notifications: true,
enable_comment_notifications: true,
max_comment_length: 1000,
enable_timed_comments: true,
rate_limit_follows_per_hour: 200,
rate_limit_likes_per_minute: 60,
rate_limit_comments_per_minute: 10,
}
}
}
impl SocialManager {
/// Crée un nouveau gestionnaire social
pub fn new(config: SocialConfig) -> Self {
let (event_sender, _) = broadcast::channel(10_000);
Self {
follow_graph: Arc::new(RwLock::new(FollowGraph::default())),
track_likes: Arc::new(RwLock::new(HashMap::new())),
track_reposts: Arc::new(RwLock::new(HashMap::new())),
track_comments: Arc::new(RwLock::new(HashMap::new())),
feed_cache: Arc::new(RwLock::new(HashMap::new())),
config,
event_sender,
}
}
/// Suivre un utilisateur
pub async fn follow_user(&self, follower_id: i64, followed_id: i64) -> Result<(), AppError> {
if follower_id == followed_id {
return Err(AppError::ValidationError("Cannot follow yourself".to_string()));
}
let mut graph = self.follow_graph.write().await;
// Vérifier la limite de following
let following_count = graph.following.get(&follower_id)
.map(|s| s.len())
.unwrap_or(0);
if following_count >= self.config.max_following_per_user {
return Err(AppError::ValidationError(format!(
"Max following limit reached: {}",
self.config.max_following_per_user
)));
}
// Ajouter la relation
let following_set = graph.following.entry(follower_id).or_insert_with(HashSet::new);
let was_new = following_set.insert(followed_id);
if was_new {
// Ajouter aux followers
let followers_set = graph.followers.entry(followed_id).or_insert_with(HashSet::new);
followers_set.insert(follower_id);
// Mettre à jour les stats
self.update_user_stats(&mut graph, follower_id, |stats| {
stats.following_count += 1;
});
self.update_user_stats(&mut graph, followed_id, |stats| {
stats.followers_count += 1;
});
// Invalider le cache du feed
self.invalidate_feed_cache(follower_id).await;
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::UserFollowed {
follower_id,
followed_id,
timestamp: SystemTime::now(),
});
tracing::info!("User {} now follows user {}", follower_id, followed_id);
}
Ok(())
}
/// Ne plus suivre un utilisateur
pub async fn unfollow_user(&self, follower_id: i64, unfollowed_id: i64) -> Result<(), AppError> {
let mut graph = self.follow_graph.write().await;
// Retirer la relation
let was_following = if let Some(following_set) = graph.following.get_mut(&follower_id) {
following_set.remove(&unfollowed_id)
} else {
false
};
if was_following {
// Retirer des followers
if let Some(followers_set) = graph.followers.get_mut(&unfollowed_id) {
followers_set.remove(&follower_id);
}
// Mettre à jour les stats
self.update_user_stats(&mut graph, follower_id, |stats| {
if stats.following_count > 0 {
stats.following_count -= 1;
}
});
self.update_user_stats(&mut graph, unfollowed_id, |stats| {
if stats.followers_count > 0 {
stats.followers_count -= 1;
}
});
// Invalider le cache du feed
self.invalidate_feed_cache(follower_id).await;
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::UserUnfollowed {
follower_id,
unfollowed_id: unfollowed_id,
timestamp: SystemTime::now(),
});
tracing::info!("User {} unfollowed user {}", follower_id, unfollowed_id);
}
Ok(())
}
/// Aimer une track
pub async fn like_track(
&self,
user_id: i64,
track_id: Uuid,
source: LikeSource
) -> Result<(), AppError> {
let mut likes = self.track_likes.write().await;
let like_data = likes.entry(track_id).or_insert_with(LikeData::default);
// Vérifier si déjà liké
if like_data.liked_by.contains(&user_id) {
return Ok(()); // Déjà liké
}
// Ajouter le like
like_data.liked_by.insert(user_id);
like_data.total_count += 1;
like_data.like_timeline.push(LikeEntry {
user_id,
timestamp: SystemTime::now(),
source: source.clone(),
});
// Mettre à jour les stats utilisateur
{
let mut graph = self.follow_graph.write().await;
self.update_user_stats(&mut graph, user_id, |stats| {
stats.likes_count += 1;
});
}
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::TrackLiked {
user_id,
track_id,
source,
timestamp: SystemTime::now(),
});
tracing::debug!("User {} liked track {}", user_id, track_id);
Ok(())
}
/// Ne plus aimer une track
pub async fn unlike_track(&self, user_id: i64, track_id: Uuid) -> Result<(), AppError> {
let mut likes = self.track_likes.write().await;
if let Some(like_data) = likes.get_mut(&track_id) {
let was_liked = like_data.liked_by.remove(&user_id);
if was_liked && like_data.total_count > 0 {
like_data.total_count -= 1;
// Mettre à jour les stats utilisateur
{
let mut graph = self.follow_graph.write().await;
self.update_user_stats(&mut graph, user_id, |stats| {
if stats.likes_count > 0 {
stats.likes_count -= 1;
}
});
}
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::TrackUnliked {
user_id,
track_id,
timestamp: SystemTime::now(),
});
tracing::debug!("User {} unliked track {}", user_id, track_id);
}
}
Ok(())
}
/// Reposter une track
pub async fn repost_track(
&self,
user_id: i64,
track_id: Uuid,
message: Option<String>,
visibility: RepostVisibility,
) -> Result<Uuid, AppError> {
let repost_id = Uuid::new_v4();
let repost = RepostEntry {
id: repost_id,
user_id,
track_id,
message,
timestamp: SystemTime::now(),
visibility,
likes_count: 0,
comments_count: 0,
};
// Ajouter le repost
{
let mut reposts = self.track_reposts.write().await;
let repost_data = reposts.entry(track_id).or_insert_with(RepostData::default);
repost_data.reposts.push(repost.clone());
repost_data.total_count += 1;
}
// Mettre à jour les stats utilisateur
{
let mut graph = self.follow_graph.write().await;
self.update_user_stats(&mut graph, user_id, |stats| {
stats.reposts_count += 1;
});
}
// Invalider le cache des feeds des followers
self.invalidate_followers_feed_cache(user_id).await;
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::TrackReposted {
user_id,
repost: repost.clone(),
timestamp: SystemTime::now(),
});
tracing::info!("User {} reposted track {}", user_id, track_id);
Ok(repost_id)
}
/// Ajouter un commentaire
pub async fn add_comment(
&self,
user_id: i64,
track_id: Uuid,
content: String,
parent_id: Option<Uuid>,
timestamp_ms: Option<u64>,
) -> Result<Uuid, AppError> {
// Valider le contenu
if content.len() > self.config.max_comment_length {
return Err(AppError::ValidationError(format!(
"Comment too long: {} chars, max: {}",
content.len(),
self.config.max_comment_length
)));
}
let comment_id = Uuid::new_v4();
let comment = CommentEntry {
id: comment_id,
user_id,
track_id,
parent_id,
content,
timestamp_ms,
created_at: SystemTime::now(),
likes_count: 0,
replies_count: 0,
edited: false,
edited_at: None,
};
// Ajouter le commentaire
{
let mut comments = self.track_comments.write().await;
let comment_data = comments.entry(track_id).or_insert_with(CommentData::default);
comment_data.comments.push(comment.clone());
comment_data.total_count += 1;
// Indexer les commentaires temporels
if let Some(timestamp) = timestamp_ms {
comment_data.timed_comments
.entry(timestamp)
.or_insert_with(Vec::new)
.push(comment_id);
}
// Mettre à jour le count des réponses si c'est une réponse
if let Some(parent_id) = parent_id {
for comment in &mut comment_data.comments {
if comment.id == parent_id {
comment.replies_count += 1;
break;
}
}
}
}
// Mettre à jour les stats utilisateur
{
let mut graph = self.follow_graph.write().await;
self.update_user_stats(&mut graph, user_id, |stats| {
stats.comments_count += 1;
});
}
// Émettre l'événement
let _ = self.event_sender.send(SocialEvent::CommentAdded {
comment: comment.clone(),
timestamp: SystemTime::now(),
});
tracing::debug!("User {} commented on track {}", user_id, track_id);
Ok(comment_id)
}
/// Obtenir les statistiques sociales d'un utilisateur
pub async fn get_user_stats(&self, user_id: i64) -> UserSocialStats {
let graph = self.follow_graph.read().await;
graph.user_stats.get(&user_id).cloned().unwrap_or_default()
}
/// Obtenir la liste des utilisateurs suivis
pub async fn get_following(&self, user_id: i64) -> Vec<i64> {
let graph = self.follow_graph.read().await;
graph.following.get(&user_id)
.map(|set| set.iter().copied().collect())
.unwrap_or_default()
}
/// Obtenir la liste des followers
pub async fn get_followers(&self, user_id: i64) -> Vec<i64> {
let graph = self.follow_graph.read().await;
graph.followers.get(&user_id)
.map(|set| set.iter().copied().collect())
.unwrap_or_default()
}
/// Obtenir les likes d'une track
pub async fn get_track_likes(&self, track_id: Uuid) -> LikeData {
let likes = self.track_likes.read().await;
likes.get(&track_id).cloned().unwrap_or_default()
}
/// Obtenir les commentaires d'une track
pub async fn get_track_comments(&self, track_id: Uuid, limit: Option<usize>) -> Vec<CommentEntry> {
let comments = self.track_comments.read().await;
if let Some(comment_data) = comments.get(&track_id) {
let mut result = comment_data.comments.clone();
// Trier par date de création (plus récents en premier)
result.sort_by(|a, b| b.created_at.cmp(&a.created_at));
if let Some(limit) = limit {
result.truncate(limit);
}
result
} else {
Vec::new()
}
}
/// Abonnement aux événements sociaux
pub fn subscribe_events(&self) -> broadcast::Receiver<SocialEvent> {
self.event_sender.subscribe()
}
/// Met à jour les stats d'un utilisateur
fn update_user_stats<F>(&self, graph: &mut FollowGraph, user_id: i64, updater: F)
where
F: FnOnce(&mut UserSocialStats),
{
let stats = graph.user_stats.entry(user_id).or_insert_with(UserSocialStats::default);
updater(stats);
}
/// Invalide le cache du feed d'un utilisateur
async fn invalidate_feed_cache(&self, user_id: i64) {
let mut cache = self.feed_cache.write().await;
cache.remove(&user_id);
}
/// Invalide le cache des feeds des followers d'un utilisateur
async fn invalidate_followers_feed_cache(&self, user_id: i64) {
let followers = self.get_followers(user_id).await;
let mut cache = self.feed_cache.write().await;
for follower_id in followers {
cache.remove(&follower_id);
}
}
}

View file

@ -1,663 +0,0 @@
/// Module d'upload et management de tracks SoundCloud-like
///
/// Features :
/// - Upload multi-format (MP3, WAV, FLAC, AIFF, OGG)
/// - Extraction automatique de métadonnées
/// - Génération de waveform avec peaks
/// - Traitement asynchrone
/// - Validation et sécurité
use std::sync::Arc;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::time::{Duration, SystemTime};
use serde::{Serialize, Deserialize};
use uuid::Uuid;
use tokio::fs;
use tokio::sync::{mpsc, RwLock};
// Note: Use tracing::info! macro directly instead of importing
use crate::error::AppError;
use crate::soundcloud::waveform::{WaveformGenerator, WaveformData};
/// Gestionnaire principal des uploads
#[derive(Debug)]
pub struct UploadManager {
/// Processeurs d'upload actifs
active_uploads: Arc<RwLock<HashMap<Uuid, UploadSession>>>,
/// Configuration
config: UploadConfig,
/// Générateur de waveform
waveform_generator: Arc<WaveformGenerator>,
/// Extracteur de métadonnées
metadata_extractor: Arc<MetadataExtractor>,
/// Stockage des fichiers
storage: Arc<dyn FileStorage + Send + Sync>,
/// Événements d'upload
event_sender: mpsc::UnboundedSender<UploadEvent>,
}
/// Session d'upload d'un fichier
#[derive(Debug, Clone)]
pub struct UploadSession {
pub id: Uuid,
pub user_id: i64,
pub filename: String,
pub file_size: u64,
pub content_type: String,
pub status: UploadStatus,
pub progress: UploadProgress,
pub metadata: Option<TrackMetadata>,
pub waveform: Option<WaveformData>,
pub created_at: SystemTime,
pub updated_at: SystemTime,
}
/// Status de l'upload
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UploadStatus {
/// Upload en cours
Uploading { bytes_received: u64 },
/// Upload terminé, processing en cours
Processing { stage: ProcessingStage },
/// Upload et processing terminés avec succès
Completed,
/// Erreur pendant l'upload ou processing
Failed { reason: String },
/// Upload annulé par l'utilisateur
Cancelled,
}
/// Étapes de processing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ProcessingStage {
ValidatingFile,
ExtractingMetadata,
GeneratingWaveform,
ConvertingFormats,
UploadingToStorage,
CreatingThumbnails,
IndexingForSearch,
}
/// Progress de l'upload avec détails
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UploadProgress {
pub total_bytes: u64,
pub uploaded_bytes: u64,
pub processing_progress: f32, // 0.0 - 1.0
pub current_stage: Option<ProcessingStage>,
pub estimated_time_remaining: Option<Duration>,
pub upload_speed_bps: u32,
}
/// Métadonnées extraites du fichier audio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackMetadata {
// Métadonnées de base
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub genre: Option<String>,
pub year: Option<u32>,
pub track_number: Option<u32>,
pub duration: Option<Duration>,
// Métadonnées techniques
pub sample_rate: u32,
pub bitrate: u32,
pub channels: u8,
pub bit_depth: Option<u8>,
pub codec: String,
pub file_format: String,
// Métadonnées avancées
pub bpm: Option<f32>,
pub key: Option<String>,
pub loudness_lufs: Option<f32>,
pub peak_db: Option<f32>,
pub dynamic_range: Option<f32>,
// Identifiants
pub isrc: Option<String>,
pub mbid: Option<String>, // MusicBrainz ID
// Artwork
pub has_artwork: bool,
pub artwork_size: Option<(u32, u32)>,
// Métadonnées personnalisées
pub custom_tags: HashMap<String, String>,
}
/// Configuration de l'upload
#[derive(Debug, Clone)]
pub struct UploadConfig {
pub max_file_size: u64, // bytes
pub allowed_formats: Vec<String>,
pub upload_directory: PathBuf,
pub temp_directory: PathBuf,
pub enable_waveform_generation: bool,
pub enable_format_conversion: bool,
pub max_concurrent_uploads: usize,
pub chunk_size: usize,
pub enable_virus_scan: bool,
}
/// Événements d'upload
#[derive(Debug, Clone)]
pub enum UploadEvent {
UploadStarted { session_id: Uuid, user_id: i64, filename: String },
UploadProgress { session_id: Uuid, progress: UploadProgress },
ProcessingStarted { session_id: Uuid, stage: ProcessingStage },
MetadataExtracted { session_id: Uuid, metadata: TrackMetadata },
WaveformGenerated { session_id: Uuid, waveform: WaveformData },
UploadCompleted { session_id: Uuid, track_id: Uuid },
UploadFailed { session_id: Uuid, reason: String },
UploadCancelled { session_id: Uuid },
}
/// Extracteur de métadonnées audio
#[derive(Debug)]
pub struct MetadataExtractor {
config: MetadataExtractorConfig,
}
/// Configuration de l'extracteur
#[derive(Debug, Clone)]
pub struct MetadataExtractorConfig {
pub enable_fingerprinting: bool,
pub enable_bpm_detection: bool,
pub enable_key_detection: bool,
pub enable_loudness_analysis: bool,
pub musicbrainz_lookup: bool,
}
/// Trait pour le stockage de fichiers
pub trait FileStorage: std::fmt::Debug {
async fn store_file(&self, file_path: &Path, metadata: &TrackMetadata) -> Result<StoredFile, AppError>;
async fn get_file(&self, file_id: &str) -> Result<StoredFile, AppError>;
async fn delete_file(&self, file_id: &str) -> Result<(), AppError>;
async fn list_user_files(&self, user_id: i64) -> Result<Vec<StoredFile>, AppError>;
}
/// Fichier stocké
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredFile {
pub id: String,
pub original_filename: String,
pub content_type: String,
pub size: u64,
pub storage_path: String,
pub public_url: Option<String>,
pub cdn_url: Option<String>,
pub checksum: String,
pub created_at: SystemTime,
}
/// Stockage local pour développement
#[derive(Debug)]
pub struct LocalFileStorage {
base_path: PathBuf,
public_url_base: String,
}
impl Default for UploadConfig {
fn default() -> Self {
Self {
max_file_size: 200 * 1024 * 1024, // 200MB
allowed_formats: vec![
"audio/mpeg".to_string(), // MP3
"audio/wav".to_string(), // WAV
"audio/flac".to_string(), // FLAC
"audio/aiff".to_string(), // AIFF
"audio/ogg".to_string(), // OGG
"audio/m4a".to_string(), // M4A
"audio/mp4".to_string(), // MP4 audio
],
upload_directory: PathBuf::from("uploads"),
temp_directory: PathBuf::from("temp"),
enable_waveform_generation: true,
enable_format_conversion: true,
max_concurrent_uploads: 10,
chunk_size: 1024 * 1024, // 1MB chunks
enable_virus_scan: false, // Désactivé par défaut en dev
}
}
}
impl UploadManager {
/// Crée un nouveau gestionnaire d'uploads
pub async fn new(config: UploadConfig) -> Result<Self, AppError> {
let (event_sender, _) = mpsc::unbounded_channel();
// Créer les répertoires si nécessaire
fs::create_dir_all(&config.upload_directory).await?;
fs::create_dir_all(&config.temp_directory).await?;
let storage = Arc::new(LocalFileStorage::new(
config.upload_directory.clone(),
"http://localhost:8080/uploads".to_string(),
));
Ok(Self {
active_uploads: Arc::new(RwLock::new(HashMap::new())),
waveform_generator: Arc::new(WaveformGenerator::new()),
metadata_extractor: Arc::new(MetadataExtractor::new()),
storage,
config,
event_sender,
})
}
/// Démarre une session d'upload
pub async fn start_upload(
&self,
user_id: i64,
filename: String,
file_size: u64,
content_type: String,
) -> Result<Uuid, AppError> {
// Validation de base
self.validate_upload_request(&filename, file_size, &content_type)?;
// Vérifier le nombre d'uploads actifs
let active_count = self.active_uploads.read().await.len();
if active_count >= self.config.max_concurrent_uploads {
return Err(AppError::RateLimitExceeded);
}
let session_id = Uuid::new_v4();
let session = UploadSession {
id: session_id,
user_id,
filename: filename.clone(),
file_size,
content_type,
status: UploadStatus::Uploading { bytes_received: 0 },
progress: UploadProgress {
total_bytes: file_size,
uploaded_bytes: 0,
processing_progress: 0.0,
current_stage: None,
estimated_time_remaining: None,
upload_speed_bps: 0,
},
metadata: None,
waveform: None,
created_at: SystemTime::now(),
updated_at: SystemTime::now(),
};
// Enregistrer la session
self.active_uploads.write().await.insert(session_id, session);
// Émettre l'événement
let _ = self.event_sender.send(UploadEvent::UploadStarted {
session_id,
user_id,
filename,
});
tracing::info!("Session d'upload démarrée: {} pour utilisateur {}", session_id, user_id);
Ok(session_id)
}
/// Reçoit un chunk de données
pub async fn receive_chunk(
&self,
session_id: Uuid,
chunk_data: &[u8],
chunk_offset: u64,
) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
let session = sessions.get_mut(&session_id)
.ok_or_else(|| AppError::UploadSessionNotFound { session_id })?;
// Vérifier le status
match &session.status {
UploadStatus::Uploading { .. } => {},
_ => return Err(AppError::InvalidUploadState {
session_id,
current_state: format!("{:?}", session.status)
}),
}
// Mettre à jour le progress
let new_uploaded = chunk_offset + chunk_data.len() as u64;
session.progress.uploaded_bytes = new_uploaded;
session.updated_at = SystemTime::now();
// Calculer la vitesse d'upload
let elapsed = session.updated_at.duration_since(session.created_at).unwrap_or_default();
if elapsed.as_secs() > 0 {
session.progress.upload_speed_bps = (new_uploaded / elapsed.as_secs()) as u32;
}
// Émettre l'événement de progress
let _ = self.event_sender.send(UploadEvent::UploadProgress {
session_id,
progress: session.progress.clone(),
});
// Si upload terminé, démarrer le processing
if new_uploaded >= session.file_size {
session.status = UploadStatus::Processing {
stage: ProcessingStage::ValidatingFile
};
// Démarrer le processing en arrière-plan
let self_clone = self.clone();
tokio::spawn(async move {
if let Err(e) = self_clone.process_uploaded_file(session_id).await {
tracing::error!("Erreur processing fichier {}: {:?}", session_id, e);
}
});
} else {
session.status = UploadStatus::Uploading {
bytes_received: new_uploaded
};
}
Ok(())
}
/// Traite un fichier uploadé
async fn process_uploaded_file(&self, session_id: Uuid) -> Result<(), AppError> {
// Étape 1: Extraction des métadonnées
self.update_processing_stage(session_id, ProcessingStage::ExtractingMetadata).await?;
let metadata = self.extract_metadata(session_id).await?;
// Étape 2: Génération de waveform
if self.config.enable_waveform_generation {
self.update_processing_stage(session_id, ProcessingStage::GeneratingWaveform).await?;
let waveform = self.generate_waveform(session_id, &metadata).await?;
self.update_session_waveform(session_id, waveform).await?;
}
// Étape 3: Stockage final
self.update_processing_stage(session_id, ProcessingStage::UploadingToStorage).await?;
let stored_file = self.store_file(session_id, &metadata).await?;
// Marquer comme terminé
self.complete_upload(session_id, stored_file.id).await?;
Ok(())
}
/// Valide une demande d'upload
fn validate_upload_request(
&self,
filename: &str,
file_size: u64,
content_type: &str,
) -> Result<(), AppError> {
// Vérifier la taille
if file_size > self.config.max_file_size {
return Err(AppError::ValidationError(format!(
"File too large: {} bytes, max: {} bytes",
file_size,
self.config.max_file_size
)));
}
// Vérifier le format
if !self.config.allowed_formats.contains(&content_type.to_string()) {
return Err(AppError::ValidationError(format!(
"Unsupported format: {}, supported: {:?}",
content_type,
self.config.allowed_formats
)));
}
// Vérifier l'extension
if let Some(extension) = Path::new(filename).extension() {
let ext_str = extension.to_string_lossy().to_lowercase();
let valid_extensions = ["mp3", "wav", "flac", "aiff", "ogg", "m4a", "mp4"];
if !valid_extensions.contains(&ext_str.as_str()) {
return Err(AppError::ValidationError(format!(
"Unsupported extension: {}, supported: {:?}",
ext_str,
valid_extensions
)));
}
}
Ok(())
}
/// Met à jour l'étape de processing
async fn update_processing_stage(
&self,
session_id: Uuid,
stage: ProcessingStage,
) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.status = UploadStatus::Processing { stage: stage.clone() };
session.progress.current_stage = Some(stage.clone());
session.updated_at = SystemTime::now();
let _ = self.event_sender.send(UploadEvent::ProcessingStarted {
session_id,
stage,
});
}
Ok(())
}
/// Extrait les métadonnées d'un fichier
async fn extract_metadata(&self, session_id: Uuid) -> Result<TrackMetadata, AppError> {
// Simulation d'extraction - en production, utiliser des libs comme `lofty` ou `mp3-metadata`
let metadata = TrackMetadata {
title: Some("Uploaded Track".to_string()),
artist: Some("Unknown Artist".to_string()),
album: None,
genre: Some("Electronic".to_string()),
year: Some(2024),
track_number: None,
duration: Some(Duration::from_secs(180)), // 3 minutes
sample_rate: 44100,
bitrate: 320000,
channels: 2,
bit_depth: Some(16),
codec: "MP3".to_string(),
file_format: "MPEG".to_string(),
bpm: Some(128.0),
key: Some("C major".to_string()),
loudness_lufs: Some(-14.0),
peak_db: Some(-1.0),
dynamic_range: Some(8.5),
isrc: None,
mbid: None,
has_artwork: false,
artwork_size: None,
custom_tags: HashMap::new(),
};
// Mettre à jour la session
self.update_session_metadata(session_id, metadata.clone()).await?;
let _ = self.event_sender.send(UploadEvent::MetadataExtracted {
session_id,
metadata: metadata.clone(),
});
Ok(metadata)
}
/// Génère la waveform d'un fichier
async fn generate_waveform(
&self,
session_id: Uuid,
_metadata: &TrackMetadata,
) -> Result<WaveformData, AppError> {
// Utiliser le générateur de waveform
let waveform = self.waveform_generator.generate_from_file("dummy_path").await?;
let _ = self.event_sender.send(UploadEvent::WaveformGenerated {
session_id,
waveform: waveform.clone(),
});
Ok(waveform)
}
/// Met à jour les métadonnées d'une session
async fn update_session_metadata(
&self,
session_id: Uuid,
metadata: TrackMetadata,
) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.metadata = Some(metadata);
session.updated_at = SystemTime::now();
}
Ok(())
}
/// Met à jour la waveform d'une session
async fn update_session_waveform(
&self,
session_id: Uuid,
waveform: WaveformData,
) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.waveform = Some(waveform);
session.updated_at = SystemTime::now();
}
Ok(())
}
/// Stocke le fichier final
async fn store_file(
&self,
session_id: Uuid,
metadata: &TrackMetadata,
) -> Result<StoredFile, AppError> {
// Simulation - en production, uploader vers S3/GCS/etc.
let file_path = self.config.upload_directory.join(format!("{}.mp3", session_id));
self.storage.store_file(&file_path, metadata).await
}
/// Termine un upload avec succès
async fn complete_upload(
&self,
session_id: Uuid,
track_id: String,
) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.status = UploadStatus::Completed;
session.progress.processing_progress = 1.0;
session.updated_at = SystemTime::now();
let _ = self.event_sender.send(UploadEvent::UploadCompleted {
session_id,
track_id: Uuid::parse_str(&track_id).unwrap_or_else(|_| Uuid::new_v4()),
});
}
Ok(())
}
/// Obtient le status d'un upload
pub async fn get_upload_status(&self, session_id: Uuid) -> Option<UploadSession> {
self.active_uploads.read().await.get(&session_id).cloned()
}
/// Annule un upload
pub async fn cancel_upload(&self, session_id: Uuid) -> Result<(), AppError> {
let mut sessions = self.active_uploads.write().await;
if let Some(session) = sessions.get_mut(&session_id) {
session.status = UploadStatus::Cancelled;
session.updated_at = SystemTime::now();
let _ = self.event_sender.send(UploadEvent::UploadCancelled { session_id });
}
Ok(())
}
}
impl Clone for UploadManager {
fn clone(&self) -> Self {
Self {
active_uploads: self.active_uploads.clone(),
config: self.config.clone(),
waveform_generator: self.waveform_generator.clone(),
metadata_extractor: self.metadata_extractor.clone(),
storage: self.storage.clone(),
event_sender: self.event_sender.clone(),
}
}
}
impl MetadataExtractor {
pub fn new() -> Self {
Self {
config: MetadataExtractorConfig {
enable_fingerprinting: true,
enable_bpm_detection: true,
enable_key_detection: true,
enable_loudness_analysis: true,
musicbrainz_lookup: false, // Désactivé par défaut
},
}
}
}
impl LocalFileStorage {
pub fn new(base_path: PathBuf, public_url_base: String) -> Self {
Self {
base_path,
public_url_base,
}
}
}
impl FileStorage for LocalFileStorage {
async fn store_file(&self, file_path: &Path, metadata: &TrackMetadata) -> Result<StoredFile, AppError> {
let file_id = Uuid::new_v4().to_string();
let stored_path = self.base_path.join(&file_id);
// Copier le fichier (simulation)
let file_size = 1024 * 1024; // 1MB simulé
Ok(StoredFile {
id: file_id.clone(),
original_filename: file_path.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
content_type: "audio/mpeg".to_string(),
size: file_size,
storage_path: stored_path.to_string_lossy().to_string(),
public_url: Some(format!("{}/{}", self.public_url_base, file_id)),
cdn_url: None,
checksum: "abc123".to_string(),
created_at: SystemTime::now(),
})
}
async fn get_file(&self, file_id: &str) -> Result<StoredFile, AppError> {
// Simulation de récupération
Err(AppError::NotFound(format!("File not found: {}", file_id)))
}
async fn delete_file(&self, _file_id: &str) -> Result<(), AppError> {
// Simulation de suppression
Ok(())
}
async fn list_user_files(&self, _user_id: i64) -> Result<Vec<StoredFile>, AppError> {
// Simulation de listing
Ok(Vec::new())
}
}

View file

@ -1,603 +0,0 @@
/// Module de génération de waveform pour visualisation audio
///
/// Features :
/// - Génération de waveform optimisée
/// - Format peaks.js compatible
/// - Analyse spectrale avancée
/// - Support multi-résolution
/// - Export JSON/binaire
use std::sync::Arc;
use std::path::Path;
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
use tokio::sync::RwLock;
// Note: Use tracing::info! macro directly instead of importing
use crate::error::AppError;
/// Générateur de waveform principal
#[derive(Debug)]
pub struct WaveformGenerator {
config: WaveformConfig,
cache: Arc<RwLock<HashMap<String, WaveformData>>>,
}
/// Configuration du générateur
#[derive(Debug, Clone)]
pub struct WaveformConfig {
pub samples_per_pixel: u32,
pub bit_depth: u8,
pub amplitude_scale: f32,
pub enable_spectral_analysis: bool,
pub peak_detection_threshold: f32,
pub cache_enabled: bool,
pub output_formats: Vec<WaveformFormat>,
}
/// Formats de sortie supportés
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WaveformFormat {
/// Format JSON compatible peaks.js
PeaksJS,
/// Format binaire compact
Binary,
/// Format SVG vectoriel
SVG,
/// Format PNG image
PNG { width: u32, height: u32 },
}
/// Données de waveform générées
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WaveformData {
/// Métadonnées de base
pub duration: f64,
pub sample_rate: u32,
pub channels: u8,
pub length: usize,
/// Données de pics (min/max par pixel)
pub peaks: Vec<WaveformPeak>,
/// Données spectrales (optionnel)
pub spectral_data: Option<SpectralData>,
/// Statistiques audio
pub audio_stats: AudioStatistics,
/// Format d'export
pub format: WaveformFormat,
/// Version du générateur
pub version: String,
}
/// Pic de waveform (min/max pour un pixel)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WaveformPeak {
pub min: f32,
pub max: f32,
pub rms: f32, // Root Mean Square pour volume perçu
pub peak: f32, // Pic absolu
}
/// Données spectrales pour analyse fréquentielle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectralData {
/// Spectrogramme par bandes de fréquence
pub spectrogram: Vec<SpectralFrame>,
/// Fréquences centrales des bandes
pub frequency_bins: Vec<f32>,
/// Résolution temporelle (ms par frame)
pub time_resolution_ms: f32,
}
/// Frame spectrale à un instant donné
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpectralFrame {
/// Magnitude par bande de fréquence
pub magnitudes: Vec<f32>,
/// Timestamp en millisecondes
pub timestamp_ms: f32,
}
/// Statistiques audio globales
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioStatistics {
/// Niveau RMS moyen
pub average_rms: f32,
/// Pic maximum absolu
pub peak_amplitude: f32,
/// Dynamic range en dB
pub dynamic_range_db: f32,
/// Loudness intégré LUFS
pub integrated_loudness: f32,
/// Facteur de crête
pub crest_factor: f32,
/// Détection de silence (pourcentage)
pub silence_percentage: f32,
/// BPM détecté (optionnel)
pub estimated_bpm: Option<f32>,
/// Clé détectée (optionnel)
pub estimated_key: Option<String>,
}
/// Analyseur de pics et événements audio
#[derive(Debug)]
pub struct PeakAnalyzer {
config: PeakAnalyzerConfig,
detection_state: PeakDetectionState,
}
/// Configuration de l'analyseur de pics
#[derive(Debug, Clone)]
pub struct PeakAnalyzerConfig {
pub threshold_db: f32,
pub min_peak_distance_ms: f32,
pub attack_time_ms: f32,
pub release_time_ms: f32,
}
/// État de détection des pics
#[derive(Debug)]
struct PeakDetectionState {
last_peak_time: f32,
envelope_follower: f32,
peak_candidates: Vec<PeakCandidate>,
}
/// Candidat de pic détecté
#[derive(Debug, Clone)]
struct PeakCandidate {
timestamp_ms: f32,
amplitude: f32,
duration_ms: f32,
}
impl Default for WaveformConfig {
fn default() -> Self {
Self {
samples_per_pixel: 1024,
bit_depth: 16,
amplitude_scale: 1.0,
enable_spectral_analysis: true,
peak_detection_threshold: -20.0, // -20dB
cache_enabled: true,
output_formats: vec![
WaveformFormat::PeaksJS,
WaveformFormat::Binary,
],
}
}
}
impl WaveformGenerator {
/// Crée un nouveau générateur de waveform
pub fn new() -> Self {
Self::with_config(WaveformConfig::default())
}
/// Crée un générateur avec configuration personnalisée
pub fn with_config(config: WaveformConfig) -> Self {
Self {
config,
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Génère une waveform depuis un fichier audio
pub async fn generate_from_file<P: AsRef<Path>>(&self, file_path: P) -> Result<WaveformData, AppError> {
let path_str = file_path.as_ref().to_string_lossy().to_string();
// Vérifier le cache d'abord
if self.config.cache_enabled {
if let Some(cached) = self.get_from_cache(&path_str).await {
tracing::debug!("Waveform trouvée en cache pour: {}", path_str);
return Ok(cached);
}
}
tracing::info!("Génération de waveform pour: {}", path_str);
// Simulation de lecture du fichier audio
let audio_data = self.load_audio_file(&path_str).await?;
// Générer la waveform
let waveform = self.generate_waveform_data(&audio_data).await?;
// Mettre en cache
if self.config.cache_enabled {
self.store_in_cache(&path_str, &waveform).await;
}
Ok(waveform)
}
/// Génère une waveform depuis des échantillons audio bruts
pub async fn generate_from_samples(
&self,
samples: &[f32],
sample_rate: u32,
channels: u8,
) -> Result<WaveformData, AppError> {
let audio_data = AudioData {
samples: samples.to_vec(),
sample_rate,
channels,
duration: samples.len() as f64 / (sample_rate as f64 * channels as f64),
};
self.generate_waveform_data(&audio_data).await
}
/// Charge un fichier audio (simulation)
async fn load_audio_file(&self, _file_path: &str) -> Result<AudioData, AppError> {
// Simulation de chargement - en production, utiliser symphonia ou similar
let sample_rate = 44100;
let channels = 2;
let duration_seconds = 180.0; // 3 minutes
let total_samples = (sample_rate as f64 * channels as f64 * duration_seconds) as usize;
// Générer des échantillons de test (sinusoïde modulée)
let mut samples = Vec::with_capacity(total_samples);
for i in 0..total_samples {
let t = i as f64 / (sample_rate as f64 * channels as f64);
let frequency = 440.0 + 100.0 * (t * 0.1).sin(); // Fréquence modulée
let amplitude = 0.5 * (1.0 + (t * 0.05).sin()); // Amplitude modulée
let sample = (amplitude * (2.0 * std::f64::consts::PI * frequency * t).sin()) as f32;
samples.push(sample);
}
Ok(AudioData {
samples,
sample_rate,
channels,
duration: duration_seconds,
})
}
/// Génère les données de waveform
async fn generate_waveform_data(&self, audio_data: &AudioData) -> Result<WaveformData, AppError> {
let start_time = std::time::Instant::now();
// Calculer le nombre de pixels nécessaires
let total_samples = audio_data.samples.len();
let samples_per_frame = self.config.samples_per_pixel as usize * audio_data.channels as usize;
let pixel_count = (total_samples + samples_per_frame - 1) / samples_per_frame;
// Générer les pics pour chaque pixel
let mut peaks = Vec::with_capacity(pixel_count);
for pixel_index in 0..pixel_count {
let start_sample = pixel_index * samples_per_frame;
let end_sample = (start_sample + samples_per_frame).min(total_samples);
if start_sample < total_samples {
let pixel_samples = &audio_data.samples[start_sample..end_sample];
let peak = self.calculate_pixel_peak(pixel_samples);
peaks.push(peak);
}
}
// Calculer les statistiques audio
let audio_stats = self.calculate_audio_statistics(&audio_data.samples, audio_data.sample_rate);
// Générer les données spectrales si activé
let spectral_data = if self.config.enable_spectral_analysis {
Some(self.generate_spectral_data(audio_data).await?)
} else {
None
};
let generation_time = start_time.elapsed();
tracing::info!("Waveform générée en {:?}: {} pixels, {} échantillons",
generation_time, peaks.len(), total_samples);
Ok(WaveformData {
duration: audio_data.duration,
sample_rate: audio_data.sample_rate,
channels: audio_data.channels,
length: peaks.len(),
peaks,
spectral_data,
audio_stats,
format: WaveformFormat::PeaksJS,
version: "1.0.0".to_string(),
})
}
/// Calcule le pic pour un groupe d'échantillons (pixel)
fn calculate_pixel_peak(&self, samples: &[f32]) -> WaveformPeak {
if samples.is_empty() {
return WaveformPeak {
min: 0.0,
max: 0.0,
rms: 0.0,
peak: 0.0,
};
}
let mut min_val = f32::MAX;
let mut max_val = f32::MIN;
let mut sum_squares = 0.0;
let mut peak_val: f32 = 0.0;
for &sample in samples {
min_val = min_val.min(sample);
max_val = max_val.max(sample);
sum_squares += sample * sample;
peak_val = peak_val.max(sample.abs());
}
let rms = (sum_squares / samples.len() as f32).sqrt();
WaveformPeak {
min: min_val * self.config.amplitude_scale,
max: max_val * self.config.amplitude_scale,
rms: rms * self.config.amplitude_scale,
peak: peak_val * self.config.amplitude_scale,
}
}
/// Calcule les statistiques audio globales
fn calculate_audio_statistics(&self, samples: &[f32], sample_rate: u32) -> AudioStatistics {
if samples.is_empty() {
return AudioStatistics::default();
}
// Calculs de base
let mut sum_squares = 0.0;
let mut peak_amplitude: f32 = 0.0;
let mut silence_samples = 0;
let silence_threshold = 0.001; // -60dB environ
for &sample in samples {
let abs_sample = sample.abs();
sum_squares += sample * sample;
peak_amplitude = peak_amplitude.max(abs_sample);
if abs_sample < silence_threshold {
silence_samples += 1;
}
}
let average_rms = (sum_squares / samples.len() as f32).sqrt();
let silence_percentage = (silence_samples as f32 / samples.len() as f32) * 100.0;
// Dynamic range (approximation)
let dynamic_range_db = if average_rms > 0.0 && peak_amplitude > 0.0 {
20.0 * (peak_amplitude / average_rms).log10()
} else {
0.0
};
// Crest factor
let crest_factor = if average_rms > 0.0 {
peak_amplitude / average_rms
} else {
0.0
};
// Loudness intégré (approximation simple)
let integrated_loudness = if average_rms > 0.0 {
-0.691 + 10.0 * average_rms.log10()
} else {
-70.0 // Silence
};
// BPM et clé (simulation - en production, utiliser des algos dédiés)
let estimated_bpm = self.estimate_bpm(samples, sample_rate);
let estimated_key = self.estimate_key(samples, sample_rate);
AudioStatistics {
average_rms,
peak_amplitude,
dynamic_range_db,
integrated_loudness,
crest_factor,
silence_percentage,
estimated_bpm,
estimated_key,
}
}
/// Estime le BPM (simulation)
fn estimate_bpm(&self, _samples: &[f32], _sample_rate: u32) -> Option<f32> {
// Simulation - en production, utiliser des algorithmes de détection de tempo
Some(128.0)
}
/// Estime la clé musicale (simulation)
fn estimate_key(&self, _samples: &[f32], _sample_rate: u32) -> Option<String> {
// Simulation - en production, utiliser des algorithmes de détection de tonalité
Some("C major".to_string())
}
/// Génère les données spectrales
async fn generate_spectral_data(&self, audio_data: &AudioData) -> Result<SpectralData, AppError> {
let fft_size = 2048;
let hop_size = fft_size / 4; // 75% overlap
let window_count = (audio_data.samples.len() + hop_size - 1) / hop_size;
let mut spectrogram = Vec::with_capacity(window_count);
let frequency_bins = self.generate_frequency_bins(fft_size, audio_data.sample_rate);
let time_resolution_ms = (hop_size as f32 / audio_data.sample_rate as f32) * 1000.0;
// Simulation de FFT - en production, utiliser rustfft
for window_index in 0..window_count {
let start_sample = window_index * hop_size;
let end_sample = (start_sample + fft_size).min(audio_data.samples.len());
if start_sample < audio_data.samples.len() {
let window_samples = &audio_data.samples[start_sample..end_sample];
let magnitudes = self.calculate_fft_magnitudes(window_samples, fft_size);
let frame = SpectralFrame {
magnitudes,
timestamp_ms: window_index as f32 * time_resolution_ms,
};
spectrogram.push(frame);
}
}
Ok(SpectralData {
spectrogram,
frequency_bins,
time_resolution_ms,
})
}
/// Génère les bins de fréquence
fn generate_frequency_bins(&self, fft_size: usize, sample_rate: u32) -> Vec<f32> {
let bin_count = fft_size / 2 + 1;
(0..bin_count)
.map(|i| i as f32 * sample_rate as f32 / fft_size as f32)
.collect()
}
/// Calcule les magnitudes FFT (simulation)
fn calculate_fft_magnitudes(&self, samples: &[f32], fft_size: usize) -> Vec<f32> {
let bin_count = fft_size / 2 + 1;
let mut magnitudes = Vec::with_capacity(bin_count);
// Simulation simple - en production, utiliser une vraie FFT
for i in 0..bin_count {
let frequency = i as f32 / bin_count as f32;
let magnitude = if !samples.is_empty() {
let avg_amplitude = samples.iter().map(|&s| s.abs()).sum::<f32>() / samples.len() as f32;
avg_amplitude * (1.0 - frequency) // Décroissance avec la fréquence
} else {
0.0
};
magnitudes.push(magnitude);
}
magnitudes
}
/// Récupère depuis le cache
async fn get_from_cache(&self, key: &str) -> Option<WaveformData> {
self.cache.read().await.get(key).cloned()
}
/// Stocke en cache
async fn store_in_cache(&self, key: &str, waveform: &WaveformData) {
self.cache.write().await.insert(key.to_string(), waveform.clone());
}
/// Exporte la waveform dans un format spécifique
pub fn export_waveform(&self, waveform: &WaveformData, format: WaveformFormat) -> Result<Vec<u8>, AppError> {
match format {
WaveformFormat::PeaksJS => {
let json = serde_json::to_string_pretty(waveform)
.map_err(|_| AppError::SerializationError)?;
Ok(json.into_bytes())
},
WaveformFormat::Binary => {
// Format binaire compact pour la performance
let mut data = Vec::new();
// Header
data.extend(&(waveform.peaks.len() as u32).to_le_bytes());
data.extend(&waveform.sample_rate.to_le_bytes());
data.extend(&(waveform.channels as u32).to_le_bytes());
data.extend(&waveform.duration.to_le_bytes());
// Peaks data
for peak in &waveform.peaks {
data.extend(&peak.min.to_le_bytes());
data.extend(&peak.max.to_le_bytes());
data.extend(&peak.rms.to_le_bytes());
data.extend(&peak.peak.to_le_bytes());
}
Ok(data)
},
WaveformFormat::SVG { .. } => {
// Génération SVG simple
let svg = self.generate_svg_waveform(waveform)?;
Ok(svg.into_bytes())
},
WaveformFormat::PNG { width, height } => {
// Génération PNG (simulation)
let _png_data = self.generate_png_waveform(waveform, width, height)?;
Ok(Vec::new()) // Placeholder
},
}
}
/// Génère une représentation SVG de la waveform
fn generate_svg_waveform(&self, waveform: &WaveformData) -> Result<String, AppError> {
let width = 800;
let height = 200;
let center_y = height / 2;
let mut svg = format!(
"<svg width=\"{}\" height=\"{}\" xmlns=\"http://www.w3.org/2000/svg\">\n<rect width=\"100%\" height=\"100%\" fill=\"#f0f0f0\"/>\n<g stroke=\"#007cba\" stroke-width=\"1\" fill=\"none\">",
width, height
);
// Dessiner la waveform
let x_scale = width as f32 / waveform.peaks.len() as f32;
let y_scale = center_y as f32;
let mut path = String::from("M");
for (i, peak) in waveform.peaks.iter().enumerate() {
let x = i as f32 * x_scale;
let y_top = center_y as f32 - (peak.max * y_scale);
let y_bottom = center_y as f32 - (peak.min * y_scale);
if i == 0 {
path.push_str(&format!("{},{}", x, y_top));
} else {
path.push_str(&format!(" L{},{}", x, y_top));
}
}
// Fermer le chemin
for (i, peak) in waveform.peaks.iter().enumerate().rev() {
let x = i as f32 * x_scale;
let y_bottom = center_y as f32 - (peak.min * y_scale);
path.push_str(&format!(" L{},{}", x, y_bottom));
}
path.push('Z');
svg.push_str(&format!("<path d=\"{}\" fill=\"#007cba\" opacity=\"0.6\"/>", path));
svg.push_str("</g></svg>");
Ok(svg)
}
/// Génère une image PNG de la waveform
fn generate_png_waveform(&self, _waveform: &WaveformData, _width: u32, _height: u32) -> Result<Vec<u8>, AppError> {
// Simulation - en production, utiliser une lib comme `image` ou `skia`
Ok(Vec::new())
}
}
/// Données audio brutes
#[derive(Debug)]
struct AudioData {
samples: Vec<f32>,
sample_rate: u32,
channels: u8,
duration: f64,
}
impl Default for AudioStatistics {
fn default() -> Self {
Self {
average_rms: 0.0,
peak_amplitude: 0.0,
dynamic_range_db: 0.0,
integrated_loudness: -70.0,
crest_factor: 0.0,
silence_percentage: 100.0,
estimated_bpm: None,
estimated_key: None,
}
}
}

View file

@ -1,732 +0,0 @@
// Advanced Streaming Engine for Phase 5
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{broadcast, RwLock};
use tracing::{span, Level};
use uuid::Uuid;
use super::live_recording::{LiveRecordingManager, RecordingConfig, RecordingQuality};
use super::sync_manager::{SyncConfig, SyncManager};
use super::webrtc::{WebRTCConfig, WebRTCManager};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedStreamingConfig {
pub webrtc: WebRTCConfig,
pub sync: SyncConfig,
pub recording: RecordingConfig,
pub max_concurrent_streams: usize,
pub adaptive_quality: bool,
pub bandwidth_monitoring: bool,
pub analytics_enabled: bool,
pub failover_support: bool,
}
impl Default for AdvancedStreamingConfig {
fn default() -> Self {
Self {
webrtc: WebRTCConfig::default(),
sync: SyncConfig::default(),
recording: RecordingConfig::default(),
max_concurrent_streams: 100,
adaptive_quality: true,
bandwidth_monitoring: true,
analytics_enabled: true,
failover_support: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamSession {
pub session_id: String,
pub stream_id: String,
pub user_id: String,
pub stream_type: StreamType,
pub state: StreamState,
pub start_time: SystemTime,
pub end_time: Option<SystemTime>,
pub current_quality: String,
pub listeners: Vec<ListenerInfo>,
pub recording_id: Option<String>,
pub webrtc_peer_id: Option<String>,
pub sync_client_id: Option<String>,
pub analytics: StreamAnalytics,
pub metadata: StreamMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StreamType {
Audio,
Video,
AudioVideo,
Screen,
Chat,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StreamState {
Initializing,
Starting,
Live,
Paused,
Buffering,
Ending,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenerInfo {
pub listener_id: String,
pub user_id: String,
pub joined_at: SystemTime,
pub connection_type: String,
pub quality_preference: String,
pub bandwidth_kbps: u32,
pub is_synchronized: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamAnalytics {
pub total_listeners: u32,
pub peak_listeners: u32,
pub average_listener_duration_ms: u64,
pub total_data_transferred_mb: f32,
pub average_bitrate_kbps: u32,
pub buffer_events: u32,
pub quality_switches: u32,
pub connection_drops: u32,
pub geographic_distribution: HashMap<String, u32>,
}
impl Default for StreamAnalytics {
fn default() -> Self {
Self {
total_listeners: 0,
peak_listeners: 0,
average_listener_duration_ms: 0,
total_data_transferred_mb: 0.0,
average_bitrate_kbps: 0,
buffer_events: 0,
quality_switches: 0,
connection_drops: 0,
geographic_distribution: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamMetadata {
pub title: String,
pub description: Option<String>,
pub tags: Vec<String>,
pub category: String,
pub language: String,
pub thumbnail_url: Option<String>,
pub duration_ms: Option<u64>,
pub is_public: bool,
pub scheduled_start: Option<SystemTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum StreamingMessage {
StreamStarted {
session_id: String,
stream_id: String,
quality: String,
},
StreamEnded {
session_id: String,
duration_ms: u64,
},
ListenerJoined {
session_id: String,
listener: ListenerInfo,
},
ListenerLeft {
session_id: String,
listener_id: String,
},
QualityChanged {
session_id: String,
old_quality: String,
new_quality: String,
},
BufferEvent {
session_id: String,
listener_id: String,
event_type: String,
},
AnalyticsUpdate {
session_id: String,
analytics: StreamAnalytics,
},
Error {
session_id: String,
error: String,
},
}
/// Moteur de streaming avancé Phase 5
#[derive(Clone)]
pub struct AdvancedStreamingEngine {
config: AdvancedStreamingConfig,
sessions: Arc<RwLock<HashMap<String, StreamSession>>>,
webrtc_manager: Arc<WebRTCManager>,
sync_manager: Arc<SyncManager>,
recording_manager: Arc<LiveRecordingManager>,
streaming_tx: broadcast::Sender<StreamingMessage>,
analytics_collector: Arc<RwLock<HashMap<String, StreamAnalytics>>>,
}
impl AdvancedStreamingEngine {
pub fn new(config: AdvancedStreamingConfig) -> Self {
let webrtc_manager = Arc::new(WebRTCManager::new(config.webrtc.clone()));
let sync_manager = Arc::new(SyncManager::new(config.sync.clone()));
let recording_manager = Arc::new(LiveRecordingManager::new(config.recording.clone()));
let (streaming_tx, _) = broadcast::channel(1000);
Self {
config,
sessions: Arc::new(RwLock::new(HashMap::new())),
webrtc_manager,
sync_manager,
recording_manager,
streaming_tx,
analytics_collector: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Démarrer le moteur de streaming avancé
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tracing::info!("Starting Advanced Streaming Engine Phase 5");
// Démarrer tous les gestionnaires
self.webrtc_manager.start().await?;
self.sync_manager.start().await?;
self.recording_manager.start().await?;
// Démarrer les services internes
self.start_session_monitor().await;
self.start_quality_adapter().await;
if self.config.analytics_enabled {
self.start_analytics_collector().await;
}
if self.config.bandwidth_monitoring {
self.start_bandwidth_monitor().await;
}
tracing::info!("Advanced Streaming Engine Phase 5 started successfully");
Ok(())
}
/// Créer une nouvelle session de streaming
pub async fn create_stream_session(
&self,
user_id: String,
stream_type: StreamType,
metadata: StreamMetadata,
enable_recording: bool,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let span = span!(Level::INFO, "create_stream_session", user_id = %user_id);
let _enter = span.enter();
let mut sessions = self.sessions.write().await;
if sessions.len() >= self.config.max_concurrent_streams {
return Err("Maximum number of concurrent streams reached".into());
}
let session_id = Uuid::new_v4().to_string();
let stream_id = format!("stream_{}", Uuid::new_v4().simple());
// Créer la session de streaming
let mut session = StreamSession {
session_id: session_id.clone(),
stream_id: stream_id.clone(),
user_id: user_id.clone(),
stream_type,
state: StreamState::Initializing,
start_time: SystemTime::now(),
end_time: None,
current_quality: "medium".to_string(),
listeners: Vec::new(),
recording_id: None,
webrtc_peer_id: None,
sync_client_id: None,
analytics: StreamAnalytics::default(),
metadata,
};
// Initialiser WebRTC peer
match self
.webrtc_manager
.create_peer_session(format!("peer_{}", session_id), session_id.clone())
.await
{
Ok(peer) => {
session.webrtc_peer_id = Some(peer.peer_id);
tracing::info!("WebRTC peer created for session: {}", session_id);
}
Err(e) => {
tracing::warn!("Failed to create WebRTC peer: {}", e);
}
}
// Ajouter client de synchronisation
match self
.sync_manager
.add_client(format!("sync_{}", session_id), session_id.clone())
.await
{
Ok(client) => {
session.sync_client_id = Some(client.client_id);
tracing::info!("Sync client added for session: {}", session_id);
}
Err(e) => {
tracing::warn!("Failed to add sync client: {}", e);
}
}
// Démarrer l'enregistrement si demandé
if enable_recording {
let recording_quality = RecordingQuality::high();
let recording_metadata = crate::streaming::live_recording::RecordingMetadata {
title: Some(session.metadata.title.clone()),
artist: Some(user_id.clone()),
album: None,
genre: Some(session.metadata.category.clone()),
duration_ms: 0,
bitrate: recording_quality.bitrate,
sample_rate: recording_quality.sample_rate,
channels: recording_quality.channels,
file_size_bytes: 0,
creation_time: SystemTime::now(),
tags: session
.metadata
.tags
.iter()
.enumerate()
.map(|(i, tag)| (format!("tag_{}", i), tag.clone()))
.collect(),
};
match self
.recording_manager
.start_recording(
session_id.clone(),
stream_id.clone(),
recording_quality,
recording_metadata,
)
.await
{
Ok(recording_id) => {
session.recording_id = Some(recording_id.clone());
tracing::info!(
"Recording started for session: {} with ID: {}",
session_id,
recording_id
);
}
Err(e) => {
tracing::warn!("Failed to start recording: {}", e);
}
}
}
session.state = StreamState::Starting;
sessions.insert(session_id.clone(), session);
tracing::info!(
"Created stream session: {} for user: {}",
session_id,
user_id
);
// Envoyer message de démarrage
let start_msg = StreamingMessage::StreamStarted {
session_id: session_id.clone(),
stream_id,
quality: "medium".to_string(),
};
if let Err(e) = self.streaming_tx.send(start_msg) {
tracing::warn!("Failed to send stream started message: {}", e);
}
Ok(session_id)
}
/// Ajouter un listener à une session
pub async fn add_listener(
&self,
session_id: &str,
user_id: String,
connection_type: String,
quality_preference: String,
bandwidth_kbps: u32,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
let listener_id = Uuid::new_v4().to_string();
let listener = ListenerInfo {
listener_id: listener_id.clone(),
user_id: user_id.clone(),
joined_at: SystemTime::now(),
connection_type,
quality_preference,
bandwidth_kbps,
is_synchronized: false,
};
session.listeners.push(listener.clone());
session.analytics.total_listeners += 1;
if session.listeners.len() as u32 > session.analytics.peak_listeners {
session.analytics.peak_listeners = session.listeners.len() as u32;
}
tracing::info!("Added listener {} to session {}", listener_id, session_id);
// Envoyer message de listener ajouté
let listener_msg = StreamingMessage::ListenerJoined {
session_id: session_id.to_string(),
listener,
};
if let Err(e) = self.streaming_tx.send(listener_msg) {
tracing::warn!("Failed to send listener joined message: {}", e);
}
// Synchroniser le nouveau listener
if let Some(sync_client_id) = &session.sync_client_id {
// Ici on ajouterait la logique de synchronisation du listener
tracing::debug!(
"Synchronizing new listener with sync client: {}",
sync_client_id
);
}
Ok(listener_id)
} else {
Err("Session not found".into())
}
}
/// Supprimer un listener d'une session
pub async fn remove_listener(
&self,
session_id: &str,
listener_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
if let Some(pos) = session
.listeners
.iter()
.position(|l| l.listener_id == listener_id)
{
let listener = session.listeners.remove(pos);
// Calculer la durée d'écoute
if let Ok(duration) = listener.joined_at.elapsed() {
let duration_ms = duration.as_millis() as u64;
// Mettre à jour les analytics
let total_duration = session.analytics.average_listener_duration_ms
* (session.analytics.total_listeners - 1) as u64
+ duration_ms;
session.analytics.average_listener_duration_ms =
total_duration / session.analytics.total_listeners as u64;
}
tracing::info!(
"Removed listener {} from session {}",
listener_id,
session_id
);
// Envoyer message de listener parti
let left_msg = StreamingMessage::ListenerLeft {
session_id: session_id.to_string(),
listener_id: listener_id.to_string(),
};
if let Err(e) = self.streaming_tx.send(left_msg) {
tracing::warn!("Failed to send listener left message: {}", e);
}
Ok(())
} else {
Err("Listener not found in session".into())
}
} else {
Err("Session not found".into())
}
}
/// Terminer une session de streaming
pub async fn end_stream_session(
&self,
session_id: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(session_id) {
session.state = StreamState::Ending;
session.end_time = Some(SystemTime::now());
let duration_ms = if let Ok(duration) = session.start_time.elapsed() {
duration.as_millis() as u64
} else {
0
};
// Arrêter l'enregistrement si actif
if let Some(recording_id) = &session.recording_id {
if let Err(e) = self.recording_manager.stop_recording(recording_id).await {
tracing::warn!("Failed to stop recording {}: {}", recording_id, e);
}
}
// Nettoyer les ressources WebRTC
if let Some(peer_id) = &session.webrtc_peer_id {
self.webrtc_manager.remove_peer(peer_id).await;
}
// Nettoyer les ressources de synchronisation
if let Some(sync_client_id) = &session.sync_client_id {
self.sync_manager.remove_client(sync_client_id).await;
}
session.state = StreamState::Completed;
tracing::info!(
"Ended stream session: {} (duration: {}ms)",
session_id,
duration_ms
);
// Envoyer message de fin
let end_msg = StreamingMessage::StreamEnded {
session_id: session_id.to_string(),
duration_ms,
};
if let Err(e) = self.streaming_tx.send(end_msg) {
tracing::warn!("Failed to send stream ended message: {}", e);
}
Ok(())
} else {
Err("Session not found".into())
}
}
/// Obtenir les statistiques globales en temps réel
pub async fn get_global_stats(&self) -> serde_json::Value {
let sessions = self.sessions.read().await;
let webrtc_stats = self.webrtc_manager.get_real_time_stats().await;
let sync_stats = self.sync_manager.get_sync_stats().await;
let recording_stats = self.recording_manager.get_recording_stats().await;
let total_sessions = sessions.len();
let active_sessions = sessions
.values()
.filter(|s| matches!(s.state, StreamState::Live))
.count();
let total_listeners: u32 = sessions.values().map(|s| s.listeners.len() as u32).sum();
let total_data_transferred_mb: f32 = sessions
.values()
.map(|s| s.analytics.total_data_transferred_mb)
.sum();
let avg_bitrate: f32 = if active_sessions > 0 {
sessions
.values()
.filter(|s| matches!(s.state, StreamState::Live))
.map(|s| s.analytics.average_bitrate_kbps as f32)
.sum::<f32>()
/ active_sessions as f32
} else {
0.0
};
serde_json::json!({
"phase5_streaming_stats": {
"total_sessions": total_sessions,
"active_sessions": active_sessions,
"total_listeners": total_listeners,
"total_data_transferred_mb": total_data_transferred_mb,
"average_bitrate_kbps": avg_bitrate,
"max_concurrent_streams": self.config.max_concurrent_streams,
"adaptive_quality_enabled": self.config.adaptive_quality,
"webrtc": webrtc_stats,
"synchronization": sync_stats,
"recording": recording_stats
}
})
}
/// Démarrer le moniteur de sessions
async fn start_session_monitor(&self) {
let sessions = self.sessions.clone();
let _streaming_tx = self.streaming_tx.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(10));
loop {
interval.tick().await;
let sessions_guard = sessions.read().await;
for (session_id, session) in sessions_guard.iter() {
match session.state {
StreamState::Live => {
tracing::debug!(
"Session {} live with {} listeners",
session_id,
session.listeners.len()
);
}
StreamState::Failed => {
tracing::warn!("Session {} in failed state", session_id);
}
_ => {}
}
}
}
});
}
/// Démarrer l'adaptateur de qualité
async fn start_quality_adapter(&self) {
if !self.config.adaptive_quality {
return;
}
let sessions = self.sessions.clone();
let _streaming_tx = self.streaming_tx.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let mut sessions_guard = sessions.write().await;
for (session_id, session) in sessions_guard.iter_mut() {
if matches!(session.state, StreamState::Live) {
// Logique d'adaptation de qualité basée sur les listeners
let avg_bandwidth: f32 = if !session.listeners.is_empty() {
session
.listeners
.iter()
.map(|l| l.bandwidth_kbps as f32)
.sum::<f32>()
/ session.listeners.len() as f32
} else {
1000.0
};
let new_quality = if avg_bandwidth > 500.0 {
"high"
} else if avg_bandwidth > 200.0 {
"medium"
} else {
"low"
};
if new_quality != session.current_quality {
let old_quality = session.current_quality.clone();
session.current_quality = new_quality.to_string();
session.analytics.quality_switches += 1;
let quality_msg = StreamingMessage::QualityChanged {
session_id: session_id.clone(),
old_quality,
new_quality: new_quality.to_string(),
};
if let Err(e) = _streaming_tx.send(quality_msg) {
tracing::warn!("Failed to send quality change message: {}", e);
}
}
}
}
}
});
}
/// Démarrer le collecteur d'analytics
async fn start_analytics_collector(&self) {
let sessions = self.sessions.clone();
let analytics_collector = self.analytics_collector.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
let sessions_guard = sessions.read().await;
let mut analytics_guard = analytics_collector.write().await;
for (session_id, session) in sessions_guard.iter() {
analytics_guard.insert(session_id.clone(), session.analytics.clone());
}
}
});
}
/// Démarrer le moniteur de bande passante
async fn start_bandwidth_monitor(&self) {
let sessions = self.sessions.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(15));
loop {
interval.tick().await;
let sessions_guard = sessions.read().await;
for (session_id, session) in sessions_guard.iter() {
if matches!(session.state, StreamState::Live) {
let total_bandwidth: u32 =
session.listeners.iter().map(|l| l.bandwidth_kbps).sum();
tracing::debug!(
"Session {} bandwidth usage: {} kbps",
session_id,
total_bandwidth
);
}
}
}
});
}
/// Obtenir un receiver pour les messages de streaming
pub fn get_streaming_receiver(&self) -> broadcast::Receiver<StreamingMessage> {
self.streaming_tx.subscribe()
}
/// Obtenir les statistiques d'une session spécifique
pub async fn get_session_stats(&self, session_id: &str) -> Option<StreamSession> {
let sessions = self.sessions.read().await;
sessions.get(session_id).cloned()
}
}

View file

@ -1,18 +1,14 @@
pub mod adaptive;
pub mod advanced_streaming;
pub mod hls; // Module legacy, sera migré vers protocols/hls
pub mod live_recording;
pub mod protocols;
pub mod sync_manager;
pub mod webrtc; // Module legacy
pub mod websocket;
pub use adaptive::*;
pub use advanced_streaming::*;
pub use hls::*;
pub use live_recording::*;
pub use sync_manager::*;
pub use webrtc::*;
pub use websocket::*;
pub mod websocket_transport;
pub use websocket_transport::*;

View file

@ -1,493 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::{broadcast, mpsc, RwLock};
use tracing::{span, Level};
pub mod config;
pub use config::WebRTCConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IceServer {
pub urls: Vec<String>,
pub username: Option<String>,
pub credential: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AudioCodec {
Opus { bitrate: u32 },
Aac { bitrate: u32 },
Mp3 { bitrate: u32 },
Pcm { sample_rate: u32 },
}
impl AudioCodec {
pub fn get_bitrate(&self) -> u32 {
match self {
AudioCodec::Opus { bitrate } => *bitrate,
AudioCodec::Aac { bitrate } => *bitrate,
AudioCodec::Mp3 { bitrate } => *bitrate,
AudioCodec::Pcm { sample_rate } => sample_rate * 16 * 2 / 1000,
}
}
pub fn get_mime_type(&self) -> &'static str {
match self {
AudioCodec::Opus { .. } => "audio/opus",
AudioCodec::Aac { .. } => "audio/aac",
AudioCodec::Mp3 { .. } => "audio/mpeg",
AudioCodec::Pcm { .. } => "audio/pcm",
}
}
}
/// Informations sur un peer WebRTC
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebRTCPeer {
pub peer_id: String,
pub session_id: String,
pub connection_state: ConnectionState,
pub ice_connection_state: IceConnectionState,
pub selected_codec: Option<AudioCodec>,
pub bandwidth_estimate: u32,
pub rtt_ms: Option<u32>,
pub jitter_ms: Option<u32>,
pub packet_loss_percentage: f32,
pub connected_at: SystemTime,
#[serde(skip, default = "default_instant")]
pub last_activity: Instant,
pub stats: PeerStats,
}
fn default_instant() -> Instant {
Instant::now()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConnectionState {
New,
Connecting,
Connected,
Disconnected,
Failed,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IceConnectionState {
New,
Checking,
Connected,
Completed,
Disconnected,
Failed,
Closed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerStats {
pub bytes_sent: u64,
pub bytes_received: u64,
pub packets_sent: u64,
pub packets_received: u64,
pub packets_lost: u64,
pub audio_level: f32,
pub quality_switches: u32,
}
impl Default for PeerStats {
fn default() -> Self {
Self {
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
packets_lost: 0,
audio_level: 0.0,
quality_switches: 0,
}
}
}
/// Messages WebRTC pour signaling
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
pub enum WebRTCMessage {
Offer {
peer_id: String,
sdp: String,
session_id: String,
},
Answer {
peer_id: String,
sdp: String,
},
IceCandidate {
peer_id: String,
candidate: String,
sdp_mid: Option<String>,
sdp_mline_index: Option<u16>,
},
BitrateChange {
peer_id: String,
new_bitrate: u32,
},
CodecChange {
peer_id: String,
codec: AudioCodec,
},
QualityUpdate {
peer_id: String,
bandwidth: u32,
rtt: u32,
packet_loss: f32,
},
PeerDisconnected {
peer_id: String,
},
Error {
peer_id: String,
message: String,
},
}
/// Gestionnaire WebRTC principal
#[derive(Clone)]
pub struct WebRTCManager {
config: WebRTCConfig,
peers: Arc<RwLock<HashMap<String, WebRTCPeer>>>,
signaling_tx: broadcast::Sender<WebRTCMessage>,
stats_tx: mpsc::Sender<PeerStats>,
}
impl WebRTCManager {
pub fn new(config: WebRTCConfig) -> Self {
let (signaling_tx, _signaling_rx) = broadcast::channel(1000);
let (stats_tx, _stats_rx) = mpsc::channel(100);
Self {
config,
peers: Arc::new(RwLock::new(HashMap::new())),
signaling_tx,
stats_tx,
}
}
/// Démarre le gestionnaire WebRTC
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tracing::info!(
"Starting WebRTC Manager with max {} peers",
self.config.max_peers
);
// Démarrer le moniteur de connexions
self.start_connection_monitor().await;
// Démarrer l'adaptation de bitrate
if self.config.bitrate_adaptation {
self.start_bitrate_adaptation().await;
}
// Démarrer le collecteur de statistiques
self.start_stats_collector().await;
Ok(())
}
/// Créer une nouvelle session peer
pub async fn create_peer_session(
&self,
peer_id: String,
session_id: String,
) -> Result<WebRTCPeer, Box<dyn std::error::Error + Send + Sync>> {
let span = span!(Level::INFO, "create_peer_session", peer_id = %peer_id);
let _enter = span.enter();
let mut peers = self.peers.write().await;
if peers.len() >= self.config.max_peers {
return Err("Maximum number of peers reached".into());
}
let peer = WebRTCPeer {
peer_id: peer_id.clone(),
session_id: session_id.clone(),
connection_state: ConnectionState::New,
ice_connection_state: IceConnectionState::New,
selected_codec: None,
bandwidth_estimate: 1000,
rtt_ms: None,
jitter_ms: None,
packet_loss_percentage: 0.0,
connected_at: SystemTime::now(),
last_activity: Instant::now(),
stats: PeerStats::default(),
};
peers.insert(peer_id.clone(), peer.clone());
tracing::info!(
"Created WebRTC peer session: {} for session: {}",
peer_id,
session_id
);
Ok(peer)
}
/// Sélectionner le meilleur codec pour un peer
pub async fn select_optimal_codec(
&self,
_peer_id: &str,
bandwidth_estimate: u32,
) -> Option<AudioCodec> {
let available_bitrates = [64, 128, 256, 320];
let optimal_bitrate = available_bitrates
.iter()
.filter(|&&bitrate| bitrate <= bandwidth_estimate * 8 / 10)
.max()
.copied()
.unwrap_or(64);
for codec in &self.config.codec_preferences {
match codec {
AudioCodec::Opus { .. } => {
return Some(AudioCodec::Opus {
bitrate: optimal_bitrate,
});
}
AudioCodec::Aac { .. } => {
return Some(AudioCodec::Aac {
bitrate: optimal_bitrate,
});
}
AudioCodec::Mp3 { .. } => {
return Some(AudioCodec::Mp3 {
bitrate: optimal_bitrate,
});
}
_ => continue,
}
}
Some(AudioCodec::Opus {
bitrate: optimal_bitrate,
})
}
/// Mettre à jour les statistiques d'un peer
pub async fn update_peer_stats(
&self,
peer_id: &str,
bandwidth: u32,
rtt: u32,
packet_loss: f32,
jitter: u32,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut peers = self.peers.write().await;
if let Some(peer) = peers.get_mut(peer_id) {
peer.bandwidth_estimate = bandwidth;
peer.rtt_ms = Some(rtt);
peer.packet_loss_percentage = packet_loss;
peer.jitter_ms = Some(jitter);
peer.last_activity = Instant::now();
// Envoyer message de mise à jour qualité
let quality_msg = WebRTCMessage::QualityUpdate {
peer_id: peer_id.to_string(),
bandwidth,
rtt,
packet_loss,
};
if let Err(e) = self.signaling_tx.send(quality_msg) {
tracing::warn!("Failed to send quality update: {}", e);
}
}
Ok(())
}
/// Obtenir les statistiques en temps réel
pub async fn get_real_time_stats(&self) -> serde_json::Value {
let peers = self.peers.read().await;
let peer_count = peers.len();
let connected_peers = peers
.values()
.filter(|p| matches!(p.connection_state, ConnectionState::Connected))
.count();
let total_bandwidth: u32 = peers.values().map(|p| p.bandwidth_estimate).sum();
let avg_rtt: f32 = {
let rtts: Vec<u32> = peers.values().filter_map(|p| p.rtt_ms).collect();
if rtts.is_empty() {
0.0
} else {
rtts.iter().sum::<u32>() as f32 / rtts.len() as f32
}
};
let avg_packet_loss: f32 = {
let losses: Vec<f32> = peers.values().map(|p| p.packet_loss_percentage).collect();
if losses.is_empty() {
0.0
} else {
losses.iter().sum::<f32>() / losses.len() as f32
}
};
serde_json::json!({
"webrtc_stats": {
"total_peers": peer_count,
"connected_peers": connected_peers,
"total_bandwidth_kbps": total_bandwidth,
"average_rtt_ms": avg_rtt,
"average_packet_loss_percent": avg_packet_loss,
"max_peers": self.config.max_peers,
"codec_preferences": self.config.codec_preferences,
"bitrate_adaptation_enabled": self.config.bitrate_adaptation
}
})
}
/// Démarrer le moniteur de connexions
async fn start_connection_monitor(&self) {
let peers = self.peers.clone();
let timeout = self.config.connection_timeout;
let heartbeat_interval = self.config.heartbeat_interval;
tokio::spawn(async move {
let mut interval = tokio::time::interval(heartbeat_interval);
loop {
interval.tick().await;
let mut peers_to_remove = Vec::new();
{
let peers_guard = peers.read().await;
let now = Instant::now();
for (peer_id, peer) in peers_guard.iter() {
if now.duration_since(peer.last_activity) > timeout {
peers_to_remove.push(peer_id.clone());
}
}
}
if !peers_to_remove.is_empty() {
let mut peers_guard = peers.write().await;
for peer_id in peers_to_remove {
if peers_guard.remove(&peer_id).is_some() {
tracing::warn!("Removed inactive WebRTC peer: {}", peer_id);
}
}
}
}
});
}
/// Démarrer l'adaptation automatique de bitrate
async fn start_bitrate_adaptation(&self) {
let peers = self.peers.clone();
let signaling_tx = self.signaling_tx.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(5));
loop {
interval.tick().await;
let peers_guard = peers.read().await;
for (peer_id, peer) in peers_guard.iter() {
// Adapter le bitrate selon les conditions réseau
let current_bitrate = peer
.selected_codec
.as_ref()
.map(|c| c.get_bitrate())
.unwrap_or(128);
let optimal_bitrate = if peer.packet_loss_percentage > 5.0 {
// Réduire le bitrate si perte de paquets élevée
std::cmp::max(64, current_bitrate - 64)
} else if peer.bandwidth_estimate > current_bitrate * 12 / 10 {
// Augmenter le bitrate si bande passante suffisante
std::cmp::min(320, current_bitrate + 64)
} else {
current_bitrate
};
if optimal_bitrate != current_bitrate {
let msg = WebRTCMessage::BitrateChange {
peer_id: peer_id.clone(),
new_bitrate: optimal_bitrate,
};
if let Err(e) = signaling_tx.send(msg) {
tracing::warn!("Failed to send bitrate change: {}", e);
}
}
}
}
});
}
/// Démarrer le collecteur de statistiques
async fn start_stats_collector(&self) {
let peers = self.peers.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
let peers_guard = peers.read().await;
let connected_count = peers_guard
.values()
.filter(|p| matches!(p.connection_state, ConnectionState::Connected))
.count();
if connected_count > 0 {
tracing::debug!("WebRTC active connections: {}", connected_count);
}
}
});
}
/// Supprimer un peer
pub async fn remove_peer(&self, peer_id: &str) -> bool {
let mut peers = self.peers.write().await;
if let Some(_peer) = peers.remove(peer_id) {
tracing::info!("Removed WebRTC peer: {}", peer_id);
let disconnect_msg = WebRTCMessage::PeerDisconnected {
peer_id: peer_id.to_string(),
};
if let Err(e) = self.signaling_tx.send(disconnect_msg) {
tracing::warn!("Failed to send peer disconnect message: {}", e);
}
true
} else {
false
}
}
/// Obtenir un receiver pour les messages de signaling
pub fn get_signaling_receiver(&self) -> broadcast::Receiver<WebRTCMessage> {
self.signaling_tx.subscribe()
}
/// Envoyer un message de signaling
pub async fn send_signaling_message(
&self,
message: WebRTCMessage,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.signaling_tx.send(message)?;
Ok(())
}
}

View file

@ -1,286 +0,0 @@
//! Configuration WebRTC pour le stream server
//!
//! Ce module fournit la configuration WebRTC avec :
//! - Configuration des serveurs ICE (STUN/TURN)
//! - Configuration du signaling
//! - Configuration des codecs audio
//! - Gestion depuis les variables d'environnement
use serde::{Deserialize, Serialize};
use std::time::Duration;
// Note: Use tracing::info! macro directly instead of importing
use super::{AudioCodec, IceServer};
/// Configuration WebRTC pour streaming audio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebRTCConfig {
pub ice_servers: Vec<IceServer>,
pub signaling_url: String,
pub max_peers: usize,
pub connection_timeout: Duration,
pub heartbeat_interval: Duration,
pub codec_preferences: Vec<AudioCodec>,
pub bitrate_adaptation: bool,
pub jitter_buffer_ms: u32,
}
impl Default for WebRTCConfig {
fn default() -> Self {
Self {
ice_servers: vec![IceServer {
urls: vec!["stun:stun.l.google.com:19302".to_string()],
username: None,
credential: None,
}],
signaling_url: "ws://localhost:3002/ws/webrtc".to_string(),
max_peers: 1000,
connection_timeout: Duration::from_secs(30),
heartbeat_interval: Duration::from_secs(10),
codec_preferences: vec![
AudioCodec::Opus { bitrate: 320 },
AudioCodec::Aac { bitrate: 256 },
AudioCodec::Mp3 { bitrate: 192 },
],
bitrate_adaptation: true,
jitter_buffer_ms: 100,
}
}
}
impl WebRTCConfig {
/// Crée une configuration WebRTC depuis les variables d'environnement
pub fn from_env() -> Self {
let mut config = Self::default();
// Configuration des serveurs ICE
if let Ok(ice_servers) = std::env::var("WEBRTC_ICE_SERVERS") {
config.ice_servers = Self::parse_ice_servers(&ice_servers);
tracing::info!("🔧 WebRTC ICE servers configurés depuis WEBRTC_ICE_SERVERS");
} else {
// Configuration par défaut avec STUN/TURN si disponibles
let mut ice_servers = vec![IceServer {
urls: vec!["stun:stun.l.google.com:19302".to_string()],
username: None,
credential: None,
}];
// Ajouter serveur STUN personnalisé si configuré
if let Ok(stun_url) = std::env::var("WEBRTC_STUN_URL") {
ice_servers.push(IceServer {
urls: vec![stun_url],
username: None,
credential: None,
});
}
// Ajouter serveur TURN si configuré
if let (Ok(turn_url), Ok(turn_username), Ok(turn_credential)) = (
std::env::var("WEBRTC_TURN_URL"),
std::env::var("WEBRTC_TURN_USERNAME"),
std::env::var("WEBRTC_TURN_CREDENTIAL"),
) {
ice_servers.push(IceServer {
urls: vec![turn_url],
username: Some(turn_username),
credential: Some(turn_credential),
});
tracing::info!("✅ Serveur TURN configuré");
} else {
tracing::warn!(
"⚠️ Serveur TURN non configuré - certaines connexions peuvent échouer"
);
}
config.ice_servers = ice_servers;
}
// Configuration du signaling URL
if let Ok(signaling_url) = std::env::var("WEBRTC_SIGNALING_URL") {
config.signaling_url = signaling_url;
tracing::info!("🔧 WebRTC signaling URL: {}", config.signaling_url);
} else {
// Utiliser le port du serveur si disponible
let port = std::env::var("STREAM_PORT")
.or_else(|_| std::env::var("PORT"))
.unwrap_or_else(|_| "3002".to_string());
config.signaling_url = format!("ws://localhost:{}/ws/webrtc", port);
tracing::info!(
"🔧 WebRTC signaling URL par défaut: {}",
config.signaling_url
);
}
// Configuration du nombre maximum de peers
if let Ok(max_peers) = std::env::var("WEBRTC_MAX_PEERS") {
if let Ok(max) = max_peers.parse::<usize>() {
config.max_peers = max;
}
}
// Configuration du timeout de connexion
if let Ok(timeout) = std::env::var("WEBRTC_CONNECTION_TIMEOUT") {
if let Ok(secs) = timeout.parse::<u64>() {
config.connection_timeout = Duration::from_secs(secs);
}
}
// Configuration de l'intervalle de heartbeat
if let Ok(interval) = std::env::var("WEBRTC_HEARTBEAT_INTERVAL") {
if let Ok(secs) = interval.parse::<u64>() {
config.heartbeat_interval = Duration::from_secs(secs);
}
}
// Configuration de l'adaptation de bitrate
if let Ok(adaptation) = std::env::var("WEBRTC_BITRATE_ADAPTATION") {
config.bitrate_adaptation = adaptation.parse().unwrap_or(true);
}
// Configuration du jitter buffer
if let Ok(jitter) = std::env::var("WEBRTC_JITTER_BUFFER_MS") {
if let Ok(ms) = jitter.parse::<u32>() {
config.jitter_buffer_ms = ms;
}
}
tracing::info!(
"✅ Configuration WebRTC initialisée: {} ICE servers, max_peers={}, signaling={}",
config.ice_servers.len(),
config.max_peers,
config.signaling_url
);
config
}
/// Parse une chaîne de serveurs ICE au format JSON ou CSV
///
/// Format JSON: `[{"urls":["stun:stun.example.com"],"username":null,"credential":null}]`
/// Format CSV: `stun:stun.example.com,turn:turn.example.com:user:pass`
fn parse_ice_servers(servers_str: &str) -> Vec<IceServer> {
// Essayer de parser comme JSON d'abord
if let Ok(servers) = serde_json::from_str::<Vec<IceServer>>(servers_str) {
return servers;
}
// Sinon, parser comme CSV
let mut servers = Vec::new();
for server_str in servers_str.split(',') {
let server_str = server_str.trim();
if server_str.is_empty() {
continue;
}
// Format: "turn:turn.example.com:user:pass" ou "stun:stun.example.com"
if server_str.contains(':') {
let parts: Vec<&str> = server_str.split(':').collect();
if parts.len() >= 3 {
let protocol = parts[0];
let url = format!("{}:{}", protocol, parts[1]);
let username = if parts.len() > 3 {
Some(parts[2].to_string())
} else {
None
};
let credential = if parts.len() > 4 {
Some(parts[3].to_string())
} else {
None
};
servers.push(IceServer {
urls: vec![url],
username,
credential,
});
} else {
servers.push(IceServer {
urls: vec![server_str.to_string()],
username: None,
credential: None,
});
}
} else {
servers.push(IceServer {
urls: vec![server_str.to_string()],
username: None,
credential: None,
});
}
}
servers
}
/// Valide la configuration WebRTC
pub fn validate(&self) -> Result<(), String> {
if self.ice_servers.is_empty() {
return Err("Au moins un serveur ICE est requis".to_string());
}
if self.signaling_url.is_empty() {
return Err("URL de signaling est requise".to_string());
}
if !self.signaling_url.starts_with("ws://") && !self.signaling_url.starts_with("wss://") {
return Err("URL de signaling doit être un WebSocket (ws:// ou wss://)".to_string());
}
if self.max_peers == 0 {
return Err("max_peers doit être supérieur à 0".to_string());
}
if self.connection_timeout.as_secs() == 0 {
return Err("connection_timeout doit être supérieur à 0".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = WebRTCConfig::default();
assert!(!config.ice_servers.is_empty());
assert!(!config.signaling_url.is_empty());
assert!(config.max_peers > 0);
assert!(config.validate().is_ok());
}
#[test]
fn test_parse_ice_servers_json() {
let json = r#"[{"urls":["stun:stun.example.com"],"username":null,"credential":null}]"#;
let servers = WebRTCConfig::parse_ice_servers(json);
assert_eq!(servers.len(), 1);
assert_eq!(servers[0].urls[0], "stun:stun.example.com");
}
#[test]
fn test_parse_ice_servers_csv() {
let csv = "stun:stun.example.com:19302,turn:turn.example.com:user:pass";
let servers = WebRTCConfig::parse_ice_servers(csv);
assert!(servers.len() >= 1);
}
#[test]
fn test_validate_config() {
let mut config = WebRTCConfig::default();
assert!(config.validate().is_ok());
config.ice_servers.clear();
assert!(config.validate().is_err());
config = WebRTCConfig::default();
config.signaling_url = "".to_string();
assert!(config.validate().is_err());
config = WebRTCConfig::default();
config.signaling_url = "http://example.com".to_string();
assert!(config.validate().is_err());
}
}