import 'dart:async'; import 'dart:io'; import 'dart:math'; 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'; /// Wiederhol-Modi für die Warteschlange. enum Wiederholmodus { aus, titel, playlist } class PlayerService extends ChangeNotifier { static final PlayerService _instanz = PlayerService._(); factory PlayerService() => _instanz; PlayerService._(); AudioPlayer? _player; final List _warteschlange = []; int _aktuellerIndex = -1; StreamSubscription? _autoNextSub; // ─── Zufallsmodus (Shuffle) ─── bool _zufallsmodus = false; /// Zufällige Reihenfolge der Warteschlangen-Indizes (ohne den aktuellen Song). /// Der aktuelle Song erscheint erst wieder, wenn er manuell gewählt wird. final List _shuffleReihenfolge = []; /// Position des zuletzt gespielten Songs in [_shuffleReihenfolge] (-1 = noch nichts). int _shufflePos = -1; // ─── Wiederholmodus (Repeat) ─── Wiederholmodus _wiederholmodus = Wiederholmodus.aus; 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; // ─── Zufallsmodus (Shuffle) ─── bool get zufallsmodus => _zufallsmodus; void setZufallsmodus(bool wert) { if (_zufallsmodus == wert) return; _zufallsmodus = wert; if (wert) _baueShuffleReihenfolge(); notifyListeners(); } // ─── Wiederholmodus (Repeat) ─── Wiederholmodus get wiederholmodus => _wiederholmodus; void setWiederholmodus(Wiederholmodus modus) { if (_wiederholmodus == modus) return; _wiederholmodus = modus; notifyListeners(); } /// Baut die Zufallsreihenfolge neu: alle Indizes außer dem aktuellen Song, gemischt. void _baueShuffleReihenfolge() { _shuffleReihenfolge.clear(); _shufflePos = -1; if (_warteschlange.isEmpty) return; if (_warteschlange.length == 1) { _shuffleReihenfolge.add(0); return; } final indices = List.generate(_warteschlange.length, (i) => i); if (_aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length) { indices.remove(_aktuellerIndex); } indices.shuffle(Random()); _shuffleReihenfolge.addAll(indices); } 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; } // Shuffle-Position synchron halten (Song kann über Queue-UI/Manuell gewählt sein) if (_zufallsmodus) { final pos = _shuffleReihenfolge.indexOf(_aktuellerIndex); if (pos < 0) { _baueShuffleReihenfolge(); } else { _shufflePos = pos; } } 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); notifyListeners(); // Abspiel-Historie für den Jahres-Recap registrieren if (song.id != null) { try { await DbHelper().abspielRegistrieren(song.id!); } catch (e) { debugPrint('Abspiel-Historie fehlgeschlagen: $e'); } } } 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(); } } /// Explizit abspielen (kein Toggle) — für System-Controls (Bluetooth, etc.) Future play() async { if (_player == null || aktuellerSong == null) return; await _p.play(); } /// Explizit pausieren (kein Toggle) — für System-Controls (Bluetooth, etc.) Future pause() async { if (_player == null) return; await _p.pause(); } /// Nur seeken ohne Reload der Quelle Future seek(Duration position) async { if (_player == null) return; await _p.seek(position); } Future vorheriges() async { if (_player == null || aktuellerSong == null) return; final pos = _p.position; if (pos.inSeconds > 5) { await _p.seek(Duration.zero); return; } if (_zufallsmodus && _shuffleReihenfolge.isNotEmpty) { _shufflePos--; if (_shufflePos < 0) _shufflePos = 0; final idx = _shuffleReihenfolge[_shufflePos]; if (idx >= 0 && idx < _warteschlange.length) { await spiele(_warteschlange[idx]); } return; } final neuerIndex = _aktuellerIndex - 1; if (neuerIndex >= 0) { await spiele(_warteschlange[neuerIndex]); } } Future naechstes() async { if (_player == null || aktuellerSong == null) return; if (_wiederholmodus == Wiederholmodus.titel) { // Gleichen Song von vorn wiederholen await _p.seek(Duration.zero); await _p.play(); return; } final idx = naechsterIndex(); if (idx == null) return; await spiele(_warteschlange[idx]); } /// Berechnet den Index des nächsten Songs in der Warteschlange — /// berücksichtigt Zufallsmodus und Wiederholmodus. Gibt `null` zurück, /// wenn die Warteschlange zu Ende ist (Repeat aus). int? naechsterIndex() { if (_aktuellerIndex < 0 || _warteschlange.isEmpty) return null; if (_wiederholmodus == Wiederholmodus.titel) return _aktuellerIndex; if (_zufallsmodus) { _shufflePos++; if (_shufflePos >= _shuffleReihenfolge.length) { if (_wiederholmodus == Wiederholmodus.playlist) { // Alle durchgespielt → neu mischen und weiter _baueShuffleReihenfolge(); _shufflePos++; } else { return null; } } if (_shufflePos < 0 || _shufflePos >= _shuffleReihenfolge.length) { return null; } final idx = _shuffleReihenfolge[_shufflePos]; return (idx >= 0 && idx < _warteschlange.length) ? idx : null; } var neuerIndex = _aktuellerIndex + 1; if (neuerIndex >= _warteschlange.length) { if (_wiederholmodus == Wiederholmodus.playlist) { neuerIndex = 0; } else { return null; } } return neuerIndex; } void setWarteschlange(List songs, {int startIndex = 0}) { _warteschlange.clear(); _warteschlange.addAll(songs); _aktuellerIndex = _warteschlange.isEmpty ? -1 : startIndex.clamp(0, _warteschlange.length - 1); _baueShuffleReihenfolge(); notifyListeners(); } @override void dispose() { _autoNextSub?.cancel(); _player?.dispose(); _player = null; _songWechsel.close(); super.dispose(); } /// 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'); } } }