import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'baka_auth.dart'; import 'yt_download_service.dart'; /// Ein Treffer der YouTube-Suche über den Baka-Proxy. class YtSuchTreffer { const YtSuchTreffer( {required this.title, required this.url, required this.duration}); final String title; final String url; final Duration duration; /// Vorschaubild über YouTubes öffentliches Thumbnail-CDN — braucht keinen /// eigenen API-Zugriff, nur die Video-ID aus [url]. String? get thumbnailUrl { final id = youtubeVideoId(url); return id == null ? null : 'https://i.ytimg.com/vi/$id/mqdefault.jpg'; } } /// Video-ID aus einer YouTube-URL — deckt `watch?v=`, `youtu.be/` und /// `/shorts/` ab. `null`, wenn sich keine ID erkennen lässt. String? youtubeVideoId(String url) { final uri = Uri.tryParse(url); if (uri == null) return null; final ausQuery = uri.queryParameters['v']; if (ausQuery != null && ausQuery.isNotEmpty) return ausQuery; final segmente = uri.pathSegments; if (segmente.isEmpty) return null; if (uri.host.contains('youtu.be')) return segmente.first; final shortsIndex = segmente.indexOf('shorts'); if (shortsIndex != -1 && shortsIndex + 1 < segmente.length) { return segmente[shortsIndex + 1]; } return null; } /// Durchsucht YouTube über den Baka-Proxy (`/api/search`, yt-dlp-gestützt) — /// dieselbe Anmeldung wie [YtDownloadService]. class YtSearchService extends ChangeNotifier { YtSearchService({required this.auth, http.Client? client}) : _client = client ?? http.Client(); static const proxyUrl = YtDownloadService.proxyUrl; final BakaAuth auth; final http.Client _client; bool _laeuft = false; String? _fehler; List _treffer = const []; bool get laeuft => _laeuft; String? get fehler => _fehler; List get treffer => _treffer; Future suchen(String query) async { final q = query.trim(); if (q.isEmpty) return; if (!auth.istAngemeldet) { _treffer = const []; _fehler = 'Bitte zuerst beim Baka-Konto anmelden'; notifyListeners(); return; } _laeuft = true; _fehler = null; notifyListeners(); final uri = Uri.parse('$proxyUrl/api/search') .replace(queryParameters: {'q': q}); final http.Response antwort; try { antwort = await _client .get(uri, headers: auth.authHeader) .timeout(const Duration(seconds: 20)); } on TimeoutException { _scheitere('Die Suche braucht zu lange — bitte später erneut versuchen'); return; } catch (e) { debugPrint('YT-Suche nicht erreichbar: $e'); _scheitere('Proxy nicht erreichbar'); return; } if (antwort.statusCode == 401) { _scheitere('Anmeldung abgelaufen — bitte neu anmelden'); return; } if (antwort.statusCode != 200) { _scheitere(_fehlerText(antwort)); return; } try { final roh = jsonDecode(antwort.body) as List; _treffer = [ for (final eintrag in roh.cast>()) YtSuchTreffer( title: eintrag['title'] as String? ?? '', url: eintrag['url'] as String? ?? '', duration: Duration(seconds: (eintrag['duration'] as num?)?.toInt() ?? 0), ), ]; } catch (e) { _scheitere('Antwort des Servers nicht lesbar'); return; } _laeuft = false; notifyListeners(); } void _scheitere(String text) { _fehler = text; _treffer = const []; _laeuft = false; notifyListeners(); } 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})'; } }