mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-24 15:38:04 +02:00
feat: introduce IPTV data models and initial Live TV UI, including EPG overlay.
This commit is contained in:
1 parent
37945b513e
commit
fa88746db6
5 files changed
+72
-40
No files matched your search
@@ -105,15 +105,18 @@ class ProxyHandler {
|
|||||||
// For API calls, enforce domain allowlist
|
// For API calls, enforce domain allowlist
|
||||||
final playlist = await _getPlaylist(request);
|
final playlist = await _getPlaylist(request);
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
return Response.forbidden('No active playlist configuration found to validate request');
|
return Response.forbidden(
|
||||||
|
'No active playlist configuration found to validate request');
|
||||||
}
|
}
|
||||||
|
|
||||||
final targetHost = targetUrl.host.toLowerCase();
|
final targetHost = targetUrl.host.toLowerCase();
|
||||||
final allowedHost = Uri.parse(playlist.dns).host.toLowerCase();
|
final allowedHost = Uri.parse(playlist.dns).host.toLowerCase();
|
||||||
|
|
||||||
if (targetHost != allowedHost) {
|
if (targetHost != allowedHost) {
|
||||||
print('[Proxy] Blocked SSRF attempt to $targetHost (Allowed: $allowedHost)');
|
print(
|
||||||
return Response.forbidden('Access to this domain is forbidden by policy');
|
'[Proxy] Blocked SSRF attempt to $targetHost (Allowed: $allowedHost)');
|
||||||
|
return Response.forbidden(
|
||||||
|
'Access to this domain is forbidden by policy');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +134,8 @@ class ProxyHandler {
|
|||||||
if (request.method == 'POST') {
|
if (request.method == 'POST') {
|
||||||
final bodyBytes = await request.read().toList();
|
final bodyBytes = await request.read().toList();
|
||||||
final body = bodyBytes.expand((i) => i).toList();
|
final body = bodyBytes.expand((i) => i).toList();
|
||||||
response = await client.post(targetUrl, headers: proxyHeaders, body: body);
|
response =
|
||||||
|
await client.post(targetUrl, headers: proxyHeaders, body: body);
|
||||||
} else {
|
} else {
|
||||||
response = await client.get(targetUrl, headers: proxyHeaders);
|
response = await client.get(targetUrl, headers: proxyHeaders);
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,8 @@ class ProxyHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response.headers['content-length'] != null) {
|
if (response.headers['content-length'] != null) {
|
||||||
responseHeaders['content-length'] = response.headers['content-length']!;
|
responseHeaders['content-length'] =
|
||||||
|
response.headers['content-length']!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
@@ -157,14 +162,15 @@ class ProxyHandler {
|
|||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
} catch (e, stackTrace) {
|
|
||||||
print('[ProxyHandler] error on $path: $e');
|
print('[ProxyHandler] error on $path: $e');
|
||||||
|
|
||||||
// Return transparent 1x1 pixel image fallback for images
|
// Return transparent 1x1 pixel image fallback for images
|
||||||
if (targetUrl?.path.endsWith('.png') == true || targetUrl?.path.endsWith('.jpg') == true) {
|
if (targetUrl?.path.endsWith('.png') == true ||
|
||||||
|
targetUrl?.path.endsWith('.jpg') == true) {
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
base64Decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='),
|
base64Decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='),
|
||||||
headers: {'content-type': 'image/png'},
|
headers: {'content-type': 'image/png'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
/// Channel model for Live TV
|
/// Channel model for Live TV
|
||||||
class Channel {
|
class Channel {
|
||||||
final String streamId;
|
final String streamId;
|
||||||
@@ -169,6 +170,21 @@ class EpgEntry {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Robustly parse date strings from Xtream API
|
||||||
|
static DateTime? parseDateTime(String? dateStr) {
|
||||||
|
if (dateStr == null || dateStr.isEmpty) return null;
|
||||||
|
try {
|
||||||
|
// Xtream often uses "YYYY-MM-DD HH:MM:SS" which is not ISO 8601
|
||||||
|
// ISO 8601 needs "T" separator: "YYYY-MM-DDTHH:MM:SS"
|
||||||
|
final normalizedDate = dateStr.contains(' ') && !dateStr.contains('T')
|
||||||
|
? dateStr.replaceFirst(' ', 'T')
|
||||||
|
: dateStr;
|
||||||
|
return DateTime.tryParse(normalizedDate);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String _decodeBase64(String? text) {
|
static String _decodeBase64(String? text) {
|
||||||
if (text == null || text.isEmpty) return '';
|
if (text == null || text.isEmpty) return '';
|
||||||
try {
|
try {
|
||||||
@@ -182,10 +198,11 @@ class EpgEntry {
|
|||||||
/// Calculate progress percentage for current program
|
/// Calculate progress percentage for current program
|
||||||
double getProgress() {
|
double getProgress() {
|
||||||
try {
|
try {
|
||||||
final startTime = DateTime.parse(start);
|
final startTime = parseDateTime(start);
|
||||||
final endTime = DateTime.parse(end);
|
final endTime = parseDateTime(end);
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (startTime == null || endTime == null) return 0.0;
|
||||||
if (now.isBefore(startTime)) return 0.0;
|
if (now.isBefore(startTime)) return 0.0;
|
||||||
if (now.isAfter(endTime)) return 1.0;
|
if (now.isAfter(endTime)) return 1.0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/models/playlist_config.dart';
|
import '../../../core/models/playlist_config.dart';
|
||||||
|
import '../../../core/models/iptv_models.dart';
|
||||||
import '../providers/xtream_provider.dart';
|
import '../providers/xtream_provider.dart';
|
||||||
|
|
||||||
/// Minimal EPG overlay - shows program title and info
|
/// Minimal EPG overlay - shows program title and info
|
||||||
@@ -32,8 +33,9 @@ class EpgOverlay extends ConsumerWidget {
|
|||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final currentProgram = epgEntries.firstWhere(
|
final currentProgram = epgEntries.firstWhere(
|
||||||
(entry) {
|
(entry) {
|
||||||
final start = DateTime.parse(entry.start);
|
final start = EpgEntry.parseDateTime(entry.start);
|
||||||
final end = DateTime.parse(entry.end);
|
final end = EpgEntry.parseDateTime(entry.end);
|
||||||
|
if (start == null || end == null) return false;
|
||||||
return now.isAfter(start) && now.isBefore(end);
|
return now.isAfter(start) && now.isBefore(end);
|
||||||
},
|
},
|
||||||
orElse: () => epgEntries.first,
|
orElse: () => epgEntries.first,
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
.where((cat) => settings.matchesLiveTvFilter(cat))
|
.where((cat) => settings.matchesLiveTvFilter(cat))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
categories.sort();
|
// REMOVED: categories.sort(); - Respect API/Settings order
|
||||||
|
|
||||||
List<Channel> displayedChannels = [];
|
List<Channel> displayedChannels = [];
|
||||||
bool showingCategoryGrid = false;
|
bool showingCategoryGrid = false;
|
||||||
@@ -388,10 +388,10 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white
|
color: Colors.white.withOpacity(
|
||||||
.withOpacity(0.05), // Subtle transparent background
|
0.05), // Subtle transparent background
|
||||||
borderRadius:
|
borderRadius: const BorderRadius.vertical(
|
||||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
top: Radius.circular(12)),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Center(
|
child: Center(
|
||||||
@@ -417,8 +417,8 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
),
|
),
|
||||||
// Footer
|
// Footer
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
horizontal: 12, vertical: 8),
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -460,8 +460,12 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
final currentProgram = epgList.firstWhere(
|
final currentProgram = epgList.firstWhere(
|
||||||
(entry) {
|
(entry) {
|
||||||
try {
|
try {
|
||||||
final start = DateTime.parse(entry.start);
|
final start =
|
||||||
final end = DateTime.parse(entry.end);
|
EpgEntry.parseDateTime(entry.start);
|
||||||
|
final end =
|
||||||
|
EpgEntry.parseDateTime(entry.end);
|
||||||
|
if (start == null || end == null)
|
||||||
|
return false;
|
||||||
return now.isAfter(start) &&
|
return now.isAfter(start) &&
|
||||||
now.isBefore(end);
|
now.isBefore(end);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,24 +1,27 @@
|
|||||||
# Fix Docker & Proxy Issues
|
# Activation de l'Agent Bmad Master
|
||||||
|
|
||||||
## Completed
|
## Context
|
||||||
|
|
||||||
- [x] Database permissions (`entrypoint.sh` + `Dockerfile`)
|
Activation de l'agent Bmad Master pour coordonner les modules BMAD et gérer le flux de travail du projet xtremflow.
|
||||||
- [x] Proxy 401 errors (removed broken mount in `server.dart`)
|
|
||||||
- [x] Proxy chunked encoding error (buffered response in `proxy_handler.dart`)
|
|
||||||
|
|
||||||
## Changes
|
## Current Focus
|
||||||
|
|
||||||
| File | Change |
|
Analyse du workflow Bmad Master et configuration initiale.
|
||||||
|------|--------|
|
|
||||||
| `entrypoint.sh` | NEW - fixes /app/data permissions, runs as xtremuser via gosu |
|
|
||||||
| `Dockerfile` | Added gosu, ENTRYPOINT |
|
|
||||||
| `bin/server.dart` | Removed broken /api/xtream mount, added logging |
|
|
||||||
| `bin/api/proxy_handler.dart` | Changed from streaming to buffered response |
|
|
||||||
|
|
||||||
## Deploy
|
## Master Plan
|
||||||
|
|
||||||
```bash
|
- [x] Analyser le workflow Bmad Master (`bmad-core-agents-bmad-master.md`)
|
||||||
git add .
|
- [x] Configurer l'environnement pour l'agent Master
|
||||||
git commit -m "fix: database permissions + proxy errors"
|
- [x] Initialiser la session avec l'agent Master
|
||||||
git push && docker compose up --build -d
|
- [x] Diagnostiquer l'absence d'EPG
|
||||||
```
|
- [x] Diagnostiquer le problème de tri des chaînes
|
||||||
|
- [x] Vérifier l'état de l'agent et des sous-modules
|
||||||
|
|
||||||
|
## Progress Log
|
||||||
|
|
||||||
|
- [x] Identification du workflow dans `.agent/workflows/bmad/`
|
||||||
|
- [x] Lecture du workflow et de la configuration
|
||||||
|
- [x] Validation du plan d'activation par Michael
|
||||||
|
- [x] Activation de l'identité Bmad Master
|
||||||
|
- [x] Correction du parsing de date EPG (Xtream format support)
|
||||||
|
- [x] Suppression du tri alphabétique forcé
|
||||||
Reference in new issue
Block a user