import 'dart:async'; import 'package:flutter/services.dart'; import '../../app/settings.dart'; import '../../core/download_detect.dart'; import '../../services/downloader.dart'; import '../windows/aria2_backend.dart'; import '../windows/ytdlp_backend.dart'; import '../../l10n/tr.dart'; /// Android 의 다운로드 프로그램 (youtubedl-android 가 앱에 넣은 Python · yt-dlp · ffmpeg · aria2c). /// MainActivity.kt 의 "downloadToolsInit" 이 처음 켤 때 풀고, 직접 실행할 경로 · 환경 변수를 알려 준다. /// 실행은 Windows 와 같은 [YtDlpBackend] · [Aria2Backend] 가 한다. class AndroidDownloadTools { static const _ch = MethodChannel('jj_mkvmaker/android'); final String python, ytdlp, ffmpeg, quickjs, aria2c, cert, version; final Map environment; AndroidDownloadTools._(Map m) : python = m['python'] as String, ytdlp = m['ytdlp'] as String, ffmpeg = m['ffmpeg'] as String, quickjs = m['quickjs'] as String, aria2c = m['aria2c'] as String, cert = m['cert'] as String, version = m['version'] as String? ?? '', environment = (m['env'] as Map).cast(); static Future? _ready; /// 작업 기록에 남길 곳 (main 에서 정함) static void Function(String)? log; /// 한 번만 준비한다 (처음 켤 때 Python 등을 푸느라 몇 초 걸림). /// YouTube 가 바뀌면 오래된 yt-dlp 는 받지 못하므로 준비할 때 새 yt-dlp 가 있으면 받는다 (못 받아도 계속). static Future ready() { final f = _ready ??= _prepare(); // 실패하면 (저장 공간 부족 등) 다음에 다시 시도 f.then( (_) {}, onError: (Object _) { if (identical(_ready, f)) _ready = null; }, ); return f; } static Future _prepare() async { final m = await _ch.invokeMethod>('downloadToolsInit'); try { final r = await _ch.invokeMethod('downloadToolsUpdate').timeout(const Duration(seconds: 30)); if (r == 'DONE') log?.call(tr('yt-dlp 를 새 버전으로 바꿨습니다')); } catch (e) { log?.call(trf('yt-dlp 업데이트 확인 실패 (지금 버전으로 받습니다): {0}', [e])); } final tools = AndroidDownloadTools._(m!); log?.call(trf('다운로드 준비됨: yt-dlp {0}', [tools.version])); return tools; } /// 다운로드 엔진: yt-dlp (동영상 사이트) · aria2 (토렌트). 실제 엔진은 처음 쓸 때 준비된다. static List backends(AppSettings Function() s) => [ _Deferred(DownloadKind.video, () async { final t = await ready(); return YtDlpBackend( ytdlp: t.python, prefixArgs: [t.ytdlp, '--no-cache-dir'], environment: t.environment, // --ffmpeg-location 은 파일 경로도 받는다 (libffmpeg.so = ffmpeg 실행 파일) ffmpegDir: t.ffmpeg, jsRuntime: 'quickjs:${t.quickjs}', formatArgs: () => ytDlpFormatArgs(s().download.ytContainer, s().download.ytQuality, preferH264: s().download.ytPreferH264), cookieArgs: () => ytDlpCookieArgs( browser: s().download.ytCookiesBrowser, file: s().download.ytCookiesFile, internalCookieFile: s().internalCookieFile, ), fallbackCookieArgs: () => ytDlpCookieArgs(browser: internalBrowserCookies, internalCookieFile: s().internalCookieFile), ); }), _Deferred(DownloadKind.torrent, () async { final t = await ready(); return Aria2Backend(aria2c: t.aria2c, environment: t.environment, extraArgs: ['--ca-certificate=${t.cert}'], stallAfter: () => Duration(seconds: s().download.torrentStallSeconds)); }), ]; } /// 처음 쓸 때 엔진을 준비하는 다운로드 엔진 (앱 시작을 늦추지 않도록) class _Deferred implements DownloadBackend { @override final DownloadKind kind; final Future Function() _create; Future? _backend; _Deferred(this.kind, this._create); Future get _b { final f = _backend ??= _create(); // 실패하면 다음에 다시 시도 f.then( (_) {}, onError: (Object _) { if (identical(_backend, f)) _backend = null; }, ); return f; } @override Future<(String, List)?> expandPlaylist(String url) async => (await _b).expandPlaylist(url); @override Future start(DownloadTask t, void Function() changed) async { final DownloadBackend b; try { b = await _b; } catch (e) { t ..state = DownloadState.failed ..error = trf('다운로드 프로그램을 준비할 수 없습니다: {0}', [e]); changed(); return; } return b.start(t, changed); } @override Future pause(DownloadTask t) async => (await _b).pause(t); @override Future cancel(DownloadTask t) async => (await _b).cancel(t); @override Future shutdown() async { if (_backend != null) await (await _backend!).shutdown(); } }