import 'dart:io'; /// Liest ID3-Tags (v1 + v2) und eingebettetes Cover aus MP3-Dateien class Id3Reader { /// Gibt Metadaten zurück: titel, kuenstler, album, coverBytes static Map lesen(String filepath) { final result = { 'titel': '', 'kuenstler': '', 'album': '', 'cover': null, }; try { final file = File(filepath); if (!file.existsSync()) return result; final bytes = file.readAsBytesSync(); // ID3v1 (letzte 128 Bytes) if (bytes.length > 128) { final tag = bytes.sublist(bytes.length - 128); if (String.fromCharCodes(tag.sublist(0, 3)) == 'TAG') { result['titel'] = _trimNull(tag.sublist(3, 33)).trim(); result['kuenstler'] = _trimNull(tag.sublist(33, 63)).trim(); result['album'] = _trimNull(tag.sublist(63, 93)).trim(); } } // ID3v2 Header (Anfang der Datei) für Cover if (bytes.length > 10 && String.fromCharCodes(bytes.sublist(0, 3)) == 'ID3') { final size = _synchSafeInt(bytes, 6); var pos = 10; // Frame-Header lesen while (pos < size && pos + 10 < bytes.length) { final frameId = String.fromCharCodes(bytes.sublist(pos, pos + 4)); final frameSize = _frameSize(bytes, pos + 4); pos += 10; if (frameId == 'APIC' && pos + frameSize <= bytes.length) { // APIC = Attached Picture var p = pos; // Encoding (1 Byte) + MIME-Type final enc = bytes[p]; p += 1; var mimeEnd = p; while (mimeEnd < bytes.length && bytes[mimeEnd] != 0) mimeEnd++; final mime = String.fromCharCodes(bytes.sublist(p, mimeEnd)); p = mimeEnd + 1; // Picture Type (1 Byte) p += 1; // Description (null-terminated) var descEnd = p; while (descEnd < bytes.length) { if (enc == 1 || enc == 2) { if (descEnd + 1 < bytes.length && bytes[descEnd] == 0 && bytes[descEnd + 1] == 0) break; descEnd += 2; } else { if (bytes[descEnd] == 0) break; descEnd += 1; } } p = descEnd + (enc == 1 || enc == 2 ? 2 : 1); final remaining = pos + frameSize - p; if (remaining > 0 && p + remaining <= bytes.length) { result['cover'] = bytes.sublist(p, p + remaining); } break; } pos += frameSize; } } } catch (_) {} // Fallback: Dateiname als Titel if (result['titel'].isEmpty) { result['titel'] = filepath.split('/').last.replaceAll('.mp3', '').replaceAll('.m4a', ''); } return result; } static String _trimNull(List bytes) { final end = bytes.indexWhere((b) => b == 0); return String.fromCharCodes(end < 0 ? bytes : bytes.sublist(0, end)); } static int _synchSafeInt(List bytes, int offset) { return (bytes[offset] << 21) | (bytes[offset + 1] << 14) | (bytes[offset + 2] << 7) | bytes[offset + 3]; } static int _frameSize(List bytes, int offset) { return (bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]; } }