feat(release): v0.202 — Lots G, H, F, C, D
- Lot G: Recherche avancée (musical_key, tri pertinence, autocomplete, facettes, historique) - Lot H: Analytics créateur (stats, charts, completion rate, export CSV/JSON) - Lot F: Seller dashboard (GET /sell/stats, liste produits) - Lot C: Player (crossfade, gapless preload, PiP) - Lot D2: Autoplay (GET /tracks/recommendations, section À écouter ensuite) Backend: GetRecommendations handler, route /tracks/recommendations Frontend: PlayerQueue recommendations, fix TS errors (GlobalPlayer, AnalyticsViewKpiGrid, etc.) Docs: FEATURE_STATUS, PROJECT_STATE, CHANGELOG, SCOPE_CONTROL
This commit is contained in:
parent
2424986ebf
commit
ede3546f4b
20 changed files with 508 additions and 72 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
# Règles de Développement UI - Projet SaaS
|
# 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`
|
- **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.201**
|
- Avant toute modification : vérifier si le changement est **dans le scope v0.202**
|
||||||
- **Autorisé v0.201** : lots E, F, G, H, C, D (Métadonnées, Seller, Recherche avancée, Analytics créateur, Player, Queue)
|
- **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é)
|
- **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.
|
- En cas de doute : ne pas ajouter. Créer une issue pour une version ultérieure.
|
||||||
|
|
||||||
|
|
|
||||||
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -1,5 +1,46 @@
|
||||||
# Changelog - Veza
|
# 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
|
## [v0.201] - 2026-02-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const Default: Story = {
|
||||||
export const Loading: Story = {
|
export const Loading: Story = {
|
||||||
name: 'Loading',
|
name: 'Loading',
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story) => {
|
(_Story) => {
|
||||||
usePlayerStore.setState({ queue: [], currentIndex: -1, currentTrack: null });
|
usePlayerStore.setState({ queue: [], currentIndex: -1, currentTrack: null });
|
||||||
return (
|
return (
|
||||||
<div className="bg-background min-h-screen p-4 max-w-4xl mx-auto space-y-6">
|
<div className="bg-background min-h-screen p-4 max-w-4xl mx-auto space-y-6">
|
||||||
|
|
|
||||||
|
|
@ -202,8 +202,8 @@ export const QueueView: React.FC = () => {
|
||||||
>
|
>
|
||||||
{upNext.map((track, i) => (
|
{upNext.map((track, i) => (
|
||||||
<QueueSortableItem
|
<QueueSortableItem
|
||||||
key={sortableIds[i]}
|
key={sortableIds[i] ?? track.id}
|
||||||
id={sortableIds[i]}
|
id={sortableIds[i] ?? track.id}
|
||||||
track={track}
|
track={track}
|
||||||
onPlay={playTrack}
|
onPlay={playTrack}
|
||||||
onRemove={() => removeFromQueue(currentIndex + 1 + i)}
|
onRemove={() => removeFromQueue(currentIndex + 1 + i)}
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,14 @@ export function useEditProfile() {
|
||||||
first_name: p.first_name || '',
|
first_name: p.first_name || '',
|
||||||
last_name: p.last_name || '',
|
last_name: p.last_name || '',
|
||||||
bio: p.bio || '',
|
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 || '',
|
location: p.location || '',
|
||||||
gender: p.gender || 'Prefer not to say',
|
gender: p.gender || 'Prefer not to say',
|
||||||
birthdate: p.birthdate || '',
|
birthdate: p.birthdate || '',
|
||||||
});
|
});
|
||||||
if (p.avatar_url) setAvatar(p.avatar_url);
|
if (p.avatar_url) setAvatar(p.avatar_url);
|
||||||
if (p.banner_url) {
|
const bannerUrl = (p as { banner_url?: string }).banner_url ?? (p as { banner?: string }).banner;
|
||||||
setBanner(p.banner_url);
|
if (bannerUrl) setBanner(bannerUrl);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to load profile settings', {
|
logger.error('Failed to load profile settings', {
|
||||||
error: e instanceof Error ? e.message : String(e),
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export function AnalyticsViewKpiGrid({ stats }: AnalyticsViewKpiGridProps) {
|
||||||
: '—'
|
: '—'
|
||||||
}
|
}
|
||||||
icon={<Target className="w-4 h-4" />}
|
icon={<Target className="w-4 h-4" />}
|
||||||
color="green"
|
color="lime"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useState, useRef, useCallback } from 'react';
|
import { useState, useRef, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { PictureInPicture2 } from 'lucide-react';
|
|
||||||
import { usePlayer } from '@/features/player/hooks/usePlayer';
|
import { usePlayer } from '@/features/player/hooks/usePlayer';
|
||||||
import { usePictureInPicture } from '@/features/player/hooks/usePictureInPicture';
|
import { usePictureInPicture } from '@/features/player/hooks/usePictureInPicture';
|
||||||
import { useKeyboardShortcuts } from '@/features/player/hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from '@/features/player/hooks/useKeyboardShortcuts';
|
||||||
|
|
@ -50,8 +49,8 @@ export function GlobalPlayer() {
|
||||||
const displayTrack = currentTrack || IDLE_TRACK;
|
const displayTrack = currentTrack || IDLE_TRACK;
|
||||||
const isIdle = !currentTrack;
|
const isIdle = !currentTrack;
|
||||||
|
|
||||||
const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isPiPSupported } = usePictureInPicture(
|
const { setVideoRef: setPiPVideoRef, togglePiP, isPiPActive, isSupported: isPiPSupported } = usePictureInPicture(
|
||||||
currentTrack?.cover_art_path ?? null,
|
currentTrack?.cover ?? null,
|
||||||
);
|
);
|
||||||
|
|
||||||
useMediaSession({
|
useMediaSession({
|
||||||
|
|
@ -72,7 +71,7 @@ export function GlobalPlayer() {
|
||||||
className="hidden w-0 h-0"
|
className="hidden w-0 h-0"
|
||||||
muted
|
muted
|
||||||
playsInline
|
playsInline
|
||||||
poster={currentTrack?.cover_art_path ?? undefined}
|
poster={currentTrack?.cover ?? undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { PlayerQueue } from './PlayerQueue';
|
import { PlayerQueue } from './PlayerQueue';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useEffect } from 'react';
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
||||||
|
|
||||||
const meta: Meta<typeof PlayerQueue> = {
|
const meta: Meta<typeof PlayerQueue> = {
|
||||||
title: 'Components/Features/Player/PlayerQueue',
|
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 = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
|
|
@ -80,3 +89,20 @@ export const Empty: Story = {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EmptyWithRecommendations: Story = {
|
||||||
|
args: {
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<>
|
||||||
|
<AuthInitializer authenticated />
|
||||||
|
<StoreInitializer tracks={[]} />
|
||||||
|
<div className="h-[600px] w-full relative bg-background overflow-hidden">
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useUIStore } from '@/stores/ui';
|
import { useUIStore } from '@/stores/ui';
|
||||||
import { cn } from '@/lib/utils';
|
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 { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { EmptyState } from '@/components/ui/empty-state';
|
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 {
|
interface PlayerQueueProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -13,9 +19,31 @@ interface PlayerQueueProps {
|
||||||
onPlay: (track: any) => void;
|
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) {
|
export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) {
|
||||||
const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore();
|
const { queue, currentIndex, removeFromQueue, clearQueue } = usePlayerStore();
|
||||||
const { sidebarOpen } = useUIStore();
|
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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
|
@ -56,13 +84,44 @@ export function PlayerQueue({ isOpen, onClose, onPlay }: PlayerQueueProps) {
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-hidden relative">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
{queue.length === 0 ? (
|
{queue.length === 0 ? (
|
||||||
<EmptyState
|
<div className="flex flex-col gap-4 p-4">
|
||||||
icon={<ListMusic className="w-full h-full" />}
|
{playerTracks.length > 0 ? (
|
||||||
title="Your queue is empty"
|
<>
|
||||||
description="Add tracks to keep the vibe going."
|
<h4 className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||||
size="sm"
|
<Sparkles className="w-4 h-4 text-primary" />
|
||||||
className="border-0 shadow-none bg-transparent"
|
À écouter ensuite
|
||||||
/>
|
</h4>
|
||||||
|
<ScrollArea className="max-h-layout-list">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{playerTracks.map((track) => (
|
||||||
|
<div
|
||||||
|
key={track.id}
|
||||||
|
className="group flex items-center gap-3 p-2 rounded-lg hover:bg-white/5 transition-colors cursor-pointer border border-transparent hover:border-white/5"
|
||||||
|
onClick={() => onPlay(track)}
|
||||||
|
>
|
||||||
|
<div className="w-6 text-center text-xs font-mono text-muted-foreground">—</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-medium truncate text-foreground">{track.title}</h4>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">{track.artist}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<p className="text-xs text-muted-foreground">Cliquez pour lire</p>
|
||||||
|
</>
|
||||||
|
) : recommendationsLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">Chargement des suggestions…</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
icon={<ListMusic className="w-full h-full" />}
|
||||||
|
title="Your queue is empty"
|
||||||
|
description="Add tracks to keep the vibe going."
|
||||||
|
size="sm"
|
||||||
|
className="border-0 shadow-none bg-transparent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-full max-h-layout-list">
|
<ScrollArea className="h-full max-h-layout-list">
|
||||||
<div className="p-2 space-y-1">
|
<div className="p-2 space-y-1">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ interface SearchPageDiscoveryProps {
|
||||||
|
|
||||||
export function SearchPageDiscovery({ onQuerySelect }: SearchPageDiscoveryProps) {
|
export function SearchPageDiscovery({ onQuerySelect }: SearchPageDiscoveryProps) {
|
||||||
const { getHistory, clearHistory } = useSearchHistory();
|
const { getHistory, clearHistory } = useSearchHistory();
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [, setRefreshKey] = useState(0);
|
||||||
const history = getHistory();
|
const history = getHistory();
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,24 @@ export async function uploadTrack(
|
||||||
* @returns La liste des tracks avec les métadonnées de pagination
|
* @returns La liste des tracks avec les métadonnées de pagination
|
||||||
* @throws Error si la requête échoue
|
* @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<Track[]> {
|
||||||
|
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(
|
export async function getTracks(
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
limit: number = 20,
|
limit: number = 20,
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@ import { setupWorker } from 'msw/browser';
|
||||||
import { handlers } from './handlers';
|
import { handlers } from './handlers';
|
||||||
|
|
||||||
// This configures a Service Worker with the given request 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<typeof h> => Boolean(h))),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
http.get('*/api/v1/tracks/suggested-tags', ({ request }) => {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const genre = url.searchParams.get('genre')?.toLowerCase() || 'default';
|
const genre = url.searchParams.get('genre')?.toLowerCase() || 'default';
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
import {
|
import {
|
||||||
uploadTrack,
|
uploadTrack,
|
||||||
getTracks,
|
getTracks,
|
||||||
|
getTrackRecommendations,
|
||||||
updateTrack,
|
updateTrack,
|
||||||
getTrackStats,
|
getTrackStats,
|
||||||
getTrackHistory,
|
getTrackHistory,
|
||||||
|
|
@ -54,6 +55,11 @@ export const tracksApi = {
|
||||||
*/
|
*/
|
||||||
get: getTrack,
|
get: getTrack,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get personalized track recommendations (D2 autoplay)
|
||||||
|
*/
|
||||||
|
getRecommendations: getTrackRecommendations,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create/upload a new track
|
* Create/upload a new track
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Statut des fonctionnalités — Veza
|
# 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.
|
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 |
|
| Playlists (CRUD, collaboration) | Oui | Oui | Complet |
|
||||||
| Chat WebSocket | Oui | Oui | Complet (Chat Server doit être démarré) |
|
| Chat WebSocket | Oui | Oui | Complet (Chat Server doit être démarré) |
|
||||||
| Dashboard | Oui | Oui | GET /api/v1/dashboard |
|
| 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 |
|
| Social (feed, posts, groups, follows, blocks) | Oui | Oui | Complet |
|
||||||
| Administration | Oui | Oui | Complet |
|
| Administration | Oui | Oui | Complet |
|
||||||
| Marketplace | Oui | Oui | Complet (Hyperswitch) |
|
| Marketplace | Oui | Oui | Complet (Hyperswitch) |
|
||||||
| Webhooks | Oui | Oui | Complet |
|
| Webhooks | Oui | Oui | Complet |
|
||||||
| Inventory / Gear | Oui | Oui | GET/POST/PUT/DELETE /api/v1/inventory/gear |
|
| 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 |
|
| 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 |
|
| Roles | Oui | Oui | Assign, revoke — flag ROLE_MANAGEMENT |
|
||||||
| Notifications | Oui | Oui | Création auto follow/like/comment |
|
| 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 |
|
| Feature | Limitation | Version cible |
|
||||||
|---------|------------|--------------|
|
|---------|------------|--------------|
|
||||||
| **Go Live** (streaming vidéo) | Non implémenté — toast « coming soon » conservé | v0.703 |
|
| **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 |
|
| **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 |
|
| **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
|
## Projets abandonnés
|
||||||
|
|
||||||
| Projet | Statut |
|
| Projet | Statut |
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
|
|
||||||
| Élément | Valeur |
|
| Élément | Valeur |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| **Dernier tag** | v0.201 |
|
| **Dernier tag** | v0.202 |
|
||||||
| **Branche release** | `release/v0.201` (taguée) |
|
| **Branche release** | `main` (v0.202 mergée) |
|
||||||
| **Phase** | Phase 2 Contenu — Lot E livré |
|
| **Phase** | Phase 2 Contenu — Lots G, H, F, C, D livrés |
|
||||||
| **Prochaine version** | v0.202 (Lot G, H, F, C, D) |
|
| **Prochaine version** | v0.203 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -26,28 +26,25 @@
|
||||||
- ✅ Lot E — Métadonnées : BPM, musical_key, lyrics, tags (E1–E4)
|
- ✅ Lot E — Métadonnées : BPM, musical_key, lyrics, tags (E1–E4)
|
||||||
- Migrations : 084 track_lyrics, 085 tracks.tags
|
- Migrations : 084 track_lyrics, 085 tracks.tags
|
||||||
|
|
||||||
### Non livré (reporté v0.202)
|
### v0.202 (Phase 2 Contenu — Lots G, H, F, C, D)
|
||||||
- Lot G : Recherche avancée (filtres, tri, autocomplete, facettes)
|
- Lot G : Recherche avancée (musical_key, tri pertinence, autocomplete, facettes type, historique)
|
||||||
- Lot H : Analytics créateur (stats, graphiques, export)
|
- Lot H : Analytics créateur (stats, graphiques, taux complétion, export CSV/JSON)
|
||||||
- Lot F : Seller dashboard (stats ventes, liste produits)
|
- Lot F : Seller dashboard (GET /sell/stats, liste produits marketplace)
|
||||||
- Lot C : Player (crossfade, gapless, PiP)
|
- Lot C : Player (crossfade, gapless preload, PiP)
|
||||||
- Lot D : Queue collaborative, Autoplay
|
- Lot D : Autoplay (GET /tracks/recommendations, section « À écouter ensuite »)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Prochaines étapes
|
## 3. Prochaines étapes
|
||||||
|
|
||||||
### Immédiat (préparation v0.202)
|
### Immédiat (préparation v0.203)
|
||||||
1. **Créer la branche** : `release/v0.202`
|
1. **Créer la branche** : `git checkout -b release/v0.203`
|
||||||
2. **Mettre à jour SCOPE_CONTROL** : référence v0.202
|
2. **Définir** le scope dans V0_203_RELEASE_SCOPE.md
|
||||||
3. **Lire** [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md) — lots G, H, F, C, D
|
3. **PR** : `gh pr create --base main --head release/v0.203 --title "Release v0.203"`
|
||||||
|
|
||||||
### Ordre d'implémentation recommandé (v0.202)
|
### Cibles v0.203+
|
||||||
1. **Lot G** (Recherche avancée) — filtres, tri, autocomplete, facettes
|
- Queue collaborative (D1)
|
||||||
2. **Lot H** (Analytics créateur) — stats, graphiques, export
|
- Recherche phonétique, booléenne
|
||||||
3. **Lot F** (Seller dashboard) — stats vendeur, liste produits
|
|
||||||
4. **Lot C** (Player) — crossfade, gapless, PiP
|
|
||||||
5. **Lot D** (Queue) — autoplay, queue collaborative
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -55,7 +52,7 @@
|
||||||
|
|
||||||
| Document | Usage |
|
| 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 |
|
| [SCOPE_CONTROL.md](SCOPE_CONTROL.md) | Anti-scope-creep, workflow |
|
||||||
| [FEATURE_STATUS.md](FEATURE_STATUS.md) | Statut des features par domaine |
|
| [FEATURE_STATUS.md](FEATURE_STATUS.md) | Statut des features par domaine |
|
||||||
| [CHANGELOG.md](../CHANGELOG.md) | Historique des versions |
|
| [CHANGELOG.md](../CHANGELOG.md) | Historique des versions |
|
||||||
|
|
@ -79,5 +76,4 @@
|
||||||
|
|
||||||
| Métrique | Valeur |
|
| Métrique | Valeur |
|
||||||
|----------|--------|
|
|----------|--------|
|
||||||
| Features livrées (cumul) | ~270 / 600 |
|
| Features livrées (cumul) | ~345 / 600 |
|
||||||
| Cible v0.201 | ~330 / 600 |
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
# Contrôle du scope — Anti-scope-creep
|
# Contrôle du scope — Anti-scope-creep
|
||||||
|
|
||||||
**Objectif** : Éviter toute dérive de scope. Chaque modification doit être intentionnelle et traçable.
|
**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)
|
**Référence active** : [V0_202_RELEASE_SCOPE.md](V0_202_RELEASE_SCOPE.md)
|
||||||
**Version précédente** : [V0_103_RELEASE_SCOPE.md](V0_103_RELEASE_SCOPE.md)
|
**Version précédente** : [V0_201_RELEASE_SCOPE.md](V0_201_RELEASE_SCOPE.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Règle d'or
|
## 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.
|
> 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é
|
### 2.1 Autorisé
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
### 2.2 Interdit
|
### 2.2 Interdit
|
||||||
|
|
||||||
- **Nouvelles features** hors scope v0.201
|
- **Nouvelles features** hors scope v0.202
|
||||||
- **Nouvelles routes** ou pages hors scope
|
- **Nouvelles routes** ou pages hors scope
|
||||||
- **Nouvelles dépendances** (sauf correctif sécurité)
|
- **Nouvelles dépendances** (sauf correctif sécurité)
|
||||||
- **Changements de comportement** sur les features HORS SCOPE
|
- **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 ?
|
- Non → **STOP.** Est-ce une correction de bug ? Si oui, la feature est-elle IN SCOPE ?
|
||||||
|
|
||||||
2. **Mon changement ajoute-t-il du code ?**
|
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.
|
- Correction, refactoring, test → OK si lié à une feature IN SCOPE.
|
||||||
|
|
||||||
3. **Mes tests passent-ils ?**
|
3. **Mes tests passent-ils ?**
|
||||||
|
|
@ -81,7 +81,7 @@ Format : `type(scope): description`
|
||||||
|
|
||||||
Dans chaque PR, le relecteur doit valider :
|
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 nouvelle feature ajoutée
|
||||||
- [ ] Aucune régression sur les flows critiques
|
- [ ] Aucune régression sur les flows critiques
|
||||||
- [ ] Les tests passent
|
- [ ] 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
|
### 5.1 Template
|
||||||
|
|
||||||
Utiliser le template [Feature request](.github/ISSUE_TEMPLATE/feature_request.md) avec :
|
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
|
- **Justification** : pourquoi cette feature est nécessaire
|
||||||
- **Effort estimé** : S / M / L / XL
|
- **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
|
### 5.2 Workflow
|
||||||
|
|
||||||
1. Créer une issue avec le template
|
1. Créer une issue avec le template
|
||||||
2. **Ne pas implémenter** tant que v0.201 n'est pas taguée
|
2. **Ne pas implémenter** tant que v0.202 n'est pas taguée
|
||||||
3. Une fois v0.201 stable, prioriser les issues "v0.202" dans un nouveau document de scope
|
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**
|
- Correctif autorisé **immédiatement**
|
||||||
- Documenter dans la PR
|
- 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
|
### 6.2 Blocage production
|
||||||
|
|
||||||
|
|
@ -140,13 +140,13 @@ Pour tout cas ambigu :
|
||||||
|
|
||||||
- Ouvrir une issue "Scope clarification"
|
- Ouvrir une issue "Scope clarification"
|
||||||
- Décision documentée dans l'issue
|
- 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
|
## 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
|
2. **Définir** explicitement les nouvelles features autorisées
|
||||||
3. **Mettre à jour** la référence active dans ce document (section header)
|
3. **Mettre à jour** la référence active dans ce document (section header)
|
||||||
4. **Reprendre** ce processus avec le nouveau document de scope
|
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.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.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.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
|
## 8. Rappel pour les contributeurs
|
||||||
|
|
||||||
- **Cursor / IA** : Les règles dans `.cursorrules` rappellent de vérifier le scope avant toute modification.
|
- **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.
|
- **En doute ?** Ouvrir une issue "Scope clarification" plutôt que de coder.
|
||||||
|
|
|
||||||
201
docs/V0_202_RELEASE_SCOPE.md
Normal file
201
docs/V0_202_RELEASE_SCOPE.md
Normal file
|
|
@ -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
|
||||||
|
|
@ -74,6 +74,9 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) {
|
||||||
notificationService := services.NewNotificationService(r.db, r.logger)
|
notificationService := services.NewNotificationService(r.db, r.logger)
|
||||||
trackHandler.SetNotificationService(notificationService)
|
trackHandler.SetNotificationService(notificationService)
|
||||||
|
|
||||||
|
trackRecommendationService := services.NewTrackRecommendationService(r.db.GormDB, r.logger)
|
||||||
|
trackHandler.SetTrackRecommendationService(trackRecommendationService)
|
||||||
|
|
||||||
tracks := router.Group("/tracks")
|
tracks := router.Group("/tracks")
|
||||||
{
|
{
|
||||||
tracks.GET("", trackHandler.ListTracks)
|
tracks.GET("", trackHandler.ListTracks)
|
||||||
|
|
@ -91,6 +94,8 @@ func (r *APIRouter) setupTrackRoutes(router *gin.RouterGroup) {
|
||||||
protected.Use(r.config.AuthMiddleware.RequireAuth())
|
protected.Use(r.config.AuthMiddleware.RequireAuth())
|
||||||
r.applyCSRFProtection(protected)
|
r.applyCSRFProtection(protected)
|
||||||
|
|
||||||
|
protected.GET("/recommendations", trackHandler.GetRecommendations)
|
||||||
|
|
||||||
uploadGroup := protected.Group("")
|
uploadGroup := protected.Group("")
|
||||||
uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole())
|
uploadGroup.Use(r.config.AuthMiddleware.RequireContentCreatorRole())
|
||||||
uploadGroup.POST("", trackHandler.UploadTrack)
|
uploadGroup.POST("", trackHandler.UploadTrack)
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,9 @@ type TrackHandler struct {
|
||||||
playbackAnalyticsService *services.PlaybackAnalyticsService // BE-API-019: Added for play analytics
|
playbackAnalyticsService *services.PlaybackAnalyticsService // BE-API-019: Added for play analytics
|
||||||
permissionService *services.PermissionService // MOD-P1-003: Added for admin check
|
permissionService *services.PermissionService // MOD-P1-003: Added for admin check
|
||||||
uploadValidator *services.UploadValidator // MOD-P1-001: Added for ClamAV scan before persistence
|
uploadValidator *services.UploadValidator // MOD-P1-001: Added for ClamAV scan before persistence
|
||||||
licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights
|
licenseChecker services.TrackDownloadLicenseChecker // A04: Verify paid track download rights
|
||||||
notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications
|
notificationService *services.NotificationService // Phase 2.2: Optional, for like notifications
|
||||||
|
trackRecommendationService *services.TrackRecommendationService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTrackHandler crée un nouveau handler de tracks
|
// NewTrackHandler crée un nouveau handler de tracks
|
||||||
|
|
@ -114,6 +115,11 @@ func (h *TrackHandler) SetNotificationService(notificationService *services.Noti
|
||||||
h.notificationService = notificationService
|
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)
|
// 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
|
// 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)
|
// 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)
|
// tagSuggestionsByGenre holds static tag suggestions per genre (E4)
|
||||||
var tagSuggestionsByGenre = map[string][]string{
|
var tagSuggestionsByGenre = map[string][]string{
|
||||||
"pop": {"Pop", "Catchy", "Radio", "Mainstream", "Vocal"},
|
"pop": {"Pop", "Catchy", "Radio", "Mainstream", "Vocal"},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue