YouTube-Gast-Zugang (App-Seite): 5 Downloads/Tag ohne Konto #4

Closed
dustin wants to merge 12 commits from feature/youtube-gast-zugang into fix/p0-vollwertigkeit
2 changed files with 112 additions and 5 deletions
Showing only changes of commit 1ff03c96ed - Show all commits
+16 -5
View File
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'baka_auth.dart';
import 'gast_zugang.dart';
/// Entfernt alles, was in einem Dateinamen Ärger macht.
String sichererDateiname(String titel) =>
@@ -35,12 +36,13 @@ class YtErgebnis {
/// 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})
YtDownloadService({required this.auth, this.gast, http.Client? client})
: _client = client ?? http.Client();
static const proxyUrl = 'https://yt.baka-net.de';
final BakaAuth auth;
final GastZugang? gast;
final http.Client _client;
bool _laeuft = false;
@@ -95,7 +97,8 @@ class YtDownloadService extends ChangeNotifier {
final abbruch = _abbruch = Completer<void>();
notifyListeners();
if (!auth.istAngemeldet) {
final gastAktiv = !auth.istAngemeldet && (gast?.hatToken ?? false);
if (!auth.istAngemeldet && !gastAktiv) {
_scheitere('Bitte zuerst beim Baka-Konto anmelden');
return null;
}
@@ -103,6 +106,7 @@ class YtDownloadService extends ChangeNotifier {
_scheitere('Das ist keine YouTube-Adresse');
return null;
}
final zugangsHeader = gastAktiv ? gast!.gastHeader : auth.authHeader;
// ─── 1/2: Proxy fragen — er lädt das Video und wandelt es um ───
_melde('Proxy wird gefragt …');
@@ -113,7 +117,7 @@ class YtDownloadService extends ChangeNotifier {
.post(
Uri.parse('$proxyUrl/api/yt-dl'),
headers: {
...auth.authHeader,
...zugangsHeader,
'Content-Type': 'application/json'
},
body: jsonEncode({'url': url, 'cookies': cookies}),
@@ -137,7 +141,10 @@ class YtDownloadService extends ChangeNotifier {
}
if (antwort.statusCode == 401) {
_scheitere('Anmeldung abgelaufen — bitte neu anmelden');
if (gastAktiv) unawaited(gast!.holeToken());
_scheitere(gastAktiv
? 'Gast-Zugang abgelaufen — bitte erneut versuchen'
: 'Anmeldung abgelaufen — bitte neu anmelden');
return null;
}
if (antwort.statusCode != 200) {
@@ -149,6 +156,10 @@ class YtDownloadService extends ChangeNotifier {
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';
final gastRest = daten['guest_remaining'] as int?;
if (gastAktiv && gastRest != null) {
gast!.merkeVerbleibend(gastRest);
}
// ─── 2/2: fertige MP3 abholen und speichern ───
_melde('Lade „$titel“ …');
@@ -161,7 +172,7 @@ class YtDownloadService extends ChangeNotifier {
try {
final mp3 = await _abbrechbar(
_client
.get(Uri.parse('$proxyUrl$mp3Url'), headers: auth.authHeader)
.get(Uri.parse('$proxyUrl$mp3Url'), headers: zugangsHeader)
.timeout(const Duration(seconds: 180)),
abbruch,
);
+96
View File
@@ -6,6 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:melo/services/baka_auth.dart';
import 'package:melo/services/gast_zugang.dart';
import 'package:melo/services/yt_download_service.dart';
class _MemorySpeicher implements TokenSpeicher {
@@ -284,4 +285,99 @@ void main() {
expect((jsonDecode(auftrag!) as Map)['cookies'], isFalse);
});
group('Gast-Zugang', () {
test('Mit Gast-Token statt Anmeldung wird der Proxy trotzdem gefragt',
() async {
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'gast-xyz';
final gast = GastZugang(
client: MockClient((_) async => http.Response('', 500)),
speicher: speicher,
);
await gast.laden();
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((req) async {
if (req.url.path == '/api/yt-dl') {
expect(req.headers['X-Guest-Token'], 'gast-xyz');
expect(req.headers.containsKey('Authorization'), isFalse);
return http.Response(
jsonEncode({
'titel': 'Gast-Lied',
'dauer': 30,
'mp3_url': '/api/dl/g.mp3',
'guest_remaining': 4,
}),
200);
}
return http.Response.bytes([1], 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNotNull);
expect(gast.verbleibend, 4);
});
test('Ohne Anmeldung UND ohne Gast-Token wird gar nicht erst gefragt',
() async {
var aufrufe = 0;
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
client: MockClient((_) async {
aufrufe++;
return http.Response('', 200);
}),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
expect(ergebnis, isNull);
expect(dienst.fehler, 'Bitte zuerst beim Baka-Konto anmelden');
expect(aufrufe, 0);
});
test('401 bei Gast-Zugang holt automatisch einen neuen Gast-Token',
() async {
var tokenAufrufe = 0;
final speicher = _MemorySpeicher();
speicher.werte['guest_token'] = 'alter-token';
final gast = GastZugang(
client: MockClient((_) async {
tokenAufrufe++;
return http.Response(
jsonEncode({'guest_token': 'neuer-token'}), 200);
}),
speicher: speicher,
);
await gast.laden();
final dienst = YtDownloadService(
auth: BakaAuth(
client: MockClient((_) async => http.Response('', 500)),
speicher: _MemorySpeicher()),
gast: gast,
client: MockClient((_) async => http.Response('Unauthorized', 401)),
);
final ergebnis = await dienst.herunterladen('https://youtu.be/abc',
zielOrdner: ziel.path);
// Das Token-Nachholen läuft unawaited im Hintergrund.
await Future<void>.delayed(Duration.zero);
expect(ergebnis, isNull);
expect(dienst.fehler, 'Gast-Zugang abgelaufen — bitte erneut versuchen');
expect(tokenAufrufe, 1);
});
});
}