mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Align the Android app with xtremflow on three fronts. EPG: add XmltvService, a streaming XMLTV parser (gunzip-aware, indexed to a -3h/+18h window, JSON disk cache with a 6h TTL). XtreamServiceMobile now falls back through the provider's xmltv.php, a user-supplied URL, then a community guide whenever get_short_epg returns nothing. Channels are matched on epg_channel_id first, then on a normalized name (accents, HD/FHD/4K suffixes, "FR:" and "|FR|" prefixes). A streamId -> Channel index populated by getLiveChannels supplies those keys, so no getShortEPG call site changed. EPG requests are also funnelled through a 3-slot semaphore, as in xtremflow: a 30-tile grid used to fire 30 parallel player_api.php calls, which rate-limited panels answered with errors, emptying the guide for the whole screen. Theme: port the Warm Cinema charter (ember/charcoal/cream, Fraunces + Karla) into app_colors, app_theme and app_decorations. The 21 Apple TV colour names already in use are kept as compatibility aliases. mobile_theme now derives from AppTheme instead of duplicating its 390 lines. Playback lifecycle: stop reacting to AppLifecycleState.inactive, which fires on transient Android interruptions such as pulling down the notification shade and was killing playback. Both engines now release on paused/hidden/detached via _releasePlayback(): stream stopped, decoder released, timers cancelled, wakelock dropped. An _isReleased latch keeps the watchdog and reconnect paths from restarting a stream from the background. Long-run stability: cancel the six player stream subscriptions on dispose, cap demuxer-max-back-bytes at 8MB (mpv defaults to 50MiB, which combined with the 150MB readahead pushed the native heap toward an Android memory kill after a few hours), rebuild on position only while the controls are visible, and dispose the Lite player's focus nodes. Startup: prewarmHost() pre-resolves DNS and opens then closes a bare TCP/TLS socket before playback. No HTTP request is issued, so no provider connection slot is consumed. Capped at 1.5s. Verified: flutter analyze clean on all touched files; 19/19 tests, including a run over a real 46MB/69802-programme dump (517 channels indexed in 1.1s, 5/5 name probes matched); flutter build apk --release succeeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.3 KiB
Dart
72 lines
2.3 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:xtremobile/features/iptv/services/xmltv_service.dart';
|
|
|
|
/// Parses a real community XMLTV dump end to end.
|
|
///
|
|
/// Skipped unless `XMLTV_FIXTURE` points at a `.xml`/`.xml.gz` file, so CI and
|
|
/// day-to-day runs stay offline and fast:
|
|
///
|
|
/// flutter test test/xmltv_real_dump_test.dart \
|
|
/// --dart-define=XMLTV_FIXTURE=C:\path\epg_ripper_FR1.xml.gz
|
|
const String _fixturePath = String.fromEnvironment('XMLTV_FIXTURE');
|
|
|
|
void main() {
|
|
test(
|
|
'indexes a real XMLTV dump within a mobile memory budget',
|
|
() async {
|
|
final file = File(_fixturePath);
|
|
expect(file.existsSync(), isTrue, reason: 'fixture not found');
|
|
|
|
final service = XmltvService(Directory.systemTemp.path);
|
|
final started = DateTime.now();
|
|
|
|
final indexed = await service.indexFromBytes(file.openRead());
|
|
|
|
final elapsed = DateTime.now().difference(started);
|
|
final peakMb = ProcessInfo.maxRss / 1048576;
|
|
|
|
// ignore: avoid_print
|
|
print(
|
|
'channels=${service.channelCount} '
|
|
'time=${elapsed.inMilliseconds}ms '
|
|
'peakRss=${peakMb.toStringAsFixed(1)}MB',
|
|
);
|
|
|
|
expect(indexed, isTrue);
|
|
expect(service.channelCount, greaterThan(100));
|
|
|
|
// The whole point of the streaming parse: a ~46 MB document must never
|
|
// be resident. Generous ceiling to stay stable across VM versions.
|
|
expect(peakMb, lessThan(400));
|
|
|
|
// Spot-check the matching paths with labels shaped the way an Xtream
|
|
// provider reports them.
|
|
const probes = <List<String>>[
|
|
['', 'TF1 HD'],
|
|
['', 'FR: France 2'],
|
|
['', 'M6 FHD'],
|
|
['', '|FR| France 5'],
|
|
['', 'Arte'],
|
|
];
|
|
|
|
var hits = 0;
|
|
for (final probe in probes) {
|
|
final epg = service.shortEpgFor(
|
|
streamId: 'x',
|
|
epgChannelId: probe[0],
|
|
channelName: probe[1],
|
|
);
|
|
// ignore: avoid_print
|
|
print(' ${probe[1].padRight(16)} -> ${epg?.title ?? "NO MATCH"}');
|
|
if (epg != null) hits++;
|
|
}
|
|
|
|
expect(hits, greaterThanOrEqualTo(4),
|
|
reason: 'name matching should resolve mainstream French channels');
|
|
},
|
|
skip: _fixturePath.isEmpty ? 'set --dart-define=XMLTV_FIXTURE' : false,
|
|
);
|
|
}
|