import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'baka_auth.dart'; /// Entfernt alles, was in einem Dateinamen Ärger macht. String sichererDateiname(String titel) => titel.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); /// Ergebnis eines geglückten Downloads. class YtErgebnis { const YtErgebnis({ required this.dateiPfad, required this.titel, required this.kuenstler, required this.dauerSekunden, }); final String dateiPfad; final String titel; final String kuenstler; final int dauerSekunden; } /// Lädt Musik über den Baka-YouTube-Proxy herunter. Der Proxy erledigt die /// eigentliche Arbeit und liefert eine fertige MP3; die App speichert sie nur. class YtDownloadService extends ChangeNotifier { YtDownloadService({required this.auth, http.Client? client}) : _client = client ?? http.Client(); static const proxyUrl = 'https://yt.baka-net.de'; final BakaAuth auth; final http.Client _client; bool _laeuft = false; String? _status; String? _fehler; bool get laeuft => _laeuft; /// Was gerade passiert — für die Anzeige während des Downloads. String? get status => _status; String? get fehler => _fehler; void _melde(String text) { _status = text; notifyListeners(); } void _scheitere(String text) { _fehler = text; _status = null; _laeuft = false; notifyListeners(); } /// Lädt [url] herunter und legt die MP3 in [zielOrdner] ab. /// Mit [cookies] nutzt der Proxy seine YouTube-Anmeldung — nötig für /// altersbeschränkte Videos. /// Gibt bei Erfolg das Ergebnis zurück, sonst `null` (siehe [fehler]). Future herunterladen(String url, {required String zielOrdner, bool cookies = true}) async { _fehler = null; _laeuft = true; notifyListeners(); if (!auth.istAngemeldet) { _scheitere('Bitte zuerst beim Baka-Konto anmelden'); return null; } if (!url.contains('youtube.com') && !url.contains('youtu.be')) { _scheitere('Das ist keine YouTube-Adresse'); return null; } // ─── 1/2: Proxy fragen — er lädt das Video und wandelt es um ─── _melde('Proxy wird gefragt …'); final http.Response antwort; try { antwort = await _client .post( Uri.parse('$proxyUrl/api/yt-dl'), headers: {...auth.authHeader, 'Content-Type': 'application/json'}, body: jsonEncode({'url': url, 'cookies': cookies}), ) .timeout(const Duration(seconds: 180)); } catch (e) { debugPrint('YT-Proxy nicht erreichbar: $e'); _scheitere('Proxy nicht erreichbar'); return null; } if (antwort.statusCode == 401) { _scheitere('Anmeldung abgelaufen — bitte neu anmelden'); return null; } if (antwort.statusCode != 200) { _scheitere(_fehlerText(antwort)); return null; } final daten = jsonDecode(antwort.body) as Map; final titel = daten['titel'] as String? ?? 'Unbekannt'; final dauer = (daten['dauer'] as int?) ?? 0; final mp3Url = daten['mp3_url'] as String? ?? '/api/dl/audio.mp3'; // ─── 2/2: fertige MP3 abholen und speichern ─── _melde('Lade „$titel“ …'); final ordner = Directory(zielOrdner); if (!await ordner.exists()) { await ordner.create(recursive: true); } final datei = File('${ordner.path}/${sichererDateiname(titel)}.mp3'); try { final mp3 = await _client .get(Uri.parse('$proxyUrl$mp3Url'), headers: auth.authHeader) .timeout(const Duration(seconds: 180)); if (mp3.statusCode != 200) { _scheitere('Herunterladen fehlgeschlagen (${mp3.statusCode})'); return null; } await datei.writeAsBytes(mp3.bodyBytes); } catch (e) { debugPrint('MP3-Abruf fehlgeschlagen: $e'); // Halbe Dateien nicht liegen lassen — sonst taucht Müll in der // Bibliothek auf, sobald der Ordner das nächste Mal gescannt wird. if (await datei.exists()) { await datei.delete(); } _scheitere('Herunterladen fehlgeschlagen'); return null; } _laeuft = false; _status = null; notifyListeners(); // yt-dlp liefert nur einen Titel. "Künstler - Lied" ist die übliche // Form; steht da nichts dergleichen, wird nichts geraten. final teile = titel.split(' - '); return YtErgebnis( dateiPfad: datei.path, titel: titel, kuenstler: teile.length >= 2 ? teile.first.trim() : '', dauerSekunden: dauer, ); } String _fehlerText(http.Response antwort) { try { final j = jsonDecode(antwort.body) as Map; final text = j['error'] as String?; if (text != null && text.isNotEmpty) return text; } catch (_) { // Kein JSON — dann eben der Statuscode. } return 'Proxy-Fehler (${antwort.statusCode})'; } }