JJ_MKVMaker/lib/app/controller/subtitle_download.dart

77 lines
3.4 KiB
Dart

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<SubtitleQuery> queryFor(VideoItem v, List<Language> 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<List<SubtitleSearchResult>> 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<int> download(VideoItem v, List<SubtitleSearchResult> 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;
}
}