From a5d9372806a53b0e43d5fbb9684d934ab4d4570c Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Sun, 16 Nov 2025 00:01:14 +0100 Subject: [PATCH] creating first structure of cinema backend server Signed-off-by: Peter Siegmund --- server/bruno/posters.bru | 2 +- server/cinema/.dockerignore | 2 + server/cinema/.gitignore | 4 + server/cinema/Dockerfile | 6 +- server/cinema/Makefile | 8 + server/cinema/assets/banner.txt | 5 + server/cinema/bin/server.dart | 77 +++----- .../feature/poster/models/poster.enums.dart | 16 ++ .../poster/models/poster_request.schema.dart | 30 +++ .../lib/feature/poster/poster.service.dart | 37 ++++ .../cinema/lib/feature/version/version.dart | 14 ++ .../feature/websocket/websocket.service.dart | 28 +++ server/cinema/lib/injectable.dart | 9 + server/cinema/pubspec.lock | 176 +++++++++++++++++- server/cinema/pubspec.yaml | 6 + 15 files changed, 362 insertions(+), 58 deletions(-) create mode 100644 server/cinema/Makefile create mode 100644 server/cinema/assets/banner.txt create mode 100644 server/cinema/lib/feature/poster/models/poster.enums.dart create mode 100644 server/cinema/lib/feature/poster/models/poster_request.schema.dart create mode 100644 server/cinema/lib/feature/poster/poster.service.dart create mode 100644 server/cinema/lib/feature/version/version.dart create mode 100644 server/cinema/lib/feature/websocket/websocket.service.dart create mode 100644 server/cinema/lib/injectable.dart diff --git a/server/bruno/posters.bru b/server/bruno/posters.bru index b8cfcce..3356008 100644 --- a/server/bruno/posters.bru +++ b/server/bruno/posters.bru @@ -20,7 +20,7 @@ body:json { "backgroundColor": "#000", "format": "png", "language": "de-DE", - "output": "lvgl_binary" + "output": "lvglBinary" } } diff --git a/server/cinema/.dockerignore b/server/cinema/.dockerignore index 21504f8..e9bab56 100644 --- a/server/cinema/.dockerignore +++ b/server/cinema/.dockerignore @@ -7,3 +7,5 @@ build/ .gitignore .idea/ .packages +*.g.dart +*.config.dart diff --git a/server/cinema/.gitignore b/server/cinema/.gitignore index 3a85790..71338e8 100644 --- a/server/cinema/.gitignore +++ b/server/cinema/.gitignore @@ -1,3 +1,7 @@ # https://dart.dev/guides/libraries/private-files # Created by `dart pub` .dart_tool/ +*.g.dart +*.freezed.dart +*.config.dart +*.log diff --git a/server/cinema/Dockerfile b/server/cinema/Dockerfile index c333dee..f911bee 100644 --- a/server/cinema/Dockerfile +++ b/server/cinema/Dockerfile @@ -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 diff --git a/server/cinema/Makefile b/server/cinema/Makefile new file mode 100644 index 0000000..934bf7e --- /dev/null +++ b/server/cinema/Makefile @@ -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 . diff --git a/server/cinema/assets/banner.txt b/server/cinema/assets/banner.txt new file mode 100644 index 0000000..530f577 --- /dev/null +++ b/server/cinema/assets/banner.txt @@ -0,0 +1,5 @@ + ____ _ ____ _ + / ___(_)_ __ ___ _ __ ___ __ _ / ___| ___ _ ____ _(_) ___ ___ + | | | | '_ \ / _ \ '_ ` _ \ / _` | \___ \ / _ \ '__\ \ / / |/ __/ _ \ + | |___| | | | | __/ | | | | | (_| | ___) | __/ | \ V /| | (_| __/ + \____|_|_| |_|\___|_| |_| |_|\__,_| |____/ \___|_| \_/ |_|\___\___| diff --git a/server/cinema/bin/server.dart b/server/cinema/bin/server.dart index c586d74..576bcc1 100644 --- a/server/cinema/bin/server.dart +++ b/server/cinema/bin/server.dart @@ -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 clients = []; - -// Configure routes. -final _router = Router() - ..get('/', _rootHandler) - ..get('/echo/', _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 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().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 combinedHandler(Request request) { if (request.url.path == 'ws') { - return wsHandler(request); + return getIt().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); + } + + getIt().printVersion(); + + print('Serving at ${server.address.host}:${server.port}'); }); } - -// Broadcast to all clients -void broadcast(String message) { - for (final client in clients) { - client.sink.add(message); - } -} diff --git a/server/cinema/lib/feature/poster/models/poster.enums.dart b/server/cinema/lib/feature/poster/models/poster.enums.dart new file mode 100644 index 0000000..640a801 --- /dev/null +++ b/server/cinema/lib/feature/poster/models/poster.enums.dart @@ -0,0 +1,16 @@ +enum PosterOrientation { + horizontal, + vertical, +} + +enum PosterFormat { + png, + jpeg, + bmp, +} + +enum PosterOutput { + image, + lvgl, + lvglBinary, +} diff --git a/server/cinema/lib/feature/poster/models/poster_request.schema.dart b/server/cinema/lib/feature/poster/models/poster_request.schema.dart new file mode 100644 index 0000000..d0b5a6a --- /dev/null +++ b/server/cinema/lib/feature/poster/models/poster_request.schema.dart @@ -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(', ')}", + ), +}); diff --git a/server/cinema/lib/feature/poster/poster.service.dart b/server/cinema/lib/feature/poster/poster.service.dart new file mode 100644 index 0000000..956517b --- /dev/null +++ b/server/cinema/lib/feature/poster/poster.service.dart @@ -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 listPosters(Request request) async { + return Response.ok('deprecated poster endpoint. use POST /poster instead'); + } + + @Route.post('/') + Future 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); +} diff --git a/server/cinema/lib/feature/version/version.dart b/server/cinema/lib/feature/version/version.dart new file mode 100644 index 0000000..fe5ee80 --- /dev/null +++ b/server/cinema/lib/feature/version/version.dart @@ -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'); + } + } +} diff --git a/server/cinema/lib/feature/websocket/websocket.service.dart b/server/cinema/lib/feature/websocket/websocket.service.dart new file mode 100644 index 0000000..b6ebdf8 --- /dev/null +++ b/server/cinema/lib/feature/websocket/websocket.service.dart @@ -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 _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, + ); + }); +} diff --git a/server/cinema/lib/injectable.dart b/server/cinema/lib/injectable.dart new file mode 100644 index 0000000..5efda29 --- /dev/null +++ b/server/cinema/lib/injectable.dart @@ -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(); diff --git a/server/cinema/pubspec.lock b/server/cinema/pubspec.lock index 932768d..f4ef752 100644 --- a/server/cinema/pubspec.lock +++ b/server/cinema/pubspec.lock @@ -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" diff --git a/server/cinema/pubspec.yaml b/server/cinema/pubspec.yaml index bc60d1f..d3228c7 100644 --- a/server/cinema/pubspec.yaml +++ b/server/cinema/pubspec.yaml @@ -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