Initial commit: AudiOhm - Alternative Spotify avec streaming YouTube

Backend:
- FastAPI avec PostgreSQL et Redis
- Authentification JWT complète
- API REST pour musique, playlists, recherche
- Streaming audio via yt-dlp
- SQLAlchemy 2.0 async

Frontend:
- Flutter avec thème néon cyberpunk
- State management Riverpod
- Layout adaptatif desktop/mobile
- Lecteur audio avec mini-player

Infrastructure:
- Docker Compose (PostgreSQL + Redis)
- Scripts d'installation automatisés
- Scripts de build pour exécutables

Fichiers ajoutés:
- BUILD_CLIENT_*.bat/sh: Scripts de compilation
- BUILD_CLIENT_README.md: Documentation compilation
- CHECK_FLUTTER.sh: Vérificateur d'environnement
- requirements.txt mis à jour pour Python 3.13
- Modèles SQLAlchemy corrigés (metadata -> extra_metadata)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-01-18 20:08:36 +00:00
commit a89c7894cf
132 changed files with 23178 additions and 0 deletions
@@ -0,0 +1,5 @@
/// API Client exports
export 'api_service.dart';
export 'auth_api_service.dart';
export 'music_api_service.dart';
export 'playlist_api_service.dart';
@@ -0,0 +1,76 @@
/// API Service - Main HTTP client using Dio
library;
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import '../../../core/constants/api_constants.dart';
import '../../providers/auth_provider.dart';
/// API Service provider
final apiServiceProvider = Provider<Dio>((ref) {
final authState = ref.watch(authProvider);
final token = authState?.accessToken;
final options = BaseOptions(
baseUrl: ApiConstants.baseUrl,
connectTimeout: const Duration(milliseconds: ApiConstants.connectionTimeoutMs),
receiveTimeout: const Duration(milliseconds: ApiConstants.receiveTimeoutMs),
sendTimeout: const Duration(milliseconds: ApiConstants.sendTimeoutMs),
headers: {
'Content-Type': 'application/json',
if (token != null) 'Authorization': 'Bearer $token',
},
);
final dio = Dio(options);
// Add logger in debug mode
dio.interceptors.add(
PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseBody: true,
responseHeader: false,
error: true,
compact: true,
),
);
// Add token refresh interceptor
dio.interceptors.add(
InterceptorsWrapper(
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
// Try to refresh token
try {
final newToken = await ref.read(authProvider.notifier).refreshToken();
if (newToken != null) {
// Retry original request with new token
final opts = options.copyWith(
headers: {
...options.headers,
'Authorization': 'Bearer $newToken',
},
);
final clonedReq = await dio.fetch(opts..path = error.requestOptions.path);
return handler.resolve(clonedReq);
}
} catch (e) {
// Refresh failed, logout user
ref.read(authProvider.notifier).logout();
}
}
return handler.next(error);
},
),
);
return dio;
});
/// Get API client
Dio getDio(Ref ref) {
return ref.read(apiServiceProvider);
}
@@ -0,0 +1,185 @@
/// Auth API Service
library;
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/api_constants.dart';
import '../../../domain/entities/user.dart';
import 'api_service.dart';
/// Auth API response models
class LoginResponse {
final String accessToken;
final String refreshToken;
final int expiresIn;
final User user;
LoginResponse({
required this.accessToken,
required this.refreshToken,
required this.expiresIn,
required this.user,
});
factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse(
accessToken: json['access_token'] as String,
refreshToken: json['refresh_token'] as String,
expiresIn: json['expires_in'] as int,
user: User.fromJson(json['user'] as Map<String, dynamic>),
);
}
}
/// Extension on User for JSON serialization
extension UserJson on User {
static User fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
email: json['email'] as String,
username: json['username'] as String,
displayName: json['display_name'] as String?,
avatarUrl: json['avatar_url'] as String?,
isPremium: json['is_premium'] as bool? ?? false,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'username': username,
if (displayName != null) 'display_name': displayName,
if (avatarUrl != null) 'avatar_url': avatarUrl,
'is_premium': isPremium,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
};
}
}
/// Auth API Service
class AuthApiService {
AuthApiService(this._dio);
final Dio _dio;
/// Login with email and password
Future<LoginResponse> login(String email, String password) async {
try {
final response = await _dio.post(
ApiConstants.login,
data: {
'email': email,
'password': password,
},
);
return LoginResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Register a new user
Future<LoginResponse> register({
required String email,
required String username,
required String password,
String? displayName,
}) async {
try {
final response = await _dio.post(
ApiConstants.register,
data: {
'email': email,
'username': username,
'password': password,
if (displayName != null) 'display_name': displayName,
},
);
return LoginResponse.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Refresh access token
Future<Map<String, dynamic>> refreshToken(String refreshToken) async {
try {
final response = await _dio.post(
ApiConstants.refresh,
data: {'refresh_token': refreshToken},
);
return response.data;
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Get current user profile
Future<User> getCurrentUser() async {
try {
final response = await _dio.get(ApiConstants.me);
return UserJson.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Update user profile
Future<User> updateProfile({
String? displayName,
String? avatarUrl,
}) async {
try {
final response = await _dio.put(
ApiConstants.me,
data: {
if (displayName != null) 'display_name': displayName,
if (avatarUrl != null) 'avatar_url': avatarUrl,
},
);
return UserJson.fromJson(response.data);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
/// Logout
Future<void> logout() async {
try {
await _dio.post(ApiConstants.logout);
} on DioException catch (e) {
throw _handleDioError(e);
}
}
Exception _handleDioError(DioException error) {
if (error.response != null) {
final statusCode = error.response!.statusCode;
final message = error.response!.data['detail'] as String? ??
'An error occurred';
return Exception('$statusCode: $message');
} else if (error.type == DioExceptionType.connectionTimeout) {
return const Exception('Connection timeout');
} else if (error.type == DioExceptionType.receiveTimeout) {
return const Exception('Receive timeout');
} else {
return Exception('Network error: ${error.message}');
}
}
}
/// Provider for Auth API Service
final authApiServiceProvider = Provider<AuthApiService>((ref) {
final dio = ref.watch(apiServiceProvider);
return AuthApiService(dio);
});
@@ -0,0 +1,164 @@
/// Music API Service
library;
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/api_constants.dart';
import '../../../domain/entities/track.dart';
import 'api_service.dart';
/// Music API Service
class MusicApiService {
MusicApiService(this._dio);
final Dio _dio;
/// Search for music
Future<Map<String, dynamic>> search(
String query, {
String type = 'all',
int limit = 20,
int offset = 0,
}) async {
try {
final response = await _dio.get(
ApiConstants.searchMusic,
queryParameters: {
'q': query,
'type': type,
'limit': limit,
'offset': offset,
},
);
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get track details
Future<Map<String, dynamic>> getTrack(String trackId) async {
try {
final response = await _dio.get('${ApiConstants.tracks}/$trackId');
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get stream URL for a track
Future<Map<String, dynamic>> getStreamUrl(String trackId) async {
try {
final response = await _dio.get('${ApiConstants.tracks}/$trackId${ApiConstants.stream}');
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get recommendations based on a track
Future<List<Map<String, dynamic>>> getRecommendations(
String trackId, {
int limit = 10,
}) async {
try {
final response = await _dio.get(
'${ApiConstants.recommendations}/$trackId/recommendations',
queryParameters: {'limit': limit},
);
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get trending tracks
Future<List<Map<String, dynamic>>> getTrending({int limit = 20}) async {
try {
final response = await _dio.get(
ApiConstants.trending,
queryParameters: {'limit': limit},
);
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get artist details
Future<Map<String, dynamic>> getArtist(String artistId) async {
try {
final response = await _dio.get('${ApiConstants.artists}/$artistId');
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get artist's top tracks
Future<List<Map<String, dynamic>>> getArtistTopTracks(
String artistId, {
int limit = 10,
}) async {
try {
final response = await _dio.get(
'${ApiConstants.artists}/$artistId/tracks',
queryParameters: {'limit': limit},
);
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get artist's albums
Future<List<Map<String, dynamic>>> getArtistAlbums(
String artistId, {
int limit = 20,
}) async {
try {
final response = await _dio.get(
'${ApiConstants.artists}/$artistId/albums',
queryParameters: {'limit': limit},
);
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get album details
Future<Map<String, dynamic>> getAlbum(String albumId) async {
try {
final response = await _dio.get('${ApiConstants.albums}/$albumId');
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get album tracks
Future<List<Map<String, dynamic>>> getAlbumTracks(String albumId) async {
try {
final response = await _dio.get('${ApiConstants.albums}/$albumId/tracks');
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
Exception _handleError(DioException error) {
if (error.response != null) {
final message = error.response!.data['detail'] ?? 'An error occurred';
return Exception('${error.response!.statusCode}: $message');
}
return Exception('Network error: ${error.message}');
}
}
final musicApiServiceProvider = Provider<MusicApiService>((ref) {
final dio = ref.watch(apiServiceProvider);
return MusicApiService(dio);
});
@@ -0,0 +1,165 @@
/// Playlist API Service
library;
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/api_constants.dart';
import 'api_service.dart';
/// Playlist API Service
class PlaylistApiService {
PlaylistApiService(this._dio);
final Dio _dio;
/// Get user playlists
Future<List<Map<String, dynamic>>> getPlaylists({
int limit = 50,
int offset = 0,
}) async {
try {
final response = await _dio.get(
ApiConstants.userPlaylists,
queryParameters: {'limit': limit, 'offset': offset},
);
return (response.data as List).cast<Map<String, dynamic>>();
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Create playlist
Future<Map<String, dynamic>> createPlaylist({
required String name,
String? description,
String? imageUrl,
bool isPublic = false,
}) async {
try {
final response = await _dio.post(
ApiConstants.userPlaylists,
data: {
'name': name,
if (description != null) 'description': description,
if (imageUrl != null) 'image_url': imageUrl,
'is_public': isPublic,
},
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Get playlist with tracks
Future<Map<String, dynamic>> getPlaylist(String playlistId) async {
try {
final response = await _dio.get('${ApiConstants.userPlaylists}/$playlistId');
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Update playlist
Future<Map<String, dynamic>> updatePlaylist(
String playlistId, {
String? name,
String? description,
String? imageUrl,
bool? isPublic,
}) async {
try {
final response = await _dio.put(
'${ApiConstants.userPlaylists}/$playlistId',
data: {
if (name != null) 'name': name,
if (description != null) 'description': description,
if (imageUrl != null) 'image_url': imageUrl,
if (isPublic != null) 'is_public': isPublic,
},
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Delete playlist
Future<void> deletePlaylist(String playlistId) async {
try {
await _dio.delete('${ApiConstants.userPlaylists}/$playlistId');
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Add tracks to playlist
Future<Map<String, dynamic>> addTracks(
String playlistId,
List<String> trackIds, {
int? position,
}) async {
try {
final response = await _dio.post(
'${ApiConstants.userPlaylists}/$playlistId${ApiConstants.playlistTracks}',
data: {
'track_ids': trackIds,
if (position != null) 'position': position,
},
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Remove track from playlist
Future<Map<String, dynamic>> removeTrack(
String playlistId,
String trackId,
) async {
try {
final response = await _dio.delete(
'${ApiConstants.userPlaylists}/$playlistId${ApiConstants.playlistTracks}/$trackId',
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
/// Reorder track in playlist
Future<Map<String, dynamic>> reorderTrack(
String playlistId,
String trackId,
int newPosition,
) async {
try {
final response = await _dio.put(
'${ApiConstants.userPlaylists}/$playlistId${ApiConstants.reorder}',
data: {
'track_id': trackId,
'new_position': newPosition,
},
);
return response.data;
} on DioException catch (e) {
throw _handleError(e);
}
}
Exception _handleError(DioException error) {
if (error.response != null) {
final message = error.response!.data['detail'] ?? 'An error occurred';
return Exception('${error.response!.statusCode}: $message');
}
return Exception('Network error: ${error.message}');
}
}
final playlistApiServiceProvider = Provider<PlaylistApiService>((ref) {
final dio = ref.watch(apiServiceProvider);
return PlaylistApiService(dio);
});