JJ_MKVMaker/test/editor_test.dart

138 lines
5.9 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/subtitle_editor_controller.dart';
import 'package:jj_mkvmaker/core/models.dart';
import 'package:jj_mkvmaker/core/output_paths.dart';
import 'package:jj_mkvmaker/core/srt.dart';
import 'package:jj_mkvmaker/core/text_codec.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:path/path.dart' as p;
void main() {
group('SRT', () {
const src = '1\r\n00:00:01,000 --> 00:00:02,500\r\n첫 줄\r\n둘째 줄\r\n\r\n'
'2\r\n00:00:03.2 --> 00:00:04,000\r\n세 번째\r\n\r\n\r\n'
'00:00:05,000 --> 00:00:06,000\r\n번호 없음\r\n';
test('해석 (BOM, 여러 줄, 마침표 밀리초, 번호 누락)', () {
final cues = parseSrt(src);
expect(cues, hasLength(3));
expect(cues[0].text, '첫 줄\n둘째 줄');
expect(cues[1].start, const Duration(seconds: 3, milliseconds: 200));
expect(cues[2].text, '번호 없음');
});
test('저장 후 다시 읽어도 같음', () {
final cues = parseSrt(src);
final again = parseSrt(formatSrt(cues));
expect(again.map((c) => c.text), cues.map((c) => c.text));
expect(formatSrt(cues), startsWith('1\r\n00:00:01,000 --> 00:00:02,500\r\n'));
});
test('시간 입력 / 전체 이동', () {
expect(parseSrtTime('01:02:03,4'), const Duration(hours: 1, minutes: 2, seconds: 3, milliseconds: 400));
expect(parseSrtTime('1:05'), const Duration(minutes: 1, seconds: 5));
expect(parseSrtTime('00:61:00'), isNull);
expect(parseSrtTime('abc'), isNull);
final cues = parseSrt(src);
shiftCues(cues, const Duration(milliseconds: -1500));
expect(cues[0].start, Duration.zero);
expect(cues[0].end, const Duration(seconds: 1));
});
test('편집기: 형식 오류·순서 오류가 있으면 저장 불가', () {
final e = SubtitleEditorController(parseSrt(src), saveCharset: 'UTF-8');
expect(e.canSave, isTrue);
expect(e.setTime(e.cues[0], '12:xx', isStart: true), isFalse);
expect(e.canSave, isFalse);
e.setTime(e.cues[0], '00:00:09,000', isStart: true); // 끝(2.5초)보다 늦음
expect(e.invalidOrder, [0]);
expect(e.canSave, isFalse);
e.setTime(e.cues[0], '00:00:01,000', isStart: true);
expect(e.canSave, isTrue);
expect(e.dirty, isTrue);
});
});
group('문자셋 변환', () {
const text = '안녕하세요 똠방각하 Hello';
for (final cs in ['UTF-8', 'UTF-8-BOM', 'UTF-16LE', 'UTF-16BE', 'CP949']) {
test('$cs 왕복', () {
final r = encodeText(text, cs);
expect(r.lostChars, 0);
expect(decodeText(r.bytes, cs), text);
});
}
test('Shift-JIS / GBK 왕복', () {
expect(decodeText(encodeText('こんにちは 漢字', 'SHIFT_JIS').bytes, 'SHIFT_JIS'), 'こんにちは 漢字');
expect(decodeText(encodeText('你好 中文', 'GBK').bytes, 'GBK'), '你好 中文');
});
test('표현 불가 글자 개수', () {
final r = encodeText('가나 abc', 'SHIFT_JIS'); // 한글은 Shift-JIS 에 없음
expect(r.lostChars, 2);
expect(latin1.decode(r.bytes), '?? abc');
});
});
test('통합: MKV 내장 자막 편집 → CP949 저장 → MKV 다시 만들기', () async {
final tool = ProcessMediaTool('ffmpeg', 'ffprobe');
if (await tool.version() == null) {
markTestSkipped('FFmpeg 없음');
return;
}
final dir = Directory.systemTemp.createTempSync('jj_edit_');
addTearDown(() => dir.deleteSync(recursive: true));
// 한국어 내장 자막이 있는 MKV 준비
final srt = p.join(dir.path, 'src.srt');
File(srt).writeAsStringSync('1\n00:00:00,500 --> 00:00:02,000\n원래 문장\n');
final video = p.join(dir.path, '드라마.mkv');
await tool.runFfmpeg([
'-y', '-f', 'lavfi', '-i', 'testsrc=size=160x120:rate=10', '-i', srt, '-t', '3',
'-c:v', 'libx264', '-c:s', 'srt', '-metadata:s:s:0', 'language=kor', video,
]);
File(srt).deleteSync(); // 같은 폴더 자막으로 잡히지 않게
final c = AppController(
PlatformServices(mediaTool: tool, storage: DesktopStorageService()));
await c.init();
await c.addVideos([video]);
final v = c.videos.single;
final embedded = v.subtitles.single;
expect(embedded.kind, SubtitleKind.embedded);
expect(embedded.language.code, 'ko');
final cues = await c.loadCues(v, embedded);
expect(cues.single.text, '원래 문장');
cues.single.text = '고친 문장 똠';
cues.add(Cue(const Duration(seconds: 2), const Duration(seconds: 3), '추가한 줄'));
final (path, lost) = await c.saveEdited(v, embedded, cues, 'CP949');
expect(lost, 0);
expect(path, p.join(outputDirFor(video), '드라마_ko.srt'));
expect(decodeText(File(path).readAsBytesSync(), 'CP949'), contains('고친 문장 똠'));
// 원래 트랙은 제외, 편집본이 외부 자막으로 추가
expect(embedded.enabled, isFalse);
expect(v.subtitles[1].path, path);
expect(v.subtitles[1].charset, 'CP949');
await c.buildAll();
expect(v.status, JobStatus.done, reason: v.message);
final out = outputMkvPath(video);
final info = await tool.probe(out);
final subs = info.ofType('subtitle').toList();
expect(subs, hasLength(1));
final check = p.join(dir.path, 'check.srt');
await tool.runFfmpeg(['-y', '-i', out, '-map', '0:${subs.single.index}', check]);
final text = File(check).readAsStringSync();
expect(text, contains('고친 문장 똠'));
expect(text, contains('추가한 줄'));
expect(text, isNot(contains('원래 문장')));
}, timeout: const Timeout(Duration(minutes: 2)));
}