Files
Melo/lib/services/yt_search_service.dart
T
Hermes (Server)andClaude Sonnet 5 9e2294a480 YtSearchService: Gast-Zugang neben Baka-Auth unterstützen
Analog zu YtDownloadService: optionaler gast-Parameter, X-Guest-
Token statt Authorization, automatisches Token-Nachholen bei 401.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 20:56:58 +02:00

147 lines
4.3 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'baka_auth.dart';
import 'gast_zugang.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, this.gast, http.Client? client})
: _client = client ?? http.Client();
static const proxyUrl = YtDownloadService.proxyUrl;
final BakaAuth auth;
final GastZugang? gast;
final http.Client _client;
bool _laeuft = false;
String? _fehler;
List<YtSuchTreffer> _treffer = const [];
bool get laeuft => _laeuft;
String? get fehler => _fehler;
List<YtSuchTreffer> get treffer => _treffer;
Future<void> suchen(String query) async {
final q = query.trim();
if (q.isEmpty) return;
final gastAktiv = !auth.istAngemeldet && (gast?.hatToken ?? false);
if (!auth.istAngemeldet && !gastAktiv) {
_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: gastAktiv ? gast!.gastHeader : 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) {
if (gastAktiv) unawaited(gast!.holeToken());
_scheitere(gastAktiv
? 'Gast-Zugang abgelaufen — bitte erneut versuchen'
: '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<Map<String, dynamic>>())
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<String, dynamic>;
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})';
}
}