From 0160c7591d043b95d66db086fca54fdfd46c0c75 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:36:15 +0900 Subject: [PATCH] =?UTF-8?q?Android=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C:?= =?UTF-8?q?=20=EC=95=B1=20=EC=95=88=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80?= =?UTF-8?q?=20[=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C]=20=C2=B7=20=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=EB=A1=9C=EB=93=9C=20=EB=AA=A9=EB=A1=9D=20(yt-dlp=20?= =?UTF-8?q?=C2=B7=20aria2c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - youtubedl-android 0.18.1 (Python · yt-dlp · ffmpeg · aria2c) 을 넣고, MainActivity 가 처음 켤 때 풀어 실행 경로 · 환경 변수를 알려 준다. 받을 때마다 새 yt-dlp 확인 (못 받아도 계속) - YtDlpBackend · Aria2Backend: 실행 명령 앞 인수 · 환경 변수 · JS 실행기 (deno / quickjs) 를 받게 해 Windows 와 같은 코드로 실행 - 다운로드 엔진은 처음 쓸 때 준비 (앱 시작을 늦추지 않음), 실패하면 다음에 다시 시도 - 기본 위치: 내장 저장소 Download/JJ_MKVMaker. 환경 설정에 다운로드 위치 · YouTube 형식 · 화질 · 쿠키 · 동시 다운로드 수 표시 - R8 keep 규칙 (youtubedl-android 의 Jackson · 압축 풀기 클래스) Co-Authored-By: Claude Opus 5.5 --- android/app/build.gradle.kts | 9 ++ android/app/proguard-rules.pro | 7 + .../kotlin/com/jj/jj_mkvmaker/MainActivity.kt | 56 ++++++- lib/app/settings.dart | 2 + lib/main.dart | 34 ++++- .../android/android_download_tools.dart | 139 ++++++++++++++++++ lib/platform/windows/aria2_backend.dart | 10 +- lib/platform/windows/ytdlp_backend.dart | 27 +++- lib/services/platform_services.dart | 4 +- lib/ui/settings_page.dart | 15 +- .../.gradle/8.14.3/checksums/checksums.lock | Bin 0 -> 17 bytes .../8.14.3/checksums/md5-checksums.bin | Bin 0 -> 26197 bytes .../8.14.3/checksums/sha1-checksums.bin | Bin 0 -> 36803 bytes .../.gradle/8.14.3/fileChanges/last-build.bin | Bin 0 -> 1 bytes .../.gradle/8.14.3/fileHashes/fileHashes.bin | Bin 0 -> 18547 bytes .../.gradle/8.14.3/fileHashes/fileHashes.lock | Bin 0 -> 17 bytes .../android/.gradle/8.14.3/gc.properties | 0 .../buildOutputCleanup.lock | Bin 0 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 + .../android/.gradle/vcs-1/gc.properties | 0 20 files changed, 284 insertions(+), 21 deletions(-) create mode 100644 android/app/proguard-rules.pro create mode 100644 lib/platform/android/android_download_tools.dart create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/sha1-checksums.bin create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/gc.properties create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties create mode 100644 packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/vcs-1/gc.properties diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2e3645f..87bb955 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -53,6 +53,7 @@ android { buildTypes { release { + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") } } @@ -76,3 +77,11 @@ kotlin { flutter { source = "../.." } + +// 다운로드 (앱 안 브라우저 · 다운로드 목록): Android 용 yt-dlp (Python 포함) · ffmpeg (영상 · 음성 합치기) · aria2c (토렌트) +// 실행 파일을 앱에 넣고 처음 켤 때 풀어 둔다. 실행은 Dart (YtDlpBackend · Aria2Backend) 가 Windows 와 같은 방식으로 한다. +dependencies { + implementation("io.github.junkfood02.youtubedl-android:library:0.18.1") + implementation("io.github.junkfood02.youtubedl-android:ffmpeg:0.18.1") + implementation("io.github.junkfood02.youtubedl-android:aria2c:0.18.1") +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..ae174cf --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,7 @@ +# youtubedl-android (Android 다운로드): 코드 축소(R8)가 Jackson · 압축 풀기 클래스를 지우거나 이름을 바꾸면 +# YoutubeDL 객체를 만들 때 ExceptionInInitializerError 가 난다 (youtubedl-android README 의 권장 규칙) +-keep class com.yausername.** { *; } +-keep class com.fasterxml.jackson.** { *; } +-keep class org.apache.commons.compress.archivers.zip.** { *; } +-dontwarn com.fasterxml.jackson.** +-dontwarn org.apache.commons.** diff --git a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt index 8deaee7..ec5a36d 100644 --- a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt +++ b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt @@ -7,11 +7,15 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings +import com.yausername.aria2c.Aria2c +import com.yausername.ffmpeg.FFmpeg +import com.yausername.youtubedl_android.YoutubeDL import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.io.File -/// 저장소 전체 접근 (동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위해) 확인 · 요청 +/// 저장소 전체 접근 (동영상 옆 jj_mkv 폴더에 MKV 를 만들기 위해) 확인 · 요청, 다운로드 프로그램 준비 class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -23,11 +27,61 @@ class MainActivity : FlutterActivity() { result.success(null) } "storageRoot" -> result.success(Environment.getExternalStorageDirectory().path) + "downloadToolsInit" -> inBackground(result) { downloadToolsInit() } + "downloadToolsUpdate" -> inBackground(result) { + YoutubeDL.updateYoutubeDL(applicationContext)?.name ?: "" + } else -> result.notImplemented() } } } + /// 오래 걸리는 일 (처음 켤 때 실행 파일 풀기 · yt-dlp 업데이트) 은 메인 스레드 밖에서 + private fun inBackground(result: MethodChannel.Result, work: () -> Any?) { + Thread { + try { + val value = work() + runOnUiThread { result.success(value) } + } catch (e: Throwable) { + android.util.Log.e("jj_mkvmaker", "download tools", e) + val cause = generateSequence(e) { it.cause }.last() + runOnUiThread { result.error("TOOLS", "${e.javaClass.simpleName}: ${cause.message ?: cause}", null) } + } + }.start() + } + + /// youtubedl-android 가 앱에 넣은 Python · yt-dlp · ffmpeg · aria2c 를 풀고, 직접 실행할 경로 · 환경 변수를 돌려준다. + /// 경로 규칙은 youtubedl-android 0.18.1 의 YoutubeDL.init / FFmpeg.init / Aria2c.init 과 같다. + private fun downloadToolsInit(): Map { + YoutubeDL.init(applicationContext) + FFmpeg.init(applicationContext) + Aria2c.init(applicationContext) + val bin = applicationInfo.nativeLibraryDir + val base = File(noBackupFilesDir, YoutubeDL.baseName) + val packages = File(base, "packages") + val python = File(packages, "python").absolutePath + val ffmpeg = File(packages, "ffmpeg").absolutePath + val aria2c = File(packages, "aria2c").absolutePath + val cert = "$python/usr/etc/tls/cert.pem" + return mapOf( + "python" to "$bin/libpython.so", + "ytdlp" to File(File(base, YoutubeDL.ytdlpDirName), YoutubeDL.ytdlpBin).absolutePath, + "ffmpeg" to "$bin/libffmpeg.so", + "quickjs" to "$bin/libqjs.so", + "aria2c" to "$bin/libaria2c.so", + "cert" to cert, + "version" to (YoutubeDL.versionName(applicationContext) ?: ""), + "env" to mapOf( + "LD_LIBRARY_PATH" to "$python/usr/lib:$ffmpeg/usr/lib:$aria2c/usr/lib", + "SSL_CERT_FILE" to cert, + "PATH" to (System.getenv("PATH") ?: "") + ":" + bin, + "PYTHONHOME" to "$python/usr", + "HOME" to "$python/usr", + "TMPDIR" to cacheDir.absolutePath, + ), + ) + } + private fun hasAllFilesAccess(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Environment.isExternalStorageManager() diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 946c361..84e7623 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -203,6 +203,8 @@ class AppSettings { /// 다운로드 기본 위치: 프로그램 폴더 (쓸 수 없으면 사용자 다운로드 폴더) String resolvedDownloadRoot() { if (downloadRoot != null && downloadRoot!.isNotEmpty) return downloadRoot!; + // Android: 프로그램 폴더가 없으므로 내장 저장소의 Download\JJ_MKVMaker + if (Platform.isAndroid) return '/storage/emulated/0/Download/JJ_MKVMaker'; final appDir = p.dirname(Platform.resolvedExecutable); try { final probe = File(p.join(appDir, '.jj_write_test')); diff --git a/lib/main.dart b/lib/main.dart index c2f1802..b291dc1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'app/download_manager.dart'; import 'app/settings.dart'; import 'core/app_update.dart'; import 'core/playlist.dart'; +import 'platform/android/android_download_tools.dart'; import 'platform/android/android_storage.dart'; import 'platform/windows/app_paths.dart'; import 'platform/windows/cef_runtime.dart'; @@ -329,10 +330,36 @@ Future runAndroid(String dataDir) async { final messengerKey = GlobalKey(); AndroidStorageService.navigatorKey = navigatorKey; + // 다운로드 (앱 안 브라우저의 [다운로드] · 다운로드 목록): 앱에 넣은 yt-dlp · aria2c. 클립보드 감시는 하지 않는다. + AndroidDownloadTools.log = controller.note; + final downloads = DownloadManager( + backends: services.createDownloadBackends?.call(() => controller.settings) ?? const [], + settings: () => controller.settings, + ); + downloads.onAdded.listen((t) => messengerKey.currentState + ?.showSnackBar(SnackBar(content: Text('다운로드 추가: ${t.source}')))); + // 다 받은 동영상 → 편집 목록 (설정에서 끌 수 있음) + downloads.onFinished.listen((t) async { + if (!controller.settings.addFinishedDownloads) return; + final files = DownloadManager.videoFilesOf(t); + if (files.isEmpty) return; + final n = await controller.addDownloaded(files); + downloads.dropFromList(t); + if (n > 0) { + messengerKey.currentState?.showSnackBar(SnackBar(content: Text('동영상 목록에 추가: ${t.title}'))); + } + }); + downloads.setAutoAdd = (on) async { + await controller.updateSettings((x) => x.addFinishedDownloads = on); + downloads.refresh(); + }; + downloads.addToEditList = controller.addDownloaded; + downloads.log = controller.note; + WidgetsBinding.instance.addPostFrameCallback((_) async { if (controller.settings.startScreen == 'browser') { - navigatorKey.currentState - ?.push(MaterialPageRoute(builder: (_) => BrowserPage(c: controller, bookmarks: bookmarks))); + navigatorKey.currentState?.push(MaterialPageRoute( + builder: (_) => BrowserPage(c: controller, downloads: downloads, bookmarks: bookmarks))); } // 동영상을 읽고 옆에 MKV 를 만들려면 저장소 전체 접근이 필요하다 if (!await AndroidAccess.hasAllFiles()) { @@ -346,11 +373,13 @@ Future runAndroid(String dataDir) async { runApp(JjCapCutApp( controller: controller, + downloads: downloads, bookmarks: bookmarks, navigatorKey: navigatorKey, messengerKey: messengerKey, onExit: () async { final ctx = navigatorKey.currentContext; + if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return; if (controller.busy && ctx != null && ctx.mounted) { final ok = await showDialog( context: ctx, @@ -366,6 +395,7 @@ Future runAndroid(String dataDir) async { if (ok != true) return; } controller.cancel(); + await downloads.shutdown(); controller.note('── 정상 종료 ──'); await services.shell.quit(); }, diff --git a/lib/platform/android/android_download_tools.dart b/lib/platform/android/android_download_tools.dart new file mode 100644 index 0000000..62d9843 --- /dev/null +++ b/lib/platform/android/android_download_tools.dart @@ -0,0 +1,139 @@ +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'; + +/// 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('yt-dlp 를 새 버전으로 바꿨습니다'); + } catch (e) { + log?.call('yt-dlp 업데이트 확인 실패 (지금 버전으로 받습니다): $e'); + } + final tools = AndroidDownloadTools._(m!); + log?.call('다운로드 준비됨: yt-dlp ${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().ytContainer, s().ytQuality), + cookieArgs: () => ytDlpCookieArgs( + browser: s().ytCookiesBrowser, + file: s().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}']); + }), + ]; +} + +/// 처음 쓸 때 엔진을 준비하는 다운로드 엔진 (앱 시작을 늦추지 않도록) +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 = '다운로드 프로그램을 준비할 수 없습니다: $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(); + } +} diff --git a/lib/platform/windows/aria2_backend.dart b/lib/platform/windows/aria2_backend.dart index 154cc7a..87e3ae5 100644 --- a/lib/platform/windows/aria2_backend.dart +++ b/lib/platform/windows/aria2_backend.dart @@ -22,7 +22,12 @@ class Aria2Backend implements DownloadBackend { Timer? _poll; final _tasks = {}; - Aria2Backend({String? aria2c}) : aria2c = aria2c ?? locateTool('aria2c'); + /// 추가 환경 변수 · 인수 (Android: 라이브러리 위치 · 인증서 파일) + final Map environment; + final List extraArgs; + + Aria2Backend({String? aria2c, this.environment = const {}, this.extraArgs = const []}) + : aria2c = aria2c ?? locateTool('aria2c'); @override DownloadKind get kind => DownloadKind.torrent; @@ -48,7 +53,8 @@ class Aria2Backend implements DownloadBackend { '--bt-save-metadata=false', '--follow-torrent=mem', '--console-log-level=warn', '--summary-interval=0', '--file-allocation=none', '--auto-file-renaming=false', - ]); + ...extraArgs, + ], environment: environment.isEmpty ? null : environment); _daemon!.stdout.drain(); _daemon!.stderr.drain(); unawaited(_daemon!.exitCode.then((_) => _daemon = null)); diff --git a/lib/platform/windows/ytdlp_backend.dart b/lib/platform/windows/ytdlp_backend.dart index 2264d4e..5f28b1d 100644 --- a/lib/platform/windows/ytdlp_backend.dart +++ b/lib/platform/windows/ytdlp_backend.dart @@ -27,8 +27,14 @@ class YtDlpBackend implements DownloadBackend { /// yt-dlp 가 영상·음성을 합칠 때 쓰는 ffmpeg 폴더 final String? ffmpegDir; - /// YouTube 추출용 JavaScript 실행기 (없으면 일부 화질이 빠질 수 있음) - final String? deno; + /// YouTube 추출용 JavaScript 실행기 "deno:경로" · "quickjs:경로" (없으면 일부 화질이 빠질 수 있음) + final String? jsRuntime; + + /// yt-dlp 앞에 붙일 인수 (Android: python 으로 yt-dlp 스크립트를 실행 → [ytdlp] 는 python, 여기에 스크립트 경로) + final List prefixArgs; + + /// 추가 환경 변수 (Android: Python · ffmpeg 라이브러리 위치 등) + final Map environment; final _procs = {}; final _stopping = {}; @@ -53,16 +59,22 @@ class YtDlpBackend implements DownloadBackend { String? ytdlp, this.ffmpegDir, String? deno, + String? jsRuntime, + this.prefixArgs = const [], + this.environment = const {}, List Function()? formatArgs, List Function()? cookieArgs, List Function()? fallbackCookieArgs, }) : ytdlp = ytdlp ?? locateTool('yt-dlp'), - deno = deno ?? _existing(locateTool('deno')), + jsRuntime = jsRuntime ?? _denoRuntime(deno ?? _existing(locateTool('deno'))), formatArgs = formatArgs ?? (() => ytDlpFormatArgs(YtContainer.mp4, YtQuality.best)), cookieArgs = cookieArgs ?? (() => const []), fallbackCookieArgs = fallbackCookieArgs ?? (() => const []); - static const _env = {'PYTHONIOENCODING': 'utf-8', 'PYTHONUTF8': '1'}; + static const _pyEnv = {'PYTHONIOENCODING': 'utf-8', 'PYTHONUTF8': '1'}; + Map get _env => {..._pyEnv, ...environment}; + + static String? _denoRuntime(String? deno) => deno == null ? null : 'deno:$deno'; @override Future<(String, List)?> expandPlaylist(String url) async { @@ -72,8 +84,9 @@ class YtDlpBackend implements DownloadBackend { Future read(List cookies) => Process.run( ytdlp, [ + ...prefixArgs, '--flat-playlist', '-J', '--no-warnings', '--encoding', 'utf-8', - if (deno != null) ...['--js-runtimes', 'deno:$deno'], + if (jsRuntime != null) ...['--js-runtimes', jsRuntime!], ...cookies, 'https://www.youtube.com/playlist?list=$listId', ], @@ -130,7 +143,7 @@ class YtDlpBackend implements DownloadBackend { ...formatArgs(), ..._cookiesFor(t), if (ffmpegDir != null) ...['--ffmpeg-location', ffmpegDir!], - if (deno != null) ...['--js-runtimes', 'deno:$deno'], + if (jsRuntime != null) ...['--js-runtimes', jsRuntime!], t.source, ]; @@ -145,7 +158,7 @@ class YtDlpBackend implements DownloadBackend { final Process proc; try { - proc = await Process.start(ytdlp, argsFor(t), environment: _env); + proc = await Process.start(ytdlp, [...prefixArgs, ...argsFor(t)], environment: _env); } on ProcessException catch (e) { t ..state = DownloadState.failed diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index c8cbe0e..1992a5c 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -9,6 +9,7 @@ import '../core/download_detect.dart'; import '../platform/common/nllb_translator.dart'; import '../platform/common/opensubtitles_provider.dart'; import '../platform/common/whisper_recognizer.dart'; +import '../platform/android/android_download_tools.dart'; import '../platform/android/android_shell.dart'; import '../platform/android/android_storage.dart'; import '../platform/android/ffmpeg_kit_media_tool.dart'; @@ -119,7 +120,7 @@ class PlatformServices { ); } if (Platform.isAndroid) { - // 다운로드 (yt-dlp · aria2) · 업데이트 · 탐색기 연결은 데스크톱 전용 + // 업데이트 · 탐색기 연결은 데스크톱 전용. 다운로드는 앱에 넣은 yt-dlp · aria2c (youtubedl-android) MediaKitPreviewPlayer.ensureInitialized(); return PlatformServices( mediaTool: FfmpegKitMediaTool(), @@ -128,6 +129,7 @@ class PlatformServices { createMediaPlayer: MediaKitFullPlayer.new, createRecognizer: WhisperRecognizer.new, createTranslator: NllbTranslator.new, + createDownloadBackends: AndroidDownloadTools.backends, shell: AndroidShell(), createSubtitleProviders: (s) => [ OpenSubtitlesProvider( diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index fb02adf..2723320 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -371,25 +371,24 @@ class _SettingsPageState extends State { }, onReset: () => c.updateSettings((x) => x.mkvOutputRoot = null), ), - if (desk) ...[ _folderTile( title: '다운로드 (jj_yt-dlp · jj_aria2)', value: s.downloadRoot, - defaultText: '프로그램 폴더 (기본): ${s.resolvedDownloadRoot()}', + defaultText: '${desk ? '프로그램 폴더' : '내장 저장소'} (기본): ${s.resolvedDownloadRoot()}', onPick: () async { final d = await _pickDir('다운로드 위치'); if (d != null) await c.updateSettings((x) => x.downloadRoot = d); }, onReset: () => c.updateSettings((x) => x.downloadRoot = null), ), - _section('다운로드'), + if (desk) _section('다운로드'), + if (desk) SwitchListTile( value: s.clipboardWatch, onChanged: (v) => c.updateSettings((x) => x.clipboardWatch = v), title: const Text('복사(Ctrl+C)한 주소 자동 다운로드'), subtitle: const Text('YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)'), ), - ], _section('재생'), ListTile( title: const Text('동영상 하나를 재생할 때'), @@ -438,7 +437,6 @@ class _SettingsPageState extends State { (v) => c.updateSettings((x) => x.openSubtitlesUser = v.trim())), _textTile('비밀번호 (선택)', s.openSubtitlesPassword, '이 PC 의 설정 파일에 저장됩니다', (v) => c.updateSettings((x) => x.openSubtitlesPassword = v), obscure: true), - if (desk) ...[ ListTile( title: const Text('YouTube 받을 형식'), subtitle: const Text('기본: MP4. 음성만 받으면 MP3 · M4A 로 저장'), @@ -479,8 +477,10 @@ class _SettingsPageState extends State { items: [ const DropdownMenuItem(value: '', child: Text('사용 안 함')), const DropdownMenuItem(value: internalBrowserCookies, child: Text('앱 안 브라우저 (권장)')), - for (final b in const ['firefox', 'chrome', 'edge', 'brave', 'whale', 'opera']) - DropdownMenuItem(value: b, child: Text(b[0].toUpperCase() + b.substring(1))), + // PC 브라우저의 쿠키는 Windows 에서만 + if (desk) + for (final b in const ['firefox', 'chrome', 'edge', 'brave', 'whale', 'opera']) + DropdownMenuItem(value: b, child: Text(b[0].toUpperCase() + b.substring(1))), const DropdownMenuItem(value: '*file', child: Text('cookies.txt 파일…')), ], onChanged: (v) async { @@ -511,7 +511,6 @@ class _SettingsPageState extends State { onChanged: (n) => c.updateSettings((x) => x.maxParallelDownloads = n), ), ), - ], _section('변환'), ListTile( title: const Text('동시 MKV 변환 수'), diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/checksums.lock new file mode 100644 index 0000000000000000000000000000000000000000..34af3b6f6e3ab8b76ca26351de76964dd7de0777 GIT binary patch literal 17 VcmZQ>DF2c*$G86@0~j!V1pqso1-JkJ literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/checksums/md5-checksums.bin new file mode 100644 index 0000000000000000000000000000000000000000..a81f0e4b62767358526a06bd4eefafa3db1e12a7 GIT binary patch literal 26197 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 literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileChanges/last-build.bin new file mode 100644 index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d GIT binary patch literal 1 IcmZPo000310RR91 literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.bin new file mode 100644 index 0000000000000000000000000000000000000000..f6c721cc7cef551e72b988689baaa4c502b80d4a GIT binary patch literal 18547 zcmeI(K?*@Z7y#g5DaA(F+7aoMHKiy!HdYSe2(oeods$oCx&k?Y3y3hUN3I}!f6dI_ z{Ii((wlgybp;~^vBYdOsO=ScK5FkK+009C72oNAZfB*pk1PBlyK!Cu%2u$Na3fW<* zt$do4<{>1-nBt<8 literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/fileHashes/fileHashes.lock new file mode 100644 index 0000000000000000000000000000000000000000..a423b02230e059cfd120152b3126e33a66a6b4e5 GIT binary patch literal 17 UcmZSHtzU5d!r9q#7$ATd06laC1ONa4 literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/gc.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/8.14.3/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000000000000000000000000000000000000..7110e2a39b33de25cd825a5fcbb60b22437c4292 GIT binary patch literal 17 UcmZSHE~dm-e_-!M1_)pT04lcwq5uE@ literal 0 HcmV?d00001 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..e85cbdf --- /dev/null +++ b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Fri Oct 02 20:36:01 KST 2026 +gradle.version=8.14.3 diff --git a/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/vcs-1/gc.properties b/packages/ffmpeg_kit_flutter_new_min_gpl/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29