fix(epg): use /api/epg/ backend for full guide (48h), fix _ProgrammeCard for Map format, remove stale EpgEntry code

This commit is contained in:
Michael committed 2026-03-09 09:07:04 +01:00
1 parent 1e1687e160
commit 37945b513e
1 file changed
+68 -51
+68 -51
View File
@@ -115,7 +115,8 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
Channel? _selectedChannel; Channel? _selectedChannel;
List<EpgEntry> _programmes = []; // Map-based pour correspondre au format retourné par /api/epg/<id>
List<Map<String, dynamic>> _programmes = [];
bool _loadingEpg = false; bool _loadingEpg = false;
String _epgError = ''; String _epgError = '';
@@ -135,7 +136,8 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
super.dispose(); super.dispose();
} }
/// Charge l'EPG via XtreamService (identique à EPGWidget) /// Charge le guide EPG complet via le backend /api/epg/<channelId>
/// Le backend appelle get_epg (48h de programmes) et décode le base64
Future<void> _loadEpg(Channel ch) async { Future<void> _loadEpg(Channel ch) async {
setState(() { setState(() {
_selectedChannel = ch; _selectedChannel = ch;
@@ -145,15 +147,26 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
}); });
try { try {
// Utiliser directement XtreamService comme EPGWidget — passe par /api/xtream/ proxy final response =
final service = ref.read(xtreamServiceProvider(widget.playlist)); await http.get(Uri.parse('/api/epg/${ch.streamId}'));
final epgEntries = await service.getShortEpg(ch.streamId);
if (mounted) { if (mounted) {
setState(() { if (response.statusCode == 200) {
_programmes = epgEntries; final data = json.decode(response.body) as Map<String, dynamic>;
_loadingEpg = false; final list =
}); (data['programmes'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.toList();
setState(() {
_programmes = list;
_loadingEpg = false;
});
} else {
setState(() {
_epgError =
'Erreur ${response.statusCode}: ${response.body}';
_loadingEpg = false;
});
}
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
@@ -383,7 +396,6 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
_ProgrammeCard( _ProgrammeCard(
programme: _programmes[i], programme: _programmes[i],
channel: _selectedChannel!, channel: _selectedChannel!,
playlist: widget.playlist,
), ),
), ),
), ),
@@ -401,53 +413,58 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
class _ProgrammeCard extends StatelessWidget { class _ProgrammeCard extends StatelessWidget {
final EpgEntry programme; final Map<String, dynamic> programme;
final Channel channel; final Channel channel;
final PlaylistConfig playlist;
const _ProgrammeCard({ const _ProgrammeCard({
required this.programme, required this.programme,
required this.channel, required this.channel,
required this.playlist,
}); });
String get _start => programme['start'] as String? ?? '';
String get _end => programme['end'] as String? ?? '';
String get _title => programme['title'] as String? ?? '';
String get _description => programme['description'] as String? ?? '';
String _fmt(String raw) { String _fmt(String raw) {
if (raw.isEmpty) return '';
try { try {
final dt = DateTime.parse(raw).toLocal(); // Format Xtream : "2026-03-09 09:00:00" ou ISO "2026-03-09T09:00:00Z"
final dt = DateTime.parse(raw.replaceFirst(' ', 'T')).toLocal();
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
} catch (_) { } catch (_) {
return raw.length >= 16 ? raw.substring(11, 16) : raw; return raw.length >= 16 ? raw.substring(11, 16) : raw;
} }
} }
bool get _isNow { DateTime? _parseTime(String raw) {
if (raw.isEmpty) return null;
try { try {
final s = DateTime.parse(programme.start); return DateTime.parse(raw.replaceFirst(' ', 'T')).toUtc();
final e = DateTime.parse(programme.end);
final now = DateTime.now().toUtc();
return now.isAfter(s) && now.isBefore(e);
} catch (_) { } catch (_) {
return false; return null;
} }
} }
bool get _isNow {
final s = _parseTime(_start);
final e = _parseTime(_end);
if (s == null || e == null) return false;
final now = DateTime.now().toUtc();
return now.isAfter(s) && now.isBefore(e);
}
bool get _isPast { bool get _isPast {
try { final e = _parseTime(_end);
return DateTime.now().toUtc().isAfter(DateTime.parse(programme.end)); if (e == null) return false;
} catch (_) { return DateTime.now().toUtc().isAfter(e);
return false;
}
} }
void _record(BuildContext context) { void _record(BuildContext context) {
try { final s = _parseTime(_start);
final start = DateTime.parse(programme.start).toUtc(); final e = _parseTime(_end);
final end = DateTime.parse(programme.end).toUtc(); if (s == null || e == null) return;
_showConfirm(context, programme.title, start, end); _showConfirm(context, _title, s, e);
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Erreur: $e')));
}
} }
void _showConfirm( void _showConfirm(
@@ -468,20 +485,20 @@ class _ProgrammeCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(title, Text(title.isEmpty ? channel.name : title,
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16)), fontSize: 16)),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'${_fmt(programme.start)} → ${_fmt(programme.end)}', '${_fmt(_start)} → ${_fmt(_end)}',
style: const TextStyle(color: Colors.white70), style: const TextStyle(color: Colors.white70),
), ),
if (programme.description.isNotEmpty) ...[ if (_description.isNotEmpty) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
programme.description, _description,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white38, fontSize: 12), style: const TextStyle(color: Colors.white38, fontSize: 12),
@@ -498,7 +515,8 @@ class _ProgrammeCard extends StatelessWidget {
ElevatedButton.styleFrom(backgroundColor: Colors.redAccent), ElevatedButton.styleFrom(backgroundColor: Colors.redAccent),
onPressed: () async { onPressed: () async {
Navigator.pop(ctx); Navigator.pop(ctx);
await _saveRecording(context, title, start, end); await _saveRecording(
context, title.isEmpty ? channel.name : title, start, end);
}, },
child: const Text('🔴 Enregistrer'), child: const Text('🔴 Enregistrer'),
), ),
@@ -510,13 +528,12 @@ class _ProgrammeCard extends StatelessWidget {
Future<void> _saveRecording( Future<void> _saveRecording(
BuildContext context, String title, DateTime start, DateTime end) async { BuildContext context, String title, DateTime start, DateTime end) async {
try { try {
final streamUrl = '/api/live/${channel.streamId}.ts';
final response = await http.post( final response = await http.post(
Uri.parse('/api/recordings'), Uri.parse('/api/recordings'),
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: json.encode({ body: json.encode({
'channel_id': channel.streamId, 'channel_id': channel.streamId,
'stream_url': streamUrl, 'stream_url': '/api/live/${channel.streamId}.ts',
'title': title, 'title': title,
'start_time': start.toIso8601String(), 'start_time': start.toIso8601String(),
'end_time': end.toIso8601String(), 'end_time': end.toIso8601String(),
@@ -564,7 +581,7 @@ class _ProgrammeCard extends StatelessWidget {
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 2), const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
leading: SizedBox( leading: SizedBox(
width: 48, width: 52,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -583,7 +600,7 @@ class _ProgrammeCard extends StatelessWidget {
) )
else else
Text( Text(
_fmt(programme.start), _fmt(_start),
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
color: isPast ? Colors.white24 : Colors.white70, color: isPast ? Colors.white24 : Colors.white70,
fontSize: 13, fontSize: 13,
@@ -592,15 +609,15 @@ class _ProgrammeCard extends StatelessWidget {
), ),
if (!isNow) if (!isNow)
Text( Text(
_fmt(programme.end), _fmt(_end),
style: GoogleFonts.outfit( style:
color: Colors.white24, fontSize: 10), GoogleFonts.outfit(color: Colors.white24, fontSize: 10),
), ),
], ],
), ),
), ),
title: Text( title: Text(
programme.title.isEmpty ? '—' : programme.title, _title.isEmpty ? '—' : _title,
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
color: isPast ? Colors.white38 : Colors.white, color: isPast ? Colors.white38 : Colors.white,
fontWeight: isNow ? FontWeight.bold : FontWeight.normal, fontWeight: isNow ? FontWeight.bold : FontWeight.normal,
@@ -608,13 +625,12 @@ class _ProgrammeCard extends StatelessWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: programme.description.isNotEmpty subtitle: _description.isNotEmpty
? Text( ? Text(
programme.description, _description,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: style: const TextStyle(color: Colors.white38, fontSize: 11),
const TextStyle(color: Colors.white38, fontSize: 11),
) )
: null, : null,
trailing: !isPast trailing: !isPast
@@ -630,6 +646,7 @@ class _ProgrammeCard extends StatelessWidget {
} }
} }
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
// ONGLET 2 — ENREGISTREMENTS // ONGLET 2 — ENREGISTREMENTS
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════