77 lines
3.4 KiB
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;
|
|
}
|
|
}
|