시험: FTP · SMB 동영상도 받지 않고 재생 · 다음 화 · 그 자막 (검토 대기)

관리자 지시 (10/20) 2번 - 위키 974 목록 1 "받지 않고 재생" · "다음 화" 의 📱 ⏸ 가운데 기기 없이 할 수 있는 것:
SFTP · WebDAV 에만 있던 시험을 FTP (pyftpdlib 시험 서버) · SMB (가짜 채널) 에도. preparePlayback 이 같은 폴더의
동영상만 차례로 (1화 → 2화), 이 기기 안의 다리 (127.0.0.1) 주소에 비밀번호 없음, Range (건너뛰기) 206, 다음 화도 바로,
다음 화의 자막을 찾음. 둘 다 통과 - 코드 변경 없음.
SMB 시험은 위젯 시험 바인딩의 가짜 HttpClient 를 그 시험 동안만 끔 (다리에 실제로 묻기 위해).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
next6
RyuJongJin 2026-10-20 11:29:03 +09:00
parent 39c6d9dce4
commit 10954a632b
2 changed files with 94 additions and 0 deletions

View File

@ -14,6 +14,11 @@ import 'package:jj_mkvmaker/core/file_ops.dart';
import 'package:jj_mkvmaker/core/rsync_filter.dart'; import 'package:jj_mkvmaker/core/rsync_filter.dart';
import 'package:jj_mkvmaker/core/sync_preview.dart'; import 'package:jj_mkvmaker/core/sync_preview.dart';
import 'support/rsync_scenes.dart'; import 'support/rsync_scenes.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/playlist.dart' show PlaylistMode;
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';
/// l (관리자 지시 10/17): FTP · FTPS (dart:io 로 직접). 실제 서버 시험은 tool/ftp_test_server.py (pyftpdlib) - /// l (관리자 지시 10/17): FTP · FTPS (dart:io 로 직접). 실제 서버 시험은 tool/ftp_test_server.py (pyftpdlib) -
/// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다 /// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다
@ -263,6 +268,45 @@ void main() {
expect(await range(7, null), '789'); expect(await range(7, null), '789');
}, skip: hasPy ? false : '시험용 파이썬 없음'); }, skip: hasPy ? false : '시험용 파이썬 없음');
test('관리자 지시 (10/20): FTP 동영상 - 같은 폴더 목록 · 다리로 받지 않고 재생 (Range · 건너뛰기) · 다음 화 · 그 자막', () async {
await start('plain');
FtpRegistry.configure([srv(security: 'none')]);
Directory(p.join(root.path, '영상')).createSync();
File(p.join(root.path, '영상', '1화.mkv')).writeAsStringSync('0123456789');
File(p.join(root.path, '영상', '2화.mkv')).writeAsStringSync('abcdefghij');
File(p.join(root.path, '영상', '2화.ko.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n둘째\n');
File(p.join(root.path, '영상', '메모.txt')).writeAsStringSync('t');
addTearDown(RemoteStreamBridge.close);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.playlistMode = PlaylistMode.folder;
final plan = await c.preparePlayback(['ftp://f1/영상/1화.mkv']);
expect(plan!.$1, ['ftp://f1/영상/1화.mkv', 'ftp://f1/영상/2화.mkv'], reason: '같은 폴더의 동영상만 차례로 (다음 화)');
final bridge = await RemoteStreamBridge.get();
final u1 = bridge.url('ftp://f1/영상/1화.mkv'), u2 = bridge.url('ftp://f1/영상/2화.mkv');
expect(u1, startsWith('http://127.0.0.1:'));
expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음');
Future<(int, String)> get(String url, [String? range]) async {
final http = HttpClient();
try {
final req = await http.getUrl(Uri.parse(url));
if (range != null) req.headers.set('Range', range);
final res = await req.close();
return (res.statusCode, await res.transform(utf8.decoder).join());
} finally {
http.close(force: true);
}
}
expect(await get(u1), (200, '0123456789'));
expect(await get(u1, 'bytes=4-6'), (206, '456'), reason: '건너뛰기 - 그 자리부터');
expect(await get(u1, 'bytes=7-'), (206, '789'));
expect(await get(u2), (200, 'abcdefghij'), reason: '다음 화도 받지 않고 바로');
final subs = await c.externalSubtitlesFor('ftp://f1/영상/2화.mkv');
expect(subs.map(p.basename), ['2화.ko.srt']);
expect(File(subs.single).readAsStringSync(), contains('둘째'));
}, skip: hasPy ? false : '시험용 파이썬 없음');
test('FTPS (암시, 990 꼴)', () async { test('FTPS (암시, 990 꼴)', () async {
await start('implicit'); await start('implicit');
final c = FtpRemote(srv(security: 'implicit', cert: fp)); final c = FtpRemote(srv(security: 'implicit', cert: fp));

View File

@ -13,6 +13,12 @@ import 'package:jj_mkvmaker/core/file_ops.dart';
import 'package:jj_mkvmaker/core/rsync_filter.dart'; import 'package:jj_mkvmaker/core/rsync_filter.dart';
import 'package:jj_mkvmaker/core/sync_preview.dart'; import 'package:jj_mkvmaker/core/sync_preview.dart';
import 'support/rsync_scenes.dart'; import 'support/rsync_scenes.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/playlist.dart' show PlaylistMode;
import 'package:jj_mkvmaker/core/remote_stream.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';
/// 88: Android SMB (smbj) 의 Dart 쪽 - 플랫폼 채널을 가짜 서버로 (실제 smbj · 서버 연결은 Android 기기 확인 몫) /// 88: Android SMB (smbj) 의 Dart 쪽 - 플랫폼 채널을 가짜 서버로 (실제 smbj · 서버 연결은 Android 기기 확인 몫)
class _FakeSmb { class _FakeSmb {
@ -294,4 +300,48 @@ void main() {
} }
expect(bad, isEmpty, reason: bad.join('\n')); expect(bad, isEmpty, reason: bad.join('\n'));
}); });
test('관리자 지시 (10/20): SMB 동영상 - 같은 폴더 목록 · 다리로 받지 않고 재생 (Range · 건너뛰기) · 다음 화 · 그 자막', () async {
SmbRegistry.configure(const [server]);
// 위젯 시험 바인딩은 HttpClient 를 가짜 (늘 400) 로 바꾼다 - 이 시험은 이 기기 안의 다리 (127.0.0.1) 에 실제로 묻는다
final saved = HttpOverrides.current;
HttpOverrides.global = null;
addTearDown(() => HttpOverrides.global = saved);
fake.files.addAll({
'영상': null,
'영상\\1화.mkv': utf8.encode('0123456789'),
'영상\\2화.mkv': utf8.encode('abcdefghij'),
'영상\\2화.ko.srt': utf8.encode('1\n00:00:01,000 --> 00:00:02,000\n둘째\n'),
'영상\\메모.txt': utf8.encode('t'),
});
addTearDown(RemoteStreamBridge.close);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.playlistMode = PlaylistMode.folder;
final plan = await c.preparePlayback(['smb://m1/영상/1화.mkv']);
expect(plan!.$1, ['smb://m1/영상/1화.mkv', 'smb://m1/영상/2화.mkv'], reason: '같은 폴더의 동영상만 차례로 (다음 화)');
final bridge = await RemoteStreamBridge.get();
final u1 = bridge.url('smb://m1/영상/1화.mkv'), u2 = bridge.url('smb://m1/영상/2화.mkv');
expect(u1, startsWith('http://127.0.0.1:'));
expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음');
Future<(int, String)> get(String url, [String? range]) async {
final http = HttpClient();
try {
final req = await http.getUrl(Uri.parse(url));
if (range != null) req.headers.set('Range', range);
final res = await req.close();
return (res.statusCode, await res.transform(utf8.decoder).join());
} finally {
http.close(force: true);
}
}
expect(await get(u1), (200, '0123456789'));
expect(await get(u1, 'bytes=4-6'), (206, '456'), reason: '건너뛰기 - 그 자리부터');
expect(await get(u1, 'bytes=7-'), (206, '789'));
expect(await get(u2), (200, 'abcdefghij'), reason: '다음 화도 받지 않고 바로');
final subs = await c.externalSubtitlesFor('smb://m1/영상/2화.mkv');
expect(subs.map(p.basename), ['2화.ko.srt']);
expect(File(subs.single).readAsStringSync(), contains('둘째'));
});
} }