실패한 다운로드 줄을 쉬운 말로 (136): 진행 막대 대신 실패 · 이유 · [자세히] · [다시 시도]
- 실패하면 진행 막대 ("준비 중…") 를 지우고 사람 말 이유 (화면 언어): 주소에 영상이 없습니다 (404) · 로그인이 필요합니다 (403) ·
연결할 수 없습니다 · 지원하지 않는 사이트입니다 · 저장 공간이 모자랍니다 · 받지 못했습니다 (그 밖). yt-dlp · aria2 (errorCode) 같은 모양.
- yt-dlp 백엔드는 원문을 저장하고 화면에서 바꾼다. [자세히] 는 원문 - 주소의 서명 · 키 (?… · #… · 아이디:비밀번호@) 는 가림 (redactUrlSecrets).
- [다시 시도] 는 기존 다시 받기. 진행 막대만 쓰는 곳도 실패면 "실패".
- download_error_test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음4
parent
26f6389f02
commit
8c1ef5a03b
|
|
@ -744,6 +744,7 @@
|
|||
"러시아어": "Russian",
|
||||
"로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). 이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:": "Login cookies are saved as cookies_youtube.txt (plain text) in this device's app folder so downloads (yt-dlp) can use them. Anyone with this file can log in to those accounts, so delete it before giving the device to someone else. Only cookies of the selected sites are passed:",
|
||||
"로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때": "Opening WebDAV videos that need a login in another app",
|
||||
"로그인이 필요합니다 (403)": "Login required (403)",
|
||||
"로그인하면 하루 받기 횟수가 늘어납니다": "Signing in raises the daily download limit",
|
||||
"루마니아어": "Romanian",
|
||||
"마스터 · 최상 비밀번호": "Master and super passwords",
|
||||
|
|
@ -881,6 +882,7 @@
|
|||
"받을 수 없습니다 ({0}): {1}": "Can't download ({0}): {1}",
|
||||
"받음": "Downloaded",
|
||||
"받음 {0}": "Received {0}",
|
||||
"받지 못했습니다": "Could not download",
|
||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "Couldn't download. Tap [Resume] in a moment",
|
||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "Download failed. Please try again shortly",
|
||||
"받지 못했습니다: {0}": "Could not download: {0}",
|
||||
|
|
@ -1215,6 +1217,7 @@
|
|||
"연결됨: 항목 {0}개{1}": "Connected: {0} items{1}",
|
||||
"연결이 끊겨 다시 잇는 중 ({0}번째)": "Connection lost, reconnecting (attempt {0})",
|
||||
"연결이 끊겼습니다. [이어 받기] 를 누르면 받은 곳부터 이어 받습니다": "The connection was lost. Tap [Resume] to continue from where it stopped",
|
||||
"연결할 수 없습니다": "Cannot connect",
|
||||
"연도": "Year",
|
||||
"열 수 없습니다: {0}": "Cannot open: {0}",
|
||||
"열기": "Open",
|
||||
|
|
@ -1556,6 +1559,7 @@
|
|||
"저장 (MKV 반영) *": "Save (apply to MKV) *",
|
||||
"저장 공간 정리": "Storage cleanup",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "Storage cleanup: {0} files · {1}",
|
||||
"저장 공간이 모자랍니다": "Not enough storage space",
|
||||
"저장 공간이 모자랍니다: {0} 이 필요한데 {1} 남았습니다": "Not enough storage: {0} needed, {1} left",
|
||||
"저장 실패: {0}": "Save failed: {0}",
|
||||
"저장 안 함": "Don't save",
|
||||
|
|
@ -1635,6 +1639,7 @@
|
|||
"주소 입력 또는 검색 (Ctrl+L)": "Enter an address or search (Ctrl+L)",
|
||||
"주소로 열기": "Open by address",
|
||||
"주소를 https 로 바꾸고 다시 확인": "Switch the address to https and check again",
|
||||
"주소에 영상이 없습니다 (404)": "No video at this address (404)",
|
||||
"준비 중": "Preparing",
|
||||
"준비 중…": "Preparing…",
|
||||
"준비하는 중": "Preparing",
|
||||
|
|
@ -1694,6 +1699,7 @@
|
|||
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "Choose items to delete (tap, or mark with the circle on the right).",
|
||||
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "Unsupported protobuf format (wire type {0}, position {1})",
|
||||
"지원하지 않는 문자셋: {0}": "Unsupported charset: {0}",
|
||||
"지원하지 않는 사이트입니다": "This site is not supported",
|
||||
"지정한 파일이 없습니다": "The specified file does not exist",
|
||||
"직접 고르기: {0}": "Choose: {0}",
|
||||
"직접 고르기…": "Choose…",
|
||||
|
|
|
|||
|
|
@ -744,6 +744,7 @@
|
|||
"러시아어": "ロシア語",
|
||||
"로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). 이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:": "ログイン Cookie はダウンロード (yt-dlp) が使えるよう、この端末のアプリフォルダーに cookies_youtube.txt (平文) として保存されます。このファイルを持つ人はそのアカウントにログインできるため、端末を人に渡すときは削除してください。選んだサイトの Cookie だけを渡します:",
|
||||
"로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때": "ログインが必要な WebDAV 動画を他のアプリで開くとき",
|
||||
"로그인이 필요합니다 (403)": "ログインが必要です (403)",
|
||||
"로그인하면 하루 받기 횟수가 늘어납니다": "ログインすると 1日のダウンロード回数が増えます",
|
||||
"루마니아어": "ルーマニア語",
|
||||
"마스터 · 최상 비밀번호": "マスター / 最上位パスワード",
|
||||
|
|
@ -881,6 +882,7 @@
|
|||
"받을 수 없습니다 ({0}): {1}": "ダウンロードできません ({0}): {1}",
|
||||
"받음": "ダウンロード済み",
|
||||
"받음 {0}": "受信 {0}",
|
||||
"받지 못했습니다": "ダウンロードできませんでした",
|
||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "ダウンロードできませんでした。少し待ってから [続きをダウンロード] を押してください",
|
||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "ダウンロードできませんでした。しばらくしてからもう一度お試しください",
|
||||
"받지 못했습니다: {0}": "ダウンロードできませんでした: {0}",
|
||||
|
|
@ -1215,6 +1217,7 @@
|
|||
"연결됨: 항목 {0}개{1}": "接続しました: 項目 {0} 個{1}",
|
||||
"연결이 끊겨 다시 잇는 중 ({0}번째)": "接続が切れたため再接続中 ({0} 回目)",
|
||||
"연결이 끊겼습니다. [이어 받기] 를 누르면 받은 곳부터 이어 받습니다": "接続が切れました。[続きをダウンロード] を押すと続きからダウンロードします",
|
||||
"연결할 수 없습니다": "接続できません",
|
||||
"연도": "年",
|
||||
"열 수 없습니다: {0}": "開けません: {0}",
|
||||
"열기": "開く",
|
||||
|
|
@ -1556,6 +1559,7 @@
|
|||
"저장 (MKV 반영) *": "保存(MKV に反映)*",
|
||||
"저장 공간 정리": "ストレージの整理",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "ストレージの整理: ファイル {0}個・{1}",
|
||||
"저장 공간이 모자랍니다": "保存容量が足りません",
|
||||
"저장 공간이 모자랍니다: {0} 이 필요한데 {1} 남았습니다": "保存領域が足りません: {0} 必要ですが残り {1} です",
|
||||
"저장 실패: {0}": "保存失敗: {0}",
|
||||
"저장 안 함": "保存しない",
|
||||
|
|
@ -1635,6 +1639,7 @@
|
|||
"주소 입력 또는 검색 (Ctrl+L)": "アドレスを入力または検索(Ctrl+L)",
|
||||
"주소로 열기": "アドレスで開く",
|
||||
"주소를 https 로 바꾸고 다시 확인": "アドレスを https に変えてもう一度確認",
|
||||
"주소에 영상이 없습니다 (404)": "このアドレスに動画がありません (404)",
|
||||
"준비 중": "準備中",
|
||||
"준비 중…": "準備中…",
|
||||
"준비하는 중": "準備中",
|
||||
|
|
@ -1694,6 +1699,7 @@
|
|||
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "削除する項目を選んでください (タップ、または右の丸でマーク)。",
|
||||
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "対応していない protobuf 形式(wire type {0}、位置 {1})",
|
||||
"지원하지 않는 문자셋: {0}": "対応していない文字コード: {0}",
|
||||
"지원하지 않는 사이트입니다": "対応していないサイトです",
|
||||
"지정한 파일이 없습니다": "指定したファイルがありません",
|
||||
"직접 고르기: {0}": "選択: {0}",
|
||||
"직접 고르기…": "選択…",
|
||||
|
|
|
|||
|
|
@ -744,6 +744,7 @@
|
|||
"러시아어": "俄语",
|
||||
"로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). 이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:": "登录 Cookie 会以 cookies_youtube.txt (明文) 保存在本设备的应用文件夹中,供下载 (yt-dlp) 使用。拥有此文件的人可以登录这些账号,将设备交给他人前请删除。仅传递所选网站的 Cookie:",
|
||||
"로그인이 필요한 WebDAV 동영상을 다른 앱으로 열 때": "用其他应用打开需要登录的 WebDAV 视频时",
|
||||
"로그인이 필요합니다 (403)": "需要登录 (403)",
|
||||
"로그인하면 하루 받기 횟수가 늘어납니다": "登录后每日下载次数会增加",
|
||||
"루마니아어": "罗马尼亚语",
|
||||
"마스터 · 최상 비밀번호": "主密码和最高密码",
|
||||
|
|
@ -881,6 +882,7 @@
|
|||
"받을 수 없습니다 ({0}): {1}": "无法下载 ({0}): {1}",
|
||||
"받음": "已下载",
|
||||
"받음 {0}": "已接收 {0}",
|
||||
"받지 못했습니다": "无法下载",
|
||||
"받지 못했습니다. 잠시 뒤 [이어 받기] 를 눌러 주세요": "下载失败。请稍后点按 [继续下载]",
|
||||
"받지 못했습니다. 잠시 뒤 다시 해 주세요": "下载失败。请稍后重试",
|
||||
"받지 못했습니다: {0}": "无法下载: {0}",
|
||||
|
|
@ -1215,6 +1217,7 @@
|
|||
"연결됨: 항목 {0}개{1}": "已连接: {0} 个项目{1}",
|
||||
"연결이 끊겨 다시 잇는 중 ({0}번째)": "连接中断,正在重新连接 (第 {0} 次)",
|
||||
"연결이 끊겼습니다. [이어 받기] 를 누르면 받은 곳부터 이어 받습니다": "连接已中断。点按 [继续下载] 将从中断处继续",
|
||||
"연결할 수 없습니다": "无法连接",
|
||||
"연도": "年份",
|
||||
"열 수 없습니다: {0}": "无法打开: {0}",
|
||||
"열기": "打开",
|
||||
|
|
@ -1556,6 +1559,7 @@
|
|||
"저장 (MKV 반영) *": "保存(应用到 MKV)*",
|
||||
"저장 공간 정리": "存储空间清理",
|
||||
"저장 공간 정리: 파일 {0}개 · {1}": "存储空间清理: 文件 {0} 个 · {1}",
|
||||
"저장 공간이 모자랍니다": "存储空间不足",
|
||||
"저장 공간이 모자랍니다: {0} 이 필요한데 {1} 남았습니다": "存储空间不足: 需要 {0},剩余 {1}",
|
||||
"저장 실패: {0}": "保存失败: {0}",
|
||||
"저장 안 함": "不保存",
|
||||
|
|
@ -1635,6 +1639,7 @@
|
|||
"주소 입력 또는 검색 (Ctrl+L)": "输入地址或搜索(Ctrl+L)",
|
||||
"주소로 열기": "按地址打开",
|
||||
"주소를 https 로 바꾸고 다시 확인": "将地址改为 https 并重新检查",
|
||||
"주소에 영상이 없습니다 (404)": "此地址没有视频 (404)",
|
||||
"준비 중": "准备中",
|
||||
"준비 중…": "准备中…",
|
||||
"준비하는 중": "正在准备",
|
||||
|
|
@ -1694,6 +1699,7 @@
|
|||
"지울 항목을 고르세요 (누르거나 오른쪽 동그라미로 표시).": "请选择要删除的项目 (点按,或用右侧圆圈标记)。",
|
||||
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "不支持的 protobuf 格式(wire type {0},位置 {1})",
|
||||
"지원하지 않는 문자셋: {0}": "不支持的字符集: {0}",
|
||||
"지원하지 않는 사이트입니다": "不支持此网站",
|
||||
"지정한 파일이 없습니다": "指定的文件不存在",
|
||||
"직접 고르기: {0}": "自选: {0}",
|
||||
"직접 고르기…": "自选…",
|
||||
|
|
|
|||
|
|
@ -288,6 +288,35 @@ String friendlyYtDlpError(String err) {
|
|||
return err;
|
||||
}
|
||||
|
||||
/// 글 속 주소의 서명 · 키가 든 부분 (?… 물음 · #… · 아이디:비밀번호@) 을 가린다 - 화면 · 기록에 서명된 주소를 보이지 않게
|
||||
String redactUrlSecrets(String text) => text.replaceAllMapped(
|
||||
RegExp(r'''(https?|wss?|ftp|dav)://(?:[^\s/@'"]+@)?([^\s?#'"]+)([?#][^\s'")\]>]*)?''', caseSensitive: false),
|
||||
(m) => '${m[1]}://${m[2]}${m[3] == null ? '' : '?…'}');
|
||||
|
||||
/// 136: 실패한 다운로드의 이유를 사람 말로 (yt-dlp · aria2 원문 → 화면 언어). 원문은 [자세히] 에서 그대로 보인다.
|
||||
/// 알아보지 못하면 "받지 못했습니다".
|
||||
String friendlyDownloadError(String raw) {
|
||||
final known = friendlyYtDlpError(raw);
|
||||
if (known != raw) return known;
|
||||
final e = raw.toLowerCase();
|
||||
bool any(List<String> xs) => xs.any(e.contains);
|
||||
if (any(['unsupported url', 'no suitable extractor'])) return tr('지원하지 않는 사이트입니다');
|
||||
if (any(['http error 404', ' 404 ', '404 not found', 'resource not found', 'errorcode=3)'])) {
|
||||
return tr('주소에 영상이 없습니다 (404)');
|
||||
}
|
||||
if (any(['http error 403', 'http error 401', '403 forbidden', 'login required', 'requires authentication',
|
||||
'use --cookies', 'errorcode=24)'])) {
|
||||
return tr('로그인이 필요합니다 (403)');
|
||||
}
|
||||
if (any(['no space left', 'disk full', 'errorcode=9)'])) return tr('저장 공간이 모자랍니다');
|
||||
if (any(['unable to download webpage', 'getaddrinfo', 'name or service not known', 'name resolution',
|
||||
'temporary failure', 'timed out', 'timeout', 'connection refused', 'connection reset', 'network is unreachable',
|
||||
'no route to host', 'network problem', 'errorcode=1)', 'errorcode=2)', 'errorcode=6)', 'errorcode=19)'])) {
|
||||
return tr('연결할 수 없습니다');
|
||||
}
|
||||
return tr('받지 못했습니다');
|
||||
}
|
||||
|
||||
/// 바이트 → 읽기 쉬운 크기
|
||||
String formatBytes(num b) {
|
||||
if (b < 1024) return '${b.round()}B';
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ class Aria2Backend implements DownloadBackend {
|
|||
case 'error':
|
||||
t
|
||||
..state = DownloadState.failed
|
||||
..error = '${s['errorMessage']}';
|
||||
// 136: 원문 + 오류 번호 (화면은 friendlyDownloadError 로 사람 말 이유)
|
||||
..error = '${s['errorMessage']} (errorCode=${s['errorCode']})';
|
||||
_tasks.remove(t.id);
|
||||
case 'removed':
|
||||
_tasks.remove(t.id);
|
||||
|
|
|
|||
|
|
@ -242,8 +242,8 @@ class YtDlpBackend implements DownloadBackend {
|
|||
}
|
||||
t
|
||||
..state = DownloadState.failed
|
||||
..error = friendlyYtDlpError(
|
||||
errTail.where((l) => l.contains('ERROR')).join('\n').ifEmpty(errTail.join('\n')));
|
||||
// 136: 원문 그대로 둔다 (화면은 friendlyDownloadError 로 사람 말 이유, [자세히] 에 원문)
|
||||
..error = errTail.where((l) => l.contains('ERROR')).join('\n').ifEmpty(errTail.join('\n'));
|
||||
}
|
||||
changed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,28 @@ class _DownloadRow extends StatelessWidget {
|
|||
final DownloadTask t;
|
||||
const _DownloadRow({required this.d, required this.t});
|
||||
|
||||
static final _small = TextButton.styleFrom(
|
||||
visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 6), minimumSize: const Size(0, 28));
|
||||
|
||||
/// 136: 실패 원문 (yt-dlp · aria2 가 남긴 그대로 - 복사해 물어볼 수 있게)
|
||||
static Future<void> _showDetail(BuildContext context, DownloadTask t) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(friendlyDownloadError(t.error ?? '')),
|
||||
content: SizedBox(
|
||||
width: 560,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// 서명 · 키가 든 주소 부분은 가린다
|
||||
SelectableText(redactUrlSecrets(t.source), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(redactUrlSecrets(t.error ?? ''), style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||
]),
|
||||
),
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('닫기')))],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (label, color) = switch (t.state) {
|
||||
|
|
@ -286,19 +308,33 @@ class _DownloadRow extends StatelessWidget {
|
|||
Text(t.title, maxLines: 1, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
const SizedBox(height: 4),
|
||||
if (t.state != DownloadState.cancelled) DownloadProgressBar(t: t),
|
||||
// 136: 실패하면 진행 막대 ("준비 중…") 를 지우고 실패 · 이유 (사람 말) · [자세히] · [다시 시도]
|
||||
if (t.state != DownloadState.cancelled && t.state != DownloadState.failed) DownloadProgressBar(t: t),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
[
|
||||
label,
|
||||
if (t.speed.isNotEmpty) t.speed,
|
||||
if (t.eta.isNotEmpty) t.eta.contains(RegExp(r'\d')) ? trf('남은 시간 {0}', [t.eta]) : t.eta, // 숫자가 없으면 "합치는 중" 같은 단계
|
||||
if (t.error != null) t.error!.split('\n').first,
|
||||
].join(' · '),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: t.error != null ? JjColors.danger : color),
|
||||
),
|
||||
if (t.state == DownloadState.failed)
|
||||
Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [
|
||||
Text('$label · ${friendlyDownloadError(t.error ?? '')}',
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
if ((t.error ?? '').trim().isNotEmpty)
|
||||
TextButton(
|
||||
style: _small,
|
||||
onPressed: () => _showDetail(context, t),
|
||||
child: Text(tr('자세히')),
|
||||
),
|
||||
TextButton(style: _small, onPressed: () => d.resume([t]), child: Text(tr('다시 시도'))),
|
||||
])
|
||||
else
|
||||
Text(
|
||||
[
|
||||
label,
|
||||
if (t.speed.isNotEmpty) t.speed,
|
||||
if (t.eta.isNotEmpty) t.eta.contains(RegExp(r'\d')) ? trf('남은 시간 {0}', [t.eta]) : t.eta, // 숫자가 없으면 "합치는 중" 같은 단계
|
||||
if (t.error != null) redactUrlSecrets(t.error!.split('\n').first),
|
||||
].join(' · '),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: t.error != null ? JjColors.danger : color),
|
||||
),
|
||||
]),
|
||||
),
|
||||
IconButton(
|
||||
|
|
@ -350,7 +386,11 @@ class DownloadProgressBar extends StatelessWidget {
|
|||
// "45.3% · 166MB / 367MB" (크기를 모르면 퍼센트만)
|
||||
final size = downloadSizeText(t);
|
||||
final text = value == null
|
||||
? (t.state == DownloadState.queued || t.extra['waiting'] == true ? tr('대기 중') : tr('준비 중…'))
|
||||
? (t.state == DownloadState.failed
|
||||
? tr('실패') // 136: 실패한 것에 "준비 중…" 이 남지 않게 (작은 다운로드 상자 등)
|
||||
: t.state == DownloadState.queued || t.extra['waiting'] == true
|
||||
? tr('대기 중')
|
||||
: tr('준비 중…'))
|
||||
: '${(value * 100).toStringAsFixed(1)}%${size.isEmpty ? '' : ' · $size'}';
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/download_manager.dart';
|
||||
import 'package:jj_mkvmaker/app/i18n_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/core/download_detect.dart';
|
||||
import 'package:jj_mkvmaker/services/downloader.dart';
|
||||
import 'package:jj_mkvmaker/ui/downloads_page.dart';
|
||||
|
||||
class _Backend implements DownloadBackend {
|
||||
var starts = 0;
|
||||
@override
|
||||
DownloadKind get kind => DownloadKind.video;
|
||||
@override
|
||||
Future<(String, List<PlaylistEntry>)?> expandPlaylist(String url) async => null;
|
||||
@override
|
||||
Future<void> start(DownloadTask t, void Function() changed) async {
|
||||
starts++;
|
||||
t
|
||||
..state = DownloadState.failed
|
||||
..error = 'ERROR: [generic] Unable to download webpage: HTTP Error 404: Not Found '
|
||||
'(https://cdn.example.com/v.mp4?sig=SECRET123&expire=1)';
|
||||
changed();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> cancel(DownloadTask t) async {}
|
||||
@override
|
||||
Future<void> shutdown() async {}
|
||||
}
|
||||
|
||||
/// 136: 실패한 다운로드 줄 - 진행 막대 대신 실패 · 사람 말 이유 · [자세히] (원문) · [다시 시도]
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test('yt-dlp · aria2 원문 → 사람 말 이유', () {
|
||||
expect(friendlyDownloadError('ERROR: [generic] x: HTTP Error 404: Not Found'), '주소에 영상이 없습니다 (404)');
|
||||
expect(friendlyDownloadError('ERROR: [vimeo] 1: HTTP Error 403: Forbidden'), '로그인이 필요합니다 (403)');
|
||||
expect(friendlyDownloadError('ERROR: Unsupported URL: https://example.com/'), '지원하지 않는 사이트입니다');
|
||||
expect(friendlyDownloadError('ERROR: [generic] Unable to download webpage: <urlopen error [Errno 11001] getaddrinfo failed>'),
|
||||
'연결할 수 없습니다');
|
||||
expect(friendlyDownloadError('Network problem has occurred. cause:Connection refused (errorCode=6)'), '연결할 수 없습니다');
|
||||
expect(friendlyDownloadError('Resource not found (errorCode=3)'), '주소에 영상이 없습니다 (404)');
|
||||
expect(friendlyDownloadError('There is not enough disk space available (errorCode=9)'), '저장 공간이 모자랍니다');
|
||||
expect(friendlyDownloadError('ERROR: [youtube] x: Sign in to confirm you’re not a bot'), contains('로봇 확인'));
|
||||
expect(friendlyDownloadError('무언가 이상함'), '받지 못했습니다');
|
||||
});
|
||||
|
||||
test('화면 언어를 따른다 (59 와 같은 방식)', () async {
|
||||
await i18n.apply('ja', save: false);
|
||||
addTearDown(() => i18n.apply('ko', save: false));
|
||||
expect(friendlyDownloadError('HTTP Error 404: Not Found'), 'このアドレスに動画がありません (404)');
|
||||
});
|
||||
|
||||
test('서명 · 키가 든 주소 부분은 가린다', () {
|
||||
expect(redactUrlSecrets('x https://cdn.example.com/v.mp4?sig=AAA&expire=1 y'), 'x https://cdn.example.com/v.mp4?… y');
|
||||
expect(redactUrlSecrets('(https://user:pw@nas.local/dav/a.mkv#t=1)'), '(https://nas.local/dav/a.mkv?…)');
|
||||
expect(redactUrlSecrets('https://www.youtube.com/watch'), 'https://www.youtube.com/watch');
|
||||
});
|
||||
|
||||
testWidgets('실패한 줄: 진행 막대 ("준비 중…") 없이 이유 · [자세히] 는 원문 (주소의 서명은 가림) · [다시 시도]', (tester) async {
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final b = _Backend();
|
||||
final d = DownloadManager(backends: [b], settings: () => AppSettings(), readClipboard: () async => null);
|
||||
final t = d.addPage('https://cdn.example.com/page')!..title = '영상';
|
||||
await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d)));
|
||||
for (var i = 0; i < 20 && t.state != DownloadState.failed; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
expect(t.state, DownloadState.failed);
|
||||
await tester.pump();
|
||||
expect(find.text('준비 중…'), findsNothing);
|
||||
expect(find.byType(DownloadProgressBar), findsNothing);
|
||||
expect(find.textContaining('주소에 영상이 없습니다 (404)'), findsOneWidget);
|
||||
expect(find.textContaining('HTTP Error'), findsNothing, reason: '원문은 [자세히] 에서만');
|
||||
|
||||
await tester.tap(find.text('자세히'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('HTTP Error 404'), findsOneWidget);
|
||||
expect(find.textContaining('SECRET123'), findsNothing, reason: '서명된 주소를 보이지 않음');
|
||||
await tester.tap(find.text('닫기'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(b.starts, 1);
|
||||
await tester.tap(find.text('다시 시도'));
|
||||
for (var i = 0; i < 20 && b.starts < 2; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
}
|
||||
expect(b.starts, 2, reason: '[다시 시도] 로 다시 받기 시작 (이 가짜 백엔드는 또 실패)');
|
||||
d.dispose();
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue