801e6a050b
- 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>
608 lines
12 KiB
Markdown
608 lines
12 KiB
Markdown
# 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)
|