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 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 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 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 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)); }