592 lines
20 KiB
Dart
592 lines
20 KiB
Dart
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<Song> _warteschlange = [];
|
||
int _aktuellerIndex = -1;
|
||
StreamSubscription<PlayerState>? _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<int> _shuffleReihenfolge = [];
|
||
|
||
/// Position des zuletzt gespielten Songs in [_shuffleReihenfolge] (-1 = noch nichts).
|
||
int _shufflePos = -1;
|
||
|
||
// ─── Wiederholmodus (Repeat) ───
|
||
Wiederholmodus _wiederholmodus = Wiederholmodus.aus;
|
||
|
||
// ─── Sleep-Timer ───
|
||
Timer? _sleepTimer;
|
||
DateTime? _sleepEnde;
|
||
|
||
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<Duration> get positionStream => _p.positionStream;
|
||
Stream<PlayerState> get stateStream => _p.playerStateStream;
|
||
|
||
final StreamController<Song?> _songWechsel = StreamController.broadcast();
|
||
Stream<Song?> get onSongWechsel => _songWechsel.stream;
|
||
|
||
// ─── Wiedergabegeschwindigkeit (F4) ───
|
||
|
||
double _geschwindigkeit = 1.0;
|
||
|
||
/// Aktuelle Wiedergabegeschwindigkeit (0.5–2.0, Default 1.0).
|
||
double get geschwindigkeit => _geschwindigkeit;
|
||
|
||
/// Setzt die Wiedergabegeschwindigkeit (just_audio `setSpeed`) und merkt
|
||
/// sie sich, damit [spiele] sie auf jede neue Quelle anwendet (just_audio
|
||
/// setzt die Geschwindigkeit bei `setUrl`/`setFilePath` auf 1.0 zurück).
|
||
Future<void> setGeschwindigkeit(double wert) async {
|
||
_geschwindigkeit = wert.clamp(0.5, 2.0).toDouble();
|
||
if (_player != null) {
|
||
try {
|
||
await _p.setSpeed(_geschwindigkeit);
|
||
} catch (e) {
|
||
debugPrint('setSpeed fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_speed', e);
|
||
}
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
// ─── 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.
|
||
/// Bei einer Einzel-Song-Queue kommt der Song nur bei Repeat-playlist in die
|
||
/// Reihenfolge — sonst ist die Queue nach dem Song zu Ende (naechsterIndex → null).
|
||
void _baueShuffleReihenfolge() {
|
||
_shuffleReihenfolge.clear();
|
||
_shufflePos = -1;
|
||
if (_warteschlange.isEmpty) return;
|
||
if (_warteschlange.length == 1) {
|
||
if (_wiederholmodus == Wiederholmodus.playlist) {
|
||
_shuffleReihenfolge.add(0);
|
||
}
|
||
return;
|
||
}
|
||
final indices = List<int>.generate(_warteschlange.length, (i) => i);
|
||
if (_aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length) {
|
||
indices.remove(_aktuellerIndex);
|
||
}
|
||
indices.shuffle(Random());
|
||
_shuffleReihenfolge.addAll(indices);
|
||
}
|
||
|
||
Future<void> 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();
|
||
// Wiedergabegeschwindigkeit aus der Einstellung anwenden (just_audio
|
||
// setzt sie bei jeder neuen Quelle zurück auf 1.0)
|
||
if (_geschwindigkeit != 1.0) {
|
||
try {
|
||
await _p.setSpeed(_geschwindigkeit);
|
||
} catch (e) {
|
||
debugPrint('setSpeed fehlgeschlagen: $e');
|
||
}
|
||
}
|
||
_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<void> playPause() async {
|
||
if (_player == null) return;
|
||
try {
|
||
if (_p.playing) {
|
||
await _p.pause();
|
||
} else if (aktuellerSong != null) {
|
||
await _p.play();
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Play/Pause fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_playpause', e);
|
||
}
|
||
}
|
||
|
||
/// Explizit abspielen (kein Toggle) — für System-Controls (Bluetooth, etc.)
|
||
Future<void> play() async {
|
||
if (_player == null || aktuellerSong == null) return;
|
||
try {
|
||
await _p.play();
|
||
} catch (e) {
|
||
debugPrint('Play fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_play', e);
|
||
}
|
||
}
|
||
|
||
/// Explizit pausieren (kein Toggle) — für System-Controls (Bluetooth, etc.)
|
||
Future<void> pause() async {
|
||
if (_player == null) return;
|
||
try {
|
||
await _p.pause();
|
||
} catch (e) {
|
||
debugPrint('Pause fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_pause', e);
|
||
}
|
||
}
|
||
|
||
/// Nur seeken ohne Reload der Quelle
|
||
Future<void> seek(Duration position) async {
|
||
if (_player == null) return;
|
||
try {
|
||
await _p.seek(position);
|
||
} catch (e) {
|
||
// Live-Streams ohne Seek-Unterstützung werfen hier — nicht crashen
|
||
debugPrint('Seek fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_seek', e);
|
||
}
|
||
}
|
||
|
||
Future<void> vorheriges() async {
|
||
if (_player == null || aktuellerSong == null) return;
|
||
try {
|
||
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]);
|
||
}
|
||
} catch (e) {
|
||
debugPrint('Vorheriger Song fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_vorheriges', e);
|
||
}
|
||
}
|
||
|
||
Future<void> naechstes() async {
|
||
if (_player == null || aktuellerSong == null) return;
|
||
if (_wiederholmodus == Wiederholmodus.titel) {
|
||
// Gleichen Song von vorn wiederholen — seek/play können bei nicht
|
||
// seekbaren Quellen (Live-Streams) werfen, daher abgesichert.
|
||
try {
|
||
await _p.seek(Duration.zero);
|
||
await _p.play();
|
||
} catch (e) {
|
||
debugPrint('Repeat-Titel fehlgeschlagen: $e');
|
||
MeloLogger().fehler('player_repeat_titel', e);
|
||
}
|
||
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];
|
||
// Sicherheitsnetz: Einzel-Song-Queue (oder stale Reihenfolge) darf bei
|
||
// Repeat-aus nicht denselben Song erneut liefern — das ist Queue-Ende.
|
||
if (idx == _aktuellerIndex && _wiederholmodus == Wiederholmodus.aus) {
|
||
return null;
|
||
}
|
||
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<Song> songs, {int startIndex = 0}) {
|
||
_warteschlange.clear();
|
||
_warteschlange.addAll(songs);
|
||
_aktuellerIndex = _warteschlange.isEmpty
|
||
? -1
|
||
: startIndex.clamp(0, _warteschlange.length - 1);
|
||
_baueShuffleReihenfolge();
|
||
notifyListeners();
|
||
}
|
||
|
||
// ─── Warteschlange (Queue-UI) ───
|
||
|
||
/// Unveränderliche Kopie der Warteschlange für die Anzeige.
|
||
List<Song> get warteschlange => List.unmodifiable(_warteschlange);
|
||
|
||
/// Index des aktuell spielenden Songs in der Warteschlange.
|
||
int get aktuellerIndex => _aktuellerIndex;
|
||
|
||
/// Die tatsächliche Wiedergabe-Reihenfolge — beginnend mit dem aktuell
|
||
/// spielenden Song (Position 0). NICHT die Bibliotheks-Reihenfolge:
|
||
/// - Normal: Warteschlange ab dem aktuellen Index
|
||
/// - Shuffle: aktuelle Shuffle-Position + Rest der Zufallsreihenfolge
|
||
List<Song> get wiedergabeReihenfolge {
|
||
final ergebnis = <Song>[];
|
||
if (_warteschlange.isEmpty) return List.unmodifiable(ergebnis);
|
||
if (_aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length) {
|
||
ergebnis.add(_warteschlange[_aktuellerIndex]);
|
||
}
|
||
if (_zufallsmodus) {
|
||
if (_shuffleReihenfolge.isNotEmpty) {
|
||
// _shufflePos == -1 → Shuffle gerade eingeschaltet, aktueller Song
|
||
// ist noch nicht in der Reihenfolge → ab Position 0 auflisten
|
||
final start = _shufflePos >= 0 ? _shufflePos + 1 : 0;
|
||
for (var i = start; i < _shuffleReihenfolge.length; i++) {
|
||
final idx = _shuffleReihenfolge[i];
|
||
// Duplikat vermeiden: den laufenden Song überspringen, wenn er in
|
||
// der Shuffle-Reihenfolge auftaucht (Einzel-Song + Repeat-playlist)
|
||
if (idx == _aktuellerIndex) continue;
|
||
if (idx >= 0 && idx < _warteschlange.length) {
|
||
ergebnis.add(_warteschlange[idx]);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
for (var i = _aktuellerIndex + 1; i < _warteschlange.length; i++) {
|
||
ergebnis.add(_warteschlange[i]);
|
||
}
|
||
}
|
||
return List.unmodifiable(ergebnis);
|
||
}
|
||
|
||
/// Verschiebt einen Eintrag in der tatsächlichen Wiedergabe-Reihenfolge
|
||
/// (Drag-and-Drop im Queue-Sheet). Position 0 = aktuell spielender Song
|
||
/// (bleibt oben und kann nicht verschoben werden).
|
||
/// - Normal: verschiebt in der Warteschlange (ab dem aktuellen Index)
|
||
/// - Shuffle: verschiebt in der Zufallsreihenfolge — das ändert die echte
|
||
/// Abspiel-Reihenfolge, ohne die Bibliotheks-Liste anzufassen
|
||
void verschiebeInWiedergabeReihenfolge(int von, int nach) {
|
||
if (von <= 0) return; // aktueller Song bleibt an Position 0
|
||
if (nach <= 0) nach = 1; // nie vor den aktuellen Song
|
||
if (von == nach) return;
|
||
if (_warteschlange.isEmpty) return;
|
||
|
||
if (_zufallsmodus) {
|
||
final start = _shufflePos >= 0 ? _shufflePos + 1 : 0;
|
||
final vonShuffle = start + von - 1;
|
||
final nachShuffle = start + nach - 1;
|
||
if (vonShuffle < 0 || vonShuffle >= _shuffleReihenfolge.length) return;
|
||
final item = _shuffleReihenfolge.removeAt(vonShuffle);
|
||
final ziel = nachShuffle > _shuffleReihenfolge.length
|
||
? _shuffleReihenfolge.length
|
||
: nachShuffle;
|
||
_shuffleReihenfolge.insert(ziel, item);
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
|
||
final vonQueue = _aktuellerIndex + von;
|
||
final nachQueue = _aktuellerIndex + nach;
|
||
if (vonQueue < 0 || vonQueue >= _warteschlange.length) return;
|
||
verschiebeInWarteschlange(vonQueue, nachQueue);
|
||
}
|
||
|
||
/// Setzt [song] als nächstes auf die Wiedergabe — direkt nach dem aktuellen
|
||
/// Song. Ist der Song bereits in der Warteschlange, wird er dorthin VERSCHOBEN
|
||
/// statt dupliziert. Im Shuffle wird die Reihenfolge nicht neu gemischt:
|
||
/// der Song wird an die nächste Position gepinnt (Play-Next hat Priorität).
|
||
void spieleAlsNaechstes(Song song) {
|
||
if (_warteschlange.isEmpty) {
|
||
_warteschlange.add(song);
|
||
if (_zufallsmodus) _baueShuffleReihenfolge();
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
if (_aktuellerIndex < 0) {
|
||
// Kein aktiver Song → ans Ende hängen
|
||
final vorhanden = _warteschlange.indexWhere((s) => s.id == song.id);
|
||
if (vorhanden < 0) _warteschlange.add(song);
|
||
if (_zufallsmodus) _baueShuffleReihenfolge();
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
|
||
final ziel = _aktuellerIndex + 1;
|
||
final vorhandenerIndex = _warteschlange.indexWhere((s) => s.id == song.id);
|
||
var gepinnt = -1;
|
||
var alterIndex = -1;
|
||
var einfuegePosition = -1;
|
||
var queueGeaendert = false;
|
||
|
||
if (vorhandenerIndex >= 0 && vorhandenerIndex != _aktuellerIndex) {
|
||
// Bereits in der Queue → verschieben statt duplizieren
|
||
if (vorhandenerIndex != ziel) {
|
||
alterIndex = vorhandenerIndex;
|
||
final item = _warteschlange.removeAt(vorhandenerIndex);
|
||
einfuegePosition = vorhandenerIndex < ziel ? ziel - 1 : ziel;
|
||
_warteschlange.insert(einfuegePosition, item);
|
||
if (vorhandenerIndex < _aktuellerIndex) _aktuellerIndex -= 1;
|
||
gepinnt = einfuegePosition;
|
||
queueGeaendert = true;
|
||
} else {
|
||
gepinnt = vorhandenerIndex; // steht schon direkt nach dem aktuellen
|
||
}
|
||
} else if (vorhandenerIndex < 0) {
|
||
einfuegePosition = ziel;
|
||
_warteschlange.insert(ziel, song);
|
||
gepinnt = ziel;
|
||
queueGeaendert = true;
|
||
}
|
||
// vorhandenerIndex == _aktuellerIndex → Song läuft gerade, nichts zu tun
|
||
|
||
if (_zufallsmodus && gepinnt >= 0) {
|
||
_pinneInShuffleReihenfolge(
|
||
gepinnt: gepinnt,
|
||
alterIndex: alterIndex,
|
||
einfuegePosition: einfuegePosition,
|
||
queueGeaendert: queueGeaendert,
|
||
);
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Korrigiert [_shuffleReihenfolge] nach einer Queue-Änderung und pinnt
|
||
/// [gepinnt] (neuer Queue-Index des Songs) als nächstes.
|
||
void _pinneInShuffleReihenfolge({
|
||
required int gepinnt,
|
||
required int alterIndex,
|
||
required int einfuegePosition,
|
||
required bool queueGeaendert,
|
||
}) {
|
||
if (queueGeaendert) {
|
||
// 1) Entfernung spiegeln (alter Index war evtl. noch in der Reihenfolge)
|
||
final entferntPos = alterIndex >= 0
|
||
? _shuffleReihenfolge.indexOf(alterIndex)
|
||
: -1;
|
||
if (entferntPos >= 0) {
|
||
_shuffleReihenfolge.removeAt(entferntPos);
|
||
if (entferntPos < _shufflePos) _shufflePos -= 1;
|
||
for (var k = 0; k < _shuffleReihenfolge.length; k++) {
|
||
if (_shuffleReihenfolge[k] > alterIndex) {
|
||
_shuffleReihenfolge[k] -= 1;
|
||
}
|
||
}
|
||
}
|
||
// 2) Einfügung spiegeln (Indizes ab der Einfügeposition rücken nach rechts)
|
||
for (var k = 0; k < _shuffleReihenfolge.length; k++) {
|
||
if (_shuffleReihenfolge[k] >= einfuegePosition) {
|
||
_shuffleReihenfolge[k] += 1;
|
||
}
|
||
}
|
||
}
|
||
// 3) Song an die Position direkt nach dem aktuellen pinnten
|
||
_shuffleReihenfolge.remove(gepinnt);
|
||
final pinPos = _shufflePos + 1 > _shuffleReihenfolge.length
|
||
? _shuffleReihenfolge.length
|
||
: _shufflePos + 1;
|
||
_shuffleReihenfolge.insert(pinPos, gepinnt);
|
||
// _shufflePos so setzen, dass der gepinnte Song als nächstes gespielt wird
|
||
_shufflePos = pinPos - 1;
|
||
}
|
||
|
||
/// Hängt [song] ans Ende der Warteschlange an.
|
||
void amEndeHinzufuegen(Song song) {
|
||
_warteschlange.add(song);
|
||
if (_zufallsmodus) _baueShuffleReihenfolge();
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Verschiebt einen Eintrag in der Warteschlange (Drag-and-Drop).
|
||
/// Der aktuelle Index wird entsprechend angepasst.
|
||
void verschiebeInWarteschlange(int von, int nach) {
|
||
if (von < 0 || von >= _warteschlange.length) return;
|
||
if (nach < 0 || nach >= _warteschlange.length) return;
|
||
if (von == nach) return;
|
||
final song = _warteschlange.removeAt(von);
|
||
_warteschlange.insert(nach, song);
|
||
if (_aktuellerIndex == von) {
|
||
_aktuellerIndex = nach;
|
||
} else if (von < _aktuellerIndex && nach >= _aktuellerIndex) {
|
||
_aktuellerIndex -= 1;
|
||
} else if (von > _aktuellerIndex && nach <= _aktuellerIndex) {
|
||
_aktuellerIndex += 1;
|
||
}
|
||
if (_zufallsmodus) _baueShuffleReihenfolge();
|
||
notifyListeners();
|
||
}
|
||
|
||
// ─── Sleep-Timer ───
|
||
|
||
bool get sleepTimerAktiv => _sleepTimer != null;
|
||
|
||
/// Verbleibende Zeit bis zum automatischen Pausieren (null wenn inaktiv).
|
||
Duration? get sleepRestzeit {
|
||
final ende = _sleepEnde;
|
||
if (ende == null) return null;
|
||
final rest = ende.difference(DateTime.now());
|
||
return rest.isNegative ? Duration.zero : rest;
|
||
}
|
||
|
||
/// Startet den Sleep-Timer — nach [dauer] wird die Wiedergabe pausiert.
|
||
void setSleepTimer(Duration dauer) {
|
||
cancelSleepTimer();
|
||
_sleepEnde = DateTime.now().add(dauer);
|
||
_sleepTimer = Timer(dauer, () {
|
||
_sleepTimer = null;
|
||
_sleepEnde = null;
|
||
pause();
|
||
notifyListeners();
|
||
});
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Bricht den Sleep-Timer ab.
|
||
void cancelSleepTimer() {
|
||
_sleepTimer?.cancel();
|
||
_sleepTimer = null;
|
||
_sleepEnde = null;
|
||
notifyListeners();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_autoNextSub?.cancel();
|
||
_sleepTimer?.cancel();
|
||
_player?.dispose();
|
||
_player = null;
|
||
_songWechsel.close();
|
||
super.dispose();
|
||
}
|
||
|
||
/// Markiert den aktuellen Song in der DB als korrupt
|
||
Future<void> _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');
|
||
}
|
||
}
|
||
}
|