Files
MatchLiveTv/mobile/lib/shared/api_client.dart
Emiliano Frascaro 70e704827e Risolve timeout creazione sessione YouTube lato app.
La setup broadcast YouTube (~23s) passa in background job; l'API risponde
subito. Aumentato anche il timeout Dio mobile per createSession YouTube.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 00:18:07 +02:00

313 lines
9.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/config.dart';
import '../providers/auth_provider.dart';
import 'models/invite_preview.dart';
import 'models/match.dart';
import 'models/recording_item.dart';
import 'models/score_state.dart';
import 'models/stream_session.dart';
import 'models/team.dart';
import 'models/user.dart';
final apiClientProvider = Provider<ApiClient>((ref) {
final auth = ref.watch(authProvider);
return ApiClient(accessToken: auth.accessToken);
});
class ApiClient {
ApiClient({this.accessToken}) {
_dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiV1,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (accessToken != null && accessToken!.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $accessToken';
}
handler.next(options);
},
),
);
}
late final Dio _dio;
final String? accessToken;
// --- Auth ---
Future<({User user, AuthTokens tokens})> login({
required String email,
required String password,
}) async {
final response = await _dio.post<Map<String, dynamic>>(
'/auth/login',
data: {'email': email, 'password': password},
);
final data = response.data!;
return (
user: User.fromJson(data['user'] as Map<String, dynamic>),
tokens: AuthTokens.fromJson(data),
);
}
Future<AuthTokens> refresh(String refreshToken) async {
final response = await _dio.post<Map<String, dynamic>>(
'/auth/refresh',
data: {'refresh_token': refreshToken},
);
return AuthTokens.fromJson(response.data!);
}
Future<User> me() async {
final response = await _dio.get<Map<String, dynamic>>('/auth/me');
return User.fromJson(response.data!);
}
Future<void> logout() async {
await _dio.post('/auth/logout');
}
// --- Teams ---
Future<List<Team>> fetchTeams() async {
final response = await _dio.get<List<dynamic>>('/teams');
return response.data!
.map((e) => Team.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<InvitePreview> fetchInvitePreview(String token) async {
final response = await _dio.get<Map<String, dynamic>>('/invitations/$token');
return InvitePreview.fromJson(response.data!);
}
Future<String> acceptInvitation(String token) async {
final response = await _dio.post<Map<String, dynamic>>('/invitations/$token/accept');
return response.data!['message'] as String? ?? 'Invito accettato';
}
Future<String> youtubeAuthorizeUrl(String teamId, {bool returnApp = true}) async {
final response = await _dio.get<Map<String, dynamic>>(
'/teams/$teamId/youtube/authorize',
queryParameters: returnApp ? {'return_app': '1'} : null,
);
return response.data!['authorization_url'] as String;
}
Future<List<RecordingItem>> fetchRecordings(String teamId) async {
final response = await _dio.get<List<dynamic>>('/teams/$teamId/recordings');
return response.data!
.map((e) => RecordingItem.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<String> fetchRecordingDownloadUrl(String recordingId) async {
final response = await _dio.get<Map<String, dynamic>>(
'/recordings/$recordingId/download',
);
return response.data!['download_url'] as String;
}
// --- Matches ---
Future<List<MatchModel>> fetchMatches(String teamId) async {
final response =
await _dio.get<List<dynamic>>('/teams/$teamId/matches');
return response.data!
.map((e) => MatchModel.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<MatchModel> fetchMatch(String matchId) async {
final response =
await _dio.get<Map<String, dynamic>>('/matches/$matchId');
return MatchModel.fromJson(response.data!);
}
Future<MatchModel> updateMatch(String matchId, Map<String, dynamic> body) async {
final response = await _dio.patch<Map<String, dynamic>>(
'/matches/$matchId',
data: {'match': body},
);
return MatchModel.fromJson(response.data!);
}
Future<void> deleteMatch(String matchId) async {
await _dio.delete<void>('/matches/$matchId');
}
Future<MatchModel> createMatch(String teamId, Map<String, dynamic> body) async {
final response = await _dio.post<Map<String, dynamic>>(
'/teams/$teamId/matches',
data: {'match': body},
);
return MatchModel.fromJson(response.data!);
}
// --- Sessions ---
Future<StreamSession> createSession(
String matchId, {
String platform = 'matchlivetv',
String? youtubeChannel,
String privacyStatus = 'public',
String qualityPreset = '720p_30_2.5mbps',
int? targetBitrate,
int? targetFps,
}) async {
// YouTube: creazione broadcast + path MediaMTX può richiedere 2030s lato server.
final sessionTimeout = platform == 'youtube'
? const Duration(seconds: 60)
: const Duration(seconds: 30);
final response = await _dio.post<Map<String, dynamic>>(
'/matches/$matchId/sessions',
data: {
'platform': platform,
'privacy_status': privacyStatus,
'quality_preset': qualityPreset,
if (youtubeChannel != null) 'youtube_channel': youtubeChannel,
if (targetBitrate != null) 'target_bitrate': targetBitrate,
if (targetFps != null) 'target_fps': targetFps,
},
options: Options(
sendTimeout: sessionTimeout,
receiveTimeout: sessionTimeout,
),
);
return StreamSession.fromJson(response.data!);
}
Future<StreamSession> fetchSession(String sessionId) async {
final response =
await _dio.get<Map<String, dynamic>>('/sessions/$sessionId');
return StreamSession.fromJson(response.data!);
}
Future<StreamSession> startSession(String sessionId) async {
final response =
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/start');
return StreamSession.fromJson(response.data!);
}
Future<StreamSession> stopSession(String sessionId) async {
final response =
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/stop');
return StreamSession.fromJson(response.data!);
}
Future<StreamSession> pauseSession(String sessionId) async {
final response =
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/pause');
return StreamSession.fromJson(response.data!);
}
Future<StreamSession> resumeSession(String sessionId) async {
final response =
await _dio.patch<Map<String, dynamic>>('/sessions/$sessionId/resume');
return StreamSession.fromJson(response.data!);
}
/// Persiste il tabellone e restituisce lo stato salvato sul server.
Future<ScoreState?> syncScore(String sessionId, Map<String, dynamic> payload) async {
final response = await _dio.patch<Map<String, dynamic>>(
'/sessions/$sessionId/score',
data: payload,
);
final score = response.data?['score'];
if (score is Map<String, dynamic>) {
return ScoreState.fromJson(score);
}
return null;
}
Future<RegiaLinkResponse> createRegiaLink(String sessionId) async {
final response = await _dio.post<Map<String, dynamic>>(
'/sessions/$sessionId/regia_link',
);
return RegiaLinkResponse.fromJson(response.data!);
}
Future<PairingTokenResponse> createPairingToken(String sessionId) async {
final response = await _dio.post<Map<String, dynamic>>(
'/sessions/$sessionId/pairing_token',
);
return PairingTokenResponse.fromJson(response.data!);
}
Future<StreamSession> claimPairing({
required String sessionId,
required String pairingToken,
required String deviceRole,
}) async {
final response = await _dio.post<Map<String, dynamic>>(
'/sessions/$sessionId/claim_pairing',
data: {
'pairing_token': pairingToken,
'device_role': deviceRole,
},
);
return StreamSession.fromJson(response.data!);
}
Future<NetworkTestResult> networkTest({
required String sessionId,
required double downloadMbps,
required double uploadMbps,
required int latencyMs,
required String networkType,
}) async {
final response = await _dio.post<Map<String, dynamic>>(
'/sessions/$sessionId/network_test',
data: {
'download_mbps': downloadMbps,
'upload_mbps': uploadMbps,
'latency_ms': latencyMs,
'network_type': networkType,
},
);
return NetworkTestResult.fromJson(response.data!);
}
Future<YoutubeStats> youtubeStats(String sessionId) async {
final response =
await _dio.get<Map<String, dynamic>>('/sessions/$sessionId/youtube_stats');
return YoutubeStats.fromJson(response.data!);
}
Future<void> postTelemetry({
required String sessionId,
required String deviceRole,
int? batteryLevel,
String? networkType,
int? signalStrength,
int? currentBitrate,
int? targetBitrate,
int? fps,
}) async {
await _dio.post(
'/sessions/$sessionId/telemetry',
data: {
'device_role': deviceRole,
if (batteryLevel != null) 'battery_level': batteryLevel,
if (networkType != null) 'network_type': networkType,
if (signalStrength != null) 'signal_strength': signalStrength,
if (currentBitrate != null) 'current_bitrate': currentBitrate,
if (targetBitrate != null) 'target_bitrate': targetBitrate,
if (fps != null) 'fps': fps,
},
);
}
}