85 lines
3.9 KiB
Dart
85 lines
3.9 KiB
Dart
import 'package:path/path.dart' as p;
|
|
|
|
import '../../core/languages.dart';
|
|
import '../../core/models.dart';
|
|
import '../../core/output_paths.dart';
|
|
import '../../core/subtitle_detector.dart';
|
|
import '../../l10n/tr.dart';
|
|
import '../../services/media_tool.dart';
|
|
import '../../services/storage_service.dart';
|
|
import 'subtitle_files.dart';
|
|
|
|
/// 동영상 분석과 자막 찾기. 약속: [load] 는 FFmpeg 로 분석해 [VideoItem.info] 와 내장 자막을 채우고,
|
|
/// 같은 폴더의 자막 (기능 4) 과 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등) 을 붙인다 (이미 있는 것은 그대로).
|
|
/// 분석에 실패하면 [VideoItem.message] 에 이유 (116: 권한이 없어 못 읽은 것이면 알아볼 수 있는 안내) 를 둔다.
|
|
class VideoProbe {
|
|
final StorageService storage;
|
|
final MediaTool tool;
|
|
final SubtitleFiles files;
|
|
final void Function(String msg) log;
|
|
|
|
/// 116 (Android): 저장소 전체 접근 권한이 있는지 (null = 묻지 않음 - Windows)
|
|
final Future<bool> Function()? Function() fileAccess;
|
|
VideoProbe({required this.storage, required this.tool, required this.files, required this.log, required this.fileAccess});
|
|
|
|
static String get accessNeededMessage => tr('"모든 파일에 대한 접근" 권한이 없어 읽을 수 없습니다. 허용하면 저절로 다시 분석합니다.');
|
|
|
|
static bool isPermissionDenied(String message) {
|
|
final m = message.toLowerCase();
|
|
return m.contains('permission denied') || m.contains('eacces') || m.contains('operation not permitted');
|
|
}
|
|
|
|
Future<void> load(VideoItem v) async {
|
|
try {
|
|
v.info = await tool.probe(v.path);
|
|
// 내장 자막 (mkv 등)
|
|
for (final s in v.info!.ofType('subtitle')) {
|
|
v.subtitles.add(SubtitleEntry.embedded(
|
|
streamIndex: s.index,
|
|
codec: s.codec,
|
|
language: languageOf(s.language),
|
|
title: s.title,
|
|
));
|
|
}
|
|
} on MediaToolException catch (e) {
|
|
// 116: 권한이 없어 못 읽은 것이면 빨간 Permission denied 대신 알아볼 수 있게 (허용하면 다시 분석)
|
|
final access = fileAccess();
|
|
final denied = isPermissionDenied(e.message) && access != null && !await access();
|
|
v.message = denied ? accessNeededMessage : e.message;
|
|
log(trf('분석 실패: {0} - {1}', [v.fileName, e.message]));
|
|
}
|
|
|
|
// 같은 폴더의 자막 자동 추가 (기능 4) - 다시 분석할 때 이미 있는 것은 그대로
|
|
List<String> folder = const [];
|
|
try {
|
|
folder = await storage.listFiles(v.directory);
|
|
} catch (_) {}
|
|
final siblings = findSiblingSubtitles(v.path, folder);
|
|
for (final d in siblings) {
|
|
if (_has(v, d.path)) continue;
|
|
v.subtitles.add(await files.external(d.path, d.language));
|
|
}
|
|
// 전에 만든 자막 (jj_mkv 의 파일명_ko.srt 등: AI 자막 · 번역 · 인터넷 자막) 도 다시 연결
|
|
// (앱을 다시 켜거나 목록에 다시 넣어도 MKV 에 빠지지 않게). 음성인식 원본 파일명_AI.srt 는 언어별 파일과 같아서 뺀다.
|
|
var made = 0;
|
|
final out = outputDirFor(v.path);
|
|
if (!p.equals(out, v.directory)) {
|
|
List<String> found = const [];
|
|
try {
|
|
found = await storage.listFiles(out);
|
|
} catch (_) {}
|
|
for (final d in findSiblingSubtitles(v.path, found)) {
|
|
if (d.isAi || d.language == undetermined) continue;
|
|
if (_has(v, d.path)) continue;
|
|
v.subtitles.add(await files.external(d.path, d.language));
|
|
made++;
|
|
}
|
|
}
|
|
final embeddedCount = v.subtitles.where((s) => s.kind == SubtitleKind.embedded).length;
|
|
log(trf('추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)', [v.fileName, embeddedCount, siblings.length]) +
|
|
(made > 0 ? trf(' · 전에 만든 자막 {0}개 (jj_mkv)', [made]) : ''));
|
|
}
|
|
|
|
static bool _has(VideoItem v, String path) => v.subtitles.any((s) => s.path != null && p.equals(s.path!, path));
|
|
}
|