Initial commit: Xtream IPTV Player for Android TV

This commit is contained in:
2026-02-25 13:01:25 -03:00
commit dd61066b3a
33 changed files with 1707 additions and 0 deletions

102
lib/main.dart Normal file
View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'services/iptv_provider.dart';
import 'screens/login_screen.dart';
import 'screens/home_screen.dart';
void main() {
runApp(const XStreamTVApp());
}
class XStreamTVApp extends StatelessWidget {
const XStreamTVApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => IPTVProvider(),
child: MaterialApp(
title: 'XStream TV',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.red,
scaffoldBackgroundColor: Colors.black,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
home: const AuthWrapper(),
),
);
}
}
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
bool _isChecking = true;
@override
void initState() {
super.initState();
_checkAuth();
}
Future<void> _checkAuth() async {
final provider = context.read<IPTVProvider>();
// Auto-login with embedded credentials
await provider.login(
'http://kenmhzxn.fqvpnw.com',
'55UDKCFH',
'6ZNP8Y81',
);
setState(() {
_isChecking = false;
});
}
@override
Widget build(BuildContext context) {
if (_isChecking) {
return const Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.live_tv, size: 80, color: Colors.red),
SizedBox(height: 24),
Text(
'XStream TV',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 24),
CircularProgressIndicator(color: Colors.red),
],
),
),
);
}
final provider = context.watch<IPTVProvider>();
if (provider.userInfo != null) {
return const HomeScreen();
}
return const LoginScreen();
}
}

View File

@@ -0,0 +1,158 @@
class XtreamCategory {
final String id;
final String name;
XtreamCategory({required this.id, required this.name});
factory XtreamCategory.fromJson(Map<String, dynamic> json) {
return XtreamCategory(
id: json['category_id']?.toString() ?? '',
name: json['category_name'] ?? '',
);
}
}
class XtreamStream {
final int streamId;
final String name;
final String? streamIcon;
final String? plot;
final String? rating;
final String? containerExtension;
String? url;
XtreamStream({
required this.streamId,
required this.name,
this.streamIcon,
this.plot,
this.rating,
this.containerExtension,
this.url,
});
factory XtreamStream.fromJson(Map<String, dynamic> json) {
return XtreamStream(
streamId: json['stream_id'] ?? 0,
name: json['name'] ?? '',
streamIcon: json['stream_icon'],
plot: json['plot'],
rating: json['rating'],
containerExtension: json['container_extension'],
url: null,
);
}
Map<String, dynamic> toJson() {
return {
'stream_id': streamId,
'name': name,
'stream_icon': streamIcon,
'plot': plot,
'rating': rating,
'container_extension': containerExtension,
'url': url,
};
}
}
class XtreamSeries {
final int seriesId;
final String name;
final String? cover;
final String? plot;
final String? rating;
final List<XtreamSeason> seasons;
XtreamSeries({
required this.seriesId,
required this.name,
this.cover,
this.plot,
this.rating,
this.seasons = const [],
});
factory XtreamSeries.fromJson(Map<String, dynamic> json) {
return XtreamSeries(
seriesId: json['series_id'] ?? 0,
name: json['name'] ?? '',
cover: json['cover'],
plot: json['plot'],
rating: json['rating'],
seasons: [],
);
}
}
class XtreamSeason {
final int seasonNumber;
final List<XtreamEpisode> episodes;
XtreamSeason({required this.seasonNumber, this.episodes = const []});
}
class XtreamEpisode {
final int episodeId;
final int seasonNumber;
final int episodeNumber;
final String title;
final String? info;
final String? containerExtension;
String? url;
XtreamEpisode({
required this.episodeId,
required this.seasonNumber,
required this.episodeNumber,
required this.title,
this.info,
this.containerExtension,
this.url,
});
factory XtreamEpisode.fromJson(Map<String, dynamic> json) {
return XtreamEpisode(
episodeId: json['id'] ?? 0,
seasonNumber: json['season'] ?? 0,
episodeNumber: json['episode_num'] ?? 0,
title: json['title'] ?? '',
info: json['info'],
containerExtension: json['container_extension'],
url: null,
);
}
}
class XtreamUserInfo {
final String username;
final String password;
final int maxConnections;
final int activeCons;
final bool isTrial;
final int? expDate;
final String status;
XtreamUserInfo({
required this.username,
required this.password,
required this.maxConnections,
required this.activeCons,
required this.isTrial,
this.expDate,
required this.status,
});
factory XtreamUserInfo.fromJson(Map<String, dynamic> json) {
final userInfo = json['user_info'] ?? json;
return XtreamUserInfo(
username: userInfo['username'] ?? '',
password: userInfo['password'] ?? '',
maxConnections: int.tryParse(userInfo['max_connections']?.toString() ?? '1') ?? 1,
activeCons: int.tryParse(userInfo['active_cons']?.toString() ?? '0') ?? 0,
isTrial: userInfo['is_trial'] == '1',
expDate: userInfo['exp_date'],
status: userInfo['status'] ?? '',
);
}
}

View File

@@ -0,0 +1,403 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/iptv_provider.dart';
import '../models/xtream_models.dart';
import 'player_screen.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _selectedTab = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadInitialData();
});
}
Future<void> _loadInitialData() async {
final provider = context.read<IPTVProvider>();
await provider.loadLiveStreams();
await provider.loadVodStreams();
await provider.loadSeries();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.red,
title: Consumer<IPTVProvider>(
builder: (context, provider, _) {
return Text(
provider.userInfo != null
? 'XStream TV - ${provider.userInfo!.username}'
: 'XStream TV',
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadInitialData,
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
context.read<IPTVProvider>().logout();
},
),
],
),
body: Row(
children: [
NavigationRail(
selectedIndex: _selectedTab,
onDestinationSelected: (index) {
setState(() => _selectedTab = index);
},
backgroundColor: Colors.grey[900],
selectedIconTheme: const IconThemeData(color: Colors.red),
unselectedIconTheme: const IconThemeData(color: Colors.grey),
labelType: NavigationRailLabelType.all,
destinations: const [
NavigationRailDestination(
icon: Icon(Icons.live_tv),
selectedIcon: Icon(Icons.live_tv, color: Colors.red),
label: Text('Live', style: TextStyle(color: Colors.white)),
),
NavigationRailDestination(
icon: Icon(Icons.movie),
selectedIcon: Icon(Icons.movie, color: Colors.red),
label: Text('Movies', style: TextStyle(color: Colors.white)),
),
NavigationRailDestination(
icon: Icon(Icons.tv),
selectedIcon: Icon(Icons.tv, color: Colors.red),
label: Text('Series', style: TextStyle(color: Colors.white)),
),
],
),
Expanded(
child: _buildContent(),
),
],
),
);
}
Widget _buildContent() {
switch (_selectedTab) {
case 0:
return _LiveTab();
case 1:
return _MoviesTab();
case 2:
return _SeriesTab();
default:
return _LiveTab();
}
}
}
class _LiveTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<IPTVProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.red));
}
return Column(
children: [
_CategoryDropdown(
categories: provider.liveCategories,
selectedCategory: provider.selectedLiveCategory,
onCategorySelected: (categoryId) {
provider.loadLiveStreams(categoryId);
},
),
Expanded(
child: _StreamList(
streams: provider.liveStreams,
onStreamSelected: (stream) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlayerScreen(stream: stream),
),
);
},
),
),
],
);
},
);
}
}
class _MoviesTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<IPTVProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.red));
}
return Column(
children: [
_CategoryDropdown(
categories: provider.vodCategories,
selectedCategory: provider.selectedVodCategory,
onCategorySelected: (categoryId) {
provider.loadVodStreams(categoryId);
},
),
Expanded(
child: _StreamList(
streams: provider.vodStreams,
onStreamSelected: (stream) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlayerScreen(stream: stream),
),
);
},
),
),
],
);
},
);
}
}
class _SeriesTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<IPTVProvider>(
builder: (context, provider, _) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator(color: Colors.red));
}
if (provider.selectedSeries != null) {
return _SeriesDetailScreen(series: provider.selectedSeries!);
}
return _StreamList(
streams: provider.seriesList.map((s) => XtreamStream(
streamId: s.seriesId,
name: s.name,
streamIcon: s.cover,
plot: s.plot,
rating: s.rating,
)).toList(),
isSeries: true,
onStreamSelected: (stream) {
final series = provider.seriesList.firstWhere(
(s) => s.seriesId == stream.streamId,
);
provider.loadSeriesEpisodes(series);
},
);
},
);
}
}
class _SeriesDetailScreen extends StatelessWidget {
final XtreamSeries series;
const _SeriesDetailScreen({required this.series});
@override
Widget build(BuildContext context) {
return Consumer<IPTVProvider>(
builder: (context, provider, _) {
final episodes = provider.seriesEpisodes;
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
provider.loadSeries();
},
),
Expanded(
child: Text(
series.name,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
Expanded(
child: ListView.builder(
itemCount: episodes.length,
itemBuilder: (context, index) {
final episode = episodes[index];
return ListTile(
leading: const Icon(Icons.play_circle_outline, color: Colors.red),
title: Text(
'S${episode.seasonNumber}E${episode.episodeNumber} - ${episode.title}',
style: const TextStyle(color: Colors.white),
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlayerScreen(
stream: XtreamStream(
streamId: episode.episodeId,
name: episode.title,
containerExtension: episode.containerExtension,
url: episode.url,
),
isLive: false,
),
),
);
},
);
},
),
),
],
);
},
);
}
}
class _CategoryDropdown extends StatelessWidget {
final List<XtreamCategory> categories;
final String selectedCategory;
final Function(String) onCategorySelected;
const _CategoryDropdown({
required this.categories,
required this.selectedCategory,
required this.onCategorySelected,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[900],
child: DropdownButton<String>(
value: selectedCategory.isEmpty ? null : selectedCategory,
hint: const Text('All Categories', style: TextStyle(color: Colors.white)),
dropdownColor: Colors.grey[800],
isExpanded: true,
items: [
const DropdownMenuItem<String>(
value: '',
child: Text('All Categories', style: TextStyle(color: Colors.white)),
),
...categories.map((c) => DropdownMenuItem<String>(
value: c.id,
child: Text(c.name, style: const TextStyle(color: Colors.white)),
)),
],
onChanged: (value) {
onCategorySelected(value ?? '');
},
),
);
}
}
class _StreamList extends StatelessWidget {
final List<XtreamStream> streams;
final Function(XtreamStream) onStreamSelected;
final bool isSeries;
const _StreamList({
required this.streams,
required this.onStreamSelected,
this.isSeries = false,
});
@override
Widget build(BuildContext context) {
if (streams.isEmpty) {
return const Center(
child: Text(
'No content available',
style: TextStyle(color: Colors.grey),
),
);
}
return ListView.builder(
itemCount: streams.length,
itemBuilder: (context, index) {
final stream = streams[index];
return ListTile(
leading: isSeries && stream.streamIcon != null
? ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
stream.streamIcon!,
width: 40,
height: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(
Icons.tv,
color: Colors.red,
size: 40,
),
),
)
: Icon(
isSeries ? Icons.tv : Icons.play_circle_outline,
color: Colors.red,
size: 40,
),
title: Text(
stream.name,
style: const TextStyle(color: Colors.white),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: stream.rating != null
? Row(
children: [
const Icon(Icons.star, color: Colors.amber, size: 14),
Text(
stream.rating ?? '',
style: const TextStyle(color: Colors.amber),
),
],
)
: null,
onTap: () => onStreamSelected(stream),
);
},
);
}
}

View File

@@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/iptv_provider.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _serverController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_serverController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _login() async {
final provider = context.read<IPTVProvider>();
await provider.login(
_serverController.text.trim(),
_usernameController.text.trim(),
_passwordController.text.trim(),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.live_tv,
size: 80,
color: Colors.red,
),
const SizedBox(height: 24),
const Text(
'XStream TV',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
const Text(
'IPTV Player for Android TV',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 48),
TextField(
controller: _serverController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Server URL',
labelStyle: const TextStyle(color: Colors.grey),
hintText: 'http://example.com',
hintStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.dns, color: Colors.grey),
filled: true,
fillColor: Colors.grey[900],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
TextField(
controller: _usernameController,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Username',
labelStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.person, color: Colors.grey),
filled: true,
fillColor: Colors.grey[900],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: 'Password',
labelStyle: const TextStyle(color: Colors.grey),
prefixIcon: const Icon(Icons.lock, color: Colors.grey),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
color: Colors.grey,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
filled: true,
fillColor: Colors.grey[900],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
const SizedBox(height: 24),
Consumer<IPTVProvider>(
builder: (context, provider, _) {
if (provider.error != null) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Text(
provider.error!,
style: const TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
);
}
return const SizedBox.shrink();
},
),
SizedBox(
width: double.infinity,
height: 50,
child: Consumer<IPTVProvider>(
builder: (context, provider, _) {
return ElevatedButton(
onPressed: provider.isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: provider.isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Login',
style: TextStyle(fontSize: 16),
),
);
},
),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';
import '../models/xtream_models.dart';
class PlayerScreen extends StatefulWidget {
final XtreamStream stream;
final bool isLive;
const PlayerScreen({
super.key,
required this.stream,
this.isLive = true,
});
@override
State<PlayerScreen> createState() => _PlayerScreenState();
}
class _PlayerScreenState extends State<PlayerScreen> {
late VideoPlayerController _videoController;
ChewieController? _chewieController;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_initPlayer();
}
Future<void> _initPlayer() async {
try {
final url = widget.stream.url;
if (url == null || url.isEmpty) {
throw Exception('No stream URL available');
}
_videoController = VideoPlayerController.networkUrl(Uri.parse(url));
await _videoController.initialize();
_chewieController = ChewieController(
videoPlayerController: _videoController,
autoPlay: true,
looping: widget.isLive,
aspectRatio: _videoController.value.aspectRatio,
allowFullScreen: true,
allowMuting: true,
showControls: true,
placeholder: Container(
color: Colors.black,
child: const Center(
child: CircularProgressIndicator(color: Colors.red),
),
),
errorBuilder: (context, errorMessage) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text(
errorMessage,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
],
),
);
},
);
setState(() {
_isLoading = false;
});
_videoController.addListener(() {
setState(() {});
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
void dispose() {
_videoController.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
widget.stream.name,
style: const TextStyle(color: Colors.white),
),
iconTheme: const IconThemeData(color: Colors.white),
),
body: Center(
child: _isLoading
? const CircularProgressIndicator(color: Colors.red)
: _error != null
? _buildError()
: _chewieController != null
? Chewie(controller: _chewieController!)
: const Text(
'No video available',
style: TextStyle(color: Colors.white),
),
),
);
}
Widget _buildError() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 64),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(16),
child: Text(
_error ?? 'Unknown error',
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
setState(() {
_isLoading = true;
_error = null;
});
_initPlayer();
},
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
),
],
);
}
}

View File

@@ -0,0 +1,174 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/xtream_api.dart';
import '../models/xtream_models.dart';
enum ContentType { live, movies, series }
class IPTVProvider extends ChangeNotifier {
final XtreamApiService _api = XtreamApiService();
bool _isLoading = false;
String? _error;
XtreamUserInfo? _userInfo;
List<XtreamCategory> _liveCategories = [];
List<XtreamCategory> _vodCategories = [];
List<XtreamCategory> _seriesCategories = [];
List<XtreamStream> _liveStreams = [];
List<XtreamStream> _vodStreams = [];
List<XtreamSeries> _seriesList = [];
List<XtreamEpisode> _seriesEpisodes = [];
String _selectedLiveCategory = '';
String _selectedVodCategory = '';
XtreamSeries? _selectedSeries;
bool get isLoading => _isLoading;
String? get error => _error;
XtreamUserInfo? get userInfo => _userInfo;
XtreamApiService get api => _api;
List<XtreamCategory> get liveCategories => _liveCategories;
List<XtreamCategory> get vodCategories => _vodCategories;
List<XtreamCategory> get seriesCategories => _seriesCategories;
List<XtreamStream> get liveStreams => _liveStreams;
List<XtreamStream> get vodStreams => _vodStreams;
List<XtreamSeries> get seriesList => _seriesList;
List<XtreamEpisode> get seriesEpisodes => _seriesEpisodes;
String get selectedLiveCategory => _selectedLiveCategory;
String get selectedVodCategory => _selectedVodCategory;
XtreamSeries? get selectedSeries => _selectedSeries;
Future<void> login(String server, String username, String password) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_api.setCredentials(server, username, password);
_userInfo = await _api.getUserInfo();
await _loadCategories();
await _saveCredentials(server, username, password);
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> _loadCategories() async {
try {
_liveCategories = await _api.getLiveCategories();
_vodCategories = await _api.getVodCategories();
_seriesCategories = await _api.getSeriesCategories();
} catch (e) {
_error = e.toString();
}
}
Future<void> loadLiveStreams([String categoryId = '']) async {
_isLoading = true;
notifyListeners();
try {
_liveStreams = await _api.getLiveStreams(categoryId);
_selectedLiveCategory = categoryId;
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> loadVodStreams([String categoryId = '']) async {
_isLoading = true;
notifyListeners();
try {
_vodStreams = await _api.getVodStreams(categoryId);
_selectedVodCategory = categoryId;
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> loadSeries() async {
_isLoading = true;
notifyListeners();
try {
_seriesList = await _api.getSeries();
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
Future<void> loadSeriesEpisodes(XtreamSeries series) async {
_isLoading = true;
notifyListeners();
try {
_selectedSeries = series;
_seriesEpisodes = await _api.getSeriesEpisodes(series.seriesId);
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
void clearError() {
_error = null;
notifyListeners();
}
Future<void> _saveCredentials(String server, String username, String password) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('server', server);
await prefs.setString('username', username);
await prefs.setString('password', password);
}
Future<bool> loadSavedCredentials() async {
final prefs = await SharedPreferences.getInstance();
final server = prefs.getString('server');
final username = prefs.getString('username');
final password = prefs.getString('password');
if (server != null && username != null && password != null) {
await login(server, username, password);
return true;
}
return false;
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('server');
await prefs.remove('username');
await prefs.remove('password');
_userInfo = null;
_liveCategories = [];
_vodCategories = [];
_seriesCategories = [];
_liveStreams = [];
_vodStreams = [];
_seriesList = [];
notifyListeners();
}
}

View File

@@ -0,0 +1,161 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/xtream_models.dart';
class XtreamApiService {
String? _server;
String? _username;
String? _password;
String? _baseUrl;
void setCredentials(String server, String username, String password) {
_server = server;
_username = username;
_password = password;
_baseUrl = server.startsWith('http') ? server : 'http://$server';
}
String? get server => _server;
String? get username => _username;
Future<Map<String, dynamic>> authenticate() async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password'),
);
if (response.statusCode == 200) {
return json.decode(response.body);
}
throw Exception('Authentication failed: ${response.statusCode}');
}
Future<XtreamUserInfo> getUserInfo() async {
final data = await authenticate();
return XtreamUserInfo.fromJson(data);
}
Future<List<XtreamCategory>> getLiveCategories() async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password&action=get_live_categories'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) => XtreamCategory.fromJson(e)).toList();
}
throw Exception('Failed to load categories');
}
Future<List<XtreamCategory>> getVodCategories() async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password&action=get_vod_categories'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) => XtreamCategory.fromJson(e)).toList();
}
throw Exception('Failed to load VOD categories');
}
Future<List<XtreamCategory>> getSeriesCategories() async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password&action=get_series_categories'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) => XtreamCategory.fromJson(e)).toList();
}
throw Exception('Failed to load series categories');
}
Future<List<XtreamStream>> getLiveStreams(String categoryId) async {
final url = '$_baseUrl/player_api.php';
String apiUrl = '$url?username=$_username&password=$_password&action=get_live_streams';
if (categoryId.isNotEmpty) {
apiUrl += '&category_id=$categoryId';
}
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) {
final stream = XtreamStream.fromJson(e);
stream.url = '$_baseUrl/live/$_username/$_password/${stream.streamId}.ts';
return stream;
}).toList();
}
throw Exception('Failed to load live streams');
}
Future<List<XtreamStream>> getVodStreams(String categoryId) async {
final url = '$_baseUrl/player_api.php';
String apiUrl = '$url?username=$_username&password=$_password&action=get_vod_streams';
if (categoryId.isNotEmpty) {
apiUrl += '&category_id=$categoryId';
}
final response = await http.get(Uri.parse(apiUrl));
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) {
final stream = XtreamStream.fromJson(e);
final ext = stream.containerExtension ?? 'm3u8';
stream.url = '$_baseUrl/vod/$_username/$_password/${stream.streamId}.$ext';
return stream;
}).toList();
}
throw Exception('Failed to load VOD streams');
}
Future<List<XtreamSeries>> getSeries() async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password&action=get_series'),
);
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
return data.map((e) => XtreamSeries.fromJson(e)).toList();
}
throw Exception('Failed to load series');
}
Future<List<XtreamEpisode>> getSeriesEpisodes(int seriesId) async {
final url = '$_baseUrl/player_api.php';
final response = await http.get(
Uri.parse('$url?username=$_username&password=$_password&action=get_series_info&series_id=$seriesId'),
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
final List<dynamic> episodesData = data['episodes'] ?? [];
final List<XtreamEpisode> allEpisodes = [];
for (final seasonData in episodesData) {
final season = seasonData['season_number'] ?? 0;
final List<dynamic> episodes = seasonData['episodes'] ?? [];
for (final ep in episodes) {
final episode = XtreamEpisode.fromJson(ep);
final ext = episode.containerExtension ?? 'm3u8';
episode.url = '$_baseUrl/series/$_username/$_password/${episode.episodeId}.$ext';
allEpisodes.add(episode);
}
}
return allEpisodes;
}
throw Exception('Failed to load series episodes');
}
String getStreamUrl(int streamId, {String type = 'live'}) {
final ext = type == 'live' ? 'ts' : 'm3u8';
return '$_baseUrl/$type/$_username/$_password/$streamId.$ext';
}
}