From 551226ead9f29444f79a21c1e2af24398f345eb3 Mon Sep 17 00:00:00 2001 From: Peter Siegmund Date: Sun, 1 Feb 2026 11:39:02 +0100 Subject: [PATCH] implement video download needs check, if converted files can be run on device Signed-off-by: Peter Siegmund --- server/bruno/video.bru | 15 +++ server/cinema/CHANGELOG.md | 4 + server/cinema/Dockerfile | 25 +++- server/cinema/bin/server.dart | 25 +++- .../cinema/lib/common/video_downloader.dart | 113 ++++++++++++++++++ .../data/repositories/image_loader.dart | 2 +- .../data/repositories/tmdb_image_loader.dart | 2 +- .../data/repositories/tmdb_video_loader.dart | 77 ++++++++++++ .../video/data/repositories/video_loader.dart | 7 ++ .../video/data/services/video.service.dart | 75 ++++++++++++ server/cinema/pubspec.lock | 88 ++++++++------ server/cinema/pubspec.yaml | 2 +- server/cinema/scripts/start.sh | 5 + 13 files changed, 392 insertions(+), 48 deletions(-) create mode 100644 server/bruno/video.bru create mode 100644 server/cinema/lib/common/video_downloader.dart create mode 100644 server/cinema/lib/feature/video/data/repositories/tmdb_video_loader.dart create mode 100644 server/cinema/lib/feature/video/data/repositories/video_loader.dart create mode 100644 server/cinema/lib/feature/video/data/services/video.service.dart create mode 100644 server/cinema/scripts/start.sh diff --git a/server/bruno/video.bru b/server/bruno/video.bru new file mode 100644 index 0000000..746b377 --- /dev/null +++ b/server/bruno/video.bru @@ -0,0 +1,15 @@ +meta { + name: video + type: http + seq: 2 +} + +get { + url: {{base_uri}}/video/OSfu4ST3dlY + body: none + auth: inherit +} + +settings { + encodeUrl: true +} diff --git a/server/cinema/CHANGELOG.md b/server/cinema/CHANGELOG.md index effe43c..f7250db 100644 --- a/server/cinema/CHANGELOG.md +++ b/server/cinema/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.0 + +- added video download + ## 1.0.0 - Initial version. diff --git a/server/cinema/Dockerfile b/server/cinema/Dockerfile index 97fda37..d0318db 100644 --- a/server/cinema/Dockerfile +++ b/server/cinema/Dockerfile @@ -13,13 +13,28 @@ RUN dart run build_runner build --delete-conflicting-outputs && \ 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/ / +# Build minimal serving image with ffmpeg and yt-dlp +FROM debian:stable-slim + +# Install ffmpeg, python3, nodejs (for yt-dlp JS runtime), and download yt-dlp +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg \ + ca-certificates \ + curl \ + python3 \ + nodejs \ + && curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp \ + && chmod a+rx /usr/local/bin/yt-dlp \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + COPY --from=build /app/bin/server /app/bin/ COPY assets /assets +COPY scripts/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +WORKDIR /app # Start server. EXPOSE 3000 -CMD ["/app/bin/server"] +CMD ["/bin/sh", "/app/start.sh"] diff --git a/server/cinema/bin/server.dart b/server/cinema/bin/server.dart index 02e197e..6135ba7 100644 --- a/server/cinema/bin/server.dart +++ b/server/cinema/bin/server.dart @@ -7,6 +7,8 @@ import 'package:cinema/feature/poster/data/repositories/image_loader.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/video/data/repositories/video_loader.dart'; +import 'package:cinema/feature/video/data/services/video.service.dart'; import 'package:cinema/injectable.dart'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; @@ -21,6 +23,7 @@ void main(List args) async { final router = Router(); router.mount("/poster", getIt().router.call); + router.mount("/video", getIt().router.call); router.mount("/", getIt().router.call); /// add middlewares (Logging, CORS) @@ -36,11 +39,25 @@ void main(List args) async { } print('Caching current trending images...'); - - final ImageLoader loader = getIt(); - final movies = await loader.getPosterURIs(); + final ImageLoader imageLoader = getIt(); + final movies = await imageLoader.getMovies(); for (var movie in movies) { - await loader.downloadImages(movie); + await imageLoader.downloadImages(movie); + } + + // Cache videos in background (non-blocking) + print('Starting video caching in background...'); + final VideoLoader videoLoader = getIt(); + for (var movie in movies) { + // Jedes Video läuft in einem eigenen async "Thread" (Future) + // ohne await, damit der Server-Start nicht blockiert wird + videoLoader.downloadVideo(movie).then((success) { + if (success) { + print('Background video cache complete: ${movie.title}'); + } + }).catchError((e) { + print('Background video cache error for ${movie.title}: $e'); + }); } getIt().printVersion(); diff --git a/server/cinema/lib/common/video_downloader.dart b/server/cinema/lib/common/video_downloader.dart new file mode 100644 index 0000000..56b0ed9 --- /dev/null +++ b/server/cinema/lib/common/video_downloader.dart @@ -0,0 +1,113 @@ +import 'dart:io'; + +import 'package:injectable/injectable.dart'; + +@singleton +class VideoDownloader { + static const cacheDir = 'cache/video'; + static const int maxWidth = 480; + static const int maxHeight = 320; + + /// Downloads and resizes a YouTube video + /// Returns the path to the resized video file, or null on failure + Future downloadAndResize(String videoId) async { + final videoDir = Directory('$cacheDir/$videoId'); + final originalVideoFile = File('${videoDir.path}/video_original.mp4'); + final resizedVideoFile = File('${videoDir.path}/video.mp4'); + + // Check if resized video already exists in cache + if (await resizedVideoFile.exists()) { + return resizedVideoFile.path; + } + + // Create cache directory if it doesn't exist + if (!await videoDir.exists()) { + await videoDir.create(recursive: true); + } + + final youtubeUrl = 'https://www.youtube.com/watch?v=$videoId'; + + try { + // Use yt-dlp to download the video + final downloadResult = await Process.run( + 'yt-dlp', + [ + '--js-runtimes', 'node', + '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', + '-o', originalVideoFile.path, + '--merge-output-format', 'mp4', + youtubeUrl, + ], + ); + + if (downloadResult.exitCode != 0) { + print('Failed to download video $videoId: ${downloadResult.stderr}'); + await _cleanup(videoDir); + return null; + } + + // Resize video to fit within maxWidth x maxHeight using ffmpeg + final resizeResult = await Process.run( + 'ffmpeg', + [ + '-i', originalVideoFile.path, + '-vf', 'scale=w=$maxWidth:h=$maxHeight:force_original_aspect_ratio=decrease', + '-c:v', 'libx264', + '-preset', 'medium', + '-crf', '23', + '-c:a', 'aac', + '-b:a', '128k', + '-movflags', '+faststart', + '-y', + resizedVideoFile.path, + ], + ); + + if (resizeResult.exitCode != 0) { + print('Failed to resize video $videoId: ${resizeResult.stderr}'); + await _cleanup(videoDir); + return null; + } + + // Delete original video to save space + if (await originalVideoFile.exists()) { + await originalVideoFile.delete(); + } + + return resizedVideoFile.path; + } catch (e) { + print('Error processing video $videoId: $e'); + await _cleanup(videoDir); + return null; + } + } + + /// Check if video is already cached + Future isCached(String videoId) async { + final resizedVideoFile = File('$cacheDir/$videoId/video.mp4'); + return resizedVideoFile.exists(); + } + + /// Get all cached video IDs + Future> getCachedVideos() async { + final dir = Directory(cacheDir); + if (!await dir.exists()) { + return []; + } + + final videos = []; + await for (final entity in dir.list()) { + if (entity is Directory) { + final videoId = entity.path.split('/').last; + videos.add(videoId); + } + } + return videos; + } + + Future _cleanup(Directory videoDir) async { + if (await videoDir.exists()) { + await videoDir.delete(recursive: true); + } + } +} diff --git a/server/cinema/lib/feature/poster/data/repositories/image_loader.dart b/server/cinema/lib/feature/poster/data/repositories/image_loader.dart index 4640210..382503e 100644 --- a/server/cinema/lib/feature/poster/data/repositories/image_loader.dart +++ b/server/cinema/lib/feature/poster/data/repositories/image_loader.dart @@ -1,7 +1,7 @@ import 'package:cinema/feature/poster/domain/movie.dart'; abstract class ImageLoader { - Future> getPosterURIs({String? language = 'de'}); + Future> getMovies({String? language = 'de'}); Future downloadImages(Movie movie); } diff --git a/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart b/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart index 7a435f9..0b9f469 100644 --- a/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart +++ b/server/cinema/lib/feature/poster/data/repositories/tmdb_image_loader.dart @@ -23,7 +23,7 @@ class TmDBImageLoader implements ImageLoader { ); @override - Future> getPosterURIs({String? language = 'de'}) async { + Future> getMovies({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); diff --git a/server/cinema/lib/feature/video/data/repositories/tmdb_video_loader.dart b/server/cinema/lib/feature/video/data/repositories/tmdb_video_loader.dart new file mode 100644 index 0000000..eefa30d --- /dev/null +++ b/server/cinema/lib/feature/video/data/repositories/tmdb_video_loader.dart @@ -0,0 +1,77 @@ +import 'package:cinema/common/dio_module.dart'; +import 'package:cinema/common/video_downloader.dart'; +import 'package:cinema/feature/poster/domain/movie.dart'; +import 'package:cinema/feature/video/data/repositories/video_loader.dart'; +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; + +@Injectable(as: VideoLoader) +class TmdbVideoLoader implements VideoLoader { + final Dio api; + final VideoDownloader _videoDownloader; + + TmdbVideoLoader(@Named(dioAPI) this.api, this._videoDownloader); + + @override + Future getYouTubeVideoId(int movieId, {String language = 'de'}) async { + try { + final response = await api.get('/movie/$movieId/videos?language=$language'); + if (response.data != null && response.data['results'] != null) { + final results = response.data['results'] as List; + + // Filtere nur YouTube Videos + final youtubeVideos = results.where((v) => v['site'] == 'YouTube').toList(); + if (youtubeVideos.isEmpty) { + return null; + } + + // Sortiere nach published_at absteigend (neueste zuerst) + youtubeVideos.sort((a, b) { + final dateA = DateTime.tryParse(a['published_at'] ?? '') ?? DateTime(1970); + final dateB = DateTime.tryParse(b['published_at'] ?? '') ?? DateTime(1970); + return dateB.compareTo(dateA); + }); + + // Suche den neuesten Trailer oder Teaser + for (final video in youtubeVideos) { + if (video['type'] == 'Trailer' || video['type'] == 'Teaser') { + return video['key'] as String?; + } + } + + // Falls kein Trailer/Teaser, nimm das neueste YouTube Video + return youtubeVideos.first['key'] as String?; + } + } catch (e) { + print('Error fetching video for movie $movieId: $e'); + } + return null; + } + + @override + Future downloadVideo(Movie movie) async { + + final videoId = await getYouTubeVideoId(movie.id); + if (videoId == null) { + print('No YouTube video found for movie ${movie.id} (${movie.title})'); + return false; + } + + // Check if already cached + if (await _videoDownloader.isCached(videoId)) { + print('Video already cached: ${movie.title} ($videoId)'); + return true; + } + + print('Downloading video: ${movie.title} ($videoId)...'); + final path = await _videoDownloader.downloadAndResize(videoId); + + if (path != null) { + print('Video cached successfully: ${movie.title} ($videoId)'); + return true; + } + + print('Failed to cache video: ${movie.title} ($videoId)'); + return false; + } +} diff --git a/server/cinema/lib/feature/video/data/repositories/video_loader.dart b/server/cinema/lib/feature/video/data/repositories/video_loader.dart new file mode 100644 index 0000000..96adf19 --- /dev/null +++ b/server/cinema/lib/feature/video/data/repositories/video_loader.dart @@ -0,0 +1,7 @@ +import 'package:cinema/feature/poster/domain/movie.dart'; + +abstract class VideoLoader { + Future getYouTubeVideoId(int movieId, {String language = 'de'}); + + Future downloadVideo(Movie movie); +} diff --git a/server/cinema/lib/feature/video/data/services/video.service.dart b/server/cinema/lib/feature/video/data/services/video.service.dart new file mode 100644 index 0000000..fdaff1b --- /dev/null +++ b/server/cinema/lib/feature/video/data/services/video.service.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:cinema/common/video_downloader.dart'; +import 'package:injectable/injectable.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_router/shelf_router.dart'; + +part 'video.service.g.dart'; + +@injectable +class VideoService { + final VideoDownloader _videoDownloader; + + VideoService(this._videoDownloader); + + @Route.get('/') + Future downloadVideo(Request request, String videoId) async { + // Validate video ID (YouTube IDs are 11 characters, alphanumeric with - and _) + if (!RegExp(r'^[a-zA-Z0-9_-]{11}$').hasMatch(videoId)) { + return Response( + 400, + body: jsonEncode({'error': 'Invalid YouTube video ID'}), + headers: {'Content-Type': 'application/json'}, + ); + } + + // Check if already cached + if (await _videoDownloader.isCached(videoId)) { + return Response.ok( + jsonEncode({ + 'status': 'cached', + 'videoId': videoId, + 'path': '${VideoDownloader.cacheDir}/$videoId/video.mp4', + }), + headers: {'Content-Type': 'application/json'}, + ); + } + + // Download and resize + final path = await _videoDownloader.downloadAndResize(videoId); + + if (path == null) { + return Response( + 500, + body: jsonEncode({ + 'error': 'Failed to download or resize video', + 'videoId': videoId, + }), + headers: {'Content-Type': 'application/json'}, + ); + } + + return Response.ok( + jsonEncode({ + 'status': 'downloaded', + 'videoId': videoId, + 'path': path, + 'size': '${VideoDownloader.maxWidth}x${VideoDownloader.maxHeight} (max)', + }), + headers: {'Content-Type': 'application/json'}, + ); + } + + @Route.get('/') + Future listVideos(Request request) async { + final videos = await _videoDownloader.getCachedVideos(); + + return Response.ok( + jsonEncode({'videos': videos}), + headers: {'Content-Type': 'application/json'}, + ); + } + + Router get router => _$VideoServiceRouter(this); +} diff --git a/server/cinema/pubspec.lock b/server/cinema/pubspec.lock index 0baaec0..a3783f7 100644 --- a/server/cinema/pubspec.lock +++ b/server/cinema/pubspec.lock @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 + sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.4" build_config: dependency: transitive description: @@ -77,10 +77,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" + sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072 url: "https://pub.dev" source: hosted - version: "2.10.4" + version: "2.10.5" built_collection: dependency: "direct main" description: @@ -93,18 +93,26 @@ packages: dependency: "direct main" description: name: built_value - sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.12.1" + version: "8.12.3" built_value_generator: dependency: "direct dev" description: name: built_value_generator - sha256: "7f337721c07a53dce3d2ea062747709ed63d57880058f4fd62ae0f16d1e6cb0e" + sha256: e5d1374e35a163507037733a9a0902f88647b8869b487fd66ae915da4e91ff8f url: "https://pub.dev" source: hosted - version: "8.12.1" + version: "8.12.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -125,10 +133,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.9.1" dio_web_adapter: dependency: transitive description: @@ -189,10 +197,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -277,18 +285,18 @@ packages: dependency: "direct main" description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.7.2" injectable: dependency: "direct main" description: name: injectable - sha256: "8fc24421cfeff76d1d38484d8b9617beeb54a58b6edfd002b10cc896b8b8f3fe" + sha256: "32b36a9d87f18662bee0b1951b81f47a01f2bf28cd6ea94f60bc5453c7bf598c" url: "https://pub.dev" source: hosted - version: "2.7.1+2" + version: "2.7.1+4" injectable_generator: dependency: "direct dev" description: @@ -309,18 +317,18 @@ packages: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" lints: dependency: "direct dev" description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" logging: dependency: transitive description: @@ -341,10 +349,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.1" mime: dependency: transitive description: @@ -477,10 +485,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" + sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.2.0" source_map_stack_trace: dependency: transitive description: @@ -529,6 +537,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + string_normalizer: + dependency: transitive + description: + name: string_normalizer + sha256: "4bc0b4edafe1517fafa79e9e6e163ea078205bcb9399d069377df6bbd6f7c57d" + url: "https://pub.dev" + source: hosted + version: "0.4.0" string_scanner: dependency: transitive description: @@ -549,26 +565,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.28.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4 + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.14" + version: "0.6.15" typed_data: dependency: transitive description: @@ -589,10 +605,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" web: dependency: transitive description: @@ -645,9 +661,9 @@ packages: dependency: "direct main" description: name: zard - sha256: "51a5f07ebe5754ba67e01b3e2fcc67bb0bd92a269d5d087922431789b0f32d87" + sha256: "1a8f4bcbb9a2402b00a88a27e9238884ce886cfb120b9b762a1b4dc41f8b2d28" url: "https://pub.dev" source: hosted - version: "0.0.24" + version: "0.0.25" sdks: dart: ">=3.9.0 <4.0.0" diff --git a/server/cinema/pubspec.yaml b/server/cinema/pubspec.yaml index 8e10a94..e0f3a6c 100644 --- a/server/cinema/pubspec.yaml +++ b/server/cinema/pubspec.yaml @@ -1,6 +1,6 @@ name: cinema description: A server app using the shelf package and Docker. -version: 1.0.0 +version: 1.1.0 # repository: https://github.com/my_org/my_repo environment: diff --git a/server/cinema/scripts/start.sh b/server/cinema/scripts/start.sh new file mode 100644 index 0000000..910ade1 --- /dev/null +++ b/server/cinema/scripts/start.sh @@ -0,0 +1,5 @@ +#!/bin/sh +yt-dlp -U 2>/dev/null || true +export YT_DLP_JS_RUNTIMES="nodejs" +(sleep 86400 && kill 1) & +exec /app/bin/server