import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; /// Ein Metadaten-Vorschlag aus der MusicBrainz-Datenbank. class MbVorschlag { const MbVorschlag({ required this.titel, required this.kuenstler, required this.album, required this.releaseMbid, required this.score, }); final String titel; final String kuenstler; /// Titel der ersten Veröffentlichung — leer, wenn MusicBrainz keine kennt. final String album; /// Kennung der Veröffentlichung, mit der sich das Cover holen lässt. final String releaseMbid; /// Wie gut der Treffer zur Anfrage passt (0–100). final int score; } /// MusicBrainz verlangt eine erkennbare Kennung; anonyme Anfragen werden /// gesperrt. const _userAgent = 'Melo/1.0 (https://baka-net.de)'; /// Schlägt Titel, Künstler und Album eines Stücks online bei MusicBrainz nach. class MusicBrainzService { MusicBrainzService({http.Client? client}) : _client = client ?? http.Client(); final http.Client _client; /// Liest die Vorschläge aus einer MusicBrainz-Antwort. Was sich nicht lesen /// lässt, ergibt eine leere Liste — die Suche ist nur eine Hilfe. static List parseAntwort(String body) { final Object? daten; try { daten = jsonDecode(body); } catch (_) { return const []; } if (daten is! Map) return const []; final aufnahmen = daten['recordings']; if (aufnahmen is! List) return const []; final vorschlaege = []; for (final eintrag in aufnahmen) { if (eintrag is! Map) continue; final credits = eintrag['artist-credit']; final erstesCredit = credits is List && credits.isNotEmpty ? credits.first as Map? : null; final releases = eintrag['releases']; final erstesRelease = releases is List && releases.isNotEmpty ? releases.first as Map? : null; vorschlaege.add(MbVorschlag( titel: eintrag['title'] as String? ?? '', kuenstler: erstesCredit?['name'] as String? ?? '', album: erstesRelease?['title'] as String? ?? '', releaseMbid: erstesRelease?['id'] as String? ?? '', score: eintrag['score'] as int? ?? 0, )); } return vorschlaege; } /// Adresse des Frontcovers einer Veröffentlichung im Cover Art Archive. String coverUrl(String releaseMbid) => 'https://coverartarchive.org/release/$releaseMbid/front-250'; /// Sucht zu [titel] (und wenn bekannt [kuenstler]) passende Aufnahmen. Future> suche({ required String titel, String? kuenstler, }) async { final teile = ['recording:"${_maskiere(titel)}"']; final name = kuenstler?.trim() ?? ''; if (name.isNotEmpty) teile.add('artist:"${_maskiere(name)}"'); final ziel = Uri.https('musicbrainz.org', '/ws/2/recording', { 'query': teile.join(' AND '), 'fmt': 'json', 'limit': '5', }); final antwort = await _client .get(ziel, headers: const {'User-Agent': _userAgent}) .timeout(const Duration(seconds: 15)); if (antwort.statusCode != 200) { debugPrint('MusicBrainz antwortet mit ${antwort.statusCode}'); throw Exception('MusicBrainz antwortet mit ${antwort.statusCode}'); } return parseAntwort(utf8.decode(antwort.bodyBytes)); } /// Anführungszeichen und Backslashes würden die Lucene-Abfrage zerlegen. static String _maskiere(String text) => text.replaceAll('\\', r'\\').replaceAll('"', r'\"'); }