122 lines
5.1 KiB
Dart
122 lines
5.1 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
/// 필수 프로그램
|
|
class RequiredTool {
|
|
final String name;
|
|
final String purpose;
|
|
|
|
/// 프로그램 폴더 기준 설치 위치
|
|
final String relPath;
|
|
|
|
/// 대략적인 받는 크기 (MB)
|
|
final int sizeMb;
|
|
|
|
const RequiredTool(this.name, this.purpose, this.relPath, this.sizeMb);
|
|
}
|
|
|
|
const requiredTools = [
|
|
RequiredTool('FFmpeg', 'MKV 만들기 · 인코딩 · 음성 추출', r'ffmpeg\ffmpeg.exe', 100),
|
|
RequiredTool('yt-dlp', 'YouTube 다운로드', r'tools\yt-dlp.exe', 17),
|
|
RequiredTool('aria2', '토렌트 · 마그넷 다운로드', r'tools\aria2c.exe', 3),
|
|
RequiredTool('Deno', 'yt-dlp 의 YouTube 화질 목록 추출', r'tools\deno.exe', 41),
|
|
];
|
|
|
|
/// 처음 실행 시 없는 필수 프로그램을 찾아 내려받는다 (공식 배포처에서 다운로드만).
|
|
class ToolInstaller {
|
|
final String appDir;
|
|
final HttpClient _http = HttpClient()..userAgent = 'JJMKVMaker';
|
|
|
|
ToolInstaller([String? appDir]) : appDir = appDir ?? p.dirname(Platform.resolvedExecutable);
|
|
|
|
/// 프로그램 폴더에 없고 PATH 에서도 찾을 수 없는 것
|
|
Future<List<RequiredTool>> missing() async {
|
|
final out = <RequiredTool>[];
|
|
for (final t in requiredTools) {
|
|
if (File(p.join(appDir, t.relPath)).existsSync()) continue;
|
|
final exe = p.basename(t.relPath);
|
|
final where = await Process.run('where', [exe]);
|
|
if (where.exitCode == 0) continue;
|
|
out.add(t);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Future<String> _latestAsset(String repo, RegExp pattern) async {
|
|
final req = await _http.getUrl(Uri.parse('https://api.github.com/repos/$repo/releases/latest'));
|
|
req.headers.set('Accept', 'application/vnd.github+json');
|
|
final res = await req.close();
|
|
final json = jsonDecode(await res.transform(utf8.decoder).join()) as Map;
|
|
for (final a in (json['assets'] as List)) {
|
|
if (pattern.hasMatch(a['name'] as String)) return a['browser_download_url'] as String;
|
|
}
|
|
throw HttpException('$repo 에서 받을 파일을 찾지 못했습니다');
|
|
}
|
|
|
|
Future<void> _download(String url, String target, void Function(int got, int total) onBytes) async {
|
|
final req = await _http.getUrl(Uri.parse(url));
|
|
final res = await req.close();
|
|
if (res.statusCode != 200) throw HttpException('내려받기 실패 (${res.statusCode}): $url');
|
|
await Directory(p.dirname(target)).create(recursive: true);
|
|
final part = File('$target.part');
|
|
final sink = part.openWrite();
|
|
var got = 0;
|
|
await for (final c in res) {
|
|
sink.add(c);
|
|
got += c.length;
|
|
onBytes(got, res.contentLength);
|
|
}
|
|
await sink.close();
|
|
if (await File(target).exists()) await File(target).delete();
|
|
await part.rename(target);
|
|
}
|
|
|
|
/// zip 을 풀어 [names] 파일을 [destDir] 로 복사 (Windows 내장 tar 사용)
|
|
Future<void> _extract(String zip, String destDir, Map<String, String> names) async {
|
|
final tmp = await Directory.systemTemp.createTemp('jj_tool_');
|
|
try {
|
|
final r = await Process.run('tar', ['-xf', zip, '-C', tmp.path]);
|
|
if (r.exitCode != 0) throw ProcessException('tar', [zip], '${r.stderr}', r.exitCode);
|
|
await Directory(destDir).create(recursive: true);
|
|
final all = await tmp.list(recursive: true).where((e) => e is File).toList();
|
|
for (final e in names.entries) {
|
|
final f = all.firstWhere((x) => p.basename(x.path).toLowerCase() == e.key.toLowerCase(),
|
|
orElse: () => throw FileSystemException('압축 파일 안에 없습니다', e.key));
|
|
await File(f.path).copy(p.join(destDir, e.value));
|
|
}
|
|
} finally {
|
|
await tmp.delete(recursive: true);
|
|
await File(zip).delete().catchError((_) => File(zip));
|
|
}
|
|
}
|
|
|
|
/// [tools] 설치. [onProgress] (지금 받는 프로그램 이름, 0~1)
|
|
Future<void> install(List<RequiredTool> tools, void Function(String, double) onProgress) async {
|
|
for (final t in tools) {
|
|
void prog(int got, int total) =>
|
|
onProgress(t.name, total > 0 ? got / total : (got / (t.sizeMb * 1e6)).clamp(0, 0.99));
|
|
final tmp = p.join(Directory.systemTemp.path, 'jj_${t.name}.download');
|
|
switch (t.name) {
|
|
case 'FFmpeg':
|
|
await _download('https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip', tmp, prog);
|
|
await _extract(tmp, p.join(appDir, 'ffmpeg'),
|
|
{'ffmpeg.exe': 'ffmpeg.exe', 'LICENSE': 'FFMPEG_LICENSE.txt'});
|
|
case 'yt-dlp':
|
|
await _download(await _latestAsset('yt-dlp/yt-dlp', RegExp(r'^yt-dlp\.exe$')),
|
|
p.join(appDir, 'tools', 'yt-dlp.exe'), prog);
|
|
case 'aria2':
|
|
await _download(await _latestAsset('aria2/aria2', RegExp(r'win-64bit.*\.zip$')), tmp, prog);
|
|
await _extract(tmp, p.join(appDir, 'tools'),
|
|
{'aria2c.exe': 'aria2c.exe', 'COPYING': 'ARIA2_COPYING.txt'});
|
|
case 'Deno':
|
|
await _download(
|
|
await _latestAsset('denoland/deno', RegExp(r'^deno-x86_64-pc-windows-msvc\.zip$')), tmp, prog);
|
|
await _extract(tmp, p.join(appDir, 'tools'), {'deno.exe': 'deno.exe'});
|
|
}
|
|
onProgress(t.name, 1);
|
|
}
|
|
}
|
|
}
|