load trending images from tmdb at startup
- need TMDB_API_KEY as environment variable - mount /cache for data persistence Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
1
server/cinema/.gitignore
vendored
1
server/cinema/.gitignore
vendored
@@ -5,3 +5,4 @@
|
|||||||
*.freezed.dart
|
*.freezed.dart
|
||||||
*.config.dart
|
*.config.dart
|
||||||
*.log
|
*.log
|
||||||
|
assets/cache
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ RUN dart pub get
|
|||||||
|
|
||||||
# Copy app source code (except anything in .dockerignore) and AOT compile app.
|
# Copy app source code (except anything in .dockerignore) and AOT compile app.
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dart run build_runner build --delete-conflicting-outputs
|
|
||||||
RUN APP_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml) && \
|
RUN APP_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml) && \
|
||||||
dart compile exe bin/server.dart -o bin/server \
|
dart compile exe bin/server.dart -o bin/server \
|
||||||
-DAPP_VERSION=$APP_VERSION
|
-DAPP_VERSION=$APP_VERSION
|
||||||
@@ -21,5 +20,5 @@ COPY --from=build /app/bin/server /app/bin/
|
|||||||
COPY assets /
|
COPY assets /
|
||||||
|
|
||||||
# Start server.
|
# Start server.
|
||||||
EXPOSE 8080
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/server"]
|
CMD ["/app/bin/server"]
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ build:
|
|||||||
watch:
|
watch:
|
||||||
dart run build_runner watch --delete-conflicting-outputs
|
dart run build_runner watch --delete-conflicting-outputs
|
||||||
|
|
||||||
docker:
|
docker: build
|
||||||
docker build -t cinema-display .
|
docker build -t cinema-display .
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cinema/feature/poster/poster.service.dart';
|
import 'package:cinema/feature/poster/data/repositories/image_loader.dart';
|
||||||
import 'package:cinema/feature/root/root.service.dart';
|
import 'package:cinema/feature/poster/data/services/poster.service.dart';
|
||||||
|
import 'package:cinema/feature/root/data/service/root.service.dart';
|
||||||
import 'package:cinema/feature/version/version.dart';
|
import 'package:cinema/feature/version/version.dart';
|
||||||
import 'package:cinema/injectable.dart';
|
import 'package:cinema/injectable.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
@@ -32,6 +33,14 @@ void main(List<String> args) async {
|
|||||||
print(banner);
|
print(banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('Caching current trending images...');
|
||||||
|
|
||||||
|
final ImageLoader loader = getIt();
|
||||||
|
final movies = await loader.getPosterURIs();
|
||||||
|
for (var movie in movies) {
|
||||||
|
await loader.downloadImages(movie);
|
||||||
|
}
|
||||||
|
|
||||||
getIt<Version>().printVersion();
|
getIt<Version>().printVersion();
|
||||||
|
|
||||||
print('Serving at ${server.address.host}:${server.port}\n');
|
print('Serving at ${server.address.host}:${server.port}\n');
|
||||||
|
|||||||
39
server/cinema/lib/common/dio_module.dart
Normal file
39
server/cinema/lib/common/dio_module.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cinema/feature/version/version.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
const dioAPI = 'api';
|
||||||
|
const dioIMAGES = 'images';
|
||||||
|
|
||||||
|
@module
|
||||||
|
abstract class DioModule {
|
||||||
|
@Named(dioAPI)
|
||||||
|
@lazySingleton
|
||||||
|
Dio get apiDio =>
|
||||||
|
Dio( // Der Getter selbst wird annotiert und gibt die Instanz zurück
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: 'https://api.themoviedb.org/3',
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ${Platform.environment['TMDB_API_KEY']}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'Cinema Service (v${Version().appVersion})'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@Named(dioIMAGES)
|
||||||
|
@lazySingleton
|
||||||
|
Dio get imagesDio =>
|
||||||
|
Dio( // Der Getter selbst wird annotiert und gibt die Instanz zurück
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: 'https://image.tmdb.org/t/p/original',
|
||||||
|
connectTimeout: const Duration(seconds: 10),
|
||||||
|
receiveTimeout: const Duration(seconds: 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:built_value/serializer.dart';
|
||||||
|
|
||||||
|
/// A custom serializer for [DateTime] objects that are represented as
|
||||||
|
/// a simple date string "yyyy-MM-dd" in JSON.
|
||||||
|
class CustomDateTimeSerializer implements PrimitiveSerializer<DateTime> {
|
||||||
|
@override
|
||||||
|
final Iterable<Type> types = const [DateTime];
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String wireName = 'DateTime';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object serialize(Serializers serializers, DateTime dateTime,
|
||||||
|
{FullType specifiedType = FullType.unspecified}) {
|
||||||
|
// On serialization, convert DateTime to a "yyyy-MM-dd" string.
|
||||||
|
return dateTime.toIso8601String().substring(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime deserialize(Serializers serializers, Object serialized,
|
||||||
|
{FullType specifiedType = FullType.unspecified}) {
|
||||||
|
// On deserialization, parse the string to a DateTime object.
|
||||||
|
// This handles formats like "2025-09-23".
|
||||||
|
final parts = (serialized as String).split('-');
|
||||||
|
final dateUtc = DateTime.utc(
|
||||||
|
int.parse(parts[0]),
|
||||||
|
int.parse(parts[1]),
|
||||||
|
int.parse(parts[2]),
|
||||||
|
);
|
||||||
|
return dateUtc;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
server/cinema/lib/common/domain/serializers.dart
Normal file
20
server/cinema/lib/common/domain/serializers.dart
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:built_collection/built_collection.dart';
|
||||||
|
import 'package:built_value/serializer.dart';
|
||||||
|
import 'package:built_value/standard_json_plugin.dart';
|
||||||
|
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||||
|
import 'package:cinema/feature/poster/domain/tmdb_trending_response.dart';
|
||||||
|
|
||||||
|
import 'custom_date_time_serializer.dart';
|
||||||
|
|
||||||
|
part 'serializers.g.dart';
|
||||||
|
|
||||||
|
@SerializersFor([TmdbTrendingResponse, TmdbMovieResult])
|
||||||
|
final Serializers serializers =
|
||||||
|
(_$serializers.toBuilder()
|
||||||
|
..add(CustomDateTimeSerializer())
|
||||||
|
..addPlugin(StandardJsonPlugin())
|
||||||
|
..addBuilderFactory(
|
||||||
|
const FullType(BuiltList, [FullType(TmdbMovieResult), FullType(Movie)]),
|
||||||
|
() => ListBuilder<TmdbMovieResult>(),
|
||||||
|
))
|
||||||
|
.build();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||||
|
|
||||||
|
abstract class ImageLoader {
|
||||||
|
Future<List<Movie>> getPosterURIs({String? language = 'de'});
|
||||||
|
|
||||||
|
Future<bool> downloadImages(Movie movie);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cinema/common/dio_module.dart';
|
||||||
|
import 'package:cinema/common/domain/serializers.dart';
|
||||||
|
import 'package:cinema/feature/poster/data/repositories/image_loader.dart';
|
||||||
|
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||||
|
import 'package:cinema/feature/poster/domain/tmdb_trending_response.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:injectable/injectable.dart';
|
||||||
|
|
||||||
|
@Injectable(as: ImageLoader)
|
||||||
|
class TmDBImageLoader implements ImageLoader {
|
||||||
|
final Dio api;
|
||||||
|
final Dio images;
|
||||||
|
final String imageBaseUrl = 'https://image.tmdb.org/t/p/w500';
|
||||||
|
|
||||||
|
TmDBImageLoader(@Named(dioAPI) this.api, @Named(dioIMAGES) this.images);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Movie>> getPosterURIs({String? language = 'de'}) async {
|
||||||
|
final response = await api.get('/trending/movie/week?language=$language');
|
||||||
|
if (response.data != null) {
|
||||||
|
final data = serializers.deserializeWith(TmdbTrendingResponse.serializer, response.data);
|
||||||
|
return data?.results
|
||||||
|
.map(
|
||||||
|
(movie) => Movie(
|
||||||
|
(b) => b
|
||||||
|
..id = movie.id
|
||||||
|
..poster = movie.posterPath
|
||||||
|
..backdrop = movie.backdropPath,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> downloadImages(Movie movie) async {
|
||||||
|
await downloadImage(movie.poster, "cache/movie/${movie.id}/poster.png");
|
||||||
|
await downloadImage(movie.backdrop, "cache/movie/${movie.id}/backdrop.png");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> downloadImage(String url, String filename) async {
|
||||||
|
final file = File(filename);
|
||||||
|
if (await file.exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
final response = await images.get(url, options: Options(responseType: ResponseType.bytes));
|
||||||
|
await file.writeAsBytes(response.data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:cinema/feature/poster/models/poster_request.schema.dart';
|
import 'package:cinema/feature/poster/domain/poster_request.schema.dart';
|
||||||
import 'package:injectable/injectable.dart';
|
import 'package:injectable/injectable.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
18
server/cinema/lib/feature/poster/domain/movie.dart
Normal file
18
server/cinema/lib/feature/poster/domain/movie.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:built_value/built_value.dart';
|
||||||
|
import 'package:built_value/serializer.dart';
|
||||||
|
|
||||||
|
part 'movie.g.dart';
|
||||||
|
|
||||||
|
abstract class Movie implements Built<Movie, MovieBuilder> {
|
||||||
|
static Serializer<Movie> get serializer => _$movieSerializer;
|
||||||
|
|
||||||
|
int get id;
|
||||||
|
|
||||||
|
String get poster;
|
||||||
|
|
||||||
|
String get backdrop;
|
||||||
|
|
||||||
|
Movie._();
|
||||||
|
|
||||||
|
factory Movie([void Function(MovieBuilder) updates]) = _$Movie;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:cinema/feature/poster/models/poster.enums.dart';
|
import 'package:cinema/feature/poster/domain/poster.enums.dart';
|
||||||
import 'package:zard/zard.dart';
|
import 'package:zard/zard.dart';
|
||||||
|
|
||||||
final _orientations = PosterOrientation.values.map((e) => e.name);
|
final _orientations = PosterOrientation.values.map((e) => e.name);
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:built_collection/built_collection.dart';
|
||||||
|
import 'package:built_value/built_value.dart';
|
||||||
|
import 'package:built_value/serializer.dart';
|
||||||
|
|
||||||
|
part 'tmdb_trending_response.g.dart';
|
||||||
|
|
||||||
|
abstract class TmdbTrendingResponse implements Built<TmdbTrendingResponse, TmdbTrendingResponseBuilder> {
|
||||||
|
static Serializer<TmdbTrendingResponse> get serializer => _$tmdbTrendingResponseSerializer;
|
||||||
|
|
||||||
|
int get page;
|
||||||
|
|
||||||
|
BuiltList<TmdbMovieResult> get results;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'total_pages')
|
||||||
|
int get totalPages;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'total_results')
|
||||||
|
int get totalResults;
|
||||||
|
|
||||||
|
TmdbTrendingResponse._();
|
||||||
|
|
||||||
|
factory TmdbTrendingResponse([void Function(TmdbTrendingResponseBuilder) updates]) = _$TmdbTrendingResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class TmdbMovieResult implements Built<TmdbMovieResult, TmdbMovieResultBuilder> {
|
||||||
|
static Serializer<TmdbMovieResult> get serializer => _$tmdbMovieResultSerializer;
|
||||||
|
|
||||||
|
bool get adult;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'backdrop_path')
|
||||||
|
String get backdropPath;
|
||||||
|
|
||||||
|
int get id;
|
||||||
|
|
||||||
|
String get title;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'original_title')
|
||||||
|
String get originalTitle;
|
||||||
|
|
||||||
|
String get overview;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'poster_path')
|
||||||
|
String get posterPath;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'media_type')
|
||||||
|
String get mediaType;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'genre_ids')
|
||||||
|
BuiltList<int> get genreIds;
|
||||||
|
|
||||||
|
double get popularity;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'release_date')
|
||||||
|
DateTime get releaseDate;
|
||||||
|
|
||||||
|
bool get video;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'vote_average')
|
||||||
|
double get voteAverage;
|
||||||
|
|
||||||
|
@BuiltValueField(wireName: 'vote_count')
|
||||||
|
int get voteCount;
|
||||||
|
|
||||||
|
TmdbMovieResult._();
|
||||||
|
|
||||||
|
factory TmdbMovieResult([void Function(TmdbMovieResultBuilder) updates]) = _$TmdbMovieResult;
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.3"
|
version: "2.10.3"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: built_collection
|
name: built_collection
|
||||||
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
|
||||||
@@ -90,13 +90,21 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
built_value:
|
built_value:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.0"
|
version: "8.12.0"
|
||||||
|
built_value_generator:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: built_value_generator
|
||||||
|
sha256: "65f5823a2c4158384ebc845218e19286fdf5dd04f8ac2cf607b01a502be40b1b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "8.12.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -161,6 +169,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
|
dio:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: dio
|
||||||
|
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.9.0"
|
||||||
|
dio_web_adapter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dio_web_adapter
|
||||||
|
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -290,13 +314,21 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.2"
|
version: "0.7.2"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.9.0"
|
||||||
|
json_serializable:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: json_serializable
|
||||||
|
sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.11.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -465,6 +497,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
source_helper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_helper
|
||||||
|
sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.8"
|
||||||
source_map_stack_trace:
|
source_map_stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ environment:
|
|||||||
sdk: ^3.9.0
|
sdk: ^3.9.0
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
built_collection: ^5.1.1
|
||||||
|
built_value: ^8.9.2
|
||||||
|
dio: ^5.9.0
|
||||||
get_it: ^9.0.5
|
get_it: ^9.0.5
|
||||||
image: ^4.5.4
|
image: ^4.5.4
|
||||||
injectable: ^2.6.0
|
injectable: ^2.6.0
|
||||||
@@ -17,6 +20,7 @@ dependencies:
|
|||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.10.3
|
build_runner: ^2.10.3
|
||||||
|
built_value_generator: ^8.9.2
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
injectable_generator: ^2.9.1
|
injectable_generator: ^2.9.1
|
||||||
lints: ^6.0.0
|
lints: ^6.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user