## LyricsService (neu: lib/services/lyrics_service.dart) - LyricsZeile-Modell (zeit + text) - LRC-Parser: [mm:ss], [mm:ss.xx], [mm:ss.xxx], mehrere Zeitstempel/Zeile, [offset:±ms], Meta-Tags ignoriert, Karaoke-Tags <...> entfernt, sortiert - Quellen: lokale .lrc neben dem Song (song.dateiPfad → .lrc) → Netease-API (Suche + Lyric, public, kein Key) → null - Keine neuen Packages (nur http) ## Tests (test/lyrics_service_test.dart, 14 neu) - parseLrc: Formate, Multi-Timestamps, Offset, Sortierung, Karaoke-Tags - indexFuerPosition: Grenzfälle - ladeLyrics: lokale .lrc-Datei (Temp-Datei), ohne Datei → null (HTTP im Test geblockt) - flutter analyze 0 Issues, Tests 35/35
139 lines
4.6 KiB
Dart
139 lines
4.6 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:melo_app/models/song.dart';
|
|
import 'package:melo_app/services/lyrics_service.dart';
|
|
|
|
void main() {
|
|
// Blockiert echte HTTP-Requests im Test (Netease-Fallback → 400 → null)
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('parseLrc', () {
|
|
test('parst einfache [mm:ss.xx]-Zeilen', () {
|
|
const lrc = '[00:12.34]Erste Zeile\n[00:45.67]Zweite Zeile\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.length, 2);
|
|
expect(zeilen[0].zeit, const Duration(milliseconds: 12340));
|
|
expect(zeilen[0].text, 'Erste Zeile');
|
|
expect(zeilen[1].zeit, const Duration(milliseconds: 45670));
|
|
expect(zeilen[1].text, 'Zweite Zeile');
|
|
});
|
|
|
|
test('unterstützt [mm:ss] ohne Millisekunden', () {
|
|
const lrc = '[01:05]Nur Sekunden\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.length, 1);
|
|
expect(zeilen[0].zeit, const Duration(minutes: 1, seconds: 5));
|
|
});
|
|
|
|
test('unterstützt mehrere Zeitstempel pro Zeile', () {
|
|
const lrc = '[00:10.00][00:20.00][00:30.00]Refrain\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.length, 3);
|
|
expect(zeilen.map((z) => z.text).toSet(), {'Refrain'});
|
|
expect(zeilen[0].zeit, const Duration(seconds: 10));
|
|
expect(zeilen[1].zeit, const Duration(seconds: 20));
|
|
expect(zeilen[2].zeit, const Duration(seconds: 30));
|
|
});
|
|
|
|
test('ignoriert Meta-Tags und wertet [offset:] aus', () {
|
|
const lrc = '[ti:Testtitel]\n[ar:Testkünstler]\n[offset:500]\n'
|
|
'[00:10.00]Zeile mit Offset\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.length, 1);
|
|
expect(zeilen[0].zeit, const Duration(milliseconds: 10500));
|
|
});
|
|
|
|
test('sortiert Zeilen nach Zeitstempel', () {
|
|
const lrc = '[00:30.00]Spät\n[00:10.00]Früh\n[00:20.00]Mitte\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.map((z) => z.text).toList(), ['Früh', 'Mitte', 'Spät']);
|
|
});
|
|
|
|
test('entfernt Karaoke-Tags <...>', () {
|
|
const lrc = '[00:10.00]<00:10.00>Hal<00:11.00>lo\n';
|
|
final zeilen = LyricsService.parseLrc(lrc);
|
|
expect(zeilen.length, 1);
|
|
expect(zeilen[0].text, 'Hallo');
|
|
});
|
|
|
|
test('leere Eingabe liefert leere Liste', () {
|
|
expect(LyricsService.parseLrc(''), isEmpty);
|
|
expect(LyricsService.parseLrc('nur Text ohne Zeitstempel'), isEmpty);
|
|
});
|
|
});
|
|
|
|
group('indexFuerPosition', () {
|
|
final zeilen = LyricsService.parseLrc(
|
|
'[00:10.00]Eins\n[00:20.00]Zwei\n[00:30.00]Drei\n',
|
|
);
|
|
|
|
test('vor der ersten Zeile → 0', () {
|
|
expect(
|
|
LyricsService.indexFuerPosition(const Duration(seconds: 5), zeilen),
|
|
0,
|
|
);
|
|
});
|
|
|
|
test('genau auf einer Zeile → deren Index', () {
|
|
expect(
|
|
LyricsService.indexFuerPosition(const Duration(seconds: 20), zeilen),
|
|
1,
|
|
);
|
|
});
|
|
|
|
test('zwischen zwei Zeilen → vorherige', () {
|
|
expect(
|
|
LyricsService.indexFuerPosition(const Duration(seconds: 25), zeilen),
|
|
1,
|
|
);
|
|
});
|
|
|
|
test('nach der letzten Zeile → letzter Index', () {
|
|
expect(
|
|
LyricsService.indexFuerPosition(const Duration(seconds: 99), zeilen),
|
|
2,
|
|
);
|
|
});
|
|
|
|
test('leere Liste → -1', () {
|
|
expect(LyricsService.indexFuerPosition(Duration.zero, []), -1);
|
|
});
|
|
});
|
|
|
|
group('ladeLyrics (lokale .lrc)', () {
|
|
test('liest .lrc-Datei neben dem Song', () async {
|
|
final dir = await Directory.systemTemp.createTemp('melo_lrc_test');
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final songDatei = '${dir.path}/test.mp3';
|
|
final lrcDatei = '${dir.path}/test.lrc';
|
|
File(songDatei).writeAsStringSync('dummy');
|
|
File(lrcDatei).writeAsStringSync('[00:05.00]Lokal\n[00:10.00]Zeile\n');
|
|
|
|
final song = Song(
|
|
titel: 'Test',
|
|
kuenstler: 'K',
|
|
dauerSekunden: 60,
|
|
dateiPfad: songDatei,
|
|
);
|
|
final zeilen = await LyricsService().ladeLyrics(song);
|
|
expect(zeilen, isNotNull);
|
|
expect(zeilen!.length, 2);
|
|
expect(zeilen.first.text, 'Lokal');
|
|
});
|
|
|
|
test('ohne .lrc-Datei → null', () async {
|
|
final dir = await Directory.systemTemp.createTemp('melo_lrc_test2');
|
|
addTearDown(() => dir.delete(recursive: true));
|
|
final song = Song(
|
|
titel: 'Test',
|
|
kuenstler: 'K',
|
|
dauerSekunden: 60,
|
|
dateiPfad: '${dir.path}/ohne.mp3',
|
|
);
|
|
// Keine .lrc-Datei anlegen → Netease wird nicht getroffen (kein Netz im Test)
|
|
final zeilen = await LyricsService().ladeLyrics(song);
|
|
expect(zeilen, isNull);
|
|
});
|
|
});
|
|
}
|