mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
Merge pull request #15 from R0m1k3/antigravity
feat: implement Hive service for local data persistence, user managem…
This commit is contained in:
commit
83adf21f84
6 files changed
+200
-29
No files matched your search
+9
-5
@@ -30,14 +30,18 @@ RUN flutter pub get
|
||||
RUN flutter build web --release --base-href="/" --verbose
|
||||
|
||||
# ============================================
|
||||
# Stage 2: Serve with dhttpd (Dart HTTP server)
|
||||
# Stage 2: Serve with custom Dart server (with API proxy)
|
||||
# ============================================
|
||||
FROM dart:stable
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dhttpd globally
|
||||
RUN dart pub global activate dhttpd
|
||||
# Copy server code and pubspec
|
||||
COPY bin/server.dart ./bin/
|
||||
COPY bin/pubspec.yaml ./
|
||||
|
||||
# Get dependencies
|
||||
RUN dart pub get
|
||||
|
||||
# Copy built web application from builder stage
|
||||
COPY --from=builder /app/build/web /app/web
|
||||
@@ -45,5 +49,5 @@ COPY --from=builder /app/build/web /app/web
|
||||
# Expose port (internal only, not mapped to host)
|
||||
EXPOSE 8089
|
||||
|
||||
# Serve web application
|
||||
CMD ["/root/.pub-cache/bin/dhttpd", "--host", "0.0.0.0", "--port", "8089", "--path", "/app/web"]
|
||||
# Serve web application with API proxy
|
||||
CMD ["dart", "run", "bin/server.dart", "--port", "8089", "--path", "/app/web"]
|
||||
@@ -0,0 +1,13 @@
|
||||
name: xtremflow_server
|
||||
description: Backend server with API proxy
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
shelf: ^1.4.1
|
||||
shelf_static: ^1.1.2
|
||||
http: ^1.2.0
|
||||
args: ^2.4.2
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import 'dart:io';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_static/shelf_static.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:args/args.dart';
|
||||
|
||||
void main(List<String> args) async {
|
||||
// Parse command line arguments
|
||||
final parser = ArgParser()
|
||||
..addOption('port', abbr: 'p', defaultsTo: '8089')
|
||||
..addOption('path', defaultsTo: '/app/web');
|
||||
|
||||
final result = parser.parse(args);
|
||||
final port = int.parse(result['port']);
|
||||
final webPath = result['path'];
|
||||
|
||||
// Create handlers
|
||||
final staticHandler = createStaticHandler(
|
||||
webPath,
|
||||
defaultDocument: 'index.html',
|
||||
listDirectories: false,
|
||||
);
|
||||
|
||||
// Main handler with API proxy
|
||||
final handler = Cascade()
|
||||
.add(_createApiProxyHandler())
|
||||
.add(staticHandler)
|
||||
.handler;
|
||||
|
||||
// Add middleware
|
||||
final pipeline = Pipeline()
|
||||
.addMiddleware(logRequests())
|
||||
.addMiddleware(_corsMiddleware())
|
||||
.addHandler(handler);
|
||||
|
||||
// Start server
|
||||
final server = await shelf_io.serve(
|
||||
pipeline,
|
||||
InternetAddress.anyIPv4,
|
||||
port,
|
||||
);
|
||||
|
||||
print('Server started on port ${server.port}');
|
||||
print('Serving static files from: $webPath');
|
||||
print('API proxy available at: /api/xtream/*');
|
||||
}
|
||||
|
||||
/// CORS middleware to allow cross-origin requests
|
||||
Middleware _corsMiddleware() {
|
||||
return (Handler handler) {
|
||||
return (Request request) async {
|
||||
// Handle preflight requests
|
||||
if (request.method == 'OPTIONS') {
|
||||
return Response.ok('', headers: _corsHeaders);
|
||||
}
|
||||
|
||||
// Process request and add CORS headers to response
|
||||
final response = await handler(request);
|
||||
return response.change(headers: _corsHeaders);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
final _corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Origin, Content-Type, Accept, Authorization',
|
||||
};
|
||||
|
||||
/// Create API proxy handler
|
||||
Handler _createApiProxyHandler() {
|
||||
return (Request request) async {
|
||||
final path = request.url.path;
|
||||
|
||||
// Only handle /api/xtream/* requests
|
||||
if (!path.startsWith('api/xtream/')) {
|
||||
return Response.notFound('Not found');
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract target URL from request
|
||||
// Format: /api/xtream/http://server:port/path
|
||||
final apiPath = path.substring('api/xtream/'.length);
|
||||
|
||||
// The client will send the full URL after /api/xtream/
|
||||
if (!apiPath.startsWith('http://') && !apiPath.startsWith('https://')) {
|
||||
return Response.badRequest(
|
||||
body: 'Invalid API URL. Expected format: /api/xtream/http://...',
|
||||
);
|
||||
}
|
||||
|
||||
final targetUrl = Uri.parse(apiPath + '?' + (request.url.query));
|
||||
|
||||
print('Proxying request to: $targetUrl');
|
||||
|
||||
// Forward the request
|
||||
final client = http.Client();
|
||||
try {
|
||||
final proxyRequest = http.Request(request.method, targetUrl);
|
||||
|
||||
// Copy headers (excluding host)
|
||||
request.headers.forEach((key, value) {
|
||||
if (key.toLowerCase() != 'host') {
|
||||
proxyRequest.headers[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Copy body if present
|
||||
if (request.method != 'GET' && request.method != 'HEAD') {
|
||||
proxyRequest.bodyBytes = await request.read().toList()
|
||||
.then((chunks) => chunks.expand((chunk) => chunk).toList());
|
||||
}
|
||||
|
||||
final response = await client.send(proxyRequest);
|
||||
final responseBody = await response.stream.toBytes();
|
||||
|
||||
return Response(
|
||||
response.statusCode,
|
||||
body: responseBody,
|
||||
headers: {
|
||||
'content-type': response.headers['content-type'] ?? 'application/json',
|
||||
...response.headers,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
print('Proxy error: $e');
|
||||
print(stackTrace);
|
||||
return Response.internalServerError(
|
||||
body: 'Proxy error: $e',
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -19,23 +19,15 @@ class HiveService {
|
||||
// Initialize Hive for Web (uses IndexedDB)
|
||||
await Hive.initFlutter();
|
||||
|
||||
// Generate or retrieve encryption key
|
||||
final encryptionKey = await _getOrCreateEncryptionKey();
|
||||
|
||||
// Register adapters
|
||||
Hive.registerAdapter(AppUserAdapter());
|
||||
Hive.registerAdapter(PlaylistConfigAdapter());
|
||||
|
||||
// Open encrypted boxes
|
||||
await Hive.openBox<AppUser>(
|
||||
_usersBoxName,
|
||||
encryptionCipher: HiveAesCipher(encryptionKey),
|
||||
);
|
||||
|
||||
await Hive.openBox<PlaylistConfig>(
|
||||
_playlistsBoxName,
|
||||
encryptionCipher: HiveAesCipher(encryptionKey),
|
||||
);
|
||||
// Open boxes WITHOUT encryption on Web
|
||||
// Note: On Web, IndexedDB is already isolated by origin,
|
||||
// and session-based encryption keys cause decrypt errors on reload
|
||||
await Hive.openBox<AppUser>(_usersBoxName);
|
||||
await Hive.openBox<PlaylistConfig>(_playlistsBoxName);
|
||||
|
||||
// Seed default admin if no users exist
|
||||
await _seedDefaultAdmin();
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_stor
|
||||
import '../../../core/models/playlist_config.dart';
|
||||
import '../../../core/models/iptv_models.dart';
|
||||
import '../models/xtream_models.dart' as xm;
|
||||
import 'dart:html' as html;
|
||||
|
||||
/// Xtream Codes API Service
|
||||
///
|
||||
@@ -31,6 +32,21 @@ class XtreamService {
|
||||
_dio.interceptors.add(DioCacheInterceptor(options: _cacheOptions));
|
||||
}
|
||||
|
||||
/// Check if we're running on HTTPS (production)
|
||||
bool get _isHttps {
|
||||
return html.window.location.protocol == 'https:';
|
||||
}
|
||||
|
||||
/// Wrap URL with proxy if needed (for HTTPS to HTTP bridge)
|
||||
String _wrapWithProxy(String url) {
|
||||
// If we're on HTTPS and the target URL is HTTP, use the proxy
|
||||
if (_isHttps && url.startsWith('http://')) {
|
||||
final baseUrl = html.window.location.origin;
|
||||
return '$baseUrl/api/xtream/$url';
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/// Initialize connection with a playlist
|
||||
void setPlaylist(PlaylistConfig playlist) {
|
||||
_currentPlaylist = playlist;
|
||||
@@ -40,21 +56,24 @@ class XtreamService {
|
||||
String getLiveStreamUrl(String streamId) {
|
||||
if (_currentPlaylist == null) throw Exception('No playlist configured');
|
||||
|
||||
return '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
|
||||
final url = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
|
||||
return _wrapWithProxy(url);
|
||||
}
|
||||
|
||||
/// 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';
|
||||
final url = '${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
return _wrapWithProxy(url);
|
||||
}
|
||||
|
||||
/// 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';
|
||||
final url = '${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension';
|
||||
return _wrapWithProxy(url);
|
||||
}
|
||||
|
||||
/// Authenticate and get server info
|
||||
@@ -63,7 +82,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -85,7 +104,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -119,7 +138,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -153,7 +172,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -187,7 +206,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -218,7 +237,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
@@ -251,7 +270,7 @@ class XtreamService {
|
||||
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_currentPlaylist!.apiBaseUrl,
|
||||
_wrapWithProxy(_currentPlaylist!.apiBaseUrl),
|
||||
queryParameters: {
|
||||
'username': _currentPlaylist!.username,
|
||||
'password': _currentPlaylist!.password,
|
||||
|
||||
+7
-1
@@ -22,13 +22,14 @@ dependencies:
|
||||
dio: ^5.4.0
|
||||
dio_cache_interceptor: ^3.5.0
|
||||
dio_cache_interceptor_hive_store: ^3.2.2
|
||||
http: ^1.2.0
|
||||
|
||||
# Routing
|
||||
go_router: ^13.0.0
|
||||
|
||||
# Video Player
|
||||
video_player: ^2.8.2
|
||||
video_player_web: ^2.3.0 # Required for web platform
|
||||
video_player_web: ^2.3.0
|
||||
chewie: ^1.7.5
|
||||
media_kit: ^1.1.10
|
||||
media_kit_video: ^1.2.4
|
||||
@@ -45,6 +46,11 @@ dependencies:
|
||||
intl: ^0.19.0
|
||||
equatable: ^2.0.5
|
||||
|
||||
# Server dependencies (for backend proxy)
|
||||
shelf: ^1.4.1
|
||||
shelf_static: ^1.1.2
|
||||
args: ^2.4.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in new issue
Block a user