mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-12 01:36:20 +02:00
148 lines
4.7 KiB
Dart
148 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
import 'package:shelf/shelf.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import '../database/database.dart';
|
|
import '../models/playlist_config.dart';
|
|
|
|
/// API EPG — proxy vers Xtream avec cache 30 minutes
|
|
/// GET /api/epg/<channel_id>?days=1
|
|
class EpgApi {
|
|
final AppDatabase _db;
|
|
final Future<PlaylistConfig?> Function(Request) _getPlaylist;
|
|
|
|
// Cache simple en mémoire : channelId → {data, expiresAt}
|
|
final Map<String, _CacheEntry> _cache = {};
|
|
|
|
EpgApi(this._db, this._getPlaylist);
|
|
|
|
Future<Response> handleGetEpg(Request request, String channelId) async {
|
|
// Vérifier le cache
|
|
final cached = _cache[channelId];
|
|
if (cached != null && DateTime.now().isBefore(cached.expiresAt)) {
|
|
return Response.ok(
|
|
cached.data,
|
|
headers: {'Content-Type': 'application/json', 'X-Cache': 'HIT'},
|
|
);
|
|
}
|
|
|
|
try {
|
|
final playlist = await _getPlaylist(request);
|
|
if (playlist == null) {
|
|
return Response.forbidden(
|
|
json.encode({'error': 'Playlist non trouvée'}),
|
|
headers: {'Content-Type': 'application/json'},
|
|
);
|
|
}
|
|
|
|
final dns = playlist.dns;
|
|
// 1. Tenter d'abord l'EPG complet (48h)
|
|
var url =
|
|
'$dns/player_api.php?username=${playlist.username}&password=${playlist.password}'
|
|
'&action=get_epg&stream_id=$channelId&limit=48';
|
|
|
|
var response =
|
|
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 60));
|
|
Map<String, dynamic> epgData = {
|
|
'channel_id': channelId,
|
|
'programmes': [],
|
|
};
|
|
|
|
if (response.statusCode == 200) {
|
|
final raw = json.decode(response.body);
|
|
epgData = _transformEpgData(raw, channelId);
|
|
}
|
|
|
|
// 2. Fallback EPG court si le complet est vide
|
|
if ((epgData['programmes'] as List).isEmpty) {
|
|
url =
|
|
'$dns/player_api.php?username=${playlist.username}&password=${playlist.password}'
|
|
'&action=get_short_epg&stream_id=$channelId';
|
|
response =
|
|
await http.get(Uri.parse(url)).timeout(const Duration(seconds: 60));
|
|
if (response.statusCode == 200) {
|
|
final raw = json.decode(response.body);
|
|
epgData = _transformEpgData(raw, channelId);
|
|
}
|
|
}
|
|
|
|
final jsonStr = json.encode(epgData);
|
|
|
|
// Mettre en cache 30 minutes
|
|
_cache[channelId] = _CacheEntry(
|
|
data: jsonStr,
|
|
expiresAt: DateTime.now().add(const Duration(minutes: 30)),
|
|
);
|
|
|
|
return Response.ok(
|
|
jsonStr,
|
|
headers: {'Content-Type': 'application/json', 'X-Cache': 'MISS'},
|
|
);
|
|
} catch (e) {
|
|
return Response.internalServerError(
|
|
body: json.encode({'error': 'Erreur lors de la récupération EPG: $e'}),
|
|
headers: {'Content-Type': 'application/json'},
|
|
);
|
|
}
|
|
}
|
|
|
|
Map<String, dynamic> _transformEpgData(dynamic raw, String channelId) {
|
|
try {
|
|
List<dynamic> listings = [];
|
|
|
|
if (raw is Map && raw.containsKey('epg_listings')) {
|
|
listings = raw['epg_listings'] as List<dynamic>? ?? [];
|
|
} else if (raw is List) {
|
|
listings = raw;
|
|
}
|
|
|
|
final programmes = listings.map((item) {
|
|
final startRaw = item['start'] as String? ?? '';
|
|
final endRaw = item['stop'] as String? ?? item['end'] as String? ?? '';
|
|
|
|
// Normaliser les dates pour le frontend (Xtream format support)
|
|
final start = startRaw.contains(' ') && !startRaw.contains('T')
|
|
? startRaw.replaceFirst(' ', 'T')
|
|
: startRaw;
|
|
final end = endRaw.contains(' ') && !endRaw.contains('T')
|
|
? endRaw.replaceFirst(' ', 'T')
|
|
: endRaw;
|
|
|
|
// Décoder le titre (base64 si nécessaire)
|
|
String title = item['title'] as String? ?? '';
|
|
try {
|
|
if (title.isNotEmpty) {
|
|
final decoded = utf8.decode(base64Decode(title));
|
|
if (decoded.isNotEmpty) title = decoded;
|
|
}
|
|
} catch (_) {}
|
|
|
|
String description = item['description'] as String? ?? '';
|
|
try {
|
|
if (description.isNotEmpty) {
|
|
final decoded = utf8.decode(base64Decode(description));
|
|
if (decoded.isNotEmpty) description = decoded;
|
|
}
|
|
} catch (_) {}
|
|
|
|
return {
|
|
'title': title,
|
|
'description': description,
|
|
'start': start,
|
|
'end': end,
|
|
'channel_id': channelId,
|
|
};
|
|
}).toList();
|
|
|
|
return {'channel_id': channelId, 'programmes': programmes};
|
|
} catch (e) {
|
|
return {'channel_id': channelId, 'programmes': [], 'error': e.toString()};
|
|
}
|
|
}
|
|
}
|
|
|
|
class _CacheEntry {
|
|
final String data;
|
|
final DateTime expiresAt;
|
|
_CacheEntry({required this.data, required this.expiresAt});
|
|
}
|