mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-20 21:48:06 +02:00
chore: Update dependencies, switch Flutter web renderer to HTML, replace media_kit with video_player/chewie, and remove Hive volume from Docker setup.
This commit is contained in:
1 parent
3c0d472a31
commit
0ca2835cba
16 files changed
+1282
-688
No files matched your search
@@ -1,201 +1,225 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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 '../models/xtream_models.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
|
||||
/// Xtream Codes API Service
|
||||
///
|
||||
/// Handles all communication with Xtream API servers
|
||||
class XtreamService {
|
||||
final PlaylistConfig config;
|
||||
late final Dio _dio;
|
||||
late final CacheOptions _cacheOptions;
|
||||
|
||||
PlaylistConfig? _currentPlaylist;
|
||||
|
||||
// Cache for reducing API calls
|
||||
final Map<String, dynamic> _cache = {};
|
||||
static const _cacheDuration = Duration(minutes: 10);
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
XtreamService() {
|
||||
_dio = Dio(BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 15),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
));
|
||||
|
||||
XtreamService(this.config) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: config.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
queryParameters: {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
},
|
||||
),
|
||||
// Setup caching for API responses
|
||||
_cacheOptions = CacheOptions(
|
||||
store: HiveCacheStore('./cache'),
|
||||
policy: CachePolicy.forceCache,
|
||||
maxStale: const Duration(hours: 1),
|
||||
priority: CachePriority.high,
|
||||
);
|
||||
|
||||
// Add caching interceptor
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
// Check cache before making request
|
||||
final cacheKey = options.uri.toString();
|
||||
if (_cache.containsKey(cacheKey)) {
|
||||
final timestamp = _cacheTimestamps[cacheKey];
|
||||
if (timestamp != null &&
|
||||
DateTime.now().difference(timestamp) < _cacheDuration) {
|
||||
// Return cached response
|
||||
return handler.resolve(
|
||||
Response(
|
||||
requestOptions: options,
|
||||
data: _cache[cacheKey],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
// Cache successful responses
|
||||
final cacheKey = response.requestOptions.uri.toString();
|
||||
_cache[cacheKey] = response.data;
|
||||
_cacheTimestamps[cacheKey] = DateTime.now();
|
||||
handler.next(response);
|
||||
},
|
||||
),
|
||||
);
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
}
|
||||
|
||||
/// Initialize connection with a playlist
|
||||
void setPlaylist(PlaylistConfig playlist) {
|
||||
_currentPlaylist = playlist;
|
||||
}
|
||||
|
||||
/// Generate stream URL for live TV
|
||||
String getLiveStreamUrl(String streamId) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
return '${_currentPlaylist!.dns}/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';
|
||||
}
|
||||
|
||||
/// 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';
|
||||
}
|
||||
|
||||
/// Authenticate and get server info
|
||||
Future<Map<String, dynamic>> authenticate() async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
final response = await _dio.get('', queryParameters: {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
});
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
);
|
||||
|
||||
return response.data as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
throw Exception('Authentication failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get live streams with pagination
|
||||
Future<List<LiveChannel>> getLiveStreams({
|
||||
String? categoryId,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
/// 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');
|
||||
|
||||
try {
|
||||
final params = {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
'action': 'get_live_streams',
|
||||
if (categoryId != null) 'category_id': categoryId,
|
||||
};
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_live_streams',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
);
|
||||
|
||||
final response = await _dio.get('', queryParameters: params);
|
||||
final data = response.data as List;
|
||||
final List<dynamic> streams = response.data as List<dynamic>;
|
||||
final Map<String, List<Channel>> groupedChannels = {};
|
||||
|
||||
// Apply manual pagination (Xtream doesn't support it natively)
|
||||
final paginatedData = data.skip(offset).take(limit).toList();
|
||||
for (final streamData in streams) {
|
||||
final channel = Channel.fromJson(streamData as Map<String, dynamic>);
|
||||
final categoryName = channel.categoryName.isEmpty ? 'Uncategorized' : channel.categoryName;
|
||||
|
||||
return paginatedData
|
||||
.map((json) => LiveChannel.fromJson(json as Map<String, dynamic>))
|
||||
if (!groupedChannels.containsKey(categoryName)) {
|
||||
groupedChannels[categoryName] = [];
|
||||
}
|
||||
groupedChannels[categoryName]!.add(channel);
|
||||
}
|
||||
|
||||
return groupedChannels;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch live channels: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all VOD items (movies) grouped by category
|
||||
Future<Map<String, List<VodItem>>> getVodItems() async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_vod_streams',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
);
|
||||
|
||||
final List<dynamic> vods = response.data as List<dynamic>;
|
||||
final Map<String, List<VodItem>> groupedVods = {};
|
||||
|
||||
for (final vodData in vods) {
|
||||
final vod = VodItem.fromJson(vodData as Map<String, dynamic>);
|
||||
final categoryName = vod.categoryName.isEmpty ? 'Uncategorized' : vod.categoryName;
|
||||
|
||||
if (!groupedVods.containsKey(categoryName)) {
|
||||
groupedVods[categoryName] = [];
|
||||
}
|
||||
groupedVods[categoryName]!.add(vod);
|
||||
}
|
||||
|
||||
return groupedVods;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch VOD items: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all series grouped by category
|
||||
Future<Map<String, List<Series>>> getSeries() async {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_series',
|
||||
},
|
||||
options: Options(extra: _cacheOptions.toExtra()),
|
||||
);
|
||||
|
||||
final List<dynamic> seriesList = response.data as List<dynamic>;
|
||||
final Map<String, List<Series>> groupedSeries = {};
|
||||
|
||||
for (final seriesData in seriesList) {
|
||||
final series = Series.fromJson(seriesData as Map<String, dynamic>);
|
||||
final categoryName = series.categoryName.isEmpty ? 'Uncategorized' : series.categoryName;
|
||||
|
||||
if (!groupedSeries.containsKey(categoryName)) {
|
||||
groupedSeries[categoryName] = [];
|
||||
}
|
||||
groupedSeries[categoryName]!.add(series);
|
||||
}
|
||||
|
||||
return groupedSeries;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to fetch series: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 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');
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
'action': 'get_short_epg',
|
||||
'stream_id': streamId,
|
||||
},
|
||||
options: Options(
|
||||
extra: CacheOptions(
|
||||
store: _cacheOptions.store,
|
||||
policy: CachePolicy.request,
|
||||
maxStale: const Duration(minutes: 5), // EPG changes frequently
|
||||
).toExtra(),
|
||||
),
|
||||
);
|
||||
|
||||
if (response.data == null || response.data['epg_listings'] == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<dynamic> epgData = response.data['epg_listings'] as List<dynamic>;
|
||||
return epgData
|
||||
.map((entry) => EpgEntry.fromJson(entry as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load live streams: $e');
|
||||
// EPG is optional, don't throw on failure
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Get VOD streams (movies) with pagination
|
||||
Future<List<Movie>> getMovies({
|
||||
String? categoryId,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
try {
|
||||
final params = {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
'action': 'get_vod_streams',
|
||||
if (categoryId != null) 'category_id': categoryId,
|
||||
};
|
||||
|
||||
final response = await _dio.get('', queryParameters: params);
|
||||
final data = response.data as List;
|
||||
|
||||
final paginatedData = data.skip(offset).take(limit).toList();
|
||||
|
||||
return paginatedData
|
||||
.map((json) => Movie.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load movies: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get series with pagination
|
||||
Future<List<Series>> getSeries({
|
||||
String? categoryId,
|
||||
int offset = 0,
|
||||
int limit = 100,
|
||||
}) async {
|
||||
try {
|
||||
final params = {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
'action': 'get_series',
|
||||
if (categoryId != null) 'category_id': categoryId,
|
||||
};
|
||||
|
||||
final response = await _dio.get('', queryParameters: params);
|
||||
final data = response.data as List;
|
||||
|
||||
final paginatedData = data.skip(offset).take(limit).toList();
|
||||
|
||||
return paginatedData
|
||||
.map((json) => Series.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load series: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get live categories
|
||||
Future<List<Category>> getLiveCategories() async {
|
||||
try {
|
||||
final response = await _dio.get('', queryParameters: {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
'action': 'get_live_categories',
|
||||
});
|
||||
|
||||
final data = response.data as List;
|
||||
return data
|
||||
.map((json) => Category.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load categories: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get short EPG for a specific channel
|
||||
Future<ShortEPG> getShortEPG(String channelId) async {
|
||||
try {
|
||||
final response = await _dio.get('', queryParameters: {
|
||||
'username': config.username,
|
||||
'password': config.password,
|
||||
'action': 'get_short_epg',
|
||||
'stream_id': channelId,
|
||||
'limit': '2', // Only get current and next
|
||||
});
|
||||
|
||||
return ShortEPG.fromJson(response.data as Map<String, dynamic>);
|
||||
} catch (e) {
|
||||
// EPG might not be available for all channels
|
||||
return const ShortEPG();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear cache
|
||||
void clearCache() {
|
||||
_cache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
/// Dispose resources
|
||||
void dispose() {
|
||||
_dio.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for Xtream service (requires playlist config)
|
||||
final xtreamServiceProvider = Provider.family<XtreamService, PlaylistConfig>(
|
||||
(ref, config) => XtreamService(config),
|
||||
);
|
||||
Reference in new issue
Block a user