🎉 Initial commit: AudiOhm - Alternative à Spotify avec streaming YouTube
Features: - Frontend Flutter avec thème néon cyberpunk - Backend FastAPI avec streaming YouTube - Base de données PostgreSQL + Redis - Authentification JWT complète - Recherche multi-source (DB + YouTube) - Playlists CRUD avec drag & drop - Queue management - Settings avec audio quality - Interface adaptative (Desktop + Mobile) Tech Stack: - Frontend: Flutter 3.2+, Riverpod - Backend: Python 3.11+, FastAPI - Database: PostgreSQL 15+ - Cache: Redis 7+ - Streaming: yt-dlp + FFmpeg 🚀 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user