import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'baka_auth.dart'; /// Ein von ACRCloud erkanntes Stück. class AcrTreffer { const AcrTreffer({ required this.titel, required this.kuenstler, required this.album, }); final String titel; final String kuenstler; final String album; } /// Fehler, den die Erkennung selbst meldet (falscher Schlüssel, Limit /// erreicht, kein Netz …) — mit einem Text, der dem Nutzer gezeigt werden darf. class AcrCloudException implements Exception { const AcrCloudException(this.nachricht); final String nachricht; @override String toString() => nachricht; } /// Musikerkennung über ACRCloud: eine kurze Aufnahme hochladen und den /// erkannten Titel zurückbekommen. class AcrCloudService { AcrCloudService({ required this.accessKey, required this.secretKey, this.host = standardHost, http.Client? client, }) : _client = client ?? http.Client(); static const standardHost = 'identify-eu-west-1.acrcloud.com'; final String accessKey; final String secretKey; final String host; final http.Client _client; /// HMAC-SHA1 über die von ACRCloud vorgeschriebene Zeichenkette, /// base64-codiert. Muss zum mitgeschickten [timestamp] passen. static String signatur({ required String accessKey, required String secretKey, required int timestamp, }) { final zeichenkette = 'POST\n/v1/identify\n$accessKey\naudio\n1\n$timestamp'; final hmac = Hmac(sha1, utf8.encode(secretKey)); return base64.encode(hmac.convert(utf8.encode(zeichenkette)).bytes); } /// Wertet die Antwort aus: Treffer, `null` (nichts erkannt) oder Ausnahme. static AcrTreffer? parseAntwort(String body) { final Map daten; try { daten = jsonDecode(body) as Map; } catch (_) { throw const AcrCloudException('Musikerkennung antwortet unverständlich'); } final status = daten['status'] as Map?; final code = status?['code'] as int?; // 1001 = "No result" — das ist kein Fehler, nur kein Treffer. if (code == 1001) return null; if (code != 0) { throw AcrCloudException( status?['msg'] as String? ?? 'Musikerkennung fehlgeschlagen ($code)'); } final metadaten = daten['metadata'] as Map?; final musik = metadaten?['music'] as List?; if (musik == null || musik.isEmpty) return null; final erster = musik.first as Map; final kuenstler = erster['artists'] as List?; return AcrTreffer( titel: erster['title'] as String? ?? '', kuenstler: kuenstler == null || kuenstler.isEmpty ? '' : (kuenstler.first as Map)['name'] as String? ?? '', album: (erster['album'] as Map?)?['name'] as String? ?? '', ); } /// Schickt die Aufnahme [sample] (WAV) zur Erkennung. /// Gibt bei Erfolg den Treffer zurück, `null` wenn nichts erkannt wurde. Future erkenne(List sample) async { final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; final anfrage = http.MultipartRequest('POST', Uri.https(host, '/v1/identify')) ..fields['access_key'] = accessKey ..fields['data_type'] = 'audio' ..fields['signature_version'] = '1' ..fields['timestamp'] = '$timestamp' ..fields['signature'] = signatur( accessKey: accessKey, secretKey: secretKey, timestamp: timestamp) ..fields['sample_bytes'] = '${sample.length}' ..files.add(http.MultipartFile.fromBytes('sample', sample, filename: 'sample.wav')); final http.Response antwort; try { final gestreamt = await _client.send(anfrage).timeout(const Duration(seconds: 30)); antwort = await http.Response.fromStream(gestreamt); } catch (e) { debugPrint('Musikerkennung nicht erreichbar: $e'); throw const AcrCloudException('Keine Verbindung zur Musikerkennung'); } return parseAntwort(antwort.body); } } /// Die ACRCloud-Zugangsdaten. Sie stehen nirgends im Code — der Nutzer gibt /// sie einmalig ein, danach liegen sie im verschlüsselten Gerätespeicher. class AcrZugang { AcrZugang({TokenSpeicher? speicher}) : _speicher = speicher ?? const SicherenSpeicher(); static const _accessKeyKey = 'acr_access_key'; static const _secretKeyKey = 'acr_secret_key'; final TokenSpeicher _speicher; String? accessKey; String? secretKey; bool get istKonfiguriert => (accessKey?.isNotEmpty ?? false) && (secretKey?.isNotEmpty ?? false); Future laden() async { accessKey = await _speicher.lesen(_accessKeyKey); secretKey = await _speicher.lesen(_secretKeyKey); } Future speichern(String accessKey, String secretKey) async { this.accessKey = accessKey; this.secretKey = secretKey; await _speicher.schreiben(_accessKeyKey, accessKey); await _speicher.schreiben(_secretKeyKey, secretKey); } }