시험: 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
parent
39c6d9dce4
commit
10954a632b
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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('둘째'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue