prod: UI Optimisée mise en production
- Documentation archivée et réorganisée - Backend: Ajout tests, migrations, library service, rate limiting - Frontend: Suppression Flutter, focus sur interface web HTML/JS - Tailwind CSS ajouté pour le style - Améliorations UX et corrections bugs Generated with [Claude Code](https://claude.com/claude-code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -0,0 +1,607 @@
|
||||
# Guide d'Utilisation de l'API Bibliothèque
|
||||
|
||||
Ce guide présente comment utiliser les endpoints de l'API Bibliothèque d'AudiOhm depuis le frontend.
|
||||
|
||||
## Base URL
|
||||
|
||||
Tous les endpoints sont préfixés par: `/api/v1`
|
||||
|
||||
## Authentication
|
||||
|
||||
Tous les endpoints nécessitent une authentification via JWT token dans le header:
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints d'Historique d'Écoute
|
||||
|
||||
### 1. Ajouter une entrée d'historique
|
||||
|
||||
**Endpoint:** `POST /api/v1/library/history`
|
||||
|
||||
**Description:** Enregistre une écoute de morceau dans l'historique de l'utilisateur.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"track_id": "uuid-du-morceau",
|
||||
"played_for": 180,
|
||||
"completed": true,
|
||||
"source": "library"
|
||||
}
|
||||
```
|
||||
|
||||
**Champs:**
|
||||
- `track_id` (UUID, requis): ID du morceau écouté
|
||||
- `played_for` (int, requis): Durée écoutée en secondes
|
||||
- `completed` (bool, optionnel): Si le morceau a été écouté entièrement (défaut: false)
|
||||
- `source` (string, optionnel): Source de lecture (library, playlist, search, etc.)
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-entrée",
|
||||
"user_id": "uuid-utilisateur",
|
||||
"track_id": "uuid-morceau",
|
||||
"played_for": 180,
|
||||
"completed": true,
|
||||
"source": "library",
|
||||
"played_at": "2026-01-19T10:30:00",
|
||||
"created_at": "2026-01-19T10:30:00",
|
||||
"track": {
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
"duration": 240,
|
||||
"artist": {
|
||||
"id": "uuid-artiste",
|
||||
"name": "Nom de l'artiste"
|
||||
},
|
||||
"album": {
|
||||
"id": "uuid-album",
|
||||
"name": "Nom de l'album"
|
||||
},
|
||||
"image_url": "https://..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<void> addToListeningHistory(String trackId, int playedFor) async {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/api/v1/library/history'),
|
||||
headers: {
|
||||
'Authorization': 'Bearer $token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode({
|
||||
'track_id': trackId,
|
||||
'played_for': playedFor,
|
||||
'completed': true,
|
||||
'source': 'library',
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode != 201) {
|
||||
throw Exception('Failed to add to history');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Lister l'historique
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/history`
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (1-100, défaut: 50): Nombre maximum de résultats
|
||||
- `offset` (défaut: 0): Pagination offset
|
||||
- `days` (optionnel): Filtrer les derniers N jours (1-365)
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid-entrée",
|
||||
"track_id": "uuid-morceau",
|
||||
"played_for": 180,
|
||||
"completed": true,
|
||||
"played_at": "2026-01-19T10:30:00",
|
||||
"track": {
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
"duration": 240,
|
||||
"artist": {...},
|
||||
"album": {...},
|
||||
"image_url": "https://..."
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<List<ListeningHistory>> getListeningHistory({
|
||||
int limit = 50,
|
||||
int offset = 0,
|
||||
int? days,
|
||||
}) async {
|
||||
final queryParams = {
|
||||
'limit': limit.toString(),
|
||||
'offset': offset.toString(),
|
||||
if (days != null) 'days': days.toString(),
|
||||
};
|
||||
|
||||
final uri = Uri.parse('$baseUrl/api/v1/library/history')
|
||||
.replace(queryParameters: queryParams);
|
||||
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => ListeningHistory.fromJson(e)).toList();
|
||||
}
|
||||
throw Exception('Failed to load history');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Morceaux récemment écoutés
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/history/recent`
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (1-50, défaut: 20): Nombre maximum de résultats
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"tracks": [
|
||||
{
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
"duration": 240,
|
||||
"artist": {...},
|
||||
"album": {...},
|
||||
"image_url": "https://...",
|
||||
"play_count": 15
|
||||
}
|
||||
],
|
||||
"total": 20
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Morceaux les plus écoutés
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/history/most-played`
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (1-50, défaut: 20): Nombre maximum de résultats
|
||||
- `days` (optionnel): Filtrer les derniers N jours
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"tracks": [
|
||||
{
|
||||
"track": {
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
...
|
||||
},
|
||||
"play_count": 45
|
||||
}
|
||||
],
|
||||
"total": 20
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Effacer l'historique
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/library/history`
|
||||
|
||||
**Query Parameters:**
|
||||
- `before_date` (optionnel, ISO 8601): Effacer avant cette date
|
||||
|
||||
**Response (204 No Content)**
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<void> clearHistory({DateTime? beforeDate}) async {
|
||||
final queryParams = {
|
||||
if (beforeDate != null)
|
||||
'before_date': beforeDate.toIso8601String(),
|
||||
};
|
||||
|
||||
final uri = Uri.parse('$baseUrl/api/v1/library/history')
|
||||
.replace(queryParameters: queryParams);
|
||||
|
||||
final response = await http.delete(
|
||||
uri,
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
throw Exception('Failed to clear history');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints de Morceaux Likés
|
||||
|
||||
### 6. Liké un morceau
|
||||
|
||||
**Endpoint:** `POST /api/v1/library/liked`
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"track_id": "uuid-du-morceau",
|
||||
"notes": "Excellent morceau!"
|
||||
}
|
||||
```
|
||||
|
||||
**Champs:**
|
||||
- `track_id` (UUID, requis): ID du morceau à liker
|
||||
- `notes` (string, optionnel, max 1000 caractères): Notes personnelles
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-entrée",
|
||||
"user_id": "uuid-utilisateur",
|
||||
"track_id": "uuid-morceau",
|
||||
"notes": "Excellent morceau!",
|
||||
"created_at": "2026-01-19T10:30:00",
|
||||
"updated_at": "2026-01-19T10:30:00",
|
||||
"track": {
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Erreurs:**
|
||||
- `409 Conflict`: Le morceau est déjà liké
|
||||
|
||||
---
|
||||
|
||||
### 7. Unliké un morceau
|
||||
|
||||
**Endpoint:** `DELETE /api/v1/library/liked/{track_id}`
|
||||
|
||||
**Response (204 No Content)**
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<void> unlikeTrack(String trackId) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('$baseUrl/api/v1/library/liked/$trackId'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
if (response.statusCode != 204) {
|
||||
throw Exception('Failed to unlike track');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. Lister les morceaux likés
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/liked`
|
||||
|
||||
**Query Parameters:**
|
||||
- `limit` (1-100, défaut: 50)
|
||||
- `offset` (défaut: 0)
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid-entrée",
|
||||
"track_id": "uuid-morceau",
|
||||
"notes": "Excellent morceau!",
|
||||
"created_at": "2026-01-19T10:30:00",
|
||||
"track": {
|
||||
"id": "uuid-morceau",
|
||||
"title": "Nom du morceau",
|
||||
...
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. Vérifier si un morceau est liké
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/liked/check/{track_id}`
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"is_liked": true
|
||||
}
|
||||
```
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<bool> isTrackLiked(String trackId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/api/v1/library/liked/check/$trackId'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return data['is_liked'] as bool;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. Mettre à jour les notes
|
||||
|
||||
**Endpoint:** `PUT /api/v1/library/liked/{track_id}/notes`
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"notes": "Nouvelles notes personnelles"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"id": "uuid-entrée",
|
||||
"track_id": "uuid-morceau",
|
||||
"notes": "Nouvelles notes personnelles",
|
||||
"created_at": "2026-01-19T10:30:00",
|
||||
"updated_at": "2026-01-19T11:00:00",
|
||||
"track": {...}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoint de Statistiques
|
||||
|
||||
### 11. Statistiques de la bibliothèque
|
||||
|
||||
**Endpoint:** `GET /api/v1/library/stats`
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"liked_tracks_count": 145,
|
||||
"total_plays": 2340,
|
||||
"plays_last_30_days": 320,
|
||||
"unique_tracks_played": 89
|
||||
}
|
||||
```
|
||||
|
||||
**Exemple Flutter:**
|
||||
```dart
|
||||
Future<LibraryStats> getLibraryStats() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('$baseUrl/api/v1/library/stats'),
|
||||
headers: {'Authorization': 'Bearer $token'},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
return LibraryStats.fromJson(data);
|
||||
}
|
||||
throw Exception('Failed to load stats');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Codes d'Erreur
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 200 | Succès |
|
||||
| 201 | Ressource créée |
|
||||
| 204 | Succès sans contenu (DELETE) |
|
||||
| 400 | Requête invalide (ID invalide, etc.) |
|
||||
| 403 | Non autorisé |
|
||||
| 404 | Ressource non trouvée |
|
||||
| 409 | Conflit (déjà liké, etc.) |
|
||||
| 500 | Erreur serveur interne |
|
||||
|
||||
---
|
||||
|
||||
## Bonnes Pratiques
|
||||
|
||||
### 1. Tracking des Écoutes
|
||||
|
||||
```dart
|
||||
// Quand un utilisateur commence à écouter un morceau
|
||||
DateTime startTime = DateTime.now();
|
||||
|
||||
// Quand l'utilisateur arrête ou change de morceau
|
||||
void onTrackEnd(String trackId) {
|
||||
final playedFor = DateTime.now().difference(startTime).inSeconds;
|
||||
|
||||
addToListeningHistory(trackId, playedFor).catchError((e) {
|
||||
// Gérer l'erreur silencieusement pour ne pas interrompre l'expérience
|
||||
print('Failed to track play: $e');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Pagination
|
||||
|
||||
```dart
|
||||
// Charger plus d'entrées avec pagination
|
||||
Future<void> loadMoreHistory() async {
|
||||
final newEntries = await getListeningHistory(
|
||||
limit: 50,
|
||||
offset: currentHistory.length,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
currentHistory.addAll(newEntries);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Cache Local
|
||||
|
||||
```dart
|
||||
// Mettre en cache les résultats pour éviter les requêtes inutiles
|
||||
Map<String, bool> _likedCache = {};
|
||||
|
||||
Future<bool> isTrackLiked(String trackId) async {
|
||||
if (_likedCache.containsKey(trackId)) {
|
||||
return _likedCache[trackId]!;
|
||||
}
|
||||
|
||||
final isLiked = await _fetchIsTrackLiked(trackId);
|
||||
_likedCache[trackId] = isLiked;
|
||||
return isLiked;
|
||||
}
|
||||
|
||||
void toggleLike(String trackId, bool currentState) {
|
||||
_likedCache[trackId] = !currentState;
|
||||
// Effectuer la requête API...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Gestion des Erreurs
|
||||
|
||||
```dart
|
||||
Future<void> safeApiCall(Future<void> Function() apiCall) async {
|
||||
try {
|
||||
await apiCall();
|
||||
} on HTTPException catch (e) {
|
||||
// Gérer les erreurs HTTP connues
|
||||
switch (e.statusCode) {
|
||||
case 401:
|
||||
// Rediriger vers login
|
||||
break;
|
||||
case 409:
|
||||
// Afficher message "déjà liké"
|
||||
break;
|
||||
default:
|
||||
// Afficher erreur générique
|
||||
}
|
||||
} catch (e) {
|
||||
// Gérer les erreurs inattendues
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exemples d'Intégration
|
||||
|
||||
### Player Audio avec Tracking
|
||||
|
||||
```dart
|
||||
class AudioPlayerWithTracking {
|
||||
Timer? _trackingTimer;
|
||||
DateTime? _startTime;
|
||||
String? _currentTrackId;
|
||||
|
||||
Future<void> playTrack(String trackId) async {
|
||||
// Logique de lecture audio...
|
||||
_startTime = DateTime.now();
|
||||
_currentTrackId = trackId;
|
||||
}
|
||||
|
||||
Future<void> stopTrack() async {
|
||||
if (_startTime != null && _currentTrackId != null) {
|
||||
final playedFor = DateTime.now().difference(_startTime!).inSeconds;
|
||||
|
||||
// Enregistrer dans l'historique
|
||||
await addToListeningHistory(_currentTrackId!, playedFor);
|
||||
}
|
||||
|
||||
// Logique d'arrêt audio...
|
||||
_startTime = null;
|
||||
_currentTrackId = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Écran "Morceaux Likés"
|
||||
|
||||
```dart
|
||||
class LikedTracksScreen extends StatefulWidget {
|
||||
@override
|
||||
_LikedTracksScreenState createState() => _LikedTracksScreenState();
|
||||
}
|
||||
|
||||
class _LikedTracksScreenState extends State<LikedTracksScreen> {
|
||||
List<LikedTrack> _likedTracks = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadLikedTracks();
|
||||
}
|
||||
|
||||
Future<void> _loadLikedTracks() async {
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final tracks = await getLikedTracks(limit: 50);
|
||||
setState(() {
|
||||
_likedTracks = tracks;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
// Afficher erreur
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Morceaux Likés')),
|
||||
body: _isLoading
|
||||
? CircularProgressIndicator()
|
||||
: ListView.builder(
|
||||
itemCount: _likedTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = _likedTracks[index];
|
||||
return TrackTile(track: track.track);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Pour toute question ou problème, consultez:
|
||||
- Documentation technique: `LIBRARY_IMPLEMENTATION.md`
|
||||
- Tests: `test_library_features.py`
|
||||
- Schéma OpenAPI: `/api/docs` (when server is running)
|
||||
Reference in New Issue
Block a user