diff --git a/.cursorrules b/.cursorrules index f13f1199e..047c3486d 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,10 +1,10 @@ # Règles de Développement UI - Projet SaaS -## 0. Scope v0.201 (priorité absolue) +## 0. Scope v0.202 (priorité absolue) -- **Référence** : `docs/V0_201_RELEASE_SCOPE.md` et `docs/SCOPE_CONTROL.md` -- Avant toute modification : vérifier si le changement est **dans le scope v0.201** -- **Autorisé v0.201** : lots E, F, G, H, C, D (Métadonnées, Seller, Recherche avancée, Analytics créateur, Player, Queue) +- **Référence** : `docs/V0_202_RELEASE_SCOPE.md` et `docs/SCOPE_CONTROL.md` +- Avant toute modification : vérifier si le changement est **dans le scope v0.202** +- **Autorisé v0.202** : lots G, H, F, C, D (Recherche avancée, Analytics créateur, Seller, Player, Queue) - **Interdit** : nouvelles routes/pages hors scope, nouvelles dépendances (sauf correctif sécurité) - En cas de doute : ne pas ajouter. Créer une issue pour une version ultérieure. diff --git a/CHANGELOG.md b/CHANGELOG.md index 43de4f5ad..c9dac0823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog - Veza +## [v0.202] - 2026-02-20 + +### Added + +- **Lot G — Recherche avancée** + - Filtre musical_key dans track_search (G1) + - Tri pertinence (relevance) dans SearchService (G2) + - Autocomplete : GET /search/suggestions, dropdown debounced (G3) + - Facettes type (tracks/artistes/playlists/users) dans SearchPage (G4) + - Historique recherche localStorage (G5) +- **Lot H — Analytics créateur** + - GET /analytics/creator/stats, carte Completion Rate (H1) + - GET /analytics/creator/charts, graphiques (H2) + - Taux de complétion intégré dashboard (H3) + - GET /analytics/creator/export CSV/JSON (H4) +- **Lot F — Seller dashboard** + - GET /sell/stats, connexion commerceService (F1) + - Support seller_id=me dans ListProducts (F2) +- **Lot C — Player avancé** + - Crossfade configurable (1–12 s) depuis Settings (C1) + - Gapless préchargement via preloadTrack (C2) + - PiP (Picture-in-Picture) si supporté (C3) +- **Lot D — Autoplay** + - GET /tracks/recommendations (auth), section « À écouter ensuite » dans PlayerQueue (D2) + +### Changed + +- SearchPage : onglets type, suggestions dropdown, historique récent +- AnalyticsViewKpiGrid : métrique Completion Rate +- AnalyticsViewChart : graphiques creator +- SettingsPage : slider crossfade +- PlayerQueue : recommandations quand queue vide (authentifié) +- PlayerStore : crossfadeSeconds, préchargement ~5 s avant fin + +### Documented + +- D1 (queue collaborative) reporté v0.203+ +- V0_202_RELEASE_SCOPE.md, FEATURE_STATUS.md, PROJECT_STATE.md mis à jour + +--- + ## [v0.201] - 2026-02-20 ### Added diff --git a/apps/web/src/components/library/playlists/QueueView.stories.tsx b/apps/web/src/components/library/playlists/QueueView.stories.tsx index 58684ee1b..b01e5da96 100644 --- a/apps/web/src/components/library/playlists/QueueView.stories.tsx +++ b/apps/web/src/components/library/playlists/QueueView.stories.tsx @@ -63,7 +63,7 @@ export const Default: Story = { export const Loading: Story = { name: 'Loading', decorators: [ - (Story) => { + (_Story) => { usePlayerStore.setState({ queue: [], currentIndex: -1, currentTrack: null }); return (
diff --git a/apps/web/src/components/library/playlists/QueueView.tsx b/apps/web/src/components/library/playlists/QueueView.tsx index 69e57f1a6..f1f154708 100644 --- a/apps/web/src/components/library/playlists/QueueView.tsx +++ b/apps/web/src/components/library/playlists/QueueView.tsx @@ -202,8 +202,8 @@ export const QueueView: React.FC = () => { > {upNext.map((track, i) => ( removeFromQueue(currentIndex + 1 + i)} diff --git a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts index 076c0fc5c..c943aa77d 100644 --- a/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts +++ b/apps/web/src/components/settings/profile/edit-profile/useEditProfile.ts @@ -39,15 +39,14 @@ export function useEditProfile() { first_name: p.first_name || '', last_name: p.last_name || '', bio: p.bio || '', - banner_url: p.banner_url || '', + banner_url: (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner ?? '', location: p.location || '', gender: p.gender || 'Prefer not to say', birthdate: p.birthdate || '', }); if (p.avatar_url) setAvatar(p.avatar_url); - if (p.banner_url) { - setBanner(p.banner_url); - } + const bannerUrl = (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner; + if (bannerUrl) setBanner(bannerUrl); } catch (e) { logger.error('Failed to load profile settings', { error: e instanceof Error ? e.message : String(e), diff --git a/apps/web/src/features/analytics/pages/analytics-page/AnalyticsViewKpiGrid.tsx b/apps/web/src/features/analytics/pages/analytics-page/AnalyticsViewKpiGrid.tsx index bcefcdb99..7746d62c2 100644 --- a/apps/web/src/features/analytics/pages/analytics-page/AnalyticsViewKpiGrid.tsx +++ b/apps/web/src/features/analytics/pages/analytics-page/AnalyticsViewKpiGrid.tsx @@ -49,7 +49,7 @@ export function AnalyticsViewKpiGrid({ stats }: AnalyticsViewKpiGridProps) { : '—' } icon={} - color="green" + color="lime" />
); diff --git a/apps/web/src/features/player/components/GlobalPlayer.tsx b/apps/web/src/features/player/components/GlobalPlayer.tsx index 38d4757ac..3d3605dc2 100644 --- a/apps/web/src/features/player/components/GlobalPlayer.tsx +++ b/apps/web/src/features/player/components/GlobalPlayer.tsx @@ -1,6 +1,5 @@ import { useState, useRef, useCallback } from 'react'; import { createPortal } from 'react-dom'; -import { PictureInPicture2 } from 'lucide-react'; import { usePlayer } from '@/features/player/hooks/usePlayer'; import { usePictureInPicture } from '@/features/player/hooks/usePictureInPicture'; import { useKeyboardShortcuts } from '@/features/player/hooks/useKeyboardShortcuts'; @@ -50,8 +49,8 @@ export function GlobalPlayer() { const displayTrack = currentTrack || IDLE_TRACK; const isIdle = !currentTrack; - const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isPiPSupported } = usePictureInPicture( - currentTrack?.cover_art_path ?? null, + const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isSupported: isPiPSupported } = usePictureInPicture( + currentTrack?.cover ?? null, ); useMediaSession({ @@ -72,7 +71,7 @@ export function GlobalPlayer() { className="hidden w-0 h-0" muted playsInline - poster={currentTrack?.cover_art_path ?? undefined} + poster={currentTrack?.cover ?? undefined} /> )} diff --git a/apps/web/src/features/player/components/PlayerQueue.stories.tsx b/apps/web/src/features/player/components/PlayerQueue.stories.tsx index b9ba13bd1..3087f0eaf 100644 --- a/apps/web/src/features/player/components/PlayerQueue.stories.tsx +++ b/apps/web/src/features/player/components/PlayerQueue.stories.tsx @@ -1,7 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { useEffect } from 'react'; import { PlayerQueue } from './PlayerQueue'; import { usePlayerStore } from '../store/playerStore'; -import { useEffect } from 'react'; +import { useAuthStore } from '@/features/auth/store/authStore'; const meta: Meta = { title: 'Components/Features/Player/PlayerQueue', @@ -65,6 +66,14 @@ export const Default: Story = { ], }; +const AuthInitializer = ({ authenticated }: { authenticated: boolean }) => { + useEffect(() => { + useAuthStore.setState({ isAuthenticated: authenticated }); + return () => useAuthStore.setState({ isAuthenticated: false }); + }, [authenticated]); + return null; +}; + export const Empty: Story = { args: { isOpen: true, @@ -80,3 +89,20 @@ export const Empty: Story = { ), ], }; + +export const EmptyWithRecommendations: Story = { + args: { + isOpen: true, + }, + decorators: [ + (Story) => ( + <> + + +
+ +
+ + ), + ], +}; diff --git a/apps/web/src/features/player/components/PlayerQueue.tsx b/apps/web/src/features/player/components/PlayerQueue.tsx index f61faa917..a19805d36 100644 --- a/apps/web/src/features/player/components/PlayerQueue.tsx +++ b/apps/web/src/features/player/components/PlayerQueue.tsx @@ -1,10 +1,16 @@ import { usePlayerStore } from '../store/playerStore'; import { useUIStore } from '@/stores/ui'; import { cn } from '@/lib/utils'; -import { X, GripVertical, ListMusic } from 'lucide-react'; +import { X, GripVertical, ListMusic, Sparkles } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { EmptyState } from '@/components/ui/empty-state'; +import { useQuery } from '@tanstack/react-query'; +import { useAuthStore } from '@/features/auth/store/authStore'; +import { tracksApi } from '@/services/api/tracks'; +import { getHLSMasterPlaylistURL } from '@/features/streaming/services/hlsService'; +import type { Track as PlayerTrack } from '../types'; +import type { Track as ApiTrack } from '@/features/tracks/types/track'; interface PlayerQueueProps { isOpen: boolean; @@ -13,9 +19,31 @@ interface PlayerQueueProps { onPlay: (track: any) => void; } +function mapApiTrackToPlayerTrack(t: ApiTrack): PlayerTrack { + const apiTrack = t as ApiTrack & { stream_manifest_url?: string; cover_art_path?: string }; + return { + id: t.id, + title: t.title, + artist: t.artist, + duration: t.duration ?? 0, + url: apiTrack.stream_manifest_url ?? getHLSMasterPlaylistURL(t.id), + cover: apiTrack.cover_art_path, + genre: t.genre, + like_count: t.like_count, + }; +} + export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) { const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore(); const { sidebarOpen } = useUIStore(); + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); + const { data: recommendations = [], isLoading: recommendationsLoading } = useQuery({ + queryKey: ['track-recommendations'], + queryFn: () => tracksApi.getRecommendations({ limit: 10 }), + enabled: isOpen && isAuthenticated && queue.length === 0, + staleTime: 60_000, + }); + const playerTracks = recommendations.map(mapApiTrackToPlayerTrack); if (!isOpen) return null; @@ -56,13 +84,44 @@ export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) { {/* Content */}
{queue.length === 0 ? ( - } - title="Your queue is empty" - description="Add tracks to keep the vibe going." - size="sm" - className="border-0 shadow-none bg-transparent" - /> +
+ {playerTracks.length > 0 ? ( + <> +

+ + À écouter ensuite +

+ +
+ {playerTracks.map((track) => ( +
onPlay(track)} + > +
+
+

{track.title}

+

{track.artist}

+
+
+ ))} +
+
+

Cliquez pour lire

+ + ) : recommendationsLoading ? ( +
Chargement des suggestions…
+ ) : ( + } + title="Your queue is empty" + description="Add tracks to keep the vibe going." + size="sm" + className="border-0 shadow-none bg-transparent" + /> + )} +
) : (
diff --git a/apps/web/src/features/search/components/search-page/SearchPageDiscovery.tsx b/apps/web/src/features/search/components/search-page/SearchPageDiscovery.tsx index 1d22d98fd..ed0a6fa4f 100644 --- a/apps/web/src/features/search/components/search-page/SearchPageDiscovery.tsx +++ b/apps/web/src/features/search/components/search-page/SearchPageDiscovery.tsx @@ -9,7 +9,7 @@ interface SearchPageDiscoveryProps { export function SearchPageDiscovery({ onQuerySelect }: SearchPageDiscoveryProps) { const { getHistory, clearHistory } = useSearchHistory(); - const [refreshKey, setRefreshKey] = useState(0); + const [, setRefreshKey] = useState(0); const history = getHistory(); const handleClear = () => { diff --git a/apps/web/src/features/tracks/api/trackApi.ts b/apps/web/src/features/tracks/api/trackApi.ts index de1fa62e4..59e47d15d 100644 --- a/apps/web/src/features/tracks/api/trackApi.ts +++ b/apps/web/src/features/tracks/api/trackApi.ts @@ -300,6 +300,24 @@ export async function uploadTrack( * @returns La liste des tracks avec les métadonnées de pagination * @throws Error si la requête échoue */ +/** + * Get personalized track recommendations (D2 autoplay) + * Backend: GET /api/v1/tracks/recommendations (auth required) + */ +export async function getTrackRecommendations( + options?: { limit?: number; seedTrackId?: string }, +): Promise { + const limit = Math.min(Math.max(options?.limit ?? 20, 1), 100); + const params = new URLSearchParams({ limit: String(limit) }); + if (options?.seedTrackId) { + params.set('seed_track_id', options.seedTrackId); + } + const { data } = await apiClient.get<{ tracks: Track[] }>( + `/tracks/recommendations?${params.toString()}`, + ); + return data?.tracks ?? []; +} + export async function getTracks( page: number = 1, limit: number = 20, diff --git a/apps/web/src/mocks/browser.ts b/apps/web/src/mocks/browser.ts index a061ad6b3..4ff10c4e4 100644 --- a/apps/web/src/mocks/browser.ts +++ b/apps/web/src/mocks/browser.ts @@ -2,4 +2,6 @@ import { setupWorker } from 'msw/browser'; import { handlers } from './handlers'; // This configures a Service Worker with the given request handlers. -export const worker = setupWorker(...handlers); +export const worker = setupWorker( + ...(handlers.filter((h): h is NonNullable => Boolean(h))), +); diff --git a/apps/web/src/mocks/handlers-tracks.ts b/apps/web/src/mocks/handlers-tracks.ts index c5970a670..60952735d 100644 --- a/apps/web/src/mocks/handlers-tracks.ts +++ b/apps/web/src/mocks/handlers-tracks.ts @@ -53,6 +53,21 @@ export const handlersTracks = [ }); }), + http.get('*/api/v1/tracks/recommendations', ({ request }) => { + const url = new URL(request.url); + const limit = Math.min(Math.max(parseInt(url.searchParams.get('limit') || '20', 10), 1), 100); + const tracks = Array.from({ length: Math.min(limit, 5) }, (_, i) => + mockTrack({ + id: `rec-${i + 1}`, + title: `Recommended Track ${i + 1}`, + artist: 'Suggested Artist', + duration: 200 + i * 10, + cover_art_path: 'https://picsum.photos/200', + }), + ); + return HttpResponse.json({ success: true, data: { tracks } }); + }), + http.get('*/api/v1/tracks/suggested-tags', ({ request }) => { const url = new URL(request.url); const genre = url.searchParams.get('genre')?.toLowerCase() || 'default'; diff --git a/apps/web/src/services/api/tracks.ts b/apps/web/src/services/api/tracks.ts index 0e43f2f20..d158b2ff4 100644 --- a/apps/web/src/services/api/tracks.ts +++ b/apps/web/src/services/api/tracks.ts @@ -8,6 +8,7 @@ import { uploadTrack, getTracks, + getTrackRecommendations, updateTrack, getTrackStats, getTrackHistory, @@ -54,6 +55,11 @@ export const tracksApi = { */ get: getTrack, + /** + * Get personalized track recommendations (D2 autoplay) + */ + getRecommendations: getTrackRecommendations, + /** * Create/upload a new track */ diff --git a/docs/FEATURE_STATUS.md b/docs/FEATURE_STATUS.md index 245a596b6..8e7142066 100644 --- a/docs/FEATURE_STATUS.md +++ b/docs/FEATURE_STATUS.md @@ -1,6 +1,6 @@ # Statut des fonctionnalités — Veza -**Dernière mise à jour** : février 2026 — v0.201 (Lot E Métadonnées : BPM, key, lyrics, tags) +**Dernière mise à jour** : février 2026 — v0.202 livrée Ce document décrit le statut réel des fonctionnalités par rapport au code. @@ -17,14 +17,14 @@ Ce document décrit le statut réel des fonctionnalités par rapport au code. | Playlists (CRUD, collaboration) | Oui | Oui | Complet | | Chat WebSocket | Oui | Oui | Complet (Chat Server doit être démarré) | | Dashboard | Oui | Oui | GET /api/v1/dashboard | -| Recherche | Oui | Oui | GET /api/v1/search unifié + endpoints par ressource | +| Recherche | Oui | Oui | GET /search unifié, GET /tracks/search. v0.202 : musical_key, tri pertinence, autocomplete /suggestions, facettes type, historique localStorage | | Social (feed, posts, groups, follows, blocks) | Oui | Oui | Complet | | Administration | Oui | Oui | Complet | | Marketplace | Oui | Oui | Complet (Hyperswitch) | | Webhooks | Oui | Oui | Complet | | Inventory / Gear | Oui | Oui | GET/POST/PUT/DELETE /api/v1/inventory/gear | | Live Streaming (métadonnées) | Oui | Oui | GET /api/v1/live/streams — stream vidéo via Stream Server | -| Analytics | Oui | Oui | Routes /api/v1/analytics/* | +| Analytics | Oui | Oui | Routes /api/v1/analytics/*. v0.202 : creator stats/charts/export (Lot H) | | Roles | Oui | Oui | Assign, revoke — flag ROLE_MANAGEMENT | | Notifications | Oui | Oui | Création auto follow/like/comment | @@ -50,10 +50,29 @@ Ce document décrit le statut réel des fonctionnalités par rapport au code. | Feature | Limitation | Version cible | |---------|------------|--------------| | **Go Live** (streaming vidéo) | Non implémenté — toast « coming soon » conservé | v0.703 | -| **Social Trending** (tags tendance) | SocialViewTrending utilise tags statiques ; pas d'API `/social/trending` | v0.103 | +| **Social Trending** (tags tendance) | SocialViewTrending utilise tags statiques ; pas d'API `/social/trending` | v0.203 | | **2FA SMS** | Option « Envoyer par SMS » pendant la vérification 2FA — requiert infra Twilio + users.phone_number | v0.104 | | **Passkeys / WebAuthn** | Login sans mot de passe — requiert go-webauthn, table webauthn_credentials, frontend navigator.credentials | v0.104 | +## Livré en v0.202 + +| Lot | Feature | +|-----|---------| +| G | Recherche avancée (musical_key, tri pertinence, autocomplete, facettes type, historique) | +| H | Analytics créateur (stats, graphiques, taux complétion, export CSV/JSON) | +| F | Seller dashboard (GET /sell/stats, liste produits marketplace) | +| C | Player (crossfade, gapless preload, PiP) | +| D | Autoplay (GET /tracks/recommendations, section « À écouter ensuite » dans PlayerQueue) | + +## Prochaines fonctionnalités v0.203+ + +| Feature | Priorité | +|---------|----------| +| Queue collaborative (D1) | Basse | +| Recherche phonétique, booléenne | v0.203+ | + +--- + ## Projets abandonnés | Projet | Statut | diff --git a/docs/PROJECT_STATE.md b/docs/PROJECT_STATE.md index 20d3a7620..96c7715cc 100644 --- a/docs/PROJECT_STATE.md +++ b/docs/PROJECT_STATE.md @@ -8,10 +8,10 @@ | Élément | Valeur | |---------|--------| -| **Dernier tag** | v0.201 | -| **Branche release** | `release/v0.201` (taguée) | -| **Phase** | Phase 2 Contenu — Lot E livré | -| **Prochaine version** | v0.202 (Lot G, H, F, C, D) | +| **Dernier tag** | v0.202 | +| **Branche release** | `main` (v0.202 mergée) | +| **Phase** | Phase 2 Contenu — Lots G, H, F, C, D livrés | +| **Prochaine version** | v0.203 | --- @@ -26,28 +26,25 @@ - ✅ Lot E — Métadonnées : BPM, musical_key, lyrics, tags (E1–E4) - Migrations : 084 track_lyrics, 085 tracks.tags -### Non livré (reporté v0.202) -- Lot G : Recherche avancée (filtres, tri, autocomplete, facettes) -- Lot H : Analytics créateur (stats, graphiques, export) -- Lot F : Seller dashboard (stats ventes, liste produits) -- Lot C : Player (crossfade, gapless, PiP) -- Lot D : Queue collaborative, Autoplay +### v0.202 (Phase 2 Contenu — Lots G, H, F, C, D) +- Lot G : Recherche avancée (musical_key, tri pertinence, autocomplete, facettes type, historique) +- Lot H : Analytics créateur (stats, graphiques, taux complétion, export CSV/JSON) +- Lot F : Seller dashboard (GET /sell/stats, liste produits marketplace) +- Lot C : Player (crossfade, gapless preload, PiP) +- Lot D : Autoplay (GET /tracks/recommendations, section « À écouter ensuite ») --- ## 3. Prochaines étapes -### Immédiat (préparation v0.202) -1. **Créer la branche** : `release/v0.202` -2. **Mettre à jour SCOPE_CONTROL** : référence v0.202 -3. **Lire** [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) — lots G, H, F, C, D +### Immédiat (préparation v0.203) +1. **Créer la branche** : `git checkout -b release/v0.203` +2. **Définir** le scope dans V0_203_RELEASE_SCOPE.md +3. **PR** : `gh pr create --base main --head release/v0.203 --title "Release v0.203"` -### Ordre d'implémentation recommandé (v0.202) -1. **Lot G** (Recherche avancée) — filtres, tri, autocomplete, facettes -2. **Lot H** (Analytics créateur) — stats, graphiques, export -3. **Lot F** (Seller dashboard) — stats vendeur, liste produits -4. **Lot C** (Player) — crossfade, gapless, PiP -5. **Lot D** (Queue) — autoplay, queue collaborative +### Cibles v0.203+ +- Queue collaborative (D1) +- Recherche phonétique, booléenne --- @@ -55,7 +52,7 @@ | Document | Usage | |----------|-------| -| [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) | Scope détaillé v0.201 | +| [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md) | Scope détaillé v0.202 | | [SCOPE_CONTROL.md](SCOPE_CONTROL.md) | Anti-scope-creep, workflow | | [FEATURE_STATUS.md](FEATURE_STATUS.md) | Statut des features par domaine | | [CHANGELOG.md](../CHANGELOG.md) | Historique des versions | @@ -79,5 +76,4 @@ | Métrique | Valeur | |----------|--------| -| Features livrées (cumul) | ~270 / 600 | -| Cible v0.201 | ~330 / 600 | +| Features livrées (cumul) | ~345 / 600 | diff --git a/docs/SCOPE_CONTROL.md b/docs/SCOPE_CONTROL.md index 7bdac6049..919f35e49 100644 --- a/docs/SCOPE_CONTROL.md +++ b/docs/SCOPE_CONTROL.md @@ -1,19 +1,19 @@ # Contrôle du scope — Anti-scope-creep **Objectif** : Éviter toute dérive de scope. Chaque modification doit être intentionnelle et traçable. -**Référence active** : [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) -**Version précédente** : [V0_103_RELEASE_SCOPE.md](V0_103_RELEASE_SCOPE.md) +**Référence active** : [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md) +**Version précédente** : [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) --- ## 1. Règle d'or -> **Avant d'ajouter quoi que ce soit : vérifier si c'est dans le scope v0.201.** +> **Avant d'ajouter quoi que ce soit : vérifier si c'est dans le scope v0.202.** > Si non → ne pas ajouter. Créer un ticket pour une version ultérieure. --- -## 2. Pendant la phase v0.201 (jusqu'au tag) +## 2. Pendant la phase v0.202 (jusqu'au tag) ### 2.1 Autorisé @@ -26,7 +26,7 @@ ### 2.2 Interdit -- **Nouvelles features** hors scope v0.201 +- **Nouvelles features** hors scope v0.202 - **Nouvelles routes** ou pages hors scope - **Nouvelles dépendances** (sauf correctif sécurité) - **Changements de comportement** sur les features HORS SCOPE @@ -52,7 +52,7 @@ - Non → **STOP.** Est-ce une correction de bug ? Si oui, la feature est-elle IN SCOPE ? 2. **Mon changement ajoute-t-il du code ?** - - Nouvelle route, nouveau composant, nouveau service → **STOP.** Hors scope v0.101. + - Nouvelle route, nouveau composant, nouveau service → **STOP.** Hors scope v0.202. - Correction, refactoring, test → OK si lié à une feature IN SCOPE. 3. **Mes tests passent-ils ?** @@ -81,7 +81,7 @@ Format : `type(scope): description` Dans chaque PR, le relecteur doit valider : -- [ ] Le changement est dans le scope v0.201 (voir [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md)) +- [ ] Le changement est dans le scope v0.202 (voir [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md)) - [ ] Aucune nouvelle feature ajoutée - [ ] Aucune régression sur les flows critiques - [ ] Les tests passent @@ -98,22 +98,22 @@ Une PR sera rejetée si : --- -## 5. Proposer une feature pour APRÈS v0.101 +## 5. Proposer une feature pour APRÈS v0.202 ### 5.1 Template Utiliser le template [Feature request](.github/ISSUE_TEMPLATE/feature_request.md) avec : -- **Alignement scope** : cocher "Hors scope v0.201 — pour v0.202+" +- **Alignement scope** : cocher "Hors scope v0.202 — pour v0.203+" - **Justification** : pourquoi cette feature est nécessaire - **Effort estimé** : S / M / L / XL -- **Dépendances** : quelles features v0.101 doivent être stables avant +- **Dépendances** : quelles features v0.202 doivent être stables avant ### 5.2 Workflow 1. Créer une issue avec le template -2. **Ne pas implémenter** tant que v0.201 n'est pas taguée -3. Une fois v0.201 stable, prioriser les issues "v0.202" dans un nouveau document de scope +2. **Ne pas implémenter** tant que v0.202 n'est pas taguée +3. Une fois v0.202 stable, prioriser les issues "v0.203" dans un nouveau document de scope --- @@ -125,7 +125,7 @@ Si une vulnérabilité critique est identifiée : - Correctif autorisé **immédiatement** - Documenter dans la PR -- Pas besoin d'être dans le scope v0.201 +- Pas besoin d'être dans le scope v0.202 ### 6.2 Blocage production @@ -140,13 +140,13 @@ Pour tout cas ambigu : - Ouvrir une issue "Scope clarification" - Décision documentée dans l'issue -- Mise à jour de V0_201_RELEASE_SCOPE.md si le scope est étendu (exception rare) +- Mise à jour de V0_202_RELEASE_SCOPE.md si le scope est étendu (exception rare) --- ## 7. Après le tag d'une version -1. **Créer** le document de scope de la version suivante (ex: `V0_202_RELEASE_SCOPE.md`) +1. **Créer** le document de scope de la version suivante (ex: `V0_203_RELEASE_SCOPE.md`) 2. **Définir** explicitement les nouvelles features autorisées 3. **Mettre à jour** la référence active dans ce document (section header) 4. **Reprendre** ce processus avec le nouveau document de scope @@ -157,11 +157,12 @@ Pour tout cas ambigu : - v0.102 : Déblocage Coming Soon, renforcement coeur produit (taguée) - v0.103 : Complétion Phase 1 Fondation — Auth A1/A4, Profils B1-B3 (taguée) - v0.201 : Phase 2 Contenu — Lot E Métadonnées (BPM, key, lyrics, tags) — taguée +- v0.202 : Phase 2 Contenu — Lots G, H, F, C, D — taguée --- ## 8. Rappel pour les contributeurs - **Cursor / IA** : Les règles dans `.cursorrules` rappellent de vérifier le scope avant toute modification. -- **Humains** : Lire [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) avant de coder. +- **Humains** : Lire [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md) avant de coder. - **En doute ?** Ouvrir une issue "Scope clarification" plutôt que de coder. diff --git a/docs/V0_202_RELEASE_SCOPE.md b/docs/V0_202_RELEASE_SCOPE.md new file mode 100644 index 000000000..1d7658747 --- /dev/null +++ b/docs/V0_202_RELEASE_SCOPE.md @@ -0,0 +1,201 @@ +# Scope v0.202 — Phase 2 Contenu (suite) + +**Version cible** : v0.202 (X=2, Y=2) +**Prérequis** : v0.201 taguée et mergée dans main +**Objectif** : Phase 2 Contenu — Recherche avancée, Analytics créateur, Seller dashboard, Player, Queue +**Dernière mise à jour** : 20 février 2026 +**Effort estimé** : 5-7 semaines de développement + +--- + +## 1. Principe directeur + +> **v0.202 = suite Phase 2 (Contenu) après v0.201.** +> +> Cinq axes principaux : +> 1. **Recherche avancée (Lot G)** : Filtres, tri, autocomplete, facettes, historique +> 2. **Analytics créateur (Lot H)** : Stats, graphiques, export +> 3. **Seller dashboard (Lot F)** : Stats ventes, liste produits +> 4. **Player avancé (Lot C)** : Crossfade, gapless, PiP +> 5. **Queue avancée (Lot D)** : Autoplay, queue collaborative + +--- + +## 2. Contexte — État post v0.201 + +### 2.1 Livré en v0.201 + +| Lot | Feature | Statut | +|-----|---------|--------| +| E1 | BPM | ✅ | +| E2 | Musical key | ✅ | +| E3 | Lyrics | ✅ | +| E4 | Tags suggérés | ✅ | + +### 2.2 Fondation existante (à enrichir) + +| Domaine | Existant | À ajouter v0.202 | +|---------|----------|-------------------| +| **Recherche tracks** | `TrackSearchService`, GET /tracks/search, filtres BPM/durée/genre/format/date, tri (popularity, title, created_at) | musical_key, autocomplete, unification search unifiée, historique | +| **Search unifiée** | GET /search (q, type=track|user|playlist), SearchService | Filtres, tri, pagination | +| **Analytics** | Routes /api/v1/analytics/*, playback_analytics | Route créateur, graphiques, export | +| **Player** | MediaSession, usePlayer | Crossfade, gapless, PiP | +| **Queue** | Queue sync, PlayerQueue | Autoplay, queue collaborative | + +--- + +## 3. Features IN SCOPE v0.202 + +### 3.1 Lot G — Recherche avancée (priorité haute) + +**Objectif** : Enrichir la recherche existante. +**Effort** : L (5-7 jours) +**Référence** : Module 11 (veza_full_features_list) + +| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie | +|---|---------|-----------------|---------|----------|-------------------| +| G1 | **Filtres recherche** | musical_key manquant ; tags déjà dans TrackSearchParams | Ajouter `musical_key` à TrackSearchParams + handler | Vérifier UI SearchPage/TrackSearch utilise tous les filtres | Filtres appliqués, résultats mis à jour | +| G2 | **Tri avancé** | Tri pertinence, date, popularité, alphabétique | Déjà : sort_by, sort_order. Ajouter "relevance" si full-text | Dropdown tri dans SearchPage | Tri fonctionnel | +| G3 | **Autocomplete** | Suggestions pendant la frappe | Route GET /search/suggestions?q=… (tracks, users, playlists) ou extension GET /search avec limit=5 | Input avec dropdown debounced | Suggestions affichées, clic → recherche | +| G4 | **Recherche par type** | Tracks, artistes, playlists, utilisateurs | SearchService : param `type` existant. Vérifier cohérence | Onglets/facettes dans SearchPage | Résultats par catégorie | +| G5 | **Historique recherche** | Mémoriser les dernières recherches | Optionnel : table search_history. Sinon localStorage | Section "Récent" | Dernières recherches cliquables | + +**Fichiers clés** : +- Backend : `track_search_service.go`, `search_service.go`, `search_handlers.go`, `routes_search.go`, `routes_tracks.go` +- Frontend : `TrackSearch.tsx`, `TrackSearchFilters`, `trackSearchService.ts`, `SearchPage` + +--- + +### 3.2 Lot H — Analytics créateur (priorité haute) + +**Objectif** : Dashboard analytics pour créateurs. +**Effort** : L (5-7 jours) +**Référence** : Module 12.1 + +| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie | +|---|---------|-----------------|---------|----------|-------------------| +| H1 | **Stats d'écoute** | Plays par track, période, durée moyenne | Agrégation playback_analytics, route GET /analytics/creator/stats | Page /analytics ou section créateur | Chiffres affichés | +| H2 | **Graphiques** | Évolution plays, top tracks | Route GET /analytics/creator/charts | Charts (recharts ou équivalent) | Graphiques lisibles | +| H3 | **Taux de complétion** | % d'écoute complète par track | Calcul backend (plays complètes / total) | Affichage dans dashboard | Métrique visible | +| H4 | **Export données** | Export CSV/JSON des stats | Route GET /analytics/creator/export?format=csv|json | Bouton export | Fichier téléchargé | + +**Fichiers clés** : +- Backend : `playback_analytics_handler.go`, `routes`, modèles analytics +- Frontend : Page Analytics, composants charts, export + +--- + +### 3.3 Lot F — Seller dashboard (priorité moyenne) + +**Objectif** : Dashboard vendeur fonctionnel. +**Effort** : M (3-4 jours) +**Référence** : Module 7.4 + +| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie | +|---|---------|-----------------|---------|----------|-------------------| +| F1 | **Stats ventes** | Nombre ventes, revenus, période | Route GET /sell/stats ou agrégation Hyperswitch/marketplace | Page /sell | Chiffres affichés | +| F2 | **Liste produits** | Tracks/albums en vente par l'utilisateur | Extension catalogue existant | Liste dans seller dashboard | Produits listés | + +--- + +### 3.4 Lot C — Player avancé (priorité moyenne) + +**Objectif** : Améliorer l'expérience d'écoute. +**Effort** : M (2-4 jours) +**Référence** : Module 4.1 + +| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie | +|---|---------|-----------------|---------|----------|-------------------| +| C1 | **Crossfade** | Transition en fondu entre tracks | N/A | Logique player (gain, overlap) | Transition fluide 1-10s configurable | +| C2 | **Gapless playback** | Lecture sans silence entre tracks | N/A | Préchargement audio, Web Audio API ou HTMLMediaElement | Pas de coupure | +| C3 | **PiP** | Picture-in-Picture (si supporté) | N/A | Media Session + PiP API (document.pictureInPictureEnabled) | Fenêtre flottante | + +**Fichiers clés** : `GlobalPlayer.tsx`, `AudioPlayer.tsx`, `usePlayer.ts`, `playerStore.ts` + +--- + +### 3.5 Lot D — Queue avancée (priorité basse) + +**Objectif** : Queue collaborative et autoplay. +**Effort** : M (3-4 jours) +**Référence** : Module 4.2 + +| # | Feature | Tâche détaillée | Backend | Frontend | Critère de sortie | +|---|---------|-----------------|---------|----------|-------------------| +| D1 | **Queue collaborative** | Partager queue en session | Modèle session queue partagée, routes queue | UI partage | Plusieurs users, même queue | +| D2 | **Autoplay** | Recommandations quand queue vide | Route GET /recommendations ou extension playlists | Section "À écouter ensuite" | Suggestions, ajout 1 clic | + +--- + +## 4. Récapitulatif par lot + +| Lot | Nom | Priorité | Effort | Features | +|-----|-----|----------|--------|----------| +| **G** | Recherche avancée | Haute | L | 5 | +| **H** | Analytics créateur | Haute | L | 4 | +| **F** | Seller dashboard | Moyenne | M | 2 | +| **C** | Player avancé | Moyenne | M | 3 | +| **D** | Queue avancée | Basse | M | 2 | +| | **TOTAL** | | **16-27j** | **16** | + +--- + +## 5. Features HORS SCOPE v0.202 + +| Feature | Raison | Version cible | +|---------|--------|---------------| +| Recherche phonétique, booléenne | Complexité | v0.203+ | +| Collaborative filtering | Nécessite historique riche | v0.203+ | +| Social Trending API | Dépend recommandations | v0.203+ | +| 2FA SMS, Passkeys | v0.104 dédié | v0.104 | +| Go Live vidéo | v0.703 | v0.703 | + +--- + +## 6. Ordre de livraison recommandé + +| Semaine | Lots | Activités | +|---------|------|-----------| +| **S1** | G (Recherche) | G1–G5 : filtres musical_key, tri, autocomplete, facettes, historique | +| **S2** | H (Analytics) | H1–H4 : stats, graphiques, taux complétion, export | +| **S3** | F (Seller) | F1–F2 : stats ventes, liste produits | +| **S4** | C (Player) | C1–C3 : crossfade, gapless, PiP | +| **S5** | D (Queue) | D1–D2 : autoplay, queue collaborative | +| **S6** | Stabilisation | Tests, docs, polish | + +--- + +## 7. Branche et workflow + +- **Branche** : `release/v0.202` +- **Format commit** : `feat(scope): description` (ex: `feat(search): add musical_key filter`) +- **Référence scope** : [SCOPE_CONTROL.md](SCOPE_CONTROL.md) → v0.202 +- **PR** : `gh pr create --base main --head release/v0.202 --title "Release v0.202"` + +--- + +## 8. Critères de stabilité v0.202 + +### 8.1 Build & compilation +- [ ] `go build ./...` — 0 erreur +- [ ] `npm run build` — 0 erreur +- [ ] `npx tsc --noEmit` — 0 erreur + +### 8.2 Tests +- [ ] `go test ./...` — pas de régression +- [ ] `npm test -- --run` — pas de régression +- [ ] E2E : recherche, analytics, seller (si applicable) + +### 8.3 Documentation +- [ ] FEATURE_STATUS.md mis à jour +- [ ] CHANGELOG v0.202 +- [ ] MSW handlers pour nouveaux endpoints + +--- + +## Références + +- [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) — Version précédente (Lot E livré) +- [SCOPE_CONTROL.md](SCOPE_CONTROL.md) — Processus anti-scope-creep +- [FEATURE_STATUS.md](FEATURE_STATUS.md) — Statut des features +- [PROJECT_STATE.md](PROJECT_STATE.md) — État actuel du projet diff --git a/veza-backend-api/internal/api/routes_tracks.go b/veza-backend-api/internal/api/routes_tracks.go index d48d6a09b..bb197667f 100644 --- a/veza-backend-api/internal/api/routes_tracks.go +++ b/veza-backend-api/internal/api/routes_tracks.go @@ -74,6 +74,9 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { notificationService := services.NewNotificationService(r.db, r.logger) trackHandler.SetNotificationService(notificationService) + trackRecommendationService := services.NewTrackRecommendationService(r.db.GormDB, r.logger) + trackHandler.SetTrackRecommendationService(trackRecommendationService) + tracks := router.Group("/tracks") { tracks.GET("", trackHandler.ListTracks) @@ -91,6 +94,8 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) { protected.Use(r.config.AuthMiddleware.RequireAuth()) r.applyCSRFProtection(protected) + protected.GET("/recommendations", trackHandler.GetRecommendations) + uploadGroup := protected.Group("") uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole()) uploadGroup.POST("", trackHandler.UploadTrack) diff --git a/veza-backend-api/internal/core/track/handler.go b/veza-backend-api/internal/core/track/handler.go index a0b7dfc25..a21a4332d 100644 --- a/veza-backend-api/internal/core/track/handler.go +++ b/veza-backend-api/internal/core/track/handler.go @@ -40,8 +40,9 @@ type TrackHandler struct { playbackAnalyticsService *services.PlaybackAnalyticsService // BE-API-019: Added for play analytics permissionService *services.PermissionService // MOD-P1-003: Added for admin check uploadValidator *services.UploadValidator // MOD-P1-001: Added for ClamAV scan before persistence - licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights - notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications + licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights + notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications + trackRecommendationService *services.TrackRecommendationService } // NewTrackHandler crée un nouveau handler de tracks @@ -114,6 +115,11 @@ func (h *TrackHandler) SetNotificationService(notificationService *services.Noti h.notificationService = notificationService } +// SetTrackRecommendationService définit le service de recommandations +func (h *TrackHandler) SetTrackRecommendationService(svc *services.TrackRecommendationService) { + h.trackRecommendationService = svc +} + // getUserID récupère l'ID utilisateur du contexte de manière sécurisée (fail-secure) // MOD-P1-RES-003: Remplace c.MustGet() pour éviter les panics // Retourne false si user_id est absent ou invalide (répond déjà avec 401) @@ -880,6 +886,49 @@ func (h *TrackHandler) ListTracks(c *gin.Context) { }) } +// GetRecommendations returns personalized track recommendations (D2 autoplay) +func (h *TrackHandler) GetRecommendations(c *gin.Context) { + if h.trackRecommendationService == nil { + response.InternalServerError(c, "recommendations unavailable") + return + } + var userID uuid.UUID + if uid, exists := c.Get("user_id"); exists { + if parsed, ok := uid.(uuid.UUID); ok { + userID = parsed + } + } + limitStr := c.DefaultQuery("limit", "20") + var limit int + if _, err := fmt.Sscanf(limitStr, "%d", &limit); err != nil || limit < 1 { + limit = 20 + } + if limit > 100 { + limit = 100 + } + params := services.TrackRecommendationParams{ + UserID: userID, + Limit: limit, + } + if seedStr := c.Query("seed_track_id"); seedStr != "" { + if sid, err := uuid.Parse(seedStr); err == nil { + params.SeedTrackID = &sid + } + } + recs, err := h.trackRecommendationService.GetRecommendations(c.Request.Context(), params) + if err != nil { + response.InternalServerError(c, "failed to get recommendations") + return + } + tracks := make([]*models.Track, 0, len(recs)) + for _, r := range recs { + if r.Track != nil { + tracks = append(tracks, r.Track) + } + } + response.Success(c, gin.H{"tracks": tracks}) +} + // tagSuggestionsByGenre holds static tag suggestions per genre (E4) var tagSuggestionsByGenre = map[string][]string{ "pop": {"Pop", "Catchy", "Radio", "Mainstream", "Vocal"},