mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-17 04:09:03 +02:00
feat: add mobile-optimized IPTV player with HLS and MpegTS support
This commit is contained in:
1 parent
3c9a9c7be7
commit
fa6ae90d8d
38 files changed
+1280
-895
No files matched your search
@@ -120,12 +120,15 @@ class DownloadService {
|
||||
|
||||
/// Get download progress
|
||||
double getDownloadProgress(String id) {
|
||||
final task = _downloads.firstWhere((t) => t.id == id, orElse: () => DownloadTask(
|
||||
id: id,
|
||||
title: '',
|
||||
url: '',
|
||||
filePath: '',
|
||||
));
|
||||
final task = _downloads.firstWhere(
|
||||
(t) => t.id == id,
|
||||
orElse: () => DownloadTask(
|
||||
id: id,
|
||||
title: '',
|
||||
url: '',
|
||||
filePath: '',
|
||||
),
|
||||
);
|
||||
return task.progress;
|
||||
}
|
||||
|
||||
@@ -151,13 +154,12 @@ class DownloadService {
|
||||
if (available < 1024 * 1024 * 1024) {
|
||||
// Less than 1GB free
|
||||
final dir = Directory(_downloadDir);
|
||||
final files = dir.listSync()
|
||||
.whereType<File>()
|
||||
.toList();
|
||||
final files = dir.listSync().whereType<File>().toList();
|
||||
|
||||
// Sort by modification time, delete oldest first
|
||||
files.sort((a, b) =>
|
||||
a.statSync().modified.compareTo(b.statSync().modified));
|
||||
files.sort(
|
||||
(a, b) => a.statSync().modified.compareTo(b.statSync().modified),
|
||||
);
|
||||
|
||||
for (final file in files.take(files.length ~/ 3)) {
|
||||
try {
|
||||
@@ -216,7 +218,8 @@ class DownloadService {
|
||||
}
|
||||
|
||||
// Check if paused or cancelled
|
||||
final currentTask = _downloads.firstWhere((t) => t.id == task.id, orElse: () => task);
|
||||
final currentTask =
|
||||
_downloads.firstWhere((t) => t.id == task.id, orElse: () => task);
|
||||
if (currentTask.status == DownloadStatus.paused ||
|
||||
!_activeDownloads.contains(task.id)) {
|
||||
break;
|
||||
@@ -301,8 +304,6 @@ class DownloadsNotifier extends StateNotifier<List<DownloadTask>> {
|
||||
}
|
||||
|
||||
void clearCompleted() {
|
||||
state = state
|
||||
.where((d) => d.status != DownloadStatus.completed)
|
||||
.toList();
|
||||
state = state.where((d) => d.status != DownloadStatus.completed).toList();
|
||||
}
|
||||
}
|
||||
@@ -17,12 +17,12 @@ class SubtitleTrack {
|
||||
});
|
||||
|
||||
SubtitleTrack copyWith({bool? isEnabled}) => SubtitleTrack(
|
||||
id: id,
|
||||
name: name,
|
||||
language: language,
|
||||
url: url,
|
||||
isEnabled: isEnabled ?? this.isEnabled,
|
||||
);
|
||||
id: id,
|
||||
name: name,
|
||||
language: language,
|
||||
url: url,
|
||||
isEnabled: isEnabled ?? this.isEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
/// Subtitle content with timing
|
||||
@@ -42,16 +42,17 @@ class SubtitleEntry {
|
||||
|
||||
/// Service for handling subtitles
|
||||
class SubtitleService {
|
||||
static const _srtPattern = r'(\d+)\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})\n([\s\S]*?)(?=\n\n|\Z)';
|
||||
static const _srtPattern =
|
||||
r'(\d+)\n(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})\n([\s\S]*?)(?=\n\n|\Z)';
|
||||
|
||||
/// Parse SRT subtitle content
|
||||
static List<SubtitleEntry> parseSrt(String content) {
|
||||
final entries = <SubtitleEntry>[];
|
||||
final blocks = content.split('\n\n');
|
||||
|
||||
|
||||
for (final block in blocks) {
|
||||
if (block.trim().isEmpty) continue;
|
||||
|
||||
|
||||
final lines = block.trim().split('\n');
|
||||
if (lines.length < 3) continue;
|
||||
|
||||
@@ -67,12 +68,14 @@ class SubtitleService {
|
||||
final startTime = _parseTime(times[0].trim());
|
||||
final endTime = _parseTime(times[1].trim());
|
||||
|
||||
entries.add(SubtitleEntry(
|
||||
index: index,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
));
|
||||
entries.add(
|
||||
SubtitleEntry(
|
||||
index: index,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -85,10 +88,10 @@ class SubtitleService {
|
||||
static List<SubtitleEntry> parseWebVtt(String content) {
|
||||
final entries = <SubtitleEntry>[];
|
||||
final blocks = content.split('\n\n');
|
||||
|
||||
|
||||
for (final block in blocks) {
|
||||
if (block.trim().isEmpty || block.startsWith('WEBVTT')) continue;
|
||||
|
||||
|
||||
final lines = block.trim().split('\n');
|
||||
if (lines.length < 2) continue;
|
||||
|
||||
@@ -103,12 +106,14 @@ class SubtitleService {
|
||||
final startTime = _parseWebVttTime(times[0].trim());
|
||||
final endTime = _parseWebVttTime(times[1].trim());
|
||||
|
||||
entries.add(SubtitleEntry(
|
||||
index: entries.length,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
));
|
||||
entries.add(
|
||||
SubtitleEntry(
|
||||
index: entries.length,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
text: text,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
@@ -120,7 +125,8 @@ class SubtitleService {
|
||||
/// Get subtitle at specific time
|
||||
static String? getSubtitleAtTime(List<SubtitleEntry> entries, Duration time) {
|
||||
for (final entry in entries) {
|
||||
if (time.compareTo(entry.startTime) >= 0 && time.compareTo(entry.endTime) <= 0) {
|
||||
if (time.compareTo(entry.startTime) >= 0 &&
|
||||
time.compareTo(entry.endTime) <= 0) {
|
||||
return entry.text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +183,14 @@ class XtreamService {
|
||||
/// Get all live TV channels grouped by category
|
||||
///
|
||||
/// Returns a Map where key is category name and value is list of channels
|
||||
Future<Map<String, List<Channel>>> getLiveChannels({bool refresh = false}) async {
|
||||
Future<Map<String, List<Channel>>> getLiveChannels(
|
||||
{bool refresh = false}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
final options = refresh
|
||||
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
|
||||
final options = refresh
|
||||
? _cacheOptions
|
||||
.copyWith(policy: CachePolicy.refreshForceCache)
|
||||
.toExtra()
|
||||
: _cacheOptions.toExtra();
|
||||
|
||||
try {
|
||||
@@ -226,7 +229,8 @@ class XtreamService {
|
||||
return groupedChannels;
|
||||
} catch (e) {
|
||||
// Try to return from local cache if network fails
|
||||
final cached = _loadFromLocalCache<Channel>('live_channels', Channel.fromJson);
|
||||
final cached =
|
||||
_loadFromLocalCache<Channel>('live_channels', Channel.fromJson);
|
||||
if (cached.isNotEmpty) return cached;
|
||||
throw Exception('Failed to fetch live channels: $e');
|
||||
}
|
||||
@@ -236,27 +240,30 @@ class XtreamService {
|
||||
if (!kIsWeb) return;
|
||||
try {
|
||||
final jsonStr = jsonEncode(data);
|
||||
html.window.localStorage['xtream_cache_${_currentPlaylist?.id}_$key'] = jsonStr;
|
||||
html.window.localStorage['xtream_cache_${_currentPlaylist?.id}_$key'] =
|
||||
jsonStr;
|
||||
} catch (e) {
|
||||
debugPrint('Error saving to local cache ($key): $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, List<T>> _loadFromLocalCache<T>(
|
||||
String key, T Function(Map<String, dynamic>) fromJson) {
|
||||
String key,
|
||||
T Function(Map<String, dynamic>) fromJson,
|
||||
) {
|
||||
if (!kIsWeb) return {};
|
||||
try {
|
||||
final jsonStr = html.window.localStorage['xtream_cache_${_currentPlaylist?.id}_$key'];
|
||||
final jsonStr =
|
||||
html.window.localStorage['xtream_cache_${_currentPlaylist?.id}_$key'];
|
||||
if (jsonStr == null) return {};
|
||||
|
||||
|
||||
final data = jsonDecode(jsonStr) as Map<String, dynamic>;
|
||||
final result = <String, List<T>>{};
|
||||
|
||||
|
||||
data.forEach((cat, items) {
|
||||
if (items is List) {
|
||||
result[cat] = items
|
||||
.map((i) => fromJson(i as Map<String, dynamic>))
|
||||
.toList();
|
||||
result[cat] =
|
||||
items.map((i) => fromJson(i as Map<String, dynamic>)).toList();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
@@ -301,8 +308,10 @@ class XtreamService {
|
||||
Future<Map<String, List<VodItem>>> getVodItems({bool refresh = false}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
final options = refresh
|
||||
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
|
||||
final options = refresh
|
||||
? _cacheOptions
|
||||
.copyWith(policy: CachePolicy.refreshForceCache)
|
||||
.toExtra()
|
||||
: _cacheOptions.toExtra();
|
||||
|
||||
try {
|
||||
@@ -342,7 +351,8 @@ class XtreamService {
|
||||
return groupedVods;
|
||||
} catch (e) {
|
||||
// Try to return from local cache if network fails
|
||||
final cached = _loadFromLocalCache<VodItem>('vod_items', VodItem.fromJson);
|
||||
final cached =
|
||||
_loadFromLocalCache<VodItem>('vod_items', VodItem.fromJson);
|
||||
if (cached.isNotEmpty) return cached;
|
||||
throw Exception('Failed to fetch VOD items: $e');
|
||||
}
|
||||
@@ -383,8 +393,10 @@ class XtreamService {
|
||||
Future<Map<String, List<Series>>> getSeries({bool refresh = false}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
final options = refresh
|
||||
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
|
||||
final options = refresh
|
||||
? _cacheOptions
|
||||
.copyWith(policy: CachePolicy.refreshForceCache)
|
||||
.toExtra()
|
||||
: _cacheOptions.toExtra();
|
||||
|
||||
try {
|
||||
@@ -424,15 +436,18 @@ class XtreamService {
|
||||
return groupedSeries;
|
||||
} catch (e) {
|
||||
// Try to return from local cache if network fails
|
||||
final cached = _loadFromLocalCache<Series>('series_items', Series.fromJson);
|
||||
final cached =
|
||||
_loadFromLocalCache<Series>('series_items', Series.fromJson);
|
||||
if (cached.isNotEmpty) return cached;
|
||||
throw Exception('Failed to fetch series: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get movies with pagination support
|
||||
Future<List<xm.Movie>> getMoviesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
Future<List<xm.Movie>> getMoviesPaginated({
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
@@ -511,8 +526,10 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// Get series with pagination support (returns flat list)
|
||||
Future<List<xm.Series>> getSeriesPaginated(
|
||||
{int offset = 0, int limit = 100}) async {
|
||||
Future<List<xm.Series>> getSeriesPaginated({
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user