muxa_xtream 0.1.2 copy "muxa_xtream: ^0.1.2" to clipboard
muxa_xtream: ^0.1.2 copied to clipboard

Ergonomic Xtream Codes IPTV client for Dart/Flutter: typed models, resilient HTTP, URL builders, EPG, M3U/XMLTV helpers.

example/main.dart

import 'dart:async';
import 'dart:io';

import 'package:muxa_xtream/muxa_xtream.dart';

void main(List<String> args) async {
  var opts = _CliOptions.parse(args, Platform.environment);
  if (opts == null) {
    _printUsage();
    exitCode = 64; // usage
    return;
  }

  final portal = XtreamPortal.parse(opts.portal!);
  final credentials = XtreamCredentials(
    username: opts.user!,
    password: opts.pass!,
  );

  // optional http adapter with custom options
  final http = XtDefaultHttpAdapter(
    options: XtDefaultHttpOptions(
      allowSelfSignedTls: opts.selfSigned,
      connectTimeout: const Duration(seconds: 8),
      receiveTimeout: const Duration(seconds: 20),
      defaultHeaders: {
        'Accept': 'application/json, text/plain;q=0.9, */*;q=0.1',
        // Ensure the server closes the socket to avoid 15s keep-alive linger
        'Connection': 'close',
      },
    ),
  );

  final client = XtreamClient(portal, credentials, http: http);

  stdout.writeln('Fetching account/server info...');
  try {
    final info = await client.getUserAndServerInfo();
    stdout
      ..writeln('User: ${info.user.username} (active: ${info.user.active})')
      ..writeln('expires: ${info.user.expiresAt}')
      ..writeln('max connections: ${info.user.maxConnections}')
      ..writeln('Server: ${info.server.baseUrl} (https: ${info.server.https})');
  } on XtAuthError catch (err) {
    stderr.writeln('Auth error: $err');
    exit(1);
  } on XtError catch (err) {
    stderr.writeln('Error: $err');
    exit(2);
  }

  // Fetch and print categories and a few items from each catalog
  try {
    stdout.writeln('Fetching catalogs...');

    final liveCats = await client.getLiveCategories();
    stdout.writeln('Live categories: ${liveCats.length}');
    for (final category in liveCats.take(3)) {
      stdout.writeln('  - \'${category.name}\' (id=${category.id})');
    }
    if (liveCats.isNotEmpty) {
      final category = liveCats[1];
      final streams = await client.getLiveStreams(categoryId: category.id);
      if (streams.isNotEmpty) {
        final stream = streams.first;
        stdout.writeln(
          'First live in ${category.name}: \'${stream.name}\' (id=${stream.streamId})',
        );
        // Most portals work with streamId for short EPG. If a channel
        // returns empty, it may require epg_channel_id; our client will
        // retry internally when both are provided.
        // Demo cancellation: allow early cancel via --cancel-after-ms
        final cancelAfter = opts.cancelAfterMs;
        final src = cancelAfter != null ? XtCancellationSource() : null;
        if (cancelAfter != null) {
          Timer(Duration(milliseconds: cancelAfter), src!.cancel);
        }
        List<XtEpgEntry> epg;
        try {
          epg = await client.getShortEpg(
            streamId: stream.streamId,
            limit: 2,
            cancel: src?.token,
          );
        } on XtError catch (err) {
          stderr.writeln('  EPG error: $err');
          epg = const [];
        }
        if (epg.isEmpty) {
          stdout.writeln('  No EPG data available');
        } else {
          for (final epgEntry in epg) {
            stdout.writeln(
              '  EPG: ${epgEntry.startUtc.toIso8601String()} - ${epgEntry.endUtc.toIso8601String()} \'${epgEntry.title}\'',
            );
          }
        }
      }
    }

    final vodCats = await client.getVodCategories();
    stdout.writeln('VOD categories: ${vodCats.length}');
    for (final category in vodCats.take(3)) {
      stdout.writeln('  - \'${category.name}\' (id=${category.id})');
    }
    if (vodCats.isNotEmpty) {
      final vod = await client.getVodStreams(categoryId: vodCats.first.id);
      if (vod.isNotEmpty) {
        final vodItem = vod.first;
        stdout.writeln(
          'First VOD in ${vodCats.first.name}: \'${vodItem.name}\' (id=${vodItem.streamId})',
        );
        final vd = await client.getVodInfo(vodItem.streamId);
        stdout.writeln(
          '  VOD details: duration=${vd.duration}, rating=${vd.rating}',
        );
      }
    }

    final seriesCats = await client.getSeriesCategories();
    stdout.writeln('Series categories: ${seriesCats.length}');
    for (final category in seriesCats.take(3)) {
      stdout.writeln('  - \'${category.name}\' (id=${category.id})');
    }
    if (seriesCats.isNotEmpty) {
      final ser = await client.getSeries(categoryId: seriesCats.first.id);
      if (ser.isNotEmpty) {
        final seriesItem = ser.first;
        stdout.writeln(
          'First series in ${seriesCats.first.name}: \'${seriesItem.name}\' (id=${seriesItem.seriesId})',
        );
        final sd = await client.getSeriesInfo(seriesItem.seriesId);
        final season1 = sd.seasons.keys.isNotEmpty
            ? sd.seasons.keys.first
            : null;
        if (season1 != null && sd.seasons[season1]!.isNotEmpty) {
          final ep = sd.seasons[season1]!.first;
          stdout.writeln(
            '  Series details: S${ep.season}E${ep.episode} \'${ep.title}\'',
          );
        }
      }
    }
  } on XtError catch (err) {
    stderr.writeln('Catalog error: $err');
  }

  // Diagnostics: ping and capabilities
  try {
    final health = await client.ping();
    stdout.writeln(
      'Ping: ok=${health.ok} status=${health.statusCode} latency=${health.latency.inMilliseconds}ms',
    );
    final caps = await client.capabilities();
    stdout.writeln(
      'Capabilities: shortEPG=${caps.supportsShortEpg}, extEPG=${caps.supportsExtendedEpg}, m3u=${caps.supportsM3u}, xmltv=${caps.supportsXmltv}',
    );
  } on XtError catch (err) {
    stderr.writeln('Diag error: $err');
  }

  // Show URL builder examples
  final live = liveUrl(portal, credentials, opts.streamId);
  final vod = vodUrl(portal, credentials, opts.streamId);
  final series = seriesUrl(portal, credentials, opts.streamId);
  stdout
    ..writeln('Sample URLs for streamId=${opts.streamId}:')
    ..writeln('  live:   $live')
    ..writeln('  vod:    $vod')
    ..writeln('  series: $series');

  // Demo M3U fetch and parse (optional)
  try {
    stdout.writeln('Fetching M3U playlist (first 2 entries)...');
    var count = 0;
    await for (final entry in client.getM3u().take(2)) {
      count++;
      final group = entry.groupTitle ?? '-';
      stdout.writeln('  M3U #$count: [$group] ${entry.name} -> ${entry.url}');
    }
    if (count == 0) stdout.writeln('  No M3U entries available');
  } on XtError catch (err) {
    stderr.writeln('M3U error: $err');
  }

  // Demo XMLTV fetch and parse (optional)
  try {
    stdout.writeln('Fetching XMLTV (first 3 events)...');
    var chSeen = 0;
    var prSeen = 0;
    await for (final ev in client.getXmltv()) {
      if (ev is XtXmltvChannel && chSeen < 2) {
        chSeen++;
        stdout.writeln(
          '  XMLTV channel: id=${ev.id} name=${ev.displayName ?? '-'} icon=${ev.iconUrl ?? '-'}',
        );
      } else if (ev is XtXmltvProgramme && prSeen < 1) {
        prSeen++;
        stdout.writeln(
          '  XMLTV programme: ch=${ev.channelId} ${ev.start.toIso8601String()} \'${ev.title ?? '-'}\'',
        );
      }
      if (chSeen + prSeen >= 3) break;
    }
    if (chSeen + prSeen == 0) stdout.writeln('  No XMLTV events available');
  } on XtError catch (err) {
    stderr.writeln('XMLTV error: $err');
  }

  if (opts.probeUrl != null) {
    final ext = await suggestStreamExtension(http, Uri.parse(opts.probeUrl!));
    stdout.writeln('Probed stream extension: .$ext');
  }
}

void _printUsage() {
  stdout.writeln('''
Usage:
  dart run example/main.dart --portal URL --user USER --pass PASS [--self-signed] [--probe URL] [--cancel-after-ms N]

Options:
  --portal URL      Xtream portal base URL (e.g., https://host:port)
  --user USER       Username
  --pass PASS       Password
  --self-signed     Allow self-signed TLS (for dev/testing)
  --probe URL       Probe a stream URL to suggest extension
  --stream-id N     Stream id to use in sample URLs (default: 1)
  --cancel-after-ms N  Cancel the sample EPG request after N milliseconds

Env vars (fallbacks): XT_PORTAL, XT_USER, XT_PASS
''');
}

class _CliOptions {
  final String? portal;
  final String? user;
  final String? pass;
  final bool selfSigned;
  final String? probeUrl;
  final int streamId;
  final int? cancelAfterMs;

  _CliOptions({
    this.portal,
    this.user,
    this.pass,
    this.selfSigned = false,
    this.probeUrl,
    this.streamId = 1,
    this.cancelAfterMs,
  });

  _CliOptions copyWith({
    String? portal,
    String? user,
    String? pass,
    bool? selfSigned,
    String? probeUrl,
    int? streamId,
    int? cancelAfterMs,
  }) => _CliOptions(
    portal: portal ?? this.portal,
    user: user ?? this.user,
    pass: pass ?? this.pass,
    selfSigned: selfSigned ?? this.selfSigned,
    probeUrl: probeUrl ?? this.probeUrl,
    streamId: streamId ?? this.streamId,
    cancelAfterMs: cancelAfterMs ?? this.cancelAfterMs,
  );

  static _CliOptions? parse(List<String> args, Map<String, String> env) {
    var opts = _CliOptions(
      portal: env['XT_PORTAL'],
      user: env['XT_USER'],
      pass: env['XT_PASS'],
    );
    for (var index = 0; index < args.length; index++) {
      final arg = args[index];
      switch (arg) {
        case '--portal':
          if (index + 1 >= args.length) return null;
          opts = opts.copyWith(portal: args[++index]);
          break;
        case '--user':
          if (index + 1 >= args.length) return null;
          opts = opts.copyWith(user: args[++index]);
          break;
        case '--pass':
          if (index + 1 >= args.length) return null;
          opts = opts.copyWith(pass: args[++index]);
          break;
        case '--self-signed':
          opts = opts.copyWith(selfSigned: true);
          break;
        case '--probe':
          if (index + 1 >= args.length) return null;
          opts = opts.copyWith(probeUrl: args[++index]);
          break;
        case '--stream-id':
          if (index + 1 >= args.length) return null;
          final parsedStreamId = int.tryParse(args[++index]) ?? 1;
          opts = opts.copyWith(streamId: parsedStreamId);
          break;
        case '--cancel-after-ms':
          if (index + 1 >= args.length) return null;
          final ms = int.tryParse(args[++index]);
          opts = opts.copyWith(cancelAfterMs: ms);
          break;
        default:
          return null;
      }
    }
    // Validate
    if (opts.portal == null || opts.user == null || opts.pass == null) {
      return null;
    }
    return opts;
  }
}
5
likes
160
points
6
downloads

Publisher

unverified uploader

Weekly Downloads

Ergonomic Xtream Codes IPTV client for Dart/Flutter: typed models, resilient HTTP, URL builders, EPG, M3U/XMLTV helpers.

Repository (GitHub)
View/report issues
Contributing

Topics

#xtream #iptv #m3u #xmltv

Documentation

API reference

License

MIT (license)

More

Packages that depend on muxa_xtream