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>
147 lines
4.3 KiB
Dart
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})';
|
|
}
|
|
}
|