Songtext aus Datei-Tags, Warteschlangen-Aktionen, Kategorie-Cover im

Sperrbildschirm und ReplayGain

- DB-Schema 6: lyrics + gain_db
- Songtext bevorzugt lokalen Tag, Server nur als Rueckfall; Song-ID-Bug
  beim Lyrics-Aufruf behoben
- "Als Naechstes spielen" / "Zur Warteschlange hinzufuegen" ohne Eingriff
  in Favoriten oder Wiedergabelisten
- songToMediaItem nimmt eine Cover-Vorgabe: Sperrbildschirm zeigt das
  Kategorie-Cover
- ReplayGain: Tags werden gelesen, laute Titel abgesenkt, abschaltbar

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
This commit is contained in:
2026-08-20 18:41:36 +02:00
co-authored by Claude Opus 5
parent 1325badf90
commit 4ebd0a8dd4
17 changed files with 534 additions and 46 deletions
+40
View File
@@ -7,6 +7,7 @@ import '../library/database.dart';
import '../services/cache_manager.dart';
import '../services/navidrome_service.dart';
import 'headphone_autoplay.dart';
import 'replay_gain.dart';
import 'sleep_timer.dart';
/// Entscheidet, ob eine gespeicherte Wiedergabeposition beim Laden
@@ -28,6 +29,13 @@ bool shouldResumeAt(int lastPositionMs, Duration? trackDuration) {
bool shouldCountPlay(int? newIndex, int? lastCountedIndex) =>
newIndex != null && newIndex != lastCountedIndex;
/// Stelle, an der ein Titel eingefügt wird, damit er als Nächstes läuft:
/// direkt hinter dem laufenden. Läuft nichts, kommt er ans Ende.
int playNextIndex({required int? currentIndex, required int queueLength}) {
if (currentIndex == null) return queueLength;
return (currentIndex + 1).clamp(0, queueLength);
}
/// Kern der Wiedergabe: kapselt just_audio hinter audio_service,
/// damit Hintergrund-Wiedergabe + Lockscreen/Notification funktionieren.
class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
@@ -43,6 +51,10 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
/// Wird von den App-Einstellungen gesetzt.
bool autoPlayOnHeadphones = false;
/// Alle Titel auf eine ähnliche Lautstärke bringen (ReplayGain).
/// Wird von den App-Einstellungen gesetzt.
bool normalizeVolume = true;
MeloAudioHandler({required this.db}) {
_cache = CacheManager();
_cache.init();
@@ -69,6 +81,7 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final q = queue.value;
if (index != null && index < q.length) {
mediaItem.add(q[index].copyWith(duration: _player.duration));
_applyGain(q[index]);
if (shouldCountPlay(index, _lastCountedIndex)) {
_lastCountedIndex = index;
final songId = q[index].extras?['songId'] as String?;
@@ -88,6 +101,13 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
});
}
/// Gleicht die Lautstärke des Titels an. Ohne ReplayGain-Tag oder bei
/// abgeschalteter Einstellung bleibt sie unverändert.
void _applyGain(MediaItem item) {
final gain = item.extras?['gainDb'] as double?;
_player.setVolume(normalizeVolume ? volumeForGain(gain) : 1.0);
}
/// Startet die Wiedergabe, wenn Kopfhörer verbunden werden und der Nutzer
/// das in den Einstellungen erlaubt hat.
Future<void> _watchHeadphones() async {
@@ -163,6 +183,26 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
await play();
}
/// Hängt [item] hinten an die Warteschlange an, ohne die laufende
/// Wiedergabe zu unterbrechen. Favoriten und Wiedergabelisten bleiben
/// unberührt — die Warteschlange ist nur für diese Sitzung.
Future<void> addToQueue(MediaItem item) async {
await _player.addAudioSource(AudioSource.uri(Uri.parse(item.id), tag: item));
queue.add([...queue.value, item]);
}
/// Spielt [item] als Nächstes, direkt nach dem laufenden Titel.
Future<void> playNext(MediaItem item) async {
final index = playNextIndex(
currentIndex: _player.currentIndex,
queueLength: queue.value.length,
);
await _player.insertAudioSource(
index, AudioSource.uri(Uri.parse(item.id), tag: item));
final updated = [...queue.value]..insert(index, item);
queue.add(updated);
}
@override
Future<void> play() => _player.play();