mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
feat: implement initial mobile IPTV feature with player screen, live TV, movies, and series tabs, along with the Xtream API service.
This commit is contained in:
1 parent
b1f887af69
commit
829c5751ca
6 files changed
+112
-80
No files matched your search
@@ -1,25 +1,27 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../models/xtream_models.dart' as xm;
|
||||
import 'dart:html' as html;
|
||||
|
||||
/// Xtream Codes API Service
|
||||
///
|
||||
///
|
||||
/// Handles all communication with Xtream API servers
|
||||
class XtreamService {
|
||||
late final Dio _dio;
|
||||
late final CacheOptions _cacheOptions;
|
||||
|
||||
|
||||
PlaylistConfig? _currentPlaylist;
|
||||
|
||||
XtreamService() {
|
||||
_dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
),);
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
),
|
||||
);
|
||||
|
||||
// Setup caching for API responses
|
||||
_cacheOptions = CacheOptions(
|
||||
@@ -32,22 +34,21 @@ class XtreamService {
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
}
|
||||
|
||||
/// Check if we're running on HTTPS (production)
|
||||
bool get _isHttps {
|
||||
return html.window.location.protocol == 'https:';
|
||||
/// Get dynamic base URL for the local backend
|
||||
/// On Web: use localhost (proxy handles it)
|
||||
/// On Mobile: default to host machine IP for emulator
|
||||
String get _backendBaseUrl {
|
||||
if (kIsWeb) return 'http://localhost:8089';
|
||||
// On Android emulator, 10.0.2.2 points to host machine's localhost
|
||||
return 'http://10.0.2.2:8089';
|
||||
}
|
||||
|
||||
String get backendBaseUrl => _backendBaseUrl;
|
||||
|
||||
/// Wrap URL with proxy for all external IPTV URLs
|
||||
///
|
||||
/// Always use proxy for HTTP URLs to:
|
||||
/// 1. Handle HTTPS->HTTP mixed content
|
||||
/// 2. Provide Range header support for seeking
|
||||
/// 3. Normalize headers and handle redirects
|
||||
String _wrapWithProxy(String url) {
|
||||
// Always proxy HTTP URLs for consistent streaming behavior
|
||||
if (url.startsWith('http://')) {
|
||||
final baseUrl = html.window.location.origin;
|
||||
return '$baseUrl/api/xtream/$url';
|
||||
if (url.startsWith('http')) {
|
||||
return '$_backendBaseUrl/api/xtream/$url';
|
||||
}
|
||||
return url;
|
||||
}
|
||||
@@ -60,28 +61,19 @@ class XtreamService {
|
||||
/// Generate stream URL for live TV
|
||||
String getLiveStreamUrl(String streamId) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
// Point to new dedicated Live API
|
||||
final baseUrl = html.window.location.origin;
|
||||
return '$baseUrl/api/live/$streamId.ts';
|
||||
return '$_backendBaseUrl/api/live/$streamId.ts';
|
||||
}
|
||||
|
||||
/// Generate stream URL for VOD (movies)
|
||||
String getVodStreamUrl(String streamId, String containerExtension) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
// Point to new dedicated VOD API (HLS playlist)
|
||||
final baseUrl = html.window.location.origin;
|
||||
return '$baseUrl/api/vod/$streamId/playlist.m3u8';
|
||||
return '$_backendBaseUrl/api/vod/$streamId/playlist.m3u8';
|
||||
}
|
||||
|
||||
/// Generate stream URL for series episodes
|
||||
String getSeriesStreamUrl(String streamId, String containerExtension) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
// Point to new dedicated VOD API (HLS playlist) with series type
|
||||
final baseUrl = html.window.location.origin;
|
||||
return '$baseUrl/api/vod/$streamId/playlist.m3u8?type=series';
|
||||
return '$_backendBaseUrl/api/vod/$streamId/playlist.m3u8?type=series';
|
||||
}
|
||||
|
||||
/// Authenticate and get server info
|
||||
@@ -138,7 +130,7 @@ 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() async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
@@ -165,10 +157,10 @@ class XtreamService {
|
||||
// Get category name from our mapping using category_id
|
||||
final categoryId = data['category_id']?.toString() ?? '';
|
||||
final categoryName = categoryMap[categoryId] ?? 'Uncategorized';
|
||||
|
||||
|
||||
// Inject category_name into data before parsing
|
||||
data['category_name'] = categoryName;
|
||||
|
||||
|
||||
final channel = Channel.fromJson(data);
|
||||
|
||||
if (!groupedChannels.containsKey(categoryName)) {
|
||||
@@ -326,7 +318,8 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -344,13 +337,15 @@ class XtreamService {
|
||||
);
|
||||
|
||||
final List<dynamic> allMovies = response.data as List<dynamic>;
|
||||
|
||||
|
||||
// Apply pagination
|
||||
final endIndex = (offset + limit) > allMovies.length ? allMovies.length : offset + limit;
|
||||
final endIndex = (offset + limit) > allMovies.length
|
||||
? allMovies.length
|
||||
: offset + limit;
|
||||
if (offset >= allMovies.length) return [];
|
||||
|
||||
|
||||
final paginatedMovies = allMovies.sublist(offset, endIndex);
|
||||
|
||||
|
||||
return paginatedMovies.map((movieData) {
|
||||
final data = movieData as Map<String, dynamic>;
|
||||
final categoryId = data['category_id']?.toString() ?? '';
|
||||
@@ -382,7 +377,7 @@ class XtreamService {
|
||||
|
||||
final List<dynamic> allMovies = response.data as List<dynamic>;
|
||||
final queryLower = query.toLowerCase();
|
||||
|
||||
|
||||
// Filter by search query
|
||||
return allMovies
|
||||
.where((m) {
|
||||
@@ -403,7 +398,8 @@ 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 {
|
||||
@@ -421,13 +417,15 @@ class XtreamService {
|
||||
);
|
||||
|
||||
final List<dynamic> allSeries = response.data as List<dynamic>;
|
||||
|
||||
|
||||
// Apply pagination
|
||||
final endIndex = (offset + limit) > allSeries.length ? allSeries.length : offset + limit;
|
||||
final endIndex = (offset + limit) > allSeries.length
|
||||
? allSeries.length
|
||||
: offset + limit;
|
||||
if (offset >= allSeries.length) return [];
|
||||
|
||||
|
||||
final paginatedSeries = allSeries.sublist(offset, endIndex);
|
||||
|
||||
|
||||
return paginatedSeries.map((seriesData) {
|
||||
final data = seriesData as Map<String, dynamic>;
|
||||
final categoryId = data['category_id']?.toString() ?? '';
|
||||
@@ -459,7 +457,7 @@ class XtreamService {
|
||||
|
||||
final List<dynamic> allSeries = response.data as List<dynamic>;
|
||||
final queryLower = query.toLowerCase();
|
||||
|
||||
|
||||
// Filter by search query
|
||||
return allSeries
|
||||
.where((s) {
|
||||
@@ -502,7 +500,7 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// Get VOD (movie) info including duration
|
||||
///
|
||||
///
|
||||
/// Returns detailed movie information including duration in seconds
|
||||
Future<int?> getVodDuration(String vodId) async {
|
||||
if (_currentPlaylist == null) return null;
|
||||
@@ -520,18 +518,18 @@ class XtreamService {
|
||||
);
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
|
||||
// Try to get duration from movie_properties or info
|
||||
final movieData = data['movie_data'] as Map<String, dynamic>?;
|
||||
final info = data['info'] as Map<String, dynamic>?;
|
||||
|
||||
|
||||
// Duration can be in different formats: seconds (int), "HH:MM:SS", or minutes
|
||||
String? durationStr = movieData?['duration']?.toString() ??
|
||||
info?['duration']?.toString() ??
|
||||
info?['duration_secs']?.toString();
|
||||
|
||||
String? durationStr = movieData?['duration']?.toString() ??
|
||||
info?['duration']?.toString() ??
|
||||
info?['duration_secs']?.toString();
|
||||
|
||||
if (durationStr == null || durationStr.isEmpty) return null;
|
||||
|
||||
|
||||
// Parse duration - could be "01:30:00" format or seconds
|
||||
if (durationStr.contains(':')) {
|
||||
// HH:MM:SS format
|
||||
@@ -548,7 +546,7 @@ class XtreamService {
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try parsing as seconds directly
|
||||
return int.tryParse(durationStr);
|
||||
} catch (e) {
|
||||
@@ -558,7 +556,7 @@ class XtreamService {
|
||||
}
|
||||
|
||||
/// Get short EPG for a specific stream
|
||||
///
|
||||
///
|
||||
/// Returns "Now" and "Next" program info
|
||||
Future<List<EpgEntry>> getShortEpg(String streamId) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
@@ -585,7 +583,8 @@ class XtreamService {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> epgData = response.data['epg_listings'] as List<dynamic>;
|
||||
final List<dynamic> epgData =
|
||||
response.data['epg_listings'] as List<dynamic>;
|
||||
return epgData
|
||||
.map((entry) => EpgEntry.fromJson(entry as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
Reference in new issue
Block a user