diff --git a/Makefile.old b/Makefile.old deleted file mode 100644 index b6212a776..000000000 --- a/Makefile.old +++ /dev/null @@ -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 diff --git a/VEZA_VERSIONS_ROADMAP.md b/VEZA_VERSIONS_ROADMAP.md index 30ff4ddd9..305093a6a 100644 --- a/VEZA_VERSIONS_ROADMAP.md +++ b/VEZA_VERSIONS_ROADMAP.md @@ -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 | diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 6c68e62c8..73d83c3ac 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -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); } diff --git a/apps/web/src/types/forms.ts b/apps/web/src/types/forms.ts index 1fbc07b9d..32e724dce 100644 --- a/apps/web/src/types/forms.ts +++ b/apps/web/src/types/forms.ts @@ -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; diff --git a/apps/web/src/types/v2-v3-types.ts b/apps/web/src/types/v2-v3-types.ts index e4596caf8..9c676beed 100644 --- a/apps/web/src/types/v2-v3-types.ts +++ b/apps/web/src/types/v2-v3-types.ts @@ -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; diff --git a/config/env.example b/config/env.example index 2d6c94965..0400d265c 100644 --- a/config/env.example +++ b/config/env.example @@ -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 diff --git a/k8s/autoscaling/hpa-chat-server.yaml b/k8s/autoscaling/hpa-chat-server.yaml deleted file mode 100644 index 673d59d52..000000000 --- a/k8s/autoscaling/hpa-chat-server.yaml +++ /dev/null @@ -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 - diff --git a/k8s/chat-server/deployment.yaml b/k8s/chat-server/deployment.yaml deleted file mode 100644 index 96728ce61..000000000 --- a/k8s/chat-server/deployment.yaml +++ /dev/null @@ -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 - diff --git a/k8s/chat-server/service.yaml b/k8s/chat-server/service.yaml deleted file mode 100644 index 7b96a7032..000000000 --- a/k8s/chat-server/service.yaml +++ /dev/null @@ -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 - diff --git a/k8s/load-balancing/ingress-with-lb.yaml b/k8s/load-balancing/ingress-with-lb.yaml index d0781d78a..b0ab9766c 100644 --- a/k8s/load-balancing/ingress-with-lb.yaml +++ b/k8s/load-balancing/ingress-with-lb.yaml @@ -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: diff --git a/k8s/load-balancing/pod-disruption-budget.yaml b/k8s/load-balancing/pod-disruption-budget.yaml index 2cff783ec..aba3de0f9 100644 --- a/k8s/load-balancing/pod-disruption-budget.yaml +++ b/k8s/load-balancing/pod-disruption-budget.yaml @@ -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 diff --git a/k8s/network-policies/chat-server-allow.yaml b/k8s/network-policies/chat-server-allow.yaml deleted file mode 100644 index fb0f14275..000000000 --- a/k8s/network-policies/chat-server-allow.yaml +++ /dev/null @@ -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 diff --git a/veza-backend-api/go.mod b/veza-backend-api/go.mod index b65bcb6b9..c4417ae8f 100644 --- a/veza-backend-api/go.mod +++ b/veza-backend-api/go.mod @@ -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 ) diff --git a/veza-backend-api/go.sum b/veza-backend-api/go.sum index 7a21b5f95..f78ce8bfb 100644 --- a/veza-backend-api/go.sum +++ b/veza-backend-api/go.sum @@ -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= diff --git a/veza-stream-server/Makefile b/veza-stream-server/Makefile index 329577bfc..b8292023d 100644 --- a/veza-stream-server/Makefile +++ b/veza-stream-server/Makefile @@ -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)" diff --git a/veza-stream-server/src/soundcloud/creator.rs b/veza-stream-server/src/soundcloud/creator.rs deleted file mode 100644 index 38d965ae6..000000000 --- a/veza-stream-server/src/soundcloud/creator.rs +++ /dev/null @@ -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, -} - -/// 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, - pub presets: Vec, -} - -/// Effet audio -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AudioEffect { - pub name: String, - pub effect_type: EffectType, - pub parameters: HashMap, -} - -/// 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, - pub invitations: Vec, -} - -/// Projet de collaboration -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CollaborationProject { - pub id: u64, - pub name: String, - pub owner_id: u64, - pub collaborators: Vec, - 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 { - 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, -} - -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); - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/discovery.rs b/veza-stream-server/src/soundcloud/discovery.rs deleted file mode 100644 index 32c20ad8d..000000000 --- a/veza-stream-server/src/soundcloud/discovery.rs +++ /dev/null @@ -1,1182 +0,0 @@ -/// Module de découverte et recommandations SoundCloud-like -/// -/// Features : -/// - Algorithmes de recommandation ML -/// - Trending tracks par genre/région -/// - Charts Top 50, New & Hot -/// - Station radio continue -/// - Découverte personnalisée -/// - Analytics d'engagement - -use std::sync::Arc; -use std::collections::{HashMap, BTreeMap, VecDeque}; -use std::time::{Duration, SystemTime, Instant}; - -use serde::{Serialize, Deserialize}; -use uuid::Uuid; -use tokio::sync::RwLock; -use parking_lot::Mutex; -// Note: Use tracing::info! macro directly instead of importing - -use crate::error::AppError; -use crate::soundcloud::social::{SocialManager, UserSocialStats}; - -/// Gestionnaire principal de la découverte -#[derive(Debug)] -pub struct DiscoveryEngine { - /// Algorithmes de recommandation - recommendation_engine: Arc, - /// Gestionnaire de trending - trending_manager: Arc, - /// Charts globaux et par genre - charts_manager: Arc, - /// Stations radio personnalisées - radio_manager: Arc, - /// Analytics d'engagement - engagement_tracker: Arc, - /// Configuration - config: DiscoveryConfig, -} - -/// Moteur de recommandations ML -#[derive(Debug)] -pub struct RecommendationEngine { - /// Données d'écoute utilisateur - user_listening_history: Arc>>, - /// Similarité entre tracks - track_similarity_matrix: Arc>, - /// Clusters d'utilisateurs similaires - user_clusters: Arc>, - /// Modèles ML entraînés - ml_models: Arc, - /// Configuration - config: RecommendationConfig, -} - -/// Profil d'écoute d'un utilisateur -#[derive(Debug, Clone)] -pub struct UserListeningProfile { - pub user_id: i64, - pub listening_history: VecDeque, - pub genre_preferences: HashMap, - pub artist_preferences: HashMap, - pub tempo_preferences: TempoPreferences, - pub discovery_preferences: DiscoveryPreferences, - pub last_updated: SystemTime, -} - -/// Événement d'écoute -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListeningEvent { - pub track_id: Uuid, - pub listened_at: SystemTime, - pub duration_listened: Duration, - pub completion_percentage: f32, - pub source: ListeningSource, - pub skipped: bool, - pub liked: bool, - pub reposted: bool, - pub shared: bool, -} - -/// Source d'écoute -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ListeningSource { - Search, - Recommendation, - Trending, - Chart, - Radio, - Playlist, - Artist, - Album, - Feed, - External, -} - -/// Préférence de genre musical -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GenrePreference { - pub genre: String, - pub weight: f32, - pub listen_count: u32, - pub average_completion: f32, - pub last_listened: SystemTime, - pub trending: bool, -} - -/// Préférence d'artiste -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ArtistPreference { - pub artist: String, - pub weight: f32, - pub tracks_listened: u32, - pub average_completion: f32, - pub last_listened: SystemTime, - pub following: bool, -} - -/// Préférences de tempo -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct TempoPreferences { - pub preferred_bpm_range: (f32, f32), - pub energy_level: f32, // 0.0 - 1.0 - pub danceability: f32, // 0.0 - 1.0 - pub valence: f32, // 0.0 - 1.0 (positivity) -} - -/// Préférences de découverte -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DiscoveryPreferences { - pub familiarity_ratio: f32, // 0.0 = toujours nouveau, 1.0 = toujours familier - pub genre_diversity: f32, // 0.0 - 1.0 - pub popularity_bias: f32, // 0.0 = underground, 1.0 = mainstream - pub recency_preference: f32, // 0.0 = classiques, 1.0 = nouveau - pub language_preferences: Vec, - pub explicit_content: bool, -} - -/// Matrice de similarité entre tracks -#[derive(Debug, Clone)] -pub struct TrackSimilarityMatrix { - /// track_id -> Vec<(similar_track_id, similarity_score)> - similarities: HashMap>, - last_computed: SystemTime, -} - -impl Default for TrackSimilarityMatrix { - fn default() -> Self { - Self { - similarities: HashMap::new(), - last_computed: SystemTime::now(), - } - } -} - -/// Similarité entre deux tracks -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrackSimilarity { - pub track_id: Uuid, - pub similarity_score: f32, // 0.0 - 1.0 - pub similarity_factors: SimilarityFactors, -} - -/// Facteurs de similarité -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SimilarityFactors { - pub genre_similarity: f32, - pub tempo_similarity: f32, - pub key_similarity: f32, - pub energy_similarity: f32, - pub audio_features_similarity: f32, - pub collaborative_filtering: f32, // Basé sur les écoutes utilisateur -} - -/// Clusters d'utilisateurs similaires -#[derive(Debug, Clone)] -pub struct UserClusters { - /// cluster_id -> Vec - clusters: HashMap>, - /// user_id -> cluster_id - user_to_cluster: HashMap, - cluster_profiles: HashMap, - last_computed: SystemTime, -} - -impl Default for UserClusters { - fn default() -> Self { - Self { - clusters: HashMap::new(), - user_to_cluster: HashMap::new(), - cluster_profiles: HashMap::new(), - last_computed: SystemTime::now(), - } - } -} - -/// Profil d'un cluster d'utilisateurs -#[derive(Debug, Clone)] -pub struct ClusterProfile { - pub cluster_id: u32, - pub size: usize, - pub dominant_genres: Vec, - pub average_age_range: Option<(u8, u8)>, - pub geographic_regions: Vec, - pub listening_patterns: ListeningPatterns, - pub discovery_behavior: DiscoveryBehavior, -} - -/// Patterns d'écoute d'un cluster -#[derive(Debug, Clone, Default)] -pub struct ListeningPatterns { - pub peak_listening_hours: Vec, // 0-23 - pub average_session_duration: Duration, - pub skip_rate: f32, - pub completion_rate: f32, - pub playlist_usage: f32, - pub social_engagement: f32, -} - -/// Comportement de découverte d'un cluster -#[derive(Debug, Clone, Default)] -pub struct DiscoveryBehavior { - pub openness_to_new: f32, - pub genre_exploration: f32, - pub trending_influence: f32, - pub social_influence: f32, - pub recommendation_acceptance: f32, -} - -/// Modèles ML pour recommandations -#[derive(Debug)] -pub struct MLModels { - /// Modèle de collaborative filtering - collaborative_model: Arc>, - /// Modèle de content-based filtering - content_model: Arc>, - /// Modèle hybride - hybrid_model: Arc>, - /// Modèle de tendances - trending_model: Arc>, -} - -/// Modèle de collaborative filtering (simulation) -#[derive(Debug, Default)] -pub struct CollaborativeFilteringModel { - /// Matrice utilisateur-item - user_item_matrix: HashMap<(i64, Uuid), f32>, - /// Facteurs latents utilisateur - user_factors: HashMap>, - /// Facteurs latents tracks - item_factors: HashMap>, - model_accuracy: f32, - last_trained: Option, -} - -/// Modèle content-based (simulation) -#[derive(Debug, Default)] -pub struct ContentBasedModel { - /// Features audio par track - track_features: HashMap, - /// Poids des features - feature_weights: Vec, - model_accuracy: f32, - last_trained: Option, -} - -/// Features audio d'une track -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct AudioFeatures { - pub tempo: f32, - pub key: i8, - pub energy: f32, - pub danceability: f32, - pub valence: f32, - pub acousticness: f32, - pub instrumentalness: f32, - pub speechiness: f32, - pub loudness: f32, - pub duration_ms: u32, - pub time_signature: u8, -} - -/// Modèle hybride combinant collaborative et content-based -#[derive(Debug, Default)] -pub struct HybridModel { - collaborative_weight: f32, - content_weight: f32, - popularity_weight: f32, - recency_weight: f32, - social_weight: f32, - model_accuracy: f32, - last_trained: Option, -} - -/// Modèle de tendances -#[derive(Debug, Default)] -pub struct TrendingModel { - /// Scores de tendance par track - trending_scores: HashMap, - /// Decay factor pour l'ancienneté - time_decay_factor: f32, - /// Weights pour différents signaux - play_weight: f32, - like_weight: f32, - share_weight: f32, - comment_weight: f32, - velocity_weight: f32, -} - -/// Score de tendance d'une track -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrendingScore { - pub track_id: Uuid, - pub score: f32, - pub plays_velocity: f32, // Plays par heure - pub likes_velocity: f32, // Likes par heure - pub shares_velocity: f32, // Shares par heure - pub comments_velocity: f32, // Comments par heure - pub geographic_spread: f32, // Dispersion géographique - pub last_updated: SystemTime, -} - -/// Gestionnaire de trending -#[derive(Debug)] -pub struct TrendingManager { - /// Trending global - global_trending: Arc>>, - /// Trending par genre - genre_trending: Arc>>>, - /// Trending par région - regional_trending: Arc>>>, - /// Configuration - config: TrendingConfig, -} - -/// Track dans le trending -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TrendingTrack { - pub track_id: Uuid, - pub position: u32, - pub previous_position: Option, - pub trend_direction: TrendDirection, - pub trending_score: f32, - pub velocity_score: f32, - pub time_in_trending: Duration, - pub peak_position: u32, -} - -/// Direction de la tendance -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum TrendDirection { - Up(u32), // Positions gagnées - Down(u32), // Positions perdues - Stable, - New, // Nouvelle entry -} - -/// Configuration du trending -#[derive(Debug, Clone)] -pub struct TrendingConfig { - pub update_interval: Duration, - pub trending_window: Duration, // Fenêtre de temps pour calcul - pub max_trending_items: usize, - pub min_plays_threshold: u32, - pub geographic_regions: Vec, - pub decay_factor: f32, -} - -/// Gestionnaire de charts -#[derive(Debug)] -pub struct ChartsManager { - /// Chart global Top 50 - global_chart: Arc>, - /// Charts par genre - genre_charts: Arc>>, - /// Chart "New & Hot" - new_hot_chart: Arc>, - /// Chart découvertes de la semaine - weekly_discovery_chart: Arc>, - /// Configuration - config: ChartsConfig, -} - -/// Chart musical -#[derive(Debug, Clone)] -pub struct Chart { - pub chart_type: ChartType, - pub period: ChartPeriod, - pub entries: Vec, - pub last_updated: SystemTime, - pub total_entries: usize, -} - -/// Type de chart -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ChartType { - Global, - Genre(String), - NewHot, - WeeklyDiscovery, - Regional(String), -} - -/// Période du chart -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ChartPeriod { - Daily, - Weekly, - Monthly, - AllTime, -} - -/// Entrée dans un chart -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChartEntry { - pub position: u32, - pub previous_position: Option, - pub track_id: Uuid, - pub chart_score: f32, - pub plays_count: u64, - pub weeks_on_chart: u32, - pub peak_position: u32, - pub trend: TrendDirection, -} - -/// Configuration des charts -#[derive(Debug, Clone)] -pub struct ChartsConfig { - pub update_interval: Duration, - pub chart_size: usize, - pub min_chart_threshold: u32, - pub supported_genres: Vec, - pub new_track_window: Duration, // Pour "New & Hot" -} - -/// Gestionnaire de stations radio -#[derive(Debug)] -pub struct RadioManager { - /// Stations actives - active_stations: Arc>>, - /// Stations par utilisateur - user_stations: Arc>>>, - /// Configuration - config: RadioConfig, -} - -/// Station radio personnalisée -#[derive(Debug, Clone)] -pub struct RadioStation { - pub id: Uuid, - pub user_id: i64, - pub station_type: RadioStationType, - pub name: String, - pub description: Option, - pub seed_tracks: Vec, - pub generated_queue: VecDeque, - pub played_tracks: Vec, - pub current_position: usize, - pub last_updated: SystemTime, - pub total_listening_time: Duration, -} - -/// Type de station radio -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum RadioStationType { - /// Basée sur des tracks seed - TrackSeed(Vec), - /// Basée sur des artistes - ArtistSeed(Vec), - /// Basée sur un genre - GenreSeed(String), - /// Découverte personnalisée - PersonalizedDiscovery, - /// Trending mix - TrendingMix, - /// Deep cuts (tracks moins connues) - DeepCuts, - /// Focus genre avec évolution - GenreEvolution(String), -} - -/// Configuration des stations radio -#[derive(Debug, Clone)] -pub struct RadioConfig { - pub max_stations_per_user: usize, - pub queue_size: usize, - pub similarity_threshold: f32, - pub diversity_factor: f32, - pub discovery_ratio: f32, // Ratio de tracks inconnues -} - -/// Tracker d'engagement -#[derive(Debug)] -pub struct EngagementTracker { - /// Métriques d'engagement par recommandation - recommendation_metrics: Arc>>, - /// Feedback utilisateur - user_feedback: Arc>>>, - /// A/B tests actifs - ab_tests: Arc>>, -} - -/// Métriques d'une recommandation -#[derive(Debug, Clone, Default)] -pub struct RecommendationMetrics { - pub recommendation_id: Uuid, - pub user_id: i64, - pub track_id: Uuid, - pub algorithm_used: String, - pub confidence_score: f32, - pub clicked: bool, - pub played: bool, - pub completion_rate: f32, - pub liked: bool, - pub shared: bool, - pub feedback_score: Option, // 1-5 stars - pub timestamp: SystemTime, -} - -/// Feedback utilisateur -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserFeedback { - pub feedback_id: Uuid, - pub user_id: i64, - pub track_id: Uuid, - pub feedback_type: FeedbackType, - pub score: Option, - pub comment: Option, - pub timestamp: SystemTime, -} - -/// Type de feedback -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum FeedbackType { - Like, - Dislike, - NotInterested, - PlayedOften, - Shared, - AddedToPlaylist, - Rating(f32), // 1-5 stars - TextFeedback(String), -} - -/// Test A/B pour algorithmes -#[derive(Debug, Clone)] -pub struct ABTest { - pub test_id: String, - pub test_name: String, - pub algorithm_a: String, - pub algorithm_b: String, - pub user_assignments: HashMap, // user_id -> algorithm - pub metrics_a: ABTestMetrics, - pub metrics_b: ABTestMetrics, - pub start_date: SystemTime, - pub end_date: SystemTime, - pub active: bool, -} - -/// Métriques d'un test A/B -#[derive(Debug, Clone, Default)] -pub struct ABTestMetrics { - pub total_users: u32, - pub total_recommendations: u32, - pub click_through_rate: f32, - pub play_rate: f32, - pub completion_rate: f32, - pub like_rate: f32, - pub share_rate: f32, - pub average_rating: f32, -} - -/// Configuration du discovery -#[derive(Debug, Clone)] -pub struct DiscoveryConfig { - pub recommendation_config: RecommendationConfig, - pub trending_config: TrendingConfig, - pub charts_config: ChartsConfig, - pub radio_config: RadioConfig, - pub max_recommendations_per_request: usize, - pub enable_ab_testing: bool, - pub ml_model_retrain_interval: Duration, -} - -/// Configuration des recommandations -#[derive(Debug, Clone)] -pub struct RecommendationConfig { - pub max_similar_tracks: usize, - pub similarity_threshold: f32, - pub diversity_factor: f32, - pub popularity_boost: f32, - pub recency_boost: f32, - pub social_boost: f32, - pub cold_start_fallback: bool, - pub explicit_content_filter: bool, -} - -impl Default for DiscoveryConfig { - fn default() -> Self { - Self { - recommendation_config: RecommendationConfig::default(), - trending_config: TrendingConfig::default(), - charts_config: ChartsConfig::default(), - radio_config: RadioConfig::default(), - max_recommendations_per_request: 50, - enable_ab_testing: true, - ml_model_retrain_interval: Duration::from_secs(3600), // 1 heure - } - } -} - -impl Default for RecommendationConfig { - fn default() -> Self { - Self { - max_similar_tracks: 100, - similarity_threshold: 0.5, - diversity_factor: 0.3, - popularity_boost: 0.1, - recency_boost: 0.15, - social_boost: 0.2, - cold_start_fallback: true, - explicit_content_filter: false, - } - } -} - -impl Default for TrendingConfig { - fn default() -> Self { - Self { - update_interval: Duration::from_secs(300), // 5 minutes - trending_window: Duration::from_secs(86400), // 24 heures - max_trending_items: 50, - min_plays_threshold: 100, - geographic_regions: vec![ - "US".to_string(), - "UK".to_string(), - "DE".to_string(), - "FR".to_string(), - "JP".to_string(), - ], - decay_factor: 0.95, - } - } -} - -impl Default for ChartsConfig { - fn default() -> Self { - Self { - update_interval: Duration::from_secs(3600), // 1 heure - chart_size: 50, - min_chart_threshold: 1000, - supported_genres: vec![ - "Electronic".to_string(), - "Hip Hop".to_string(), - "Rock".to_string(), - "Pop".to_string(), - "Jazz".to_string(), - "Classical".to_string(), - "R&B".to_string(), - "Country".to_string(), - "Reggae".to_string(), - "Blues".to_string(), - ], - new_track_window: Duration::from_secs(604800), // 1 semaine - } - } -} - -impl Default for RadioConfig { - fn default() -> Self { - Self { - max_stations_per_user: 10, - queue_size: 100, - similarity_threshold: 0.6, - diversity_factor: 0.25, - discovery_ratio: 0.3, - } - } -} - -impl DiscoveryEngine { - /// Crée un nouveau moteur de découverte - pub async fn new( - config: DiscoveryConfig, - social_manager: Arc, - ) -> Result { - let recommendation_engine = Arc::new(RecommendationEngine::new(config.recommendation_config.clone()).await?); - let trending_manager = Arc::new(TrendingManager::new(config.trending_config.clone())); - let charts_manager = Arc::new(ChartsManager::new(config.charts_config.clone())); - let radio_manager = Arc::new(RadioManager::new(config.radio_config.clone())); - let engagement_tracker = Arc::new(EngagementTracker::new()); - - Ok(Self { - recommendation_engine, - trending_manager, - charts_manager, - radio_manager, - engagement_tracker, - config, - }) - } - - /// Obtient des recommandations personnalisées pour un utilisateur - pub async fn get_personalized_recommendations( - &self, - user_id: i64, - count: usize, - seed_tracks: Option>, - ) -> Result, AppError> { - let count = count.min(self.config.max_recommendations_per_request); - - // Obtenir le profil utilisateur - let user_profile = self.recommendation_engine - .get_user_profile(user_id) - .await - .unwrap_or_else(|| UserListeningProfile::new(user_id)); - - // Générer les recommandations selon différents algorithmes - let mut recommendations = Vec::new(); - - // 60% collaborative filtering - let collaborative_count = (count as f32 * 0.6) as usize; - let mut collaborative = self.recommendation_engine - .get_collaborative_recommendations(user_id, collaborative_count) - .await?; - recommendations.append(&mut collaborative); - - // 30% content-based - let content_count = (count as f32 * 0.3) as usize; - let mut content_based = self.recommendation_engine - .get_content_based_recommendations(user_id, content_count, seed_tracks.clone()) - .await?; - recommendations.append(&mut content_based); - - // 10% trending/social - let trending_count = count - recommendations.len(); - let mut trending = self.get_trending_recommendations(user_id, trending_count).await?; - recommendations.append(&mut trending); - - // Diversifier et re-scorer - let final_recommendations = self.diversify_and_score_recommendations( - recommendations, - &user_profile, - count, - ).await?; - - // Tracker les recommandations pour analytics - self.track_recommendations(&final_recommendations, user_id).await?; - - Ok(final_recommendations) - } - - /// Obtient les tracks trending - pub async fn get_trending_tracks( - &self, - genre: Option, - region: Option, - limit: Option, - ) -> Result, AppError> { - self.trending_manager.get_trending(genre, region, limit).await - } - - /// Obtient un chart spécifique - pub async fn get_chart( - &self, - chart_type: ChartType, - period: ChartPeriod, - limit: Option, - ) -> Result { - self.charts_manager.get_chart(chart_type, period, limit).await - } - - /// Crée une station radio personnalisée - pub async fn create_radio_station( - &self, - user_id: i64, - station_type: RadioStationType, - name: String, - ) -> Result { - self.radio_manager.create_station(user_id, station_type, name).await - } - - /// Obtient les recommendations trending - async fn get_trending_recommendations( - &self, - user_id: i64, - count: usize, - ) -> Result, AppError> { - // Simulation - obtenir les tracks trending qui correspondent au profil utilisateur - let trending = self.trending_manager.get_trending(None, None, Some(count * 2)).await?; - - let mut recommendations = Vec::new(); - for (i, track) in trending.iter().enumerate().take(count) { - recommendations.push(RecommendationResult { - track_id: track.track_id, - confidence_score: 0.8 - (i as f32 * 0.01), // Score décroissant - reason: RecommendationReason::Trending, - algorithm_used: "trending".to_string(), - metadata: None, - }); - } - - Ok(recommendations) - } - - /// Diversifie et score les recommandations - async fn diversify_and_score_recommendations( - &self, - mut recommendations: Vec, - user_profile: &UserListeningProfile, - target_count: usize, - ) -> Result, AppError> { - // Supprimer les doublons - recommendations.sort_by_key(|r| r.track_id); - recommendations.dedup_by_key(|r| r.track_id); - - // Re-scorer selon le profil utilisateur - for rec in &mut recommendations { - rec.confidence_score = self.calculate_personalized_score(rec, user_profile).await; - } - - // Trier par score - recommendations.sort_by(|a, b| b.confidence_score.partial_cmp(&a.confidence_score).unwrap_or(std::cmp::Ordering::Equal)); - - // Diversifier par genre - let diversified = self.diversify_by_genre(recommendations, target_count).await; - - Ok(diversified) - } - - /// Calcule un score personnalisé - async fn calculate_personalized_score( - &self, - recommendation: &RecommendationResult, - user_profile: &UserListeningProfile, - ) -> f32 { - // Simulation de scoring personnalisé - let mut score = recommendation.confidence_score; - - // Ajuster selon les préférences de découverte - match recommendation.reason { - RecommendationReason::Trending => { - score *= user_profile.discovery_preferences.popularity_bias; - } - RecommendationReason::SimilarToLiked => { - score *= (1.0 - user_profile.discovery_preferences.familiarity_ratio); - } - _ => {} - } - - score.clamp(0.0, 1.0) - } - - /// Diversifie par genre - async fn diversify_by_genre( - &self, - recommendations: Vec, - target_count: usize, - ) -> Vec { - // Simulation de diversification par genre - recommendations.into_iter().take(target_count).collect() - } - - /// Tracker les recommandations pour analytics - async fn track_recommendations( - &self, - recommendations: &[RecommendationResult], - user_id: i64, - ) -> Result<(), AppError> { - for rec in recommendations { - let metrics = RecommendationMetrics { - recommendation_id: Uuid::new_v4(), - user_id, - track_id: rec.track_id, - algorithm_used: rec.algorithm_used.clone(), - confidence_score: rec.confidence_score, - clicked: false, - played: false, - completion_rate: 0.0, - liked: false, - shared: false, - feedback_score: None, - timestamp: SystemTime::now(), - }; - - self.engagement_tracker.track_recommendation(metrics).await; - } - Ok(()) - } -} - -/// Résultat d'une recommandation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecommendationResult { - pub track_id: Uuid, - pub confidence_score: f32, - pub reason: RecommendationReason, - pub algorithm_used: String, - pub metadata: Option>, -} - -/// Raison de la recommandation -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum RecommendationReason { - SimilarToLiked, - PopularInGenre, - FriendsAlsoLike, - Trending, - BasedOnHistory, - NewRelease, - DeepCut, - PersonalizedRadio, -} - -impl UserListeningProfile { - fn new(user_id: i64) -> Self { - Self { - user_id, - listening_history: VecDeque::new(), - genre_preferences: HashMap::new(), - artist_preferences: HashMap::new(), - tempo_preferences: TempoPreferences::default(), - discovery_preferences: DiscoveryPreferences { - familiarity_ratio: 0.7, - genre_diversity: 0.5, - popularity_bias: 0.6, - recency_preference: 0.4, - language_preferences: vec!["en".to_string()], - explicit_content: false, - }, - last_updated: SystemTime::now(), - } - } -} - -impl RecommendationEngine { - async fn new(config: RecommendationConfig) -> Result { - Ok(Self { - user_listening_history: Arc::new(RwLock::new(HashMap::new())), - track_similarity_matrix: Arc::new(RwLock::new(TrackSimilarityMatrix::default())), - user_clusters: Arc::new(RwLock::new(UserClusters::default())), - ml_models: Arc::new(MLModels::new()), - config, - }) - } - - async fn get_user_profile(&self, user_id: i64) -> Option { - let profiles = self.user_listening_history.read().await; - profiles.get(&user_id).cloned() - } - - async fn get_collaborative_recommendations( - &self, - user_id: i64, - count: usize, - ) -> Result, AppError> { - // Simulation collaborative filtering - let mut recommendations = Vec::new(); - - for i in 0..count { - recommendations.push(RecommendationResult { - track_id: Uuid::new_v4(), - confidence_score: 0.9 - (i as f32 * 0.05), - reason: RecommendationReason::SimilarToLiked, - algorithm_used: "collaborative_filtering".to_string(), - metadata: None, - }); - } - - Ok(recommendations) - } - - async fn get_content_based_recommendations( - &self, - user_id: i64, - count: usize, - seed_tracks: Option>, - ) -> Result, AppError> { - // Simulation content-based filtering - let mut recommendations = Vec::new(); - - for i in 0..count { - recommendations.push(RecommendationResult { - track_id: Uuid::new_v4(), - confidence_score: 0.85 - (i as f32 * 0.03), - reason: RecommendationReason::BasedOnHistory, - algorithm_used: "content_based".to_string(), - metadata: None, - }); - } - - Ok(recommendations) - } -} - -impl MLModels { - fn new() -> Self { - Self { - collaborative_model: Arc::new(Mutex::new(CollaborativeFilteringModel::default())), - content_model: Arc::new(Mutex::new(ContentBasedModel::default())), - hybrid_model: Arc::new(Mutex::new(HybridModel::default())), - trending_model: Arc::new(Mutex::new(TrendingModel::default())), - } - } -} - -impl TrendingManager { - fn new(config: TrendingConfig) -> Self { - Self { - global_trending: Arc::new(RwLock::new(Vec::new())), - genre_trending: Arc::new(RwLock::new(HashMap::new())), - regional_trending: Arc::new(RwLock::new(HashMap::new())), - config, - } - } - - async fn get_trending( - &self, - genre: Option, - region: Option, - limit: Option, - ) -> Result, AppError> { - let limit = limit.unwrap_or(self.config.max_trending_items); - - let trending = match (genre, region) { - (Some(g), _) => { - let genre_trending = self.genre_trending.read().await; - genre_trending.get(&g).cloned().unwrap_or_default() - } - (None, Some(r)) => { - let regional_trending = self.regional_trending.read().await; - regional_trending.get(&r).cloned().unwrap_or_default() - } - (None, None) => { - let global_trending = self.global_trending.read().await; - global_trending.clone() - } - }; - - Ok(trending.into_iter().take(limit).collect()) - } -} - -impl ChartsManager { - fn new(config: ChartsConfig) -> Self { - Self { - global_chart: Arc::new(RwLock::new(Chart { - chart_type: ChartType::Global, - period: ChartPeriod::Weekly, - entries: Vec::new(), - last_updated: SystemTime::now(), - total_entries: 0, - })), - genre_charts: Arc::new(RwLock::new(HashMap::new())), - new_hot_chart: Arc::new(RwLock::new(Chart { - chart_type: ChartType::NewHot, - period: ChartPeriod::Weekly, - entries: Vec::new(), - last_updated: SystemTime::now(), - total_entries: 0, - })), - weekly_discovery_chart: Arc::new(RwLock::new(Chart { - chart_type: ChartType::WeeklyDiscovery, - period: ChartPeriod::Weekly, - entries: Vec::new(), - last_updated: SystemTime::now(), - total_entries: 0, - })), - config, - } - } - - async fn get_chart( - &self, - chart_type: ChartType, - period: ChartPeriod, - limit: Option, - ) -> Result { - let limit = limit.unwrap_or(self.config.chart_size); - - let mut chart = match chart_type { - ChartType::Global => self.global_chart.read().await.clone(), - ChartType::NewHot => self.new_hot_chart.read().await.clone(), - ChartType::WeeklyDiscovery => self.weekly_discovery_chart.read().await.clone(), - ChartType::Genre(genre) => { - let genre_charts = self.genre_charts.read().await; - genre_charts.get(&genre).cloned().unwrap_or_else(|| Chart { - chart_type: ChartType::Genre(genre), - period, - entries: Vec::new(), - last_updated: SystemTime::now(), - total_entries: 0, - }) - } - ChartType::Regional(region) => Chart { - chart_type: ChartType::Regional(region), - period, - entries: Vec::new(), - last_updated: SystemTime::now(), - total_entries: 0, - } - }; - - chart.entries.truncate(limit); - Ok(chart) - } -} - -impl RadioManager { - fn new(config: RadioConfig) -> Self { - Self { - active_stations: Arc::new(RwLock::new(HashMap::new())), - user_stations: Arc::new(RwLock::new(HashMap::new())), - config, - } - } - - async fn create_station( - &self, - user_id: i64, - station_type: RadioStationType, - name: String, - ) -> Result { - // Vérifier la limite par utilisateur - { - let user_stations = self.user_stations.read().await; - if let Some(stations) = user_stations.get(&user_id) { - if stations.len() >= self.config.max_stations_per_user { - return Err(AppError::ValidationError(format!( - "Max stations limit reached: {}", - self.config.max_stations_per_user - ))); - } - } - } - - let station_id = Uuid::new_v4(); - let station = RadioStation { - id: station_id, - user_id, - station_type, - name, - description: None, - seed_tracks: Vec::new(), - generated_queue: VecDeque::new(), - played_tracks: Vec::new(), - current_position: 0, - last_updated: SystemTime::now(), - total_listening_time: Duration::ZERO, - }; - - // Ajouter la station - { - let mut active_stations = self.active_stations.write().await; - active_stations.insert(station_id, station); - } - - // Ajouter à l'utilisateur - { - let mut user_stations = self.user_stations.write().await; - user_stations.entry(user_id).or_insert_with(Vec::new).push(station_id); - } - - tracing::info!("Station radio créée: {} pour utilisateur {}", station_id, user_id); - Ok(station_id) - } -} - -impl EngagementTracker { - fn new() -> Self { - Self { - recommendation_metrics: Arc::new(RwLock::new(HashMap::new())), - user_feedback: Arc::new(RwLock::new(HashMap::new())), - ab_tests: Arc::new(RwLock::new(HashMap::new())), - } - } - - async fn track_recommendation(&self, metrics: RecommendationMetrics) { - let mut recommendation_metrics = self.recommendation_metrics.write().await; - recommendation_metrics.insert(metrics.recommendation_id, metrics); - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/management.rs b/veza-stream-server/src/soundcloud/management.rs deleted file mode 100644 index cb5a193c3..000000000 --- a/veza-stream-server/src/soundcloud/management.rs +++ /dev/null @@ -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, - pub policy_rules: Vec, - pub takedown_queue: Vec, - pub appeal_system: AppealSystem, -} - -/// Gestionnaire de droits d'auteur et licences -#[derive(Debug, Clone)] -pub struct RightsManager { - pub copyright_db: HashMap, - pub licensing_deals: Vec, - pub dmca_system: DmcaSystem, - pub royalty_calculator: RoyaltyCalculator, -} - -/// Gestionnaire de communautés et groupes -#[derive(Debug, Clone)] -pub struct CommunityManager { - pub groups: HashMap, - pub events: Vec, - pub featured_content: Vec, - pub creator_programs: Vec, -} - -/// 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, - 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, - pub severity: ModerationSeverity, - pub status: ModerationStatus, - pub created_at: SystemTime, - pub reviewed_at: Option, -} - -/// 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, - pub actions: Vec, - 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 }, -} - -/// 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, - pub review_queue: Vec, - pub escalation_rules: Vec, -} - -/// 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, - pub status: AppealStatus, - pub submitted_at: SystemTime, - pub resolved_at: Option, -} - -/// 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, - 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, -} - -/// 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, - pub email: String, - pub phone: Option, - pub address: String, - pub is_rights_holder: bool, - pub authorization_details: Option, -} - -/// 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, - pub registration_number: Option, - pub infringement_description: String, - pub original_work_url: Option, -} - -/// Système DMCA -#[derive(Debug, Clone)] -pub struct DmcaSystem { - pub takedown_requests: Vec, - pub counter_notices: Vec, - 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, - pub copyright_holders: Vec, - pub license_type: LicenseType, - pub usage_rights: UsageRights, - pub expiration_date: Option, -} - -/// 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, // Codes pays ISO - pub duration: LicenseDuration, - pub royalty_rate: f32, // Pourcentage - pub minimum_guarantee: Option, // 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, - pub splits: HashMap, // 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, - 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, - pub moderator_ids: Vec, - pub rules: Vec, - pub created_at: SystemTime, -} - -/// Catégories de groupes -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum GroupCategory { - Genre { name: String }, - Location { country: String, city: Option }, - 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, - pub is_paid: bool, - pub ticket_price: Option, -} - -/// 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 { - // 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 { - 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 { - 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 { - // 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 { - // 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 { - // 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 { - 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 { - 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, -} - -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); - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/mod.rs b/veza-stream-server/src/soundcloud/mod.rs deleted file mode 100644 index 1d2af4e8c..000000000 --- a/veza-stream-server/src/soundcloud/mod.rs +++ /dev/null @@ -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::*; \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/playback.rs b/veza-stream-server/src/soundcloud/playback.rs deleted file mode 100644 index 25304c23c..000000000 --- a/veza-stream-server/src/soundcloud/playback.rs +++ /dev/null @@ -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>>>, - /// Configuration globale - config: PlaybackConfig, - /// Gestionnaire de streams - stream_manager: Arc, - /// Événements de playback - event_sender: broadcast::Sender, -} - -/// 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>, - - /// Queue de lecture - pub queue: Arc>, - - /// Configuration du player - pub config: PlayerConfig, - - /// Contrôleur de crossfade - crossfade_controller: Arc>, - - /// Gestionnaire de commentaires temporels - timed_comments: Arc>, - - /// Analytics de session - session_analytics: Arc>, - - /// Événements du player - event_sender: mpsc::UnboundedSender, -} - -/// État de lecture du player -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PlaybackState { - pub current_track: Option, - 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, - /// Pistes dans la queue - pub tracks: Vec, - /// Historique de lecture - pub play_history: VecDeque, - /// Queue "up next" priorisée - pub up_next: VecDeque, - /// 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, - pub duration: Duration, - pub stream_url: String, - pub waveform_url: Option, - pub artwork_url: Option, - pub genres: Vec, - pub bpm: Option, - pub key: Option, - 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, - pub remaining_indices: Vec, - 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, -} - -/// 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>, // 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, -} - -/// 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, - 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, - pub artists_played: HashMap, - pub skip_patterns: Vec, - 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, - }, - /// 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, - 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, - ) -> Result { - 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, 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, - ) -> 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 { - self.event_sender.subscribe() - } -} - -impl SoundCloudPlayer { - /// Crée un nouveau player - pub fn new( - user_id: i64, - config: PlayerConfig, - global_event_sender: broadcast::Sender, - ) -> Result { - 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, - ) -> 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, 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, 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 { - 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 { - let comments_manager = self.timed_comments.read().await; - comments_manager.comments.get(×tamp_ms).cloned().unwrap_or_default() - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/social.rs b/veza-stream-server/src/soundcloud/social.rs deleted file mode 100644 index d01c16f10..000000000 --- a/veza-stream-server/src/soundcloud/social.rs +++ /dev/null @@ -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>, - /// Likes par track - track_likes: Arc>>, - /// Reposts par track - track_reposts: Arc>>, - /// Commentaires par track - track_comments: Arc>>, - /// Configuration - config: SocialConfig, - /// Événements sociaux - event_sender: broadcast::Sender, - /// Cache des feeds - feed_cache: Arc>>, -} - -/// Graphe des relations sociales -#[derive(Debug, Clone, Default)] -pub struct FollowGraph { - /// user_id -> Set des utilisateurs suivis - following: HashMap>, - /// user_id -> Set des followers - followers: HashMap>, - /// Statistiques par utilisateur - user_stats: HashMap, -} - -/// 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, - /// Timeline des likes pour analytics - pub like_timeline: Vec, -} - -/// 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, -} - -/// 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, - 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, - /// Index par timestamp pour commentaires temporels - pub timed_comments: HashMap>, // 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, // Pour les réponses - pub content: String, - pub timestamp_ms: Option, // Pour commentaires temporels sur waveform - pub created_at: SystemTime, - pub likes_count: u32, - pub replies_count: u32, - pub edited: bool, - pub edited_at: Option, -} - -/// 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, - pub last_updated: SystemTime, - pub has_more: bool, - pub next_cursor: Option, -} - -/// 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, // 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, - visibility: RepostVisibility, - ) -> Result { - 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, - timestamp_ms: Option, - ) -> Result { - // 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 { - 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 { - 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) -> Vec { - 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 { - self.event_sender.subscribe() - } - - /// Met à jour les stats d'un utilisateur - fn update_user_stats(&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); - } - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/upload.rs b/veza-stream-server/src/soundcloud/upload.rs deleted file mode 100644 index 3a5fd8713..000000000 --- a/veza-stream-server/src/soundcloud/upload.rs +++ /dev/null @@ -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>>, - /// Configuration - config: UploadConfig, - /// Générateur de waveform - waveform_generator: Arc, - /// Extracteur de métadonnées - metadata_extractor: Arc, - /// Stockage des fichiers - storage: Arc, - /// Événements d'upload - event_sender: mpsc::UnboundedSender, -} - -/// 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, - pub waveform: Option, - 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, - pub estimated_time_remaining: Option, - 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, - pub artist: Option, - pub album: Option, - pub genre: Option, - pub year: Option, - pub track_number: Option, - pub duration: Option, - - // Métadonnées techniques - pub sample_rate: u32, - pub bitrate: u32, - pub channels: u8, - pub bit_depth: Option, - pub codec: String, - pub file_format: String, - - // Métadonnées avancées - pub bpm: Option, - pub key: Option, - pub loudness_lufs: Option, - pub peak_db: Option, - pub dynamic_range: Option, - - // Identifiants - pub isrc: Option, - pub mbid: Option, // MusicBrainz ID - - // Artwork - pub has_artwork: bool, - pub artwork_size: Option<(u32, u32)>, - - // Métadonnées personnalisées - pub custom_tags: HashMap, -} - -/// Configuration de l'upload -#[derive(Debug, Clone)] -pub struct UploadConfig { - pub max_file_size: u64, // bytes - pub allowed_formats: Vec, - 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; - async fn get_file(&self, file_id: &str) -> Result; - async fn delete_file(&self, file_id: &str) -> Result<(), AppError>; - async fn list_user_files(&self, user_id: i64) -> Result, 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, - pub cdn_url: Option, - 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 { - 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 { - // 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 { - // 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 { - // 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 { - // 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 { - 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 { - 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 { - // 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, AppError> { - // Simulation de listing - Ok(Vec::new()) - } -} \ No newline at end of file diff --git a/veza-stream-server/src/soundcloud/waveform.rs b/veza-stream-server/src/soundcloud/waveform.rs deleted file mode 100644 index 309d49355..000000000 --- a/veza-stream-server/src/soundcloud/waveform.rs +++ /dev/null @@ -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>>, -} - -/// 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, -} - -/// 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, - - /// Données spectrales (optionnel) - pub spectral_data: Option, - - /// 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, - /// Fréquences centrales des bandes - pub frequency_bins: Vec, - /// 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, - /// 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, - /// Clé détectée (optionnel) - pub estimated_key: Option, -} - -/// 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, -} - -/// 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>(&self, file_path: P) -> Result { - 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 { - 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 { - // 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 { - 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 { - // 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 { - // 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 { - 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 { - 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 { - 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::() / 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 { - 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, 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 { - let width = 800; - let height = 200; - let center_y = height / 2; - - let mut svg = format!( - "\n\n", - 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)); - svg.push_str(""); - - Ok(svg) - } - - /// Génère une image PNG de la waveform - fn generate_png_waveform(&self, _waveform: &WaveformData, _width: u32, _height: u32) -> Result, AppError> { - // Simulation - en production, utiliser une lib comme `image` ou `skia` - Ok(Vec::new()) - } -} - -/// Données audio brutes -#[derive(Debug)] -struct AudioData { - samples: Vec, - 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, - } - } -} \ No newline at end of file diff --git a/veza-stream-server/src/streaming/advanced_streaming.rs b/veza-stream-server/src/streaming/advanced_streaming.rs deleted file mode 100644 index 6f9bdd8d5..000000000 --- a/veza-stream-server/src/streaming/advanced_streaming.rs +++ /dev/null @@ -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, - pub current_quality: String, - pub listeners: Vec, - pub recording_id: Option, - pub webrtc_peer_id: Option, - pub sync_client_id: Option, - 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, -} - -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, - pub tags: Vec, - pub category: String, - pub language: String, - pub thumbnail_url: Option, - pub duration_ms: Option, - pub is_public: bool, - pub scheduled_start: Option, -} - -#[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>>, - webrtc_manager: Arc, - sync_manager: Arc, - recording_manager: Arc, - streaming_tx: broadcast::Sender, - analytics_collector: Arc>>, -} - -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> { - 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> { - 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> { - 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> { - 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> { - 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::() - / 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::() - / 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 { - self.streaming_tx.subscribe() - } - - /// Obtenir les statistiques d'une session spécifique - pub async fn get_session_stats(&self, session_id: &str) -> Option { - let sessions = self.sessions.read().await; - sessions.get(session_id).cloned() - } -} diff --git a/veza-stream-server/src/streaming/mod.rs b/veza-stream-server/src/streaming/mod.rs index 7a65f6b0c..0d46441e1 100644 --- a/veza-stream-server/src/streaming/mod.rs +++ b/veza-stream-server/src/streaming/mod.rs @@ -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::*; diff --git a/veza-stream-server/src/streaming/webrtc.rs b/veza-stream-server/src/streaming/webrtc.rs deleted file mode 100644 index 4eec26440..000000000 --- a/veza-stream-server/src/streaming/webrtc.rs +++ /dev/null @@ -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, - pub username: Option, - pub credential: Option, -} - -#[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, - pub bandwidth_estimate: u32, - pub rtt_ms: Option, - pub jitter_ms: Option, - 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, - sdp_mline_index: Option, - }, - 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>>, - signaling_tx: broadcast::Sender, - stats_tx: mpsc::Sender, -} - -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> { - 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> { - 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 { - 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> { - 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 = peers.values().filter_map(|p| p.rtt_ms).collect(); - if rtts.is_empty() { - 0.0 - } else { - rtts.iter().sum::() as f32 / rtts.len() as f32 - } - }; - - let avg_packet_loss: f32 = { - let losses: Vec = peers.values().map(|p| p.packet_loss_percentage).collect(); - if losses.is_empty() { - 0.0 - } else { - losses.iter().sum::() / 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 { - self.signaling_tx.subscribe() - } - - /// Envoyer un message de signaling - pub async fn send_signaling_message( - &self, - message: WebRTCMessage, - ) -> Result<(), Box> { - self.signaling_tx.send(message)?; - Ok(()) - } -} diff --git a/veza-stream-server/src/streaming/webrtc/config.rs b/veza-stream-server/src/streaming/webrtc/config.rs deleted file mode 100644 index 2abd0def3..000000000 --- a/veza-stream-server/src/streaming/webrtc/config.rs +++ /dev/null @@ -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, - pub signaling_url: String, - pub max_peers: usize, - pub connection_timeout: Duration, - pub heartbeat_interval: Duration, - pub codec_preferences: Vec, - 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::() { - 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::() { - 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::() { - 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::() { - 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 { - // Essayer de parser comme JSON d'abord - if let Ok(servers) = serde_json::from_str::>(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()); - } -}