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/models/song.dart
T
Dustin a483436490 v2.42 — Issue #7: ID3-Reader erweitern + Metadaten-Dialog verschönern
## ID3-Reader (lib/services/id3_reader.dart)
- ID3v1: Jahr (Bytes 93-97) + Genre-Index (Byte 127, vollständige Genre-Liste 0-147)
- ID3v2: Text-Frames TIT2 (Titel), TPE1 (Künstler), TALB (Album), TYER (Jahr), TCON (Genre), TRCK (Track)
- ID3v2-Frames haben Vorrang vor ID3v1 (Füll-Logik)
- UTF-16/UTF-8 Encoding-Unterstützung für Text-Frames
- Genre-Bereinigung: Klammer-Präfixe entfernt, Track-Nummer extrahiert

## Song-Model (lib/models/song.dart)
- Neue Felder: jahr, genre, track (nullable, optional)
- Neue getter: dateiFormat (MP3/M4A/FLAC/WAV/OGG aus Dateiendung)
- DB-Migration v5→v6: jahr, genre, track Spalten

## Metadaten-Dialog (lib/widgets/metadaten_dialog.dart)
- Cover-Bild (128×128) mit rotem Glow-Rahmen oben falls vorhanden
- Editable Felder mit Icons: Titel, Künstler, Album, Jahr, Genre
- Read-Only-Info-Card: Dauer, Dateigröße, Format
- Cards mit roten Akzenten + Melo-Design (schwarz/rot/dunkelgrau)
- ID3-Fallback: Jahr/Genre aus Datei nachladen wenn nicht in DB
- Speichern persistiert auch Jahr und Genre in die DB

## Musik-Scanner (lib/services/musik_scanner.dart)
- jahr, genre, track aus ID3-Tags an Song-Model durchgereicht
- _nichtLeer-Helfer für null-sichere String-Extraktion
2026-08-02 16:10:58 +02:00

107 lines
3.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.
class Song {
final int? id;
final String titel;
final String kuenstler;
final String album;
final String? jahr;
final String? genre;
final String? track;
final int dauerSekunden;
final String dateiPfad;
final String? coverPfad;
final int groesseBytes;
final bool istHeruntergeladen;
final bool istKorrupt; // true wenn Datei defekt (Magic-Byte-Check, ffprobe, Playback-Fehler)
final String hinzugefuegtAm;
final String downloadQuelle; // "local", "youtube", "server"
final String? streamUrl; // Für Server-Streaming (Navidrome) wird bewusst NICHT in der DB persistiert (Auth-Token-Schutz)
final String? ytUrl; // YouTube-Quell-URL für Auto-Redownload bei korrupten Songs
int? zuletztPosition; // Sekunden, für Wiederaufnahme
Set<int>? tagIds; // Cache für Tag-Filterung (nicht in DB gespeichert)
Song({
this.id,
required this.titel,
required this.kuenstler,
this.album = '',
this.jahr,
this.genre,
this.track,
required this.dauerSekunden,
required this.dateiPfad,
this.coverPfad,
this.groesseBytes = 0,
this.istHeruntergeladen = false,
this.istKorrupt = false,
String? hinzugefuegtAm,
this.downloadQuelle = 'local',
this.streamUrl,
this.ytUrl,
this.zuletztPosition,
}) : hinzugefuegtAm = hinzugefuegtAm ?? DateTime.now().toIso8601String();
Map<String, dynamic> toMap() => {
'id': id,
'titel': titel,
'kuenstler': kuenstler,
'album': album,
'jahr': jahr,
'genre': genre,
'track': track,
'dauer_sekunden': dauerSekunden,
'datei_pfad': dateiPfad,
'cover_pfad': coverPfad,
'groesse_bytes': groesseBytes,
'ist_heruntergeladen': istHeruntergeladen ? 1 : 0,
'ist_korrupt': istKorrupt ? 1 : 0,
'hinzugefuegt_am': hinzugefuegtAm,
'download_quelle': downloadQuelle,
'stream_url': null, // Token-haltige Stream-URLs nie persistieren (Sicherheit)
'yt_url': ytUrl,
'zuletzt_position': zuletztPosition,
};
factory Song.fromMap(Map<String, dynamic> m) => Song(
id: m['id'] as int?,
titel: m['titel'] as String,
kuenstler: m['kuenstler'] as String,
album: m['album'] as String? ?? '',
jahr: m['jahr'] as String?,
genre: m['genre'] as String?,
track: m['track'] as String?,
dauerSekunden: m['dauer_sekunden'] as int,
dateiPfad: m['datei_pfad'] as String,
coverPfad: m['cover_pfad'] as String?,
groesseBytes: m['groesse_bytes'] as int? ?? 0,
istHeruntergeladen: (m['ist_heruntergeladen'] as int?) == 1,
istKorrupt: (m['ist_korrupt'] as int?) == 1,
hinzugefuegtAm: m['hinzugefuegt_am'] as String?,
downloadQuelle: m['download_quelle'] as String? ?? 'local',
streamUrl: m['stream_url'] as String?,
ytUrl: m['yt_url'] as String?,
zuletztPosition: m['zuletzt_position'] as int?,
);
String get dauerFormatiert {
final min = dauerSekunden ~/ 60;
final sek = dauerSekunden % 60;
return '$min:${sek.toString().padLeft(2, '0')}';
}
String get groesseFormatiert {
if (groesseBytes < 1048576) return '${(groesseBytes / 1024).toStringAsFixed(0)} KB';
return '${(groesseBytes / 1048576).toStringAsFixed(1)} MB';
}
/// Dateiformat aus Dateiendung ableiten
String get dateiFormat {
final lower = dateiPfad.toLowerCase();
if (lower.endsWith('.mp3')) return 'MP3';
if (lower.endsWith('.m4a')) return 'M4A (AAC)';
if (lower.endsWith('.flac')) return 'FLAC';
if (lower.endsWith('.wav')) return 'WAV';
if (lower.endsWith('.ogg')) return 'OGG';
return lower.split('.').last.toUpperCase();
}
}