mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-20 05:38:15 +02:00
UI: Clean up player controls (remove duplicate arrows, move LIVE badge)
This commit is contained in:
1 parent
e6281ff912
commit
3888a21990
12 files changed
+1121
-377
No files matched your search
@@ -3,6 +3,7 @@ import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../../../core/api/dns_resolver.dart';
|
||||
import '../models/xtream_models.dart' as xm;
|
||||
|
||||
/// Xtream Codes API Service for Mobile (no dart:html dependency)
|
||||
@@ -13,6 +14,8 @@ class XtreamServiceMobile {
|
||||
late final CacheOptions _cacheOptions;
|
||||
|
||||
PlaylistConfig? _currentPlaylist;
|
||||
String? _resolvedIp; // Cached resolved IP for the playlist server
|
||||
String? _originalHost; // Original hostname for Host header
|
||||
|
||||
XtreamServiceMobile(String cachePath) {
|
||||
_dio = Dio(BaseOptions(
|
||||
@@ -31,9 +34,69 @@ class XtreamServiceMobile {
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
}
|
||||
|
||||
/// Initialize connection with a playlist
|
||||
/// Initialize connection with a playlist - now async for DNS resolution
|
||||
Future<void> setPlaylistAsync(PlaylistConfig playlist) async {
|
||||
_currentPlaylist = playlist;
|
||||
|
||||
// Extract hostname from DNS URL
|
||||
final uri = Uri.tryParse(playlist.dns);
|
||||
if (uri != null && uri.host.isNotEmpty) {
|
||||
_originalHost = uri.host;
|
||||
|
||||
// Proactively resolve DNS
|
||||
print('XtreamServiceMobile: Pre-resolving ${uri.host}');
|
||||
_resolvedIp = await DnsResolver.resolve(uri.host);
|
||||
|
||||
if (_resolvedIp != null) {
|
||||
print('XtreamServiceMobile: Will use IP $_resolvedIp for ${uri.host}');
|
||||
} else {
|
||||
print('XtreamServiceMobile: DNS resolution failed, will use hostname directly');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy sync method - calls async internally
|
||||
void setPlaylist(PlaylistConfig playlist) {
|
||||
_currentPlaylist = playlist;
|
||||
// Trigger async resolution in background
|
||||
final uri = Uri.tryParse(playlist.dns);
|
||||
if (uri != null && uri.host.isNotEmpty) {
|
||||
_originalHost = uri.host;
|
||||
DnsResolver.resolve(uri.host).then((ip) {
|
||||
_resolvedIp = ip;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the API base URL, using resolved IP if available
|
||||
String get _effectiveApiBaseUrl {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
if (_resolvedIp != null && _originalHost != null) {
|
||||
// Replace hostname with IP in the URL
|
||||
final originalUrl = _currentPlaylist!.apiBaseUrl;
|
||||
return originalUrl.replaceFirst(_originalHost!, _resolvedIp!);
|
||||
}
|
||||
return _currentPlaylist!.apiBaseUrl;
|
||||
}
|
||||
|
||||
/// Get effective DNS base, using resolved IP if available
|
||||
String get _effectiveDnsBase {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
if (_resolvedIp != null && _originalHost != null) {
|
||||
return _currentPlaylist!.dns.replaceFirst(_originalHost!, _resolvedIp!);
|
||||
}
|
||||
return _currentPlaylist!.dns;
|
||||
}
|
||||
|
||||
/// Get request options with Host header if using IP
|
||||
Options _getOptions() {
|
||||
final opts = Options(extra: _cacheOptions.toExtra());
|
||||
if (_resolvedIp != null && _originalHost != null) {
|
||||
opts.headers = {'Host': _originalHost!};
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
PlaylistConfig? get currentPlaylist => _currentPlaylist;
|
||||
@@ -43,21 +106,21 @@ class XtreamServiceMobile {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
// Use .m3u8 format for HLS streaming (best mobile compatibility)
|
||||
return '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
|
||||
return '$_effectiveDnsBase/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
|
||||
}
|
||||
|
||||
/// Generate stream URL for VOD (movies)
|
||||
String getVodStreamUrl(String streamId, String containerExtension) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
return '${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
return '$_effectiveDnsBase/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
}
|
||||
|
||||
/// Generate stream URL for series episodes
|
||||
String getSeriesStreamUrl(String streamId, String containerExtension) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
return '${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
return '$_effectiveDnsBase/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
}
|
||||
|
||||
/// Authenticate and get server info
|
||||
@@ -66,12 +129,12 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
return response.data as Map<String, dynamic>;
|
||||
@@ -86,13 +149,13 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_live_categories',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> categories = response.data as List<dynamic>;
|
||||
@@ -122,13 +185,13 @@ class XtreamServiceMobile {
|
||||
final categoryMap = await _getLiveCategories();
|
||||
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_live_streams',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> streams = response.data as List<dynamic>;
|
||||
@@ -163,13 +226,13 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_vod_categories',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> categories = response.data as List<dynamic>;
|
||||
@@ -194,13 +257,13 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_series_categories',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> categories = response.data as List<dynamic>;
|
||||
@@ -228,13 +291,13 @@ class XtreamServiceMobile {
|
||||
final categoryMap = await _getVodCategories();
|
||||
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_vod_streams',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> allMovies = response.data as List<dynamic>;
|
||||
@@ -265,13 +328,13 @@ class XtreamServiceMobile {
|
||||
final categoryMap = await _getVodCategories();
|
||||
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_vod_streams',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> allMovies = response.data as List<dynamic>;
|
||||
@@ -305,13 +368,13 @@ class XtreamServiceMobile {
|
||||
final categoryMap = await _getSeriesCategories();
|
||||
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_series',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> allSeries = response.data as List<dynamic>;
|
||||
@@ -342,13 +405,13 @@ class XtreamServiceMobile {
|
||||
final categoryMap = await _getSeriesCategories();
|
||||
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_series',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
final List<dynamic> allSeries = response.data as List<dynamic>;
|
||||
@@ -379,14 +442,14 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_series_info',
|
||||
'series_id': seriesId,
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
options: _getOptions(),
|
||||
);
|
||||
|
||||
return xm.SeriesInfo.fromJson(response.data as Map<String, dynamic>);
|
||||
@@ -401,7 +464,7 @@ class XtreamServiceMobile {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -409,6 +472,7 @@ class XtreamServiceMobile {
|
||||
'stream_id': streamId,
|
||||
},
|
||||
options: Options(
|
||||
headers: _resolvedIp != null && _originalHost != null ? {'Host': _originalHost!} : null,
|
||||
extra: CacheOptions(
|
||||
store: _cacheOptions.store,
|
||||
policy: CachePolicy.request,
|
||||
@@ -432,12 +496,13 @@ class XtreamServiceMobile {
|
||||
}
|
||||
|
||||
/// Get short EPG as ShortEPG object (for EPGWidget)
|
||||
Future<xm.ShortEPG> getShortEPG(String streamId) async {
|
||||
/// Caches results for 12 hours by default
|
||||
Future<xm.ShortEPG> getShortEPG(String streamId, {bool forceRefresh = false}) async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_effectiveApiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -445,10 +510,11 @@ class XtreamServiceMobile {
|
||||
'stream_id': streamId,
|
||||
},
|
||||
options: Options(
|
||||
headers: _resolvedIp != null && _originalHost != null ? {'Host': _originalHost!} : null,
|
||||
extra: CacheOptions(
|
||||
store: _cacheOptions.store,
|
||||
policy: CachePolicy.request,
|
||||
maxStale: const Duration(minutes: 5),
|
||||
policy: forceRefresh ? CachePolicy.refresh : CachePolicy.request,
|
||||
maxStale: const Duration(hours: 12), // Cache for 12h as requested
|
||||
).toExtra(),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user