mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-22 06:28:04 +02:00
feat: add new UI tabs for movies and series, including mobile-specific versions with search and pagination.
This commit is contained in:
1 parent
d48d25f65e
commit
6a71f58a36
29 files changed
+78
-78
No files matched your search
@@ -29,7 +29,7 @@ class AuthHandler {
|
|||||||
return Response(400, body: jsonEncode({
|
return Response(400, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Username and password are required',
|
'error': 'Username and password are required',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify credentials
|
// Verify credentials
|
||||||
@@ -38,7 +38,7 @@ class AuthHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Invalid credentials',
|
'error': 'Invalid credentials',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
@@ -48,7 +48,7 @@ class AuthHandler {
|
|||||||
'success': true,
|
'success': true,
|
||||||
'user': user.toJson(),
|
'user': user.toJson(),
|
||||||
'token': session.token,
|
'token': session.token,
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -65,14 +65,14 @@ class AuthHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.deleteSession(token);
|
db.deleteSession(token);
|
||||||
|
|
||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -89,7 +89,7 @@ class AuthHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final session = db.findSessionByToken(token);
|
final session = db.findSessionByToken(token);
|
||||||
@@ -97,7 +97,7 @@ class AuthHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Invalid or expired session',
|
'error': 'Invalid or expired session',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final user = db.findUserById(session.userId);
|
final user = db.findUserById(session.userId);
|
||||||
@@ -105,12 +105,12 @@ class AuthHandler {
|
|||||||
return Response(404, body: jsonEncode({
|
return Response(404, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'User not found',
|
'error': 'User not found',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'user': user.toJson(),
|
'user': user.toJson(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ class PlaylistsHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final playlists = db.getPlaylists(userId);
|
final playlists = db.getPlaylists(userId);
|
||||||
|
|
||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'playlists': playlists.map((p) => p.toJson()).toList(),
|
'playlists': playlists.map((p) => p.toJson()).toList(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -51,7 +51,7 @@ class PlaylistsHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
||||||
@@ -65,7 +65,7 @@ class PlaylistsHandler {
|
|||||||
return Response(400, body: jsonEncode({
|
return Response(400, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Missing required fields',
|
'error': 'Missing required fields',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final playlist = db.createPlaylist(
|
final playlist = db.createPlaylist(
|
||||||
@@ -80,7 +80,7 @@ class PlaylistsHandler {
|
|||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
'playlist': playlist.toJson(),
|
'playlist': playlist.toJson(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -97,7 +97,7 @@ class PlaylistsHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify playlist belongs to user
|
// Verify playlist belongs to user
|
||||||
@@ -106,14 +106,14 @@ class PlaylistsHandler {
|
|||||||
return Response(404, body: jsonEncode({
|
return Response(404, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Playlist not found',
|
'error': 'Playlist not found',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing.userId != userId) {
|
if (existing.userId != userId) {
|
||||||
return Response(403, body: jsonEncode({
|
return Response(403, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Forbidden',
|
'error': 'Forbidden',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
final payload = jsonDecode(await request.readAsString()) as Map<String, dynamic>;
|
||||||
@@ -135,7 +135,7 @@ class PlaylistsHandler {
|
|||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
'playlist': playlist.toJson(),
|
'playlist': playlist.toJson(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -152,7 +152,7 @@ class PlaylistsHandler {
|
|||||||
return Response(401, body: jsonEncode({
|
return Response(401, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Unauthorized',
|
'error': 'Unauthorized',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify playlist belongs to user
|
// Verify playlist belongs to user
|
||||||
@@ -161,21 +161,21 @@ class PlaylistsHandler {
|
|||||||
return Response(404,body: jsonEncode({
|
return Response(404,body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Playlist not found',
|
'error': 'Playlist not found',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existing.userId != userId) {
|
if (existing.userId != userId) {
|
||||||
return Response(403, body: jsonEncode({
|
return Response(403, body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Forbidden',
|
'error': 'Forbidden',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.deletePlaylist(id);
|
db.deletePlaylist(id);
|
||||||
|
|
||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class UsersHandler {
|
|||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
'users': users.map((u) => u.toJson()).toList(),
|
'users': users.map((u) => u.toJson()).toList(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'success': false, 'error': e.toString()}),
|
body: jsonEncode({'success': false, 'error': e.toString()}),
|
||||||
@@ -62,21 +62,21 @@ class UsersHandler {
|
|||||||
return Response.badRequest(body: jsonEncode({
|
return Response.badRequest(body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Missing required fields',
|
'error': 'Missing required fields',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db.findUserByUsername(username) != null) {
|
if (db.findUserByUsername(username) != null) {
|
||||||
return Response.badRequest(body: jsonEncode({
|
return Response.badRequest(body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Username already exists',
|
'error': 'Username already exists',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
final user = db.createUser(username, password, isAdmin: isAdmin);
|
final user = db.createUser(username, password, isAdmin: isAdmin);
|
||||||
return Response.ok(jsonEncode({
|
return Response.ok(jsonEncode({
|
||||||
'success': true,
|
'success': true,
|
||||||
'user': user.toJson(),
|
'user': user.toJson(),
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
@@ -100,7 +100,7 @@ class UsersHandler {
|
|||||||
return Response.badRequest(body: jsonEncode({
|
return Response.badRequest(body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Missing password',
|
'error': 'Missing password',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.updateUserPassword(id, password);
|
db.updateUserPassword(id, password);
|
||||||
@@ -151,7 +151,7 @@ class UsersHandler {
|
|||||||
return Response.badRequest(body: jsonEncode({
|
return Response.badRequest(body: jsonEncode({
|
||||||
'success': false,
|
'success': false,
|
||||||
'error': 'Cannot delete yourself',
|
'error': 'Cannot delete yourself',
|
||||||
}), headers: {'Content-Type': 'application/json'});
|
}), headers: {'Content-Type': 'application/json'},);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.deleteUser(id);
|
db.deleteUser(id);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class AppDatabase {
|
|||||||
_db.execute('''
|
_db.execute('''
|
||||||
INSERT INTO users (id, username, password_hash, is_admin)
|
INSERT INTO users (id, username, password_hash, is_admin)
|
||||||
VALUES (?, ?, ?, 1)
|
VALUES (?, ?, ?, 1)
|
||||||
''', [adminId, 'admin', passwordHash]);
|
''', [adminId, 'admin', passwordHash],);
|
||||||
|
|
||||||
print('Default admin user created (username: admin, password: admin)');
|
print('Default admin user created (username: admin, password: admin)');
|
||||||
}
|
}
|
||||||
@@ -141,7 +141,7 @@ class AppDatabase {
|
|||||||
_db.execute('''
|
_db.execute('''
|
||||||
INSERT INTO users (id, username, password_hash, is_admin)
|
INSERT INTO users (id, username, password_hash, is_admin)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
''', [userId, username, passwordHash, isAdmin ? 1 : 0]);
|
''', [userId, username, passwordHash, isAdmin ? 1 : 0],);
|
||||||
|
|
||||||
return User(
|
return User(
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -190,7 +190,7 @@ class AppDatabase {
|
|||||||
_db.execute('''
|
_db.execute('''
|
||||||
INSERT INTO sessions (id, user_id, token, expires_at)
|
INSERT INTO sessions (id, user_id, token, expires_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
''', [sessionId, userId, token, expiresAt.toIso8601String()]);
|
''', [sessionId, userId, token, expiresAt.toIso8601String()],);
|
||||||
|
|
||||||
return models.Session(
|
return models.Session(
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
@@ -272,7 +272,7 @@ class AppDatabase {
|
|||||||
_db.execute('''
|
_db.execute('''
|
||||||
INSERT INTO playlists (id, user_id, name, server_url, username, password, dns, created_at, updated_at)
|
INSERT INTO playlists (id, user_id, name, server_url, username, password, dns, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', [playlistId, userId, name, serverUrl, username, password, dns, now, now]);
|
''', [playlistId, userId, name, serverUrl, username, password, dns, now, now],);
|
||||||
|
|
||||||
return Playlist(
|
return Playlist(
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
@@ -302,7 +302,7 @@ class AppDatabase {
|
|||||||
UPDATE playlists
|
UPDATE playlists
|
||||||
SET name = ?, server_url = ?, username = ?, password = ?, dns = ?, updated_at = ?
|
SET name = ?, server_url = ?, username = ?, password = ?, dns = ?, updated_at = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
''', [name, serverUrl, username, password, dns, now, playlistId]);
|
''', [name, serverUrl, username, password, dns, now, playlistId],);
|
||||||
|
|
||||||
return getPlaylistById(playlistId)!;
|
return getPlaylistById(playlistId)!;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Middleware authMiddleware(AppDatabase db) {
|
|||||||
final updatedRequest = request.change(context: {
|
final updatedRequest = request.change(context: {
|
||||||
...request.context,
|
...request.context,
|
||||||
'userId': session.userId,
|
'userId': session.userId,
|
||||||
});
|
},);
|
||||||
|
|
||||||
return handler(updatedRequest);
|
return handler(updatedRequest);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Middleware securityHeadersMiddleware() {
|
|||||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
||||||
// Note: CSP is tricky with Flutter Web (requires 'unsafe-eval' for Dart),
|
// Note: CSP is tricky with Flutter Web (requires 'unsafe-eval' for Dart),
|
||||||
// so we omit it here to avoid breaking the app, or use a permissive one.
|
// so we omit it here to avoid breaking the app, or use a permissive one.
|
||||||
});
|
},);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ Middleware honeypotMiddleware() {
|
|||||||
'/config.php',
|
'/config.php',
|
||||||
'/api/.env',
|
'/api/.env',
|
||||||
'/console',
|
'/console',
|
||||||
'/actuator/health'
|
'/actuator/health',
|
||||||
];
|
];
|
||||||
|
|
||||||
return (Handler handler) {
|
return (Handler handler) {
|
||||||
|
|||||||
+7
-7
@@ -43,11 +43,11 @@ void main(List<String> args) async {
|
|||||||
// Playlists endpoints (with auth middleware)
|
// Playlists endpoints (with auth middleware)
|
||||||
..mount('/api/playlists', Pipeline()
|
..mount('/api/playlists', Pipeline()
|
||||||
.addMiddleware(authMiddleware(db))
|
.addMiddleware(authMiddleware(db))
|
||||||
.addHandler(playlistsHandler.router.call))
|
.addHandler(playlistsHandler.router.call),)
|
||||||
// Users endpoints (with auth middleware)
|
// Users endpoints (with auth middleware)
|
||||||
..mount('/api/users', Pipeline()
|
..mount('/api/users', Pipeline()
|
||||||
.addMiddleware(authMiddleware(db))
|
.addMiddleware(authMiddleware(db))
|
||||||
.addHandler(usersHandler.router.call));
|
.addHandler(usersHandler.router.call),);
|
||||||
|
|
||||||
// Initialize Cleanup Service
|
// Initialize Cleanup Service
|
||||||
final cleanupService = CleanupService();
|
final cleanupService = CleanupService();
|
||||||
@@ -75,12 +75,12 @@ void main(List<String> args) async {
|
|||||||
final result = await cleanupService.runCleanup();
|
final result = await cleanupService.runCleanup();
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
jsonEncode(result),
|
jsonEncode(result),
|
||||||
headers: {'content-type': 'application/json'}
|
headers: {'content-type': 'application/json'},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return router(request);
|
return router(request);
|
||||||
}));
|
}),);
|
||||||
|
|
||||||
// Create handlers
|
// Create handlers
|
||||||
final staticHandler = createStaticHandler(
|
final staticHandler = createStaticHandler(
|
||||||
@@ -532,14 +532,14 @@ Handler _createStreamHandler() {
|
|||||||
'-vf', 'scale=-2:480',
|
'-vf', 'scale=-2:480',
|
||||||
'-b:v', '800k',
|
'-b:v', '800k',
|
||||||
'-maxrate', '800k',
|
'-maxrate', '800k',
|
||||||
'-bufsize', '1600k'
|
'-bufsize', '1600k',
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// Medium: Original res (or cap at 720p?), 2500k
|
// Medium: Original res (or cap at 720p?), 2500k
|
||||||
ffmpegArgs.addAll([
|
ffmpegArgs.addAll([
|
||||||
'-b:v', '2500k',
|
'-b:v', '2500k',
|
||||||
'-maxrate', '2500k',
|
'-maxrate', '2500k',
|
||||||
'-bufsize', '5000k'
|
'-bufsize', '5000k',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -556,7 +556,7 @@ Handler _createStreamHandler() {
|
|||||||
'-fflags', '+genpts',
|
'-fflags', '+genpts',
|
||||||
|
|
||||||
// Output to stdout pipe
|
// Output to stdout pipe
|
||||||
'pipe:1'
|
'pipe:1',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Start the process
|
// Start the process
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class CleanupService {
|
|||||||
'deleted_files': deletedCount,
|
'deleted_files': deletedCount,
|
||||||
'freed_bytes': freedBytes,
|
'freed_bytes': freedBytes,
|
||||||
'timestamp': DateTime.now().toIso8601String(),
|
'timestamp': DateTime.now().toIso8601String(),
|
||||||
'errors': errors
|
'errors': errors,
|
||||||
};
|
};
|
||||||
|
|
||||||
print('CleanupService: Cleanup complete. Deleted $deletedCount files, freed ${(freedBytes / 1024 / 1024).toStringAsFixed(2)} MB');
|
print('CleanupService: Cleanup complete. Deleted $deletedCount files, freed ${(freedBytes / 1024 / 1024).toStringAsFixed(2)} MB');
|
||||||
|
|||||||
Binary file not shown.
@@ -15,13 +15,13 @@ class ApiClient {
|
|||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
connectTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 10),
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
));
|
),);
|
||||||
|
|
||||||
// Add interceptor for logging
|
// Add interceptor for logging
|
||||||
_dio.interceptors.add(LogInterceptor(
|
_dio.interceptors.add(LogInterceptor(
|
||||||
requestBody: true,
|
requestBody: true,
|
||||||
responseBody: true,
|
responseBody: true,
|
||||||
));
|
),);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get base URL (same origin for production)
|
/// Get base URL (same origin for production)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class AuthApiService {
|
|||||||
final response = await _api.post('/api/auth/login', data: {
|
final response = await _api.post('/api/auth/login', data: {
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class PlaylistApiService {
|
|||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'dns': dns,
|
'dns': dns,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ class PlaylistApiService {
|
|||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'dns': dns,
|
'dns': dns,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class UserApiService {
|
|||||||
passwordHash: '',
|
passwordHash: '',
|
||||||
isAdmin: u['isAdmin'] as bool? ?? false,
|
isAdmin: u['isAdmin'] as bool? ?? false,
|
||||||
createdAt: DateTime.tryParse(u['createdAt'] ?? '') ?? DateTime.now(),
|
createdAt: DateTime.tryParse(u['createdAt'] ?? '') ?? DateTime.now(),
|
||||||
)).toList();
|
),).toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -40,7 +40,7 @@ class UserApiService {
|
|||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'isAdmin': isAdmin,
|
'isAdmin': isAdmin,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class UserApiService {
|
|||||||
_api.restoreToken();
|
_api.restoreToken();
|
||||||
final response = await _api.put('/api/users/$userId/password', data: {
|
final response = await _api.put('/api/users/$userId/password', data: {
|
||||||
'password': newPassword,
|
'password': newPassword,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ class UserApiService {
|
|||||||
_api.restoreToken();
|
_api.restoreToken();
|
||||||
final response = await _api.put('/api/users/$userId', data: {
|
final response = await _api.put('/api/users/$userId', data: {
|
||||||
'isAdmin': isAdmin,
|
'isAdmin': isAdmin,
|
||||||
});
|
},);
|
||||||
|
|
||||||
final data = response.data as Map<String, dynamic>;
|
final data = response.data as Map<String, dynamic>;
|
||||||
|
|
||||||
|
|||||||
@@ -373,14 +373,14 @@ class _MediaCardState extends State<MediaCard>
|
|||||||
color: Colors.black.withOpacity(0.5),
|
color: Colors.black.withOpacity(0.5),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withOpacity(0.2),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class _UsersTab extends ConsumerWidget {
|
|||||||
child: Icon(
|
child: Icon(
|
||||||
user.isAdmin ? Icons.admin_panel_settings : Icons.person,
|
user.isAdmin ? Icons.admin_panel_settings : Icons.person,
|
||||||
color: user.isAdmin ? Colors.black : AppColors.textPrimary,
|
color: user.isAdmin ? Colors.black : AppColors.textPrimary,
|
||||||
size: 24
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class PlaybackPositionsNotifier extends StateNotifier<PlaybackPositions> {
|
|||||||
if (positionsJson != null) {
|
if (positionsJson != null) {
|
||||||
final decoded = jsonDecode(positionsJson) as Map<String, dynamic>;
|
final decoded = jsonDecode(positionsJson) as Map<String, dynamic>;
|
||||||
final positions = decoded.map((key, value) =>
|
final positions = decoded.map((key, value) =>
|
||||||
MapEntry(key, (value as num).toDouble())
|
MapEntry(key, (value as num).toDouble()),
|
||||||
);
|
);
|
||||||
|
|
||||||
state = PlaybackPositions(positions: positions);
|
state = PlaybackPositions(positions: positions);
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> with SingleTi
|
|||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white.withOpacity(0.8),
|
color: Colors.white.withOpacity(0.8),
|
||||||
height: 0.8
|
height: 0.8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
_totalDuration = duration > 0 ? duration : 1;
|
_totalDuration = duration > 0 ? duration : 1;
|
||||||
});
|
});
|
||||||
ref.read(playbackPositionsProvider.notifier).savePosition(
|
ref.read(playbackPositionsProvider.notifier).savePosition(
|
||||||
_contentId, currentTime, duration
|
_contentId, currentTime, duration,
|
||||||
);
|
);
|
||||||
} else if (type == 'playback_status') {
|
} else if (type == 'playback_status') {
|
||||||
final status = data['status'];
|
final status = data['status'];
|
||||||
@@ -674,7 +674,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
appBar: _showControls ? AppBar(
|
appBar: _showControls ? AppBar(
|
||||||
title: Text(widget.channels != null && widget.channels!.isNotEmpty
|
title: Text(widget.channels != null && widget.channels!.isNotEmpty
|
||||||
? widget.channels![_currentIndex].name
|
? widget.channels![_currentIndex].name
|
||||||
: widget.title),
|
: widget.title,),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@@ -775,7 +775,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
|
|||||||
? Icons.volume_off
|
? Icons.volume_off
|
||||||
: _volume < 0.5 ? Icons.volume_down : Icons.volume_up,
|
: _volume < 0.5 ? Icons.volume_down : Icons.volume_up,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 20
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: _toggleMute,
|
onPressed: _toggleMute,
|
||||||
tooltip: _volume == 0 ? 'Unmute' : 'Mute',
|
tooltip: _volume == 0 ? 'Unmute' : 'Mute',
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class _PlaylistCardState extends State<_PlaylistCard> {
|
|||||||
color: Colors.black.withOpacity(0.3),
|
color: Colors.black.withOpacity(0.3),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
)
|
),
|
||||||
] : [],
|
] : [],
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class XtreamService {
|
|||||||
_dio = Dio(BaseOptions(
|
_dio = Dio(BaseOptions(
|
||||||
connectTimeout: const Duration(seconds: 15),
|
connectTimeout: const Duration(seconds: 15),
|
||||||
receiveTimeout: const Duration(seconds: 15),
|
receiveTimeout: const Duration(seconds: 15),
|
||||||
));
|
),);
|
||||||
|
|
||||||
// Setup caching for API responses
|
// Setup caching for API responses
|
||||||
_cacheOptions = CacheOptions(
|
_cacheOptions = CacheOptions(
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class _ChannelCardState extends ConsumerState<ChannelCard>
|
|||||||
super.initState();
|
super.initState();
|
||||||
_pulseController = AnimationController(
|
_pulseController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 1000)
|
duration: const Duration(milliseconds: 1000),
|
||||||
)..repeat(reverse: true);
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
// Fetch EPG immediately when card is built
|
// Fetch EPG immediately when card is built
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class EpgOverlay extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final epgAsync = ref.watch(epgByPlaylistProvider(
|
final epgAsync = ref.watch(epgByPlaylistProvider(
|
||||||
EpgRequestKey(playlist: playlist, streamId: streamId),
|
EpgRequestKey(playlist: playlist, streamId: streamId),
|
||||||
));
|
),);
|
||||||
|
|
||||||
return epgAsync.when(
|
return epgAsync.when(
|
||||||
data: (epgEntries) {
|
data: (epgEntries) {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
if (_searchQuery.isNotEmpty)
|
if (_searchQuery.isNotEmpty)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _searchController.clear(),
|
onTap: () => _searchController.clear(),
|
||||||
child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary)
|
child: const Icon(Icons.close, size: 16, color: AppColors.textSecondary),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ class _MoviesTabState extends ConsumerState<MoviesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||||
String? _getProxiedImageUrl(String? originalUrl) {
|
String _getProxiedImageUrl(String? originalUrl) {
|
||||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||||
if (originalUrl.startsWith('http://')) {
|
if (originalUrl.startsWith('http://')) {
|
||||||
return '/api/xtream/$originalUrl';
|
return '/api/xtream/$originalUrl';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ class _SeriesTabState extends ConsumerState<SeriesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||||
String? _getProxiedImageUrl(String? originalUrl) {
|
String _getProxiedImageUrl(String? originalUrl) {
|
||||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||||
if (originalUrl.startsWith('http://')) {
|
if (originalUrl.startsWith('http://')) {
|
||||||
return '/api/xtream/$originalUrl';
|
return '/api/xtream/$originalUrl';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ class _MobileMoviesTabState extends ConsumerState<MobileMoviesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||||
String? _getProxiedImageUrl(String? originalUrl) {
|
String _getProxiedImageUrl(String? originalUrl) {
|
||||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||||
if (originalUrl.startsWith('http://')) {
|
if (originalUrl.startsWith('http://')) {
|
||||||
return '/api/xtream/$originalUrl';
|
return '/api/xtream/$originalUrl';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ class _MobileSeriesTabState extends ConsumerState<MobileSeriesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
/// Proxy HTTP images through backend to avoid CORS/mixed-content issues
|
||||||
String? _getProxiedImageUrl(String? originalUrl) {
|
String _getProxiedImageUrl(String? originalUrl) {
|
||||||
if (originalUrl == null || originalUrl.isEmpty) return null;
|
if (originalUrl == null || originalUrl.isEmpty) return '';
|
||||||
if (originalUrl.startsWith('http://')) {
|
if (originalUrl.startsWith('http://')) {
|
||||||
return '/api/xtream/$originalUrl';
|
return '/api/xtream/$originalUrl';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Appearance
|
// Appearance
|
||||||
_SectionHeader(title: 'Appearance'),
|
const _SectionHeader(title: 'Appearance'),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
@@ -151,7 +151,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Streaming Settings
|
// Streaming Settings
|
||||||
_SectionHeader(title: 'Streaming'),
|
const _SectionHeader(title: 'Streaming'),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
@@ -206,7 +206,7 @@ class _MobileSettingsTabState extends ConsumerState<MobileSettingsTab> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Content Filters
|
// Content Filters
|
||||||
_SectionHeader(title: 'Content Filters'),
|
const _SectionHeader(title: 'Content Filters'),
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
- [x] Analyze `Dockerfile` and `docker-compose.yml` dependencies.
|
- [x] Analyze `Dockerfile` and `docker-compose.yml` dependencies.
|
||||||
- [x] Attempt to reproduce the build failure or gather more logs.
|
- [x] Attempt to reproduce the build failure or gather more logs.
|
||||||
- [x] Fix the build issue.
|
- [x] Fix the build issue.
|
||||||
- [ ] Verify the fix.
|
- [x] Verify the fix.
|
||||||
|
|
||||||
# Audio Investigation in Movies (Paused)
|
# Audio Investigation in Movies (Paused)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user