🎉 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:
feldenr
2026-01-18 17:08:59 +01:00
commit 9c504d2c3d
128 changed files with 22638 additions and 0 deletions
@@ -0,0 +1,224 @@
/// Music Provider - Player state management
library;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import '../../../domain/entities/track.dart';
/// Player state
class PlayerState {
final List<Track> queue;
final int currentIndex;
final bool isPlaying;
final Duration position;
final Duration duration;
final bool isLoading;
final String? errorMessage;
const PlayerState({
this.queue = const [],
this.currentIndex = -1,
this.isPlaying = false,
this.position = Duration.zero,
this.duration = Duration.zero,
this.isLoading = false,
this.errorMessage,
});
Track? get currentTrack =>
currentIndex >= 0 && currentIndex < queue.length
? queue[currentIndex]
: null;
PlayerState copyWith({
List<Track>? queue,
int? currentIndex,
bool? isPlaying,
Duration? position,
Duration? duration,
bool? isLoading,
String? errorMessage,
}) {
return PlayerState(
queue: queue ?? this.queue,
currentIndex: currentIndex ?? this.currentIndex,
isPlaying: isPlaying ?? this.isPlaying,
position: position ?? this.position,
duration: duration ?? this.duration,
isLoading: isLoading ?? this.isLoading,
errorMessage: errorMessage,
);
}
}
/// Player notifier
class PlayerNotifier extends StateNotifier<PlayerState> {
PlayerNotifier() : super(const PlayerState()) {
_player = AudioPlayer();
_init();
}
late final AudioPlayer _player;
void _init() {
_player.positionStream.listen((position) {
state = state.copyWith(position: position);
});
_player.durationStream.listen((duration) {
state = state.copyWith(duration: duration ?? Duration.zero);
});
_player.playerStateStream.listen((playerState) {
state = state.copyWith(
isPlaying: playerState.playing,
isLoading: playerState.processingState == ProcessingState.loading,
);
});
}
Future<void> loadTrack(Track track) async {
state = state.copyWith(isLoading: true);
try {
// Get stream URL from API
final streamUrl = track.audioUrl ?? '';
await _player.setUrl(streamUrl);
if (state.queue.isEmpty) {
state = state.copyWith(queue: [track], currentIndex: 0);
}
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
}
}
Future<void> play() async {
if (state.currentTrack != null) {
await _player.play();
state = state.copyWith(isPlaying: true);
}
}
Future<void> pause() async {
await _player.pause();
state = state.copyWith(isPlaying: false);
}
Future<void> seek(Duration position) async {
await _player.seek(position);
}
Future<void> next() async {
if (state.currentIndex < state.queue.length - 1) {
final nextTrack = state.queue[state.currentIndex + 1];
await loadTrack(nextTrack);
state = state.copyWith(currentIndex: state.currentIndex + 1);
await play();
}
}
Future<void> previous() async {
if (state.currentIndex > 0) {
final previousTrack = state.queue[state.currentIndex - 1];
await loadTrack(previousTrack);
state = state.copyWith(currentIndex: state.currentIndex - 1);
await play();
}
}
void setQueue(List<Track> tracks, {int startIndex = 0}) {
state = state.copyWith(
queue: tracks,
currentIndex: startIndex,
);
}
void addToQueue(Track track) {
final newQueue = [...state.queue, track];
state = state.copyWith(queue: newQueue);
}
void removeFromQueue(int index) {
if (index >= 0 && index < state.queue.length) {
final newQueue = [...state.queue]..removeAt(index);
state = state.copyWith(queue: newQueue);
}
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
}
/// Player provider
final playerProvider =
StateNotifierProvider<PlayerNotifier, PlayerState>((ref) {
return PlayerNotifier();
});
/// Current track provider
final currentTrackProvider = Provider<Track?>((ref) {
return ref.watch(playerProvider).currentTrack;
});
/// Queue view data class
class QueueViewData {
final Track? currentTrack;
final List<Track> queue;
final int currentIndex;
final bool isPlaying;
const QueueViewData({
required this.currentTrack,
required this.queue,
required this.currentIndex,
required this.isPlaying,
});
/// Get upcoming tracks (after current)
List<Track> get nextTracks {
if (currentIndex < 0 || currentIndex >= queue.length - 1) {
return [];
}
return queue.sublist(currentIndex + 1);
}
/// Get previously played tracks (before current)
List<Track> get previousTracks {
if (currentIndex <= 0) {
return [];
}
return queue.sublist(0, currentIndex);
}
/// Check if queue has tracks
bool get hasQueue => queue.isNotEmpty;
/// Check if there are upcoming tracks
bool get hasNextTracks => nextTracks.isNotEmpty;
/// Check if there are previous tracks
bool get hasPreviousTracks => previousTracks.isNotEmpty;
/// Get total queue count excluding current
int get queueCount => queue.length - 1;
}
/// Queue view provider
final queueProvider = Provider<QueueViewData>((ref) {
final playerState = ref.watch(playerProvider);
return QueueViewData(
currentTrack: playerState.currentTrack,
queue: playerState.queue,
currentIndex: playerState.currentIndex,
isPlaying: playerState.isPlaying,
);
});