Merge pull request #15 from R0m1k3/antigravity

feat: implement Hive service for local data persistence, user managem…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-06 01:12:04 +01:00
commit fc30f682a1
6 files changed
+200 -29

No files matched your search

+9 -5
View File
@@ -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"]
+13
View File
@@ -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
View File
@@ -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',
);
}
};
}
+5 -13
View File
@@ -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();
+29 -10
View File
@@ -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
View File
@@ -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
@@ -44,6 +45,11 @@ dependencies:
# Utilities
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: