import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; import 'melo_logger.dart'; class PlayerService { static final PlayerService _instanz = PlayerService._(); factory PlayerService() => _instanz; PlayerService._(); AudioPlayer? _player; final List _warteschlange = []; int _aktuellerIndex = -1; StreamSubscription? _autoNextSub; AudioPlayer get _p { if (_player == null) { _player = AudioPlayer(); _autoNextSub = _player!.playerStateStream.listen((state) { if (state.processingState == ProcessingState.completed) { naechstes(); } else if (state.processingState == ProcessingState.idle && _aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length) { final song = _warteschlange[_aktuellerIndex]; // Nur markieren wenn kein Stream (Stream-Fehler sind oft temporär) if (song.streamUrl == null || song.streamUrl!.isEmpty) { _markiereAlsKorrupt(song); } } } return _player!; } Song? get aktuellerSong => _aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length ? _warteschlange[_aktuellerIndex] : null; bool get spieltAb => _player?.playing ?? false; Duration get position => _player?.position ?? Duration.zero; Duration get dauer => _player?.duration ?? Duration.zero; Stream get positionStream => _p.positionStream; Stream get stateStream => _p.playerStateStream; final StreamController _songWechsel = StreamController.broadcast(); Stream get onSongWechsel => _songWechsel.stream; Future spiele(Song song, {int position = 0}) async { _aktuellerIndex = _warteschlange.indexWhere((s) => s.id == song.id); if (_aktuellerIndex < 0) { _warteschlange.add(song); _aktuellerIndex = _warteschlange.length - 1; } try { if (song.streamUrl != null && song.streamUrl!.isNotEmpty) { // Streaming vom Server await _p.setUrl(song.streamUrl!); } else if (song.dateiPfad.isNotEmpty && await File(song.dateiPfad).exists()) { // Lokale Datei await _p.setFilePath(song.dateiPfad); } else { MeloLogger().fehler('player_keine_quelle', 'Keine gültige Quelle für: ${song.titel}'); return; } if (position > 0) await _p.seek(Duration(seconds: position)); await _p.play(); _songWechsel.add(song); } catch (e) { debugPrint('Fehler beim Abspielen: $e'); MeloLogger().fehler('player_abspiel_fehler', e); // Song als korrupt markieren wenn lokale Datei if (song.streamUrl == null || song.streamUrl!.isEmpty) { _markiereAlsKorrupt(song); } } } Future playPause() async { if (_player == null) return; if (_p.playing) { await _p.pause(); } else if (aktuellerSong != null) { await _p.play(); } } Future vorheriges() async { if (_player == null || aktuellerSong == null) return; final pos = _p.position; if (pos.inSeconds > 5) { await _p.seek(Duration.zero); } else { final neuerIndex = _aktuellerIndex - 1; if (neuerIndex >= 0) { await spiele(_warteschlange[neuerIndex]); } } } Future naechstes() async { final neuerIndex = _aktuellerIndex + 1; if (neuerIndex < _warteschlange.length) { await spiele(_warteschlange[neuerIndex]); } } void setWarteschlange(List songs, {int startIndex = 0}) { _warteschlange.clear(); _warteschlange.addAll(songs); _aktuellerIndex = startIndex; } void dispose() { _autoNextSub?.cancel(); _player?.dispose(); _player = null; _songWechsel.close(); } /// Markiert den aktuellen Song in der DB als korrupt Future _markiereAlsKorrupt(Song song) async { if (song.id == null) return; try { final db = DbHelper(); final s = await db.songNachId(song.id!); if (s != null && !s.istKorrupt) { await db.alsKorruptMarkieren(song.id!); debugPrint('⚠️ Song als korrupt markiert: ${song.titel}'); MeloLogger().fehler('korrupt_markiert', 'Playback-Fehler: ${song.titel} (${song.dateiPfad})'); } } catch (e) { debugPrint('Korrupt-Markierung fehlgeschlagen: $e'); } } }