implement video download
All checks were successful
Build and Push Multi-Arch Docker Image / build-and-push (push) Successful in 18m34s
All checks were successful
Build and Push Multi-Arch Docker Image / build-and-push (push) Successful in 18m34s
needs check, if converted files can be run on device Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
15
server/bruno/video.bru
Normal file
15
server/bruno/video.bru
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
meta {
|
||||||
|
name: video
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{base_uri}}/video/OSfu4ST3dlY
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
- added video download
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
- Initial version.
|
- Initial version.
|
||||||
|
|||||||
@@ -13,13 +13,28 @@ RUN dart run build_runner build --delete-conflicting-outputs && \
|
|||||||
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
|
||||||
|
|
||||||
# Build minimal serving image from AOT-compiled `/server`
|
# Build minimal serving image with ffmpeg and yt-dlp
|
||||||
# and the pre-built AOT-runtime in the `/runtime/` directory of the base image.
|
FROM debian:stable-slim
|
||||||
FROM scratch
|
|
||||||
COPY --from=build /runtime/ /
|
# 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 --from=build /app/bin/server /app/bin/
|
||||||
COPY assets /assets
|
COPY assets /assets
|
||||||
|
COPY scripts/start.sh /app/start.sh
|
||||||
|
RUN chmod +x /app/start.sh
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
# Start server.
|
# Start server.
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/server"]
|
CMD ["/bin/sh", "/app/start.sh"]
|
||||||
|
|||||||
@@ -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/poster/data/services/poster.service.dart';
|
||||||
import 'package:cinema/feature/root/data/service/root.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/feature/video/data/repositories/video_loader.dart';
|
||||||
|
import 'package:cinema/feature/video/data/services/video.service.dart';
|
||||||
import 'package:cinema/injectable.dart';
|
import 'package:cinema/injectable.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf/shelf_io.dart' as io;
|
import 'package:shelf/shelf_io.dart' as io;
|
||||||
@@ -21,6 +23,7 @@ void main(List<String> args) async {
|
|||||||
|
|
||||||
final router = Router();
|
final router = Router();
|
||||||
router.mount("/poster", getIt<PosterService>().router.call);
|
router.mount("/poster", getIt<PosterService>().router.call);
|
||||||
|
router.mount("/video", getIt<VideoService>().router.call);
|
||||||
router.mount("/", getIt<RootService>().router.call);
|
router.mount("/", getIt<RootService>().router.call);
|
||||||
|
|
||||||
/// add middlewares (Logging, CORS)
|
/// add middlewares (Logging, CORS)
|
||||||
@@ -36,11 +39,25 @@ void main(List<String> args) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print('Caching current trending images...');
|
print('Caching current trending images...');
|
||||||
|
final ImageLoader imageLoader = getIt();
|
||||||
final ImageLoader loader = getIt();
|
final movies = await imageLoader.getMovies();
|
||||||
final movies = await loader.getPosterURIs();
|
|
||||||
for (var movie in movies) {
|
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<Version>().printVersion();
|
getIt<Version>().printVersion();
|
||||||
|
|||||||
113
server/cinema/lib/common/video_downloader.dart
Normal file
113
server/cinema/lib/common/video_downloader.dart
Normal file
@@ -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<String?> 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<bool> isCached(String videoId) async {
|
||||||
|
final resizedVideoFile = File('$cacheDir/$videoId/video.mp4');
|
||||||
|
return resizedVideoFile.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all cached video IDs
|
||||||
|
Future<List<String>> getCachedVideos() async {
|
||||||
|
final dir = Directory(cacheDir);
|
||||||
|
if (!await dir.exists()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final videos = <String>[];
|
||||||
|
await for (final entity in dir.list()) {
|
||||||
|
if (entity is Directory) {
|
||||||
|
final videoId = entity.path.split('/').last;
|
||||||
|
videos.add(videoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cleanup(Directory videoDir) async {
|
||||||
|
if (await videoDir.exists()) {
|
||||||
|
await videoDir.delete(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:cinema/feature/poster/domain/movie.dart';
|
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||||
|
|
||||||
abstract class ImageLoader {
|
abstract class ImageLoader {
|
||||||
Future<List<Movie>> getPosterURIs({String? language = 'de'});
|
Future<List<Movie>> getMovies({String? language = 'de'});
|
||||||
|
|
||||||
Future<bool> downloadImages(Movie movie);
|
Future<bool> downloadImages(Movie movie);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class TmDBImageLoader implements ImageLoader {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Movie>> getPosterURIs({String? language = 'de'}) async {
|
Future<List<Movie>> getMovies({String? language = 'de'}) async {
|
||||||
final response = await api.get('/trending/movie/week?language=$language');
|
final response = await api.get('/trending/movie/week?language=$language');
|
||||||
if (response.data != null) {
|
if (response.data != null) {
|
||||||
final data = serializers.deserializeWith(TmdbTrendingResponse.serializer, response.data);
|
final data = serializers.deserializeWith(TmdbTrendingResponse.serializer, response.data);
|
||||||
|
|||||||
@@ -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<String?> 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<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import 'package:cinema/feature/poster/domain/movie.dart';
|
||||||
|
|
||||||
|
abstract class VideoLoader {
|
||||||
|
Future<String?> getYouTubeVideoId(int movieId, {String language = 'de'});
|
||||||
|
|
||||||
|
Future<bool> downloadVideo(Movie movie);
|
||||||
|
}
|
||||||
@@ -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('/<videoId>')
|
||||||
|
Future<Response> 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<Response> 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);
|
||||||
|
}
|
||||||
@@ -53,10 +53,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413
|
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.3"
|
version: "4.0.4"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -77,10 +77,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057"
|
sha256: b4d854962a32fd9f8efc0b76f98214790b833af8b2e9b2df6bfc927c0415a072
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.10.4"
|
version: "2.10.5"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -93,18 +93,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139"
|
sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.1"
|
version: "8.12.3"
|
||||||
built_value_generator:
|
built_value_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: built_value_generator
|
name: built_value_generator
|
||||||
sha256: "7f337721c07a53dce3d2ea062747709ed63d57880058f4fd62ae0f16d1e6cb0e"
|
sha256: e5d1374e35a163507037733a9a0902f88647b8869b487fd66ae915da4e91ff8f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -125,10 +133,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_builder
|
name: code_builder
|
||||||
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
|
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.11.0"
|
version: "4.11.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -173,10 +181,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
|
sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.0"
|
version: "5.9.1"
|
||||||
dio_web_adapter:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -189,10 +197,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,18 +285,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.4"
|
version: "4.7.2"
|
||||||
injectable:
|
injectable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: injectable
|
name: injectable
|
||||||
sha256: "8fc24421cfeff76d1d38484d8b9617beeb54a58b6edfd002b10cc896b8b8f3fe"
|
sha256: "32b36a9d87f18662bee0b1951b81f47a01f2bf28cd6ea94f60bc5453c7bf598c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.1+2"
|
version: "2.7.1+4"
|
||||||
injectable_generator:
|
injectable_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -309,18 +317,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.10.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.1.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -341,10 +349,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.18.1"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -477,10 +485,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75"
|
sha256: "1d562a3c1f713904ebbed50d2760217fd8a51ca170ac4b05b0db490699dbac17"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.2.0"
|
||||||
source_map_stack_trace:
|
source_map_stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -529,6 +537,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,26 +565,26 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "77cc98ea27006c84e71a7356cf3daf9ddbde2d91d84f77dbfe64cf0e4d9611ae"
|
sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.28.0"
|
version: "1.29.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.8"
|
version: "0.7.9"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: f1072617a6657e5fc09662e721307f7fb009b4ed89b19f47175d11d5254a62d4
|
sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.14"
|
version: "0.6.15"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -589,10 +605,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: watcher
|
name: watcher
|
||||||
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
|
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.4"
|
version: "1.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -645,9 +661,9 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: zard
|
name: zard
|
||||||
sha256: "51a5f07ebe5754ba67e01b3e2fcc67bb0bd92a269d5d087922431789b0f32d87"
|
sha256: "1a8f4bcbb9a2402b00a88a27e9238884ce886cfb120b9b762a1b4dc41f8b2d28"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.24"
|
version: "0.0.25"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: cinema
|
name: cinema
|
||||||
description: A server app using the shelf package and Docker.
|
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
|
# repository: https://github.com/my_org/my_repo
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
5
server/cinema/scripts/start.sh
Normal file
5
server/cinema/scripts/start.sh
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user