creating first structure of cinema backend server

Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
2025-11-16 00:01:14 +01:00
parent 92da4423b2
commit a5d9372806
15 changed files with 362 additions and 58 deletions

View File

@@ -20,7 +20,7 @@ body:json {
"backgroundColor": "#000",
"format": "png",
"language": "de-DE",
"output": "lvgl_binary"
"output": "lvglBinary"
}
}

View File

@@ -7,3 +7,5 @@ build/
.gitignore
.idea/
.packages
*.g.dart
*.config.dart

View File

@@ -1,3 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
*.g.dart
*.freezed.dart
*.config.dart
*.log

View File

@@ -8,13 +8,17 @@ RUN dart pub get
# Copy app source code (except anything in .dockerignore) and AOT compile app.
COPY . .
RUN dart compile exe bin/server.dart -o bin/server
RUN dart run build_runner build --delete-conflicting-outputs
RUN APP_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml) && \
dart compile exe bin/server.dart -o bin/server \
-DAPP_VERSION=$APP_VERSION
# Build minimal serving image from AOT-compiled `/server`
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /app/bin/server /app/bin/
COPY assets /
# Start server.
EXPOSE 8080

8
server/cinema/Makefile Normal file
View File

@@ -0,0 +1,8 @@
build:
dart run build_runner build --delete-conflicting-outputs
watch:
dart run build_runner watch --delete-conflicting-outputs
docker:
docker build -t cinema-display .

View File

@@ -0,0 +1,5 @@
____ _ ____ _
/ ___(_)_ __ ___ _ __ ___ __ _ / ___| ___ _ ____ _(_) ___ ___
| | | | '_ \ / _ \ '_ ` _ \ / _` | \___ \ / _ \ '__\ \ / / |/ __/ _ \
| |___| | | | | __/ | | | | | (_| | ___) | __/ | \ V /| | (_| __/
\____|_|_| |_|\___|_| |_| |_|\__,_| |____/ \___|_| \_/ |_|\___\___|

View File

@@ -1,71 +1,44 @@
import 'dart:async';
import 'dart:io';
import 'package:cinema/feature/poster/poster.service.dart';
import 'package:cinema/feature/version/version.dart';
import 'package:cinema/feature/websocket/websocket.service.dart';
import 'package:cinema/injectable.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
final List<dynamic> clients = [];
// Configure routes.
final _router = Router()
..get('/', _rootHandler)
..get('/echo/<message>', _echoHandler);
Response _rootHandler(Request req) {
return Response.ok('Hello, World!\n');
}
Response _echoHandler(Request request) {
final message = request.params['message'];
return Response.ok('$message\n');
}
void main(List<String> args) async {
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
configureDependencies();
final router = Router();
router.mount("/poster", getIt<PosterService>().router.call);
// Configure a pipeline that logs requests.
final handler = Pipeline().addMiddleware(logRequests()).addHandler(_router.call);
var wsHandler = webSocketHandler((webSocket, _) {
clients.add(webSocket);
print('Client connected, total: ${clients.length}');
webSocket.stream.listen(
(message) {
webSocket.sink.add('echo $message');
},
onDone: () {
clients.remove(webSocket);
print('Client disconnected, total: ${clients.length}');
},
onError: (error) {
clients.remove(webSocket);
print('Client error: $error');
},
cancelOnError: true,
);
});
final handler = Pipeline().addMiddleware(logRequests()).addHandler(router.call);
FutureOr<Response> combinedHandler(Request request) {
if (request.url.path == 'ws') {
return wsHandler(request);
return getIt<WebSocketService>().handler(request);
}
return handler(request);
}
// Use any available host or container IP (usually `0.0.0.0`).
final ip = InternetAddress.anyIPv4;
// For running in containers, we respect the PORT environment variable.
final port = int.parse(Platform.environment['PORT'] ?? '8080');
final server = await shelf_io.serve(combinedHandler, ip, port).then((server) {
print('Serving at http|ws://${server.address.host}:${server.port}');
});
final port = int.parse(Platform.environment['PORT'] ?? '3000');
await io.serve(combinedHandler, ip, port, poweredByHeader: null).then((server) async {
final bannerFile = File('banner.txt');
if (await bannerFile.exists()) {
final banner = await bannerFile.readAsString();
print(banner);
}
// Broadcast to all clients
void broadcast(String message) {
for (final client in clients) {
client.sink.add(message);
}
getIt<Version>().printVersion();
print('Serving at ${server.address.host}:${server.port}');
});
}

View File

@@ -0,0 +1,16 @@
enum PosterOrientation {
horizontal,
vertical,
}
enum PosterFormat {
png,
jpeg,
bmp,
}
enum PosterOutput {
image,
lvgl,
lvglBinary,
}

View File

@@ -0,0 +1,30 @@
import 'package:cinema/feature/poster/models/poster.enums.dart';
import 'package:zard/zard.dart';
final _orientations = PosterOrientation.values.map((e) => e.name);
final _formats = PosterFormat.values.map((e) => e.name);
final _outputs = PosterOutput.values.map((e) => e.name);
final posterSchema = z.map({
'width': z.int().min(1, message: "'width' must be at least 1").max(4000, message: "'width' must be at most 4000"),
'height': z.int().min(1, message: "'height' must be at least 1").max(4000, message: "'height' must be a most 4000"),
'count': z.int().min(1, message: "'count' must be at least 1").max(20, message: "'count' must be at most 20"),
'orientation': z.string().refine(
(value) => _orientations.contains(value),
message: "'orientation' must be either ${_orientations.join(', ')}",
),
'shuffle': z.bool(message: "'shuffle' must be a boolean value"),
'language': z.string(message: "'language' must be a string").transform((value) => value.trim()),
'backgroundColor': z.string().regex(
RegExp(r'^#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})$'),
message: "The 'backgroundColor' must be a valid hexadecimal color code (e.g., #000 or #FF0000)",
),
'format': z.string().refine(
(value) => _formats.contains(value),
message: "'format' must be either ${_formats.join(', ')}",
),
'output': z.string().refine(
(value) => _outputs.contains(value),
message: "'output' must be either ${_outputs.join(', ')}",
),
});

View File

@@ -0,0 +1,37 @@
import 'dart:convert';
import 'package:cinema/feature/poster/models/poster_request.schema.dart';
import 'package:injectable/injectable.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
part 'poster.service.g.dart';
@injectable
class PosterService {
@Route.get('/')
Future<Response> listPosters(Request request) async {
return Response.ok('deprecated poster endpoint. use POST /poster instead');
}
@Route.post('/')
Future<Response> createPoster(Request request) async {
final payload = await request.readAsString();
final body = jsonDecode(payload);
final params = posterSchema.safeParse(body);
if (!params.success) {
return Response(
400,
body: jsonEncode({
'error': 'Invalid request',
'details': params.error?.messages,
}),
headers: {'Content-Type': 'application/json'},
);
}
return Response.ok(jsonEncode(params.data), headers: {'Content-Type': 'application/json'});
}
// Create router using the generate function defined in 'poster.g.dart'.
Router get router => _$PosterServiceRouter(this);
}

View File

@@ -0,0 +1,14 @@
import 'package:injectable/injectable.dart';
@injectable
class Version {
const Version();
String get appVersion => const String.fromEnvironment('APP_VERSION');
void printVersion() {
if (appVersion.isNotEmpty) {
print('App Version: $appVersion');
}
}
}

View File

@@ -0,0 +1,28 @@
import 'package:injectable/injectable.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_web_socket/shelf_web_socket.dart';
@LazySingleton()
class WebSocketService {
final List<dynamic> _clients = [];
Handler get handler => webSocketHandler((webSocket, _) {
_clients.add(webSocket);
print('Client connected, total: ${_clients.length}');
webSocket.stream.listen(
(message) {
webSocket.sink.add('echo $message');
},
onDone: () {
_clients.remove(webSocket);
print('Client disconnected, total: ${_clients.length}');
},
onError: (error) {
_clients.remove(webSocket);
print('Client error: $error');
},
cancelOnError: true,
);
});
}

View File

@@ -0,0 +1,9 @@
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final getIt = GetIt.instance;
@InjectableInit()
void configureDependencies() => getIt.init();

View File

@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e"
sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d
url: "https://pub.dev"
source: hosted
version: "92.0.0"
version: "91.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e"
sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "8.4.1"
archive:
dependency: transitive
description:
@@ -49,6 +49,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: dfb67ccc9a78c642193e0c2d94cb9e48c2c818b3178a86097d644acdcde6a8d9
url: "https://pub.dev"
source: hosted
version: "4.0.2"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "7b5b569f3df370590a85029148d6fc66c7d0201fc6f1847c07dd85d365ae9fcd"
url: "https://pub.dev"
source: hosted
version: "2.10.3"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
url: "https://pub.dev"
source: hosted
version: "8.12.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_config:
dependency: transitive
description:
@@ -57,6 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.11.0"
collection:
dependency: transitive
description:
@@ -89,6 +153,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b
url: "https://pub.dev"
source: hosted
version: "3.1.3"
ffi:
dependency: transitive
description:
@@ -105,6 +177,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
@@ -113,6 +193,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: "84792561b731b6463d053e9761a5236da967c369da10b134b8585a5e18429956"
url: "https://pub.dev"
source: hosted
version: "9.0.5"
glob:
dependency: transitive
description:
@@ -121,6 +209,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http:
dependency: "direct dev"
description:
@@ -161,6 +257,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
injectable:
dependency: "direct main"
description:
name: injectable
sha256: "29559f7e3daebf0084597de86a825ae7f149d9e30264b7fbc71d1069ae82697d"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
injectable_generator:
dependency: "direct dev"
description:
name: injectable_generator
sha256: "309c3f3546160dd00b575f16b341a6a3025479950441bcc7fcb2f8404a40d326"
url: "https://pub.dev"
source: hosted
version: "2.9.1"
io:
dependency: transitive
description:
@@ -177,6 +289,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
lints:
dependency: "direct dev"
description:
@@ -273,6 +393,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
shelf:
dependency: "direct main"
description:
@@ -297,6 +433,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.4"
shelf_router_generator:
dependency: "direct dev"
description:
name: shelf_router_generator
sha256: "310416e0eb5a96c8b27f2586367f07b09dc480af06485c1f7951bbed1e8b8b08"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_static:
dependency: transitive
description:
@@ -313,6 +457,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "9098ab86015c4f1d8af6486b547b11100e73b193e1899015033cb3e14ad20243"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
source_map_stack_trace:
dependency: transitive
description:
@@ -353,6 +505,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -465,5 +625,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.3"
zard:
dependency: "direct main"
description:
name: zard
sha256: "772fc9ef6088123fefaaa88cb986253f0e838aec2af2c3b956a9a1c98ea2b049"
url: "https://pub.dev"
source: hosted
version: "0.0.23"
sdks:
dart: ">=3.9.0 <4.0.0"

View File

@@ -7,12 +7,18 @@ environment:
sdk: ^3.9.0
dependencies:
get_it: ^9.0.5
image: ^4.5.4
injectable: ^2.6.0
shelf: ^1.4.2
shelf_router: ^1.1.2
shelf_web_socket: ^3.0.0
zard: ^0.0.23
dev_dependencies:
build_runner: ^2.10.3
http: ^1.2.2
injectable_generator: ^2.9.1
lints: ^6.0.0
shelf_router_generator: ^1.1.3
test: ^1.25.6