import 'package:path/path.dart' as p; import '../../core/charset_detector.dart'; import '../../core/languages.dart'; import '../../core/models.dart'; import '../../core/output_paths.dart'; import '../../core/srt.dart'; import '../../core/subtitle_search.dart'; import '../../core/text_codec.dart'; import '../../l10n/tr.dart'; import '../../services/storage_service.dart'; import '../../services/subtitle_provider.dart'; /// 인터넷 자막 (5단계). 약속: [queryFor] 는 파일명으로 검색 조건을 추정하고 영상 해시를 붙인다 (못 읽으면 해시 없이). /// [download] 는 고른 자막을 받아 jj_mkv\파일명_언어코드.srt (UTF-8, 이름이 겹치면 다른 이름) 로 저장하고 [v] 의 자막에 넣는다. /// 하나 받을 때마다 [onAdded] (받은 자막 · 그 언어). 받기 횟수가 다 되면 멈춘다. 받은 수를 준다. class SubtitleDownload { final StorageService storage; final void Function(String msg) log; SubtitleDownload(this.storage, {required this.log}); Future queryFor(VideoItem v, List langs) async { final q = guessQuery(v.path, languages: langs); String? hash; try { final size = await storage.fileSize(v.path); const chunk = 65536; if (size >= chunk) { hash = openSubtitlesHash(size, await storage.readRange(v.path, 0, chunk), await storage.readRange(v.path, size - chunk, chunk)); } } catch (_) {} return SubtitleQuery( title: q.title, year: q.year, season: q.season, episode: q.episode, movieHash: hash, languages: langs); } Future> search(SubtitleProvider provider, SubtitleQuery q) async { final r = await provider.search(q); log(trf('자막 검색 ({0}): "{1}"{2} → {3}개', [provider.name, q.title, q.season != null ? ' S${q.season}E${q.episode}' : '', r.length])); return r; } Future download(VideoItem v, List picks, SubtitleProvider provider, {required void Function(SubtitleEntry entry, SubtitleSearchResult from) onAdded}) async { final dir = outputDirFor(v.path); await storage.ensureDirectory(dir); final used = { for (final f in await storage.listFiles(dir)) p.basename(f).toLowerCase(), for (final s in v.subtitles) if (s.path != null) p.basename(s.path!).toLowerCase(), }; var ok = 0; for (final r in picks) { try { final bytes = await provider.download(r); final text = decodeText(bytes, detectCharset(bytes)); final cues = parseSrt(text); if (cues.isEmpty) throw SubtitleProviderException(tr('SRT 형식이 아니거나 비어 있습니다.')); final name = nextFreeName('${v.baseName}_${r.language.code}.srt', used); used.add(name.toLowerCase()); final path = p.join(dir, name); await storage.writeBytes(path, encodeText(formatSrt(cues), 'UTF-8').bytes); final entry = SubtitleEntry.external( path: path, language: r.language, charset: 'UTF-8', title: '${r.language.name} (${provider.name})'); v.subtitles.add(entry); ok++; onAdded(entry, r); log(trf('자막 받음: {0} ← {1}', [name, r.release])); } catch (e) { log(trf('자막 받기 실패: {0}\n{1}', [r.release, e])); if (e is SubtitleProviderException && e.quotaExceeded) break; } } if (v.status == JobStatus.done && ok > 0) v.status = JobStatus.ready; return ok; } }