This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/services/recap_service.dart
T
Dustin 20ef788178 v2.46 — Jahres-Recap (Spotify-Wrapped-Stil) mit Hörstatistiken
## Neuer Recap-Screen
- lib/screens/recap_screen.dart: Full-Screen mit 7 animierten Seiten (Intro, Top 5 Songs, Top 3 Künstler, Hörzeit, häufigster Tag, Nachtaktive %, Gesamtzahl + Teilen)
- Wrapped-Stil: Verlaufs-Gradienten, große Zahlen mit Count-up-Animation, Fade+Scale-Seitenwechsel, Fortschritts-Punkte, Weiter-Button
- Teilen-Button kopiert Recap-Text in die Zwischenablage (kein neues Package)

## Datenbasis
- lib/services/recap_service.dart: aggregiert lokale Abspiel-Historie zu Top-5-Songs, Top-3-Künstler, Hörzeit, Wochentag, Nacht-%, Gesamtzahl
- Fallback auf Gesamt-Historie wenn im Zieljahr keine Daten; freundlicher Leer-Zustand ohne History

## DB + Tracking
- db_helper.dart: Migration v6→v7 mit neuer Tabelle abspiel_historie (song_id, played_at) + Backfill aus wiedergabe_verlauf
- player_service.dart: registriert jeden Abspielvorgang in der Historie

## Einstieg
- Home-Screen: Karte 'Jahres-Recap 2026' unter der Statistik-Card
- Einstellungen: Kachel 'Jahres-Recap' in Sektion Daten & Privatsphäre

flutter analyze: 0 Issues, Tests 4/4
2026-08-03 02:54:11 +02:00

146 lines
4.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../database/db_helper.dart';
import '../models/song.dart';
/// Ein Song mit seiner Abspiel-Anzahl im Recap
class RecapSongStat {
final Song song;
final int anzahl;
RecapSongStat(this.song, this.anzahl);
}
/// Ein Künstler mit seiner Abspiel-Anzahl im Recap
class RecapKuenstlerStat {
final String name;
final int anzahl;
RecapKuenstlerStat(this.name, this.anzahl);
}
/// Aggregierte Recap-Daten (wie Spotify Wrapped)
class RecapDaten {
final int jahr;
final int gesamtAnzahl; // Gesamtzahl aller Abspielungen
final int gesamtSekunden; // Geschätzte Hörzeit (Summe Songdauer je Abspielung)
final String haeufigsterTag; // z.B. "Freitag"
final double nachtProzent; // % der Abspielungen nach 22 Uhr (0100)
final List<RecapSongStat> topSongs; // Top 5
final List<RecapKuenstlerStat> topKuenstler; // Top 3
RecapDaten({
required this.jahr,
required this.gesamtAnzahl,
required this.gesamtSekunden,
required this.haeufigsterTag,
required this.nachtProzent,
required this.topSongs,
required this.topKuenstler,
});
String get hoerzeitFormatiert {
final stunden = gesamtSekunden ~/ 3600;
final minuten = (gesamtSekunden % 3600) ~/ 60;
if (stunden > 0) return '$stunden Std $minuten Min';
return '$minuten Min';
}
}
/// Aggregiert die lokale Abspiel-Historie zu Recap-Statistiken.
///
/// Datenquelle ist die lokale `abspiel_historie`-Tabelle (nicht der Server),
/// da die History lokal aufgezeichnet wird.
class RecapService {
final DbHelper _db = DbHelper();
static const List<String> _wochentage = [
'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
'Freitag', 'Samstag', 'Sonntag',
];
/// Berechnet den Recap. Filtert auf [jahr] (Standard: aktuelles Jahr).
/// Falls im gewählten Jahr keine Daten existieren, wird auf die
/// Gesamt-Historie zurückgegriffen. Liefert `null` bei leerer History.
Future<RecapDaten?> berechne({int? jahr}) async {
final zielJahr = jahr ?? DateTime.now().year;
var rows = await _db.abspielHistorie();
if (rows.isEmpty) return null;
final gefiltert = rows.where((r) {
final t = DateTime.tryParse(r['played_at'] as String? ?? '');
return t != null && t.year == zielJahr;
}).toList();
// Fallback: wenn im Zieljahr nichts gehört wurde → gesamte History
final daten = gefiltert.isNotEmpty ? gefiltert : rows;
// ── Aggregation ──
final songCounts = <int, int>{};
final songMap = <int, Song>{};
final kuenstlerCounts = <String, int>{};
final wochentagCounts = List<int>.filled(7, 0);
var nachtCount = 0;
var gesamtSekunden = 0;
for (final r in daten) {
final songId = r['song_id'] as int;
songCounts[songId] = (songCounts[songId] ?? 0) + 1;
songMap[songId] ??= Song.fromMap({
'id': songId,
'titel': r['titel'] ?? '',
'kuenstler': r['kuenstler'] ?? 'Unbekannt',
'dauer_sekunden': r['dauer_sekunden'] ?? 0,
'datei_pfad': '',
});
final kuenstler = (r['kuenstler'] as String?) ?? 'Unbekannt';
kuenstlerCounts[kuenstler] = (kuenstlerCounts[kuenstler] ?? 0) + 1;
final t = DateTime.tryParse(r['played_at'] as String? ?? '');
if (t != null) {
wochentagCounts[t.weekday - 1]++;
if (t.hour >= 22) nachtCount++;
}
gesamtSekunden += (r['dauer_sekunden'] as int?) ?? 0;
}
final gesamtAnzahl = daten.length;
final nachtProzent = gesamtAnzahl == 0
? 0.0
: (nachtCount * 100 / gesamtAnzahl).roundToDouble();
// Häufigster Wochentag (bei Gleichstand der erste)
var maxTag = 0;
for (var i = 1; i < 7; i++) {
if (wochentagCounts[i] > wochentagCounts[maxTag]) maxTag = i;
}
// Top 5 Songs
final sortierteSongs = songCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
final topSongs = sortierteSongs
.take(5)
.map((e) => RecapSongStat(songMap[e.key]!, e.value))
.toList();
// Top 3 Künstler
final sortierteKuenstler = kuenstlerCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
final topKuenstler = sortierteKuenstler
.take(3)
.map((e) => RecapKuenstlerStat(e.key, e.value))
.toList();
return RecapDaten(
jahr: zielJahr,
gesamtAnzahl: gesamtAnzahl,
gesamtSekunden: gesamtSekunden,
haeufigsterTag: _wochentage[maxTag],
nachtProzent: nachtProzent,
topSongs: topSongs,
topKuenstler: topKuenstler,
);
}
}