From 10954a632bf29eea7f0b8226f02761202560f4de Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:29:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=ED=97=98:=20FTP=20=C2=B7=20SMB=20?= =?UTF-8?q?=EB=8F=99=EC=98=81=EC=83=81=EB=8F=84=20=EB=B0=9B=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B3=A0=20=EC=9E=AC=EC=83=9D=20=C2=B7=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=8C=20=ED=99=94=20=C2=B7=20=EA=B7=B8=20=EC=9E=90=EB=A7=89?= =?UTF-8?q?=20(=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 지시 (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 --- test/ftp_test.dart | 44 ++++++++++++++++++++++++++++++++++++++++ test/smb_test.dart | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/test/ftp_test.dart b/test/ftp_test.dart index a50dae4..8f2d24c 100644 --- a/test/ftp_test.dart +++ b/test/ftp_test.dart @@ -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/sync_preview.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) - /// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다 @@ -263,6 +268,45 @@ void main() { expect(await range(7, null), '789'); }, 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 { await start('implicit'); final c = FtpRemote(srv(security: 'implicit', cert: fp)); diff --git a/test/smb_test.dart b/test/smb_test.dart index 3d5eb4f..7d8dfdf 100644 --- a/test/smb_test.dart +++ b/test/smb_test.dart @@ -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/sync_preview.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 기기 확인 몫) class _FakeSmb { @@ -294,4 +300,48 @@ void main() { } 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('둘째')); + }); }