환경 설정 > 저장 공간 정리: 작업 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트 남은 파일 · 지난 기록 (진행 중인 것은 건너뜀). AI 자막 임시 폴더가 남던 것 수정

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-04 18:09:31 +09:00
parent 61da57aea1
commit d4ab2f87b7
8 changed files with 542 additions and 2 deletions

View File

@ -49,8 +49,10 @@
"AI 자막 중단: {0}": "AI subtitles stopped: {0}",
"AI 자막 취소: {0}": "AI subtitles cancelled: {0}",
"AI 자막: {0}": "AI subtitles: {0}",
"AI 자막용 음성 (wav) · 자막 변환 사본 · 미리보기 그림 등 작업하다 남은 파일": "Leftovers from jobs: audio for AI subtitles (wav) · converted subtitle copies · preview images",
"AI 자막을 만들고 이어서 MKV 로 만듭니다": "makes AI subtitles and then makes the MKV",
"AI 자막을 만듭니다": "makes AI subtitles",
"AI 작업 중이라 이번에는 건너뜁니다": "Skipped this time because an AI job is running",
"API 키": "API key",
"API 키 (필수, 무료)": "API key (required, free)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)",
@ -82,6 +84,7 @@
"MKV 만들기": "Make MKV",
"MKV 만들기 (대기열에 추가)": "Make MKV (add to queue)",
"MKV 만들기 {0}개": "Make {0} MKVs",
"MKV 만들기 · AI 자막 작업 중이라 이번에는 건너뜁니다": "Skipped this time because a Make MKV · AI subtitle job is running",
"MKV 만들기 · AI 자막 작업이 끝난 뒤 업데이트하세요.": "Update after the Make MKV · AI subtitle jobs finish.",
"MKV 만들기 · 인코딩 · 음성 추출": "Make MKV · encoding · audio extraction",
"MKV 만들기 목록에 동영상 {0}개를 추가했습니다.": "Added {0} videos to the Make MKV list.",
@ -159,6 +162,7 @@
"{0}: 이미 이동 폴더에 있습니다": "{0}: already in the move folder",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: not moved because it is being processed",
"{0}개": "{0}",
"{0}개 · {1}": "{0} · {1}",
"{0}개 · 주소 {1}개": "{0} · {1} links",
"{0}개 선택": "{0} selected",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "Imported {0} into \"Other bookmarks\".",
@ -233,6 +237,7 @@
"내려받기 실패 ({0})": "Download failed ({0})",
"내려받기 실패 ({0}): {1}": "Download failed ({0}): {1}",
"내려받는 중": "Downloading",
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "Interrupted model downloads (.part) · leftovers from conversion (.tmp)",
"내려받은 Chrome 엔진 지우기": "Delete the downloaded Chrome engine",
"내보냈습니다: {0}": "Exported: {0}",
"내용": "Content",
@ -360,10 +365,14 @@
"바로 재생 (기본)": "Play right away (default)",
"바로 전 작업: {0}": "Previous job: {0}",
"받는 중": "Downloading",
"받는 중이거나 일시정지한 다운로드가 있어 건너뜁니다 (이어받기가 깨지지 않게)": "Skipped because downloads are running or paused (so they can resume)",
"받는 중인 다운로드 {0}개": "{0} downloads in progress",
"받는 중인 다운로드가 없습니다.": "Nothing is downloading.",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "Download size: about {0}MB · downloaded files · AI models · settings are kept",
"받다 만 AI 모델": "Unfinished AI models",
"받다 만 다운로드": "Unfinished downloads",
"받은 동영상을 편집 목록에 추가: {0}": "Added the received video to the edit list: {0}",
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "Temporary files left from installing updates · required programs · the Chrome engine",
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "Translate received subtitles to Korean and add them too (when there are no Korean subtitles)",
"받은 파일에 libcef.dll 이 없습니다.": "The downloaded file has no libcef.dll.",
"받은 파일이 손상되었거나 다른 파일입니다 (SHA256 불일치). 설치를 중단했습니다.": "The downloaded file is corrupt or a different file (SHA256 mismatch). Installation was stopped.",
@ -480,6 +489,7 @@
"언어: 미지정": "Language: unspecified",
"언어를 더하지 못했습니다: {0}": "Could not add the language: {0}",
"업데이트": "Update",
"업데이트 · 설치하고 남은 파일": "Update · install leftovers",
"업데이트 실패: {0}": "Update failed: {0}",
"없음": "None",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "Drop here to add to the Make MKV video list",
@ -497,6 +507,7 @@
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "e.g. Ctrl+Shift+X (Ctrl·Shift·Alt·Win + letter·digit·F1~F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "Cleaned up program files of the old layout ({0}): {1}",
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "You have used all of today's subtitle downloads. ({0})",
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "Old job log files (app.log.1 etc.). The current log is kept",
"오른쪽 90°": "Right 90°",
"오픈 소스 라이선스": "Open source licenses",
"올린 이 {0}": "Uploaded by {0}",
@ -585,6 +596,7 @@
"일반": "General",
"일본어": "Japanese",
"일시정지": "Pause",
"임시 파일 · 남은 조각 정리": "Clean temporary files · leftovers",
"자동": "Auto",
"자동 감지": "Auto-detect",
"자동 설치를 할 수 없는 위치입니다. 페이지에서 직접 받아 주세요.": "Automatic installation is not possible here. Please download it from the page yourself.",
@ -623,6 +635,7 @@
"작업": "Jobs",
"작업 기록 보기": "Show job log",
"작업 기록 숨기기": "Hide job log",
"작업 임시 파일": "Job temporary files",
"작업 중": "Working",
"작업 중지: {0}{1}": "Job stopped: {0}{1}",
"작업 취소": "Cancel job",
@ -630,6 +643,7 @@
"작업 현황 닫기": "Close job status",
"작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "Show job status · split screen (Ctrl+Shift+J)",
"작업 확인 (진행 중인 작업 없음)": "Job check (no jobs in progress)",
"작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "Finds and deletes temporary files left from jobs · unfinished downloads (.part etc.) · unfinished AI models · update leftovers · old job logs. Downloaded videos · made MKVs · subtitles are not deleted, and anything used by running jobs · downloads is skipped",
"작은 용량": "Small size",
"잘못된 varint (위치 {0})": "Invalid varint (position {0})",
"재개": "Resume",
@ -647,6 +661,8 @@
"저장": "Save",
"저장 (MKV 반영)": "Save (apply to MKV)",
"저장 (MKV 반영) *": "Save (apply to MKV) *",
"저장 공간 정리": "Storage cleanup",
"저장 공간 정리: 파일 {0}개 · {1}": "Storage cleanup: {0} files · {1}",
"저장 실패: {0}": "Save failed: {0}",
"저장 위치": "Save locations",
"저장: {0}": "Save: {0}",
@ -662,6 +678,12 @@
"전체 화면": "Full screen",
"전체 화면 (F)": "Full screen (F)",
"점검": "Check",
"정리": "Clean up",
"정리 ({0})": "Clean up ({0})",
"정리…": "Clean up…",
"정리할 파일을 찾는 중…": "Looking for files to clean up…",
"정리할 파일이 없었습니다.": "There was nothing to clean up.",
"정리했습니다: 파일 {0}개 · {1}": "Cleaned up: {0} files · {1}",
"정사각 1:1": "Square 1:1",
"정지": "Stop",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "Not set: you choose it the first time you press [Move] in the MKV list",
@ -702,6 +724,7 @@
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "Current version: v{0} → new version: v{1}",
"지금: {0}": "Now: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.",
"지난 작업 기록": "Old job logs",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "Unsupported protobuf format (wire type {0}, position {1})",
"지원하지 않는 문자셋: {0}": "Unsupported charset: {0}",
"직접 입력": "Custom",
@ -731,6 +754,7 @@
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "Added: {0} ({1} embedded subtitles, {2} subtitles in the same folder)",
"출력: {0}": "Output: {0}",
"취소": "Cancel",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": ".part · .ytdl · .aria2 · unmerged fragments left by cancelled · failed downloads (jj_yt-dlp · jj_aria2)",
"취소됨": "Cancelled",
"취소했습니다.": "Cancelled.",
"켜져 있는 창에서 (기본)": "In the open window (default)",

View File

@ -49,8 +49,10 @@
"AI 자막 중단: {0}": "AI 字幕中断: {0}",
"AI 자막 취소: {0}": "AI 字幕キャンセル: {0}",
"AI 자막: {0}": "AI 字幕: {0}",
"AI 자막용 음성 (wav) · 자막 변환 사본 · 미리보기 그림 등 작업하다 남은 파일": "作業の残り: AI 字幕用の音声(wav)・字幕変換のコピー・プレビュー画像など",
"AI 자막을 만들고 이어서 MKV 로 만듭니다": "AI 字幕を作成し、続けて MKV を作成します",
"AI 자막을 만듭니다": "AI 字幕を作成します",
"AI 작업 중이라 이번에는 건너뜁니다": "AI 作業中のため今回はスキップします",
"API 키": "API キー",
"API 키 (필수, 무료)": "API キー(必須・無料)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)",
@ -82,6 +84,7 @@
"MKV 만들기": "MKV 作成",
"MKV 만들기 (대기열에 추가)": "MKV 作成(キューに追加)",
"MKV 만들기 {0}개": "MKV 作成 {0}個",
"MKV 만들기 · AI 자막 작업 중이라 이번에는 건너뜁니다": "MKV 作成・AI 字幕の作業中のため今回はスキップします",
"MKV 만들기 · AI 자막 작업이 끝난 뒤 업데이트하세요.": "MKV 作成・AI 字幕の作業が終わってから更新してください。",
"MKV 만들기 · 인코딩 · 음성 추출": "MKV 作成・エンコード・音声抽出",
"MKV 만들기 목록에 동영상 {0}개를 추가했습니다.": "MKV 作成リストに動画を {0}個追加しました。",
@ -159,6 +162,7 @@
"{0}: 이미 이동 폴더에 있습니다": "{0}: 既に移動フォルダーにあります",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 作業中のため移動しませんでした",
"{0}개": "{0}個",
"{0}개 · {1}": "{0}個・{1}",
"{0}개 · 주소 {1}개": "{0}個・URL {1}個",
"{0}개 선택": "{0}個選択",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "{0}個を「その他のブックマーク」に取り込みました。",
@ -233,6 +237,7 @@
"내려받기 실패 ({0})": "ダウンロード失敗({0})",
"내려받기 실패 ({0}): {1}": "ダウンロード失敗({0}): {1}",
"내려받는 중": "ダウンロード中",
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "ダウンロードが途切れたモデル(.part)・変換の残り(.tmp)",
"내려받은 Chrome 엔진 지우기": "ダウンロードした Chrome エンジンを削除",
"내보냈습니다: {0}": "書き出しました: {0}",
"내용": "内容",
@ -360,10 +365,14 @@
"바로 재생 (기본)": "すぐ再生(既定)",
"바로 전 작업: {0}": "直前の作業: {0}",
"받는 중": "ダウンロード中",
"받는 중이거나 일시정지한 다운로드가 있어 건너뜁니다 (이어받기가 깨지지 않게)": "ダウンロード中または一時停止中のものがあるためスキップします(再開できるように)",
"받는 중인 다운로드 {0}개": "ダウンロード中 {0}個",
"받는 중인 다운로드가 없습니다.": "ダウンロード中のものはありません。",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "ダウンロードサイズ: 約 {0}MB・ダウンロードしたファイル・AI モデル・設定はそのまま",
"받다 만 AI 모델": "ダウンロード途中の AI モデル",
"받다 만 다운로드": "ダウンロード途中のファイル",
"받은 동영상을 편집 목록에 추가: {0}": "受け取った動画を編集リストに追加: {0}",
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "更新・必須プログラム・Chrome エンジンのインストールで残った一時ファイル",
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "受け取った字幕を韓国語に翻訳して一緒に追加(韓国語字幕がないとき)",
"받은 파일에 libcef.dll 이 없습니다.": "ダウンロードしたファイルに libcef.dll がありません。",
"받은 파일이 손상되었거나 다른 파일입니다 (SHA256 불일치). 설치를 중단했습니다.": "ダウンロードしたファイルが壊れているか別のファイルです(SHA256 不一致)。インストールを中止しました。",
@ -480,6 +489,7 @@
"언어: 미지정": "言語: 未指定",
"언어를 더하지 못했습니다: {0}": "言語を追加できませんでした: {0}",
"업데이트": "更新",
"업데이트 · 설치하고 남은 파일": "更新・インストールの残り",
"업데이트 실패: {0}": "更新失敗: {0}",
"없음": "なし",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "ここにドロップすると MKV 作成の動画リストに追加します",
@ -497,6 +507,7 @@
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl・Shift・Alt・Win + 英字・数字・F1〜F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "旧構成のプログラムファイルを整理({0}個): {1}",
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今日ダウンロードできる字幕数をすべて使いました。({0})",
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "古い作業記録ファイル(app.log.1 など)。現在の記録は残します",
"오른쪽 90°": "右 90°",
"오픈 소스 라이선스": "オープンソースライセンス",
"올린 이 {0}": "投稿者 {0}",
@ -585,6 +596,7 @@
"일반": "一般",
"일본어": "日本語",
"일시정지": "一時停止",
"임시 파일 · 남은 조각 정리": "一時ファイル・残った断片を整理",
"자동": "自動",
"자동 감지": "自動検出",
"자동 설치를 할 수 없는 위치입니다. 페이지에서 직접 받아 주세요.": "自動インストールできない場所です。ページから直接ダウンロードしてください。",
@ -623,6 +635,7 @@
"작업": "作業",
"작업 기록 보기": "作業記録を表示",
"작업 기록 숨기기": "作業記録を隠す",
"작업 임시 파일": "作業の一時ファイル",
"작업 중": "作業中",
"작업 중지: {0}{1}": "作業中止: {0}{1}",
"작업 취소": "作業をキャンセル",
@ -630,6 +643,7 @@
"작업 현황 닫기": "作業状況を閉じる",
"작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "作業状況を表示・画面分割(Ctrl+Shift+J)",
"작업 확인 (진행 중인 작업 없음)": "作業確認(進行中の作業なし)",
"작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "作業で残った一時ファイル・ダウンロード途中のファイル(.part など)・途中の AI モデル・更新の残り・古い作業記録を探して削除します。ダウンロードした動画・作成した MKV・字幕は削除せず、進行中の作業・ダウンロードが使っているものはスキップします",
"작은 용량": "小さい容量",
"잘못된 varint (위치 {0})": "不正な varint(位置 {0})",
"재개": "再開",
@ -647,6 +661,8 @@
"저장": "保存",
"저장 (MKV 반영)": "保存(MKV に反映)",
"저장 (MKV 반영) *": "保存(MKV に反映)*",
"저장 공간 정리": "ストレージの整理",
"저장 공간 정리: 파일 {0}개 · {1}": "ストレージの整理: ファイル {0}個・{1}",
"저장 실패: {0}": "保存失敗: {0}",
"저장 위치": "保存先",
"저장: {0}": "保存: {0}",
@ -662,6 +678,12 @@
"전체 화면": "全画面",
"전체 화면 (F)": "全画面(F)",
"점검": "点検",
"정리": "整理",
"정리 ({0})": "整理({0})",
"정리…": "整理…",
"정리할 파일을 찾는 중…": "整理するファイルを探しています…",
"정리할 파일이 없었습니다.": "整理するファイルはありませんでした。",
"정리했습니다: 파일 {0}개 · {1}": "整理しました: ファイル {0}個・{1}",
"정사각 1:1": "正方形 1:1",
"정지": "停止",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未設定: MKV リストで[移動]を初めて押したときに選びます",
@ -702,6 +724,7 @@
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "現在のバージョン: v{0} → 新しいバージョン: v{1}",
"지금: {0}": "現在: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。",
"지난 작업 기록": "過去の作業記録",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "対応していない protobuf 形式(wire type {0}、位置 {1})",
"지원하지 않는 문자셋: {0}": "対応していない文字コード: {0}",
"직접 입력": "直接入力",
@ -731,6 +754,7 @@
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "追加: {0}(内蔵字幕 {1}個、同じフォルダーの字幕 {2}個)",
"출력: {0}": "出力: {0}",
"취소": "キャンセル",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "キャンセル・失敗したダウンロードが残した .part・.ytdl・.aria2・結合前の断片(jj_yt-dlp・jj_aria2)",
"취소됨": "キャンセル済み",
"취소했습니다.": "キャンセルしました。",
"켜져 있는 창에서 (기본)": "開いているウィンドウで(既定)",

View File

@ -49,8 +49,10 @@
"AI 자막 중단: {0}": "AI 字幕中断: {0}",
"AI 자막 취소: {0}": "AI 字幕已取消: {0}",
"AI 자막: {0}": "AI 字幕: {0}",
"AI 자막용 음성 (wav) · 자막 변환 사본 · 미리보기 그림 등 작업하다 남은 파일": "任务残留: AI 字幕用音频(wav)· 字幕转换副本 · 预览图片等",
"AI 자막을 만들고 이어서 MKV 로 만듭니다": "生成 AI 字幕后接着制作 MKV",
"AI 자막을 만듭니다": "生成 AI 字幕",
"AI 작업 중이라 이번에는 건너뜁니다": "正在进行 AI 任务,本次跳过",
"API 키": "API 密钥",
"API 키 (필수, 무료)": "API 密钥(必填,免费)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)",
@ -82,6 +84,7 @@
"MKV 만들기": "制作 MKV",
"MKV 만들기 (대기열에 추가)": "制作 MKV(加入队列)",
"MKV 만들기 {0}개": "制作 MKV {0} 个",
"MKV 만들기 · AI 자막 작업 중이라 이번에는 건너뜁니다": "正在制作 MKV · AI 字幕,本次跳过",
"MKV 만들기 · AI 자막 작업이 끝난 뒤 업데이트하세요.": "请在制作 MKV · AI 字幕任务完成后再更新。",
"MKV 만들기 · 인코딩 · 음성 추출": "制作 MKV · 编码 · 音频提取",
"MKV 만들기 목록에 동영상 {0}개를 추가했습니다.": "已向制作 MKV 列表添加 {0} 个视频。",
@ -159,6 +162,7 @@
"{0}: 이미 이동 폴더에 있습니다": "{0}: 已在移动文件夹中",
"{0}: 작업 중이라 옮기지 않았습니다": "{0}: 正在处理,未移动",
"{0}개": "{0} 个",
"{0}개 · {1}": "{0} 个 · {1}",
"{0}개 · 주소 {1}개": "{0} 个 · 地址 {1} 个",
"{0}개 선택": "已选 {0} 个",
"{0}개를 \"기타 즐겨찾기\" 에 가져왔습니다.": "已将 {0} 个导入“其他书签”。",
@ -233,6 +237,7 @@
"내려받기 실패 ({0})": "下载失败({0})",
"내려받기 실패 ({0}): {1}": "下载失败({0}): {1}",
"내려받는 중": "正在下载",
"내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)": "下载中断的模型(.part)· 转换残留(.tmp)",
"내려받은 Chrome 엔진 지우기": "删除已下载的 Chrome 引擎",
"내보냈습니다: {0}": "已导出: {0}",
"내용": "内容",
@ -360,10 +365,14 @@
"바로 재생 (기본)": "立即播放(默认)",
"바로 전 작업: {0}": "上一个任务: {0}",
"받는 중": "正在下载",
"받는 중이거나 일시정지한 다운로드가 있어 건너뜁니다 (이어받기가 깨지지 않게)": "有正在下载或已暂停的下载,已跳过(以便继续下载)",
"받는 중인 다운로드 {0}개": "正在下载 {0} 个",
"받는 중인 다운로드가 없습니다.": "没有正在进行的下载。",
"받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다": "下载大小: 约 {0}MB · 已下载的文件 · AI 模型 · 设置均保留",
"받다 만 AI 모델": "未下载完的 AI 模型",
"받다 만 다운로드": "未完成的下载",
"받은 동영상을 편집 목록에 추가: {0}": "已将收到的视频加入编辑列表: {0}",
"받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일": "安装更新 · 必需程序 · Chrome 引擎后残留的临时文件",
"받은 자막을 한국어로 번역해 함께 추가 (한국어 자막이 없을 때)": "将收到的字幕翻译成韩语并一并添加(没有韩语字幕时)",
"받은 파일에 libcef.dll 이 없습니다.": "下载的文件中没有 libcef.dll。",
"받은 파일이 손상되었거나 다른 파일입니다 (SHA256 불일치). 설치를 중단했습니다.": "下载的文件已损坏或不是正确的文件(SHA256 不匹配)。已停止安装。",
@ -480,6 +489,7 @@
"언어: 미지정": "语言: 未指定",
"언어를 더하지 못했습니다: {0}": "未能添加语言: {0}",
"업데이트": "更新",
"업데이트 · 설치하고 남은 파일": "更新 · 安装残留",
"업데이트 실패: {0}": "更新失败: {0}",
"없음": "无",
"여기에 놓으면 MKV 만들기의 동영상 목록에 추가합니다": "拖放到此处即可加入制作 MKV 的视频列表",
@ -497,6 +507,7 @@
"예: Ctrl+Shift+X (Ctrl·Shift·Alt·Win + 영문자·숫자·F1~F12)": "例: Ctrl+Shift+X(Ctrl·Shift·Alt·Win + 字母·数字·F1~F12)",
"예전 구조의 프로그램 파일 정리 ({0}개): {1}": "已清理旧结构的程序文件({0} 个): {1}",
"오늘 받을 수 있는 자막 수를 모두 사용했습니다. ({0})": "今天可下载的字幕数已用完。({0})",
"오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다": "旧的任务记录文件(app.log.1 等)。当前记录会保留",
"오른쪽 90°": "向右 90°",
"오픈 소스 라이선스": "开源许可",
"올린 이 {0}": "上传者 {0}",
@ -585,6 +596,7 @@
"일반": "常规",
"일본어": "日语",
"일시정지": "暂停",
"임시 파일 · 남은 조각 정리": "清理临时文件 · 残留碎片",
"자동": "自动",
"자동 감지": "自动检测",
"자동 설치를 할 수 없는 위치입니다. 페이지에서 직접 받아 주세요.": "此位置无法自动安装。请从页面手动下载。",
@ -623,6 +635,7 @@
"작업": "任务",
"작업 기록 보기": "显示任务记录",
"작업 기록 숨기기": "隐藏任务记录",
"작업 임시 파일": "任务临时文件",
"작업 중": "处理中",
"작업 중지: {0}{1}": "任务已停止: {0}{1}",
"작업 취소": "取消任务",
@ -630,6 +643,7 @@
"작업 현황 닫기": "关闭任务状态",
"작업 현황 보기 · 화면 분할 (Ctrl+Shift+J)": "显示任务状态 · 分屏(Ctrl+Shift+J)",
"작업 확인 (진행 중인 작업 없음)": "任务检查(没有进行中的任务)",
"작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · 업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. 받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다": "查找并删除任务残留的临时文件 · 未完成的下载(.part 等)· 未下载完的 AI 模型 · 更新残留 · 旧任务记录。不会删除已下载的视频 · 制作的 MKV · 字幕,正在进行的任务 · 下载所用的文件会跳过",
"작은 용량": "小体积",
"잘못된 varint (위치 {0})": "无效的 varint(位置 {0})",
"재개": "继续",
@ -647,6 +661,8 @@
"저장": "保存",
"저장 (MKV 반영)": "保存(应用到 MKV)",
"저장 (MKV 반영) *": "保存(应用到 MKV)*",
"저장 공간 정리": "存储空间清理",
"저장 공간 정리: 파일 {0}개 · {1}": "存储空间清理: 文件 {0} 个 · {1}",
"저장 실패: {0}": "保存失败: {0}",
"저장 위치": "保存位置",
"저장: {0}": "保存: {0}",
@ -662,6 +678,12 @@
"전체 화면": "全屏",
"전체 화면 (F)": "全屏(F)",
"점검": "检查",
"정리": "清理",
"정리 ({0})": "清理({0})",
"정리…": "清理…",
"정리할 파일을 찾는 중…": "正在查找可清理的文件…",
"정리할 파일이 없었습니다.": "没有可清理的文件。",
"정리했습니다: 파일 {0}개 · {1}": "已清理: 文件 {0} 个 · {1}",
"정사각 1:1": "正方形 1:1",
"정지": "停止",
"정하지 않음: MKV 목록에서 [이동] 을 처음 누를 때 고릅니다": "未设置: 首次在 MKV 列表中点击[移动]时选择",
@ -702,6 +724,7 @@
"지금 쓰는 버전: v{0} → 새 버전: v{1}": "当前版本: v{0} → 新版本: v{1}",
"지금: {0}": "当前: {0}",
"지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。",
"지난 작업 기록": "旧任务记录",
"지원하지 않는 protobuf 형식 (wire type {0}, 위치 {1})": "不支持的 protobuf 格式(wire type {0},位置 {1})",
"지원하지 않는 문자셋: {0}": "不支持的字符集: {0}",
"직접 입력": "手动输入",
@ -731,6 +754,7 @@
"추가: {0} (내장 자막 {1}개, 같은 폴더 자막 {2}개)": "已添加: {0}(内嵌字幕 {1} 个,同文件夹字幕 {2} 个)",
"출력: {0}": "输出: {0}",
"취소": "取消",
"취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)": "取消 · 失败的下载留下的 .part · .ytdl · .aria2 · 合并前的碎片文件(jj_yt-dlp · jj_aria2)",
"취소됨": "已取消",
"취소했습니다.": "已取消。",
"켜져 있는 창에서 (기본)": "在已打开的窗口中(默认)",

View File

@ -885,7 +885,12 @@ class AppController extends ChangeNotifier {
..message = e is MediaToolException ? e.message : '$e';
_log(trf('AI 자막 실패: {0}\n{1}', [v.fileName, v.message]));
} finally {
// 작업 폴더째 지운다 (음성인식이 만드는 변환 사본 audio.wav.wav 도 함께)
try {
await Directory(tmp).delete(recursive: true);
} catch (_) {
await storage.delete(wav);
}
notifyListeners();
}
}

185
lib/app/cleanup.dart Normal file
View File

@ -0,0 +1,185 @@
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../l10n/tr.dart';
import 'app_controller.dart';
import 'download_manager.dart';
/// 정리할 파일 · 폴더 하나
class CleanupItem {
final String path;
final int bytes;
final bool isDir;
const CleanupItem(this.path, this.bytes, {this.isDir = false});
}
/// 정리 묶음 (작업 임시 파일 · 받다 만 다운로드 …)
class CleanupGroup {
final String id;
final String title;
final String hint;
final List<CleanupItem> items;
/// 작업 중이라 이번에는 건너뛰는 이유 (있으면 지우지 않음)
final String? skipped;
CleanupGroup(this.id, this.title, this.hint, this.items, {this.skipped});
int get bytes => items.fold(0, (a, i) => a + i.bytes);
}
/// 저장 공간 정리 (환경 설정 > 저장 공간 정리): 작업하다 남은 임시 파일 · 받다 만 다운로드 조각 (.part 등) ·
/// 받다 만 AI 모델 · 업데이트 · 설치하고 남은 파일 · 지난 작업 기록을 찾아 지운다.
/// 진행 중인 작업 · 다운로드가 쓰는 곳은 건너뛴다 (이어받기 · 작업이 깨지지 않게).
class Cleaner {
final AppController c;
final DownloadManager? downloads;
/// 앱 설정 폴더 (테스트에서 바꿔 끼움)
final String? dataDir;
/// 시스템 임시 폴더 (Windows 의 업데이트 · 도구 설치 남은 파일. 테스트에서 바꿔 끼움)
final String? systemTemp;
Cleaner(this.c, {this.downloads, this.dataDir, this.systemTemp});
/// 받다 만 다운로드 조각: yt-dlp (.part · .ytdl · .part-Frag · .temp. · 합치기 전 영상.f137.mp4) · aria2 (.aria2)
static final downloadLeftover =
RegExp(r'\.part$|\.part-frag\d+|\.ytdl$|\.temp\.|\.f\d+\.[a-z0-9]+$|\.aria2$', caseSensitive: false);
static int _size(FileSystemEntity e) {
try {
if (e is File) return e.lengthSync();
if (e is Directory) {
var n = 0;
for (final f in e.listSync(recursive: true, followLinks: false)) {
if (f is File) n += f.lengthSync();
}
return n;
}
} catch (_) {}
return 0;
}
static List<FileSystemEntity> _list(String dir, {bool recursive = false}) {
try {
final d = Directory(dir);
if (!d.existsSync()) return const [];
return d.listSync(recursive: recursive, followLinks: false);
} catch (_) {
return const [];
}
}
CleanupItem _item(FileSystemEntity e) => CleanupItem(e.path, _size(e), isDir: e is Directory);
/// 정리할 것 찾기
Future<List<CleanupGroup>> scan() async {
final working = c.busy;
final downloading = (downloads?.activeCount ?? 0) > 0;
final data = dataDir ?? (await getApplicationSupportDirectory()).path;
final groups = <CleanupGroup>[];
// 1. 작업 임시 파일: 앱 임시 폴더 전체 + 설정 파일을 저장하다 남은 이름.12345.tmp
final tmpDir = await c.services.storage.tempDirectory();
groups.add(CleanupGroup(
'work',
tr('작업 임시 파일'),
tr('AI 자막용 음성 (wav) · 자막 변환 사본 · 미리보기 그림 등 작업하다 남은 파일'),
[
for (final e in _list(tmpDir)) _item(e),
for (final e in _list(data))
if (e is File && RegExp(r'\.\d+\.tmp$').hasMatch(e.path)) _item(e),
],
skipped: working ? tr('MKV 만들기 · AI 자막 작업 중이라 이번에는 건너뜁니다') : null,
));
// 2. 받다 만 다운로드 조각
final s = c.settings;
groups.add(CleanupGroup(
'download',
tr('받다 만 다운로드'),
tr('취소 · 실패한 다운로드가 남긴 .part · .ytdl · .aria2 · 합치기 전 조각 파일 (jj_yt-dlp · jj_aria2)'),
[
for (final dir in {s.ytDlpDir, s.aria2Dir})
for (final e in _list(dir, recursive: true))
if (e is File && downloadLeftover.hasMatch(p.basename(e.path))) _item(e),
],
skipped: downloading ? tr('받는 중이거나 일시정지한 다운로드가 있어 건너뜁니다 (이어받기가 깨지지 않게)') : null,
));
// 3. 받다 만 AI 모델 (.part) · 모델 변환하다 남은 .tmp
final models = await c.services.models.directory();
groups.add(CleanupGroup(
'models',
tr('받다 만 AI 모델'),
tr('내려받다 끊긴 모델 (.part) · 변환하다 남은 파일 (.tmp)'),
[
for (final e in _list(models, recursive: true))
if (e is File && (e.path.endsWith('.part') || e.path.endsWith('.tmp'))) _item(e),
],
skipped: working ? tr('AI 작업 중이라 이번에는 건너뜁니다') : null,
));
// 4. 업데이트 · 도구 설치하고 남은 파일 (Windows 임시 폴더)
final sysTmp = systemTemp ?? (Platform.isWindows ? Directory.systemTemp.path : null);
if (sysTmp != null) {
final mine = RegExp(r'^(jj_mkvmaker_update|jj_cef_|jj_tool_|jj_[\w.-]+\.download$|jj_mkvmaker_update\.(ps1|log)$)');
groups.add(CleanupGroup(
'update',
tr('업데이트 · 설치하고 남은 파일'),
tr('받은 업데이트 · 필수 프로그램 · Chrome 엔진을 설치하고 남은 임시 파일'),
[
for (final e in _list(sysTmp))
if (mine.hasMatch(p.basename(e.path))) _item(e),
],
));
}
// 5. 지난 작업 기록 (지금 쓰는 app.log 는 남김)
final log = c.logFile;
if (log != null) {
groups.add(CleanupGroup(
'logs',
tr('지난 작업 기록'),
tr('오래된 작업 기록 파일 (app.log.1 등). 지금 쓰는 기록은 남깁니다'),
[
for (final e in _list(p.dirname(log)))
if (e is File && !p.equals(e.path, log)) _item(e),
],
));
}
return groups;
}
/// 고른 묶음 지우기 (건너뛴 묶음은 지우지 않음). (지운 개수, 비운 용량)
Future<(int, int)> clean(Iterable<CleanupGroup> groups) async {
var n = 0, bytes = 0;
for (final g in groups) {
if (g.skipped != null) continue;
for (final i in g.items) {
try {
if (i.isDir) {
await Directory(i.path).delete(recursive: true);
} else {
await File(i.path).delete();
}
n++;
bytes += i.bytes;
} catch (_) {
// 쓰는 중이거나 지울 수 없는 파일은 그대로
}
}
}
return (n, bytes);
}
/// "12.3 MB" · "850 KB"
static String sizeText(int bytes) {
if (bytes >= 1024 * 1024 * 1024) return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
if (bytes >= 1024 * 1024) return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
if (bytes >= 1024) return '${(bytes / 1024).round()} KB';
return '$bytes B';
}
}

114
lib/ui/cleanup_dialog.dart Normal file
View File

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import '../app/app_controller.dart';
import '../app/cleanup.dart';
import '../app/download_manager.dart';
import '../l10n/tr.dart';
import 'theme.dart';
/// 저장 공간 정리: 찾은 묶음별 크기를 보여 주고, 고른 것만 지운다
Future<void> showCleanup(BuildContext context, AppController c, DownloadManager? downloads) async {
final cleaner = Cleaner(c, downloads: downloads);
final messenger = ScaffoldMessenger.maybeOf(context);
final result = await showDialog<(int, int)>(
context: context,
builder: (_) => _CleanupDialog(cleaner: cleaner),
);
if (result == null) return;
final (n, bytes) = result;
c.note(trf('저장 공간 정리: 파일 {0}개 · {1}', [n, Cleaner.sizeText(bytes)]));
messenger?.showSnackBar(SnackBar(
content: Text(n == 0 ? tr('정리할 파일이 없었습니다.') : trf('정리했습니다: 파일 {0}개 · {1}', [n, Cleaner.sizeText(bytes)]))));
}
class _CleanupDialog extends StatefulWidget {
final Cleaner cleaner;
const _CleanupDialog({required this.cleaner});
@override
State<_CleanupDialog> createState() => _CleanupDialogState();
}
class _CleanupDialogState extends State<_CleanupDialog> {
List<CleanupGroup>? _groups;
final _picked = <String>{};
bool _cleaning = false;
@override
void initState() {
super.initState();
widget.cleaner.scan().then((g) {
if (!mounted) return;
setState(() {
_groups = g;
// 처음에는 지울 수 있고 무언가 있는 묶음을 모두 고른 상태로
_picked.addAll([for (final x in g) if (x.skipped == null && x.items.isNotEmpty) x.id]);
});
});
}
int get _total => [
for (final g in _groups ?? const <CleanupGroup>[])
if (_picked.contains(g.id)) g.bytes,
].fold(0, (a, b) => a + b);
@override
Widget build(BuildContext context) {
final groups = _groups;
return AlertDialog(
title: Row(children: [
const Icon(Icons.cleaning_services_outlined, color: JjColors.accent),
const SizedBox(width: 10),
Text(tr('저장 공간 정리')),
]),
content: SizedBox(
width: 560,
child: groups == null
? Padding(
padding: const EdgeInsets.all(24),
child: Row(children: [
const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
const SizedBox(width: 12),
Text(tr('정리할 파일을 찾는 중…')),
]),
)
: ListView(shrinkWrap: true, children: [
for (final g in groups)
CheckboxListTile(
value: g.skipped == null && _picked.contains(g.id),
onChanged: g.skipped != null || g.items.isEmpty || _cleaning
? null
: (v) => setState(() => v! ? _picked.add(g.id) : _picked.remove(g.id)),
title: Row(children: [
Expanded(child: Text(g.title)),
Text(
g.items.isEmpty ? tr('없음') : trf('{0}개 · {1}', [g.items.length, Cleaner.sizeText(g.bytes)]),
style: const TextStyle(fontSize: 13, color: JjColors.textDim),
),
]),
subtitle: Text(g.skipped ?? g.hint,
style: TextStyle(fontSize: 12, color: g.skipped != null ? Colors.amber : JjColors.textDim)),
),
]),
),
actions: [
TextButton(onPressed: _cleaning ? null : () => Navigator.pop(context), child: Text(tr('닫기'))),
FilledButton.icon(
onPressed: groups == null || _picked.isEmpty || _total == 0 && !_hasItems || _cleaning
? null
: () async {
setState(() => _cleaning = true);
final r = await widget.cleaner.clean(groups.where((g) => _picked.contains(g.id)));
if (context.mounted) Navigator.pop(context, r);
},
icon: _cleaning
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Icon(Icons.delete_sweep_outlined, size: 18),
label: Text(groups == null ? tr('정리') : trf('정리 ({0})', [Cleaner.sizeText(_total)])),
),
],
);
}
bool get _hasItems => (_groups ?? const []).any((g) => _picked.contains(g.id) && g.items.isNotEmpty);
}

View File

@ -19,6 +19,7 @@ import 'folder_picker.dart';
import 'language_settings.dart';
import 'app_actions.dart';
import 'cef_setup.dart';
import 'cleanup_dialog.dart';
import 'theme.dart';
import '../l10n/tr.dart';
@ -652,6 +653,20 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
]),
_group('cleanup', Icons.cleaning_services_outlined, tr('저장 공간 정리'), [
ListTile(
title: Text(tr('임시 파일 · 남은 조각 정리')),
subtitle: Text(tr('작업하다 남은 임시 파일 · 받다 만 다운로드 (.part 등) · 받다 만 AI 모델 · '
'업데이트하고 남은 파일 · 지난 작업 기록을 찾아 지웁니다. '
'받은 동영상 · 만든 MKV · 자막은 지우지 않고, 진행 중인 작업 · 다운로드가 쓰는 것은 건너뜁니다')),
isThreeLine: true,
trailing: FilledButton.icon(
onPressed: () => showCleanup(context, c, AppScope.maybeOf(context)?.downloads),
icon: const Icon(Icons.cleaning_services_outlined, size: 18),
label: Text(tr('정리…')),
),
),
]),
_group('about', Icons.info_outline, tr('프로그램 정보'), [
if (desk) ...[
ListTile(
@ -693,7 +708,9 @@ class _SettingsPageState extends State<SettingsPage> {
/// 묶음 이름 → 위치 (바로가기 버튼으로 그 묶음까지 스크롤)
final _groupKeys = <String, GlobalKey>{};
static const _groupIds = ['general', 'display', 'mkv', 'subtitle', 'play', 'browser', 'download', 'run', 'about'];
static const _groupIds = [
'general', 'display', 'mkv', 'subtitle', 'play', 'browser', 'download', 'run', 'cleanup', 'about', //
];
String _groupTitle(String id) => switch (id) {
'general' => tr('일반'),
@ -704,6 +721,7 @@ class _SettingsPageState extends State<SettingsPage> {
'browser' => tr('웹 브라우저'),
'download' => tr('다운로드'),
'run' => tr('실행 · 종료'),
'cleanup' => tr('저장 공간 정리'),
_ => tr('프로그램 정보'),
};

146
test/cleanup_test.dart Normal file
View File

@ -0,0 +1,146 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/cleanup.dart';
import 'package:jj_mkvmaker/app/download_manager.dart';
import 'package:jj_mkvmaker/core/download_detect.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/downloader.dart';
import 'package:jj_mkvmaker/services/model_store.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:path/path.dart' as p;
/// 앱 임시 폴더를 시험 폴더로
class _Storage extends DesktopStorageService {
final String tmp;
_Storage(this.tmp);
@override
Future<String> tempDirectory() async => tmp;
}
class _Backend implements DownloadBackend {
@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 => t.state = DownloadState.downloading;
@override
Future<void> pause(DownloadTask t) async {}
@override
Future<void> cancel(DownloadTask t) async {}
@override
Future<void> shutdown() async {}
}
void main() {
late Directory root;
late AppController c;
late String tmp, data, models, sysTmp, logs;
File make(String path, [int size = 100]) => File(path)
..parent.createSync(recursive: true)
..writeAsBytesSync(List.filled(size, 1));
setUp(() {
root = Directory.systemTemp.createTempSync('jj_clean_');
tmp = p.join(root.path, 'apptmp');
data = p.join(root.path, 'data');
models = p.join(root.path, 'models');
sysTmp = p.join(root.path, 'systemp');
logs = p.join(data, 'Logs');
c = AppController(PlatformServices(
mediaTool: ProcessMediaTool('x', 'y'),
storage: _Storage(tmp),
models: ModelStore(models),
));
c.settings.downloadRoot = p.join(root.path, 'dl');
c.logFile = p.join(logs, 'app.log');
// 지울 것
make(p.join(tmp, 'ai_123', 'audio.wav'), 5000);
make(p.join(tmp, 'jj_adjust_after_1.png'));
make(p.join(data, 'settings.json.4567.tmp'));
make(p.join(c.settings.ytDlpDir, '영상 [abc].mp4.part'), 3000);
make(p.join(c.settings.ytDlpDir, '영상 [abc].f137.mp4'), 2000);
make(p.join(c.settings.ytDlpDir, '영상 [abc].mp4.ytdl'));
make(p.join(c.settings.ytDlpDir, '재생목록', '두번째 [def].webm.part-Frag12'));
make(p.join(c.settings.aria2Dir, 'movie.mkv.aria2'));
make(p.join(models, 'whisper', 'ggml-small.bin.part'), 4000);
make(p.join(sysTmp, 'jj_mkvmaker_update_2026.10.04_001', 'JJ_MKVMaker.zip'), 6000);
make(p.join(sysTmp, 'jj_ffmpeg.download'));
make(p.join(logs, 'app.log.1'), 700);
// 남길 것
make(p.join(c.settings.ytDlpDir, '영상 [abc].mp4'), 9000);
make(p.join(c.settings.ytDlpDir, 'jj_mkv', '영상 [abc].mkv'));
make(p.join(c.settings.aria2Dir, 'movie.mkv'));
make(p.join(models, 'whisper', 'ggml-base.bin'));
make(p.join(data, 'settings.json'));
make(p.join(sysTmp, 'other_app.tmp'));
make(p.join(logs, 'app.log'));
});
tearDown(() => root.deleteSync(recursive: true));
test('찾기: 묶음별로 남은 조각만 찾고, 받은 동영상 · MKV · 모델 · 설정 · 지금 기록은 건드리지 않는다', () async {
final groups = await Cleaner(c, dataDir: data, systemTemp: sysTmp).scan();
Map<String, List<String>> names = {
for (final g in groups) g.id: [for (final i in g.items) p.basename(i.path)]..sort(),
};
expect(names['work'], ['ai_123', 'jj_adjust_after_1.png', 'settings.json.4567.tmp']);
expect(names['download'], [
'movie.mkv.aria2',
'두번째 [def].webm.part-Frag12',
'영상 [abc].f137.mp4',
'영상 [abc].mp4.part',
'영상 [abc].mp4.ytdl',
]);
expect(names['models'], ['ggml-small.bin.part']);
expect(names['update'], ['jj_ffmpeg.download', 'jj_mkvmaker_update_2026.10.04_001']);
expect(names['logs'], ['app.log.1']);
expect(groups.firstWhere((g) => g.id == 'work').bytes, 5000 + 100 + 100);
expect(groups.every((g) => g.skipped == null), isTrue);
final (n, bytes) = await Cleaner(c, dataDir: data, systemTemp: sysTmp).clean(groups);
expect(n, 3 + 5 + 1 + 2 + 1);
expect(bytes, greaterThan(20000));
// 남길 것은 그대로
for (final keep in [
p.join(c.settings.ytDlpDir, '영상 [abc].mp4'),
p.join(c.settings.ytDlpDir, 'jj_mkv', '영상 [abc].mkv'),
p.join(c.settings.aria2Dir, 'movie.mkv'),
p.join(models, 'whisper', 'ggml-base.bin'),
p.join(data, 'settings.json'),
p.join(sysTmp, 'other_app.tmp'),
p.join(logs, 'app.log'),
]) {
expect(File(keep).existsSync(), isTrue, reason: keep);
}
// 다시 찾으면 없음
final again = await Cleaner(c, dataDir: data, systemTemp: sysTmp).scan();
expect(again.every((g) => g.items.isEmpty), isTrue);
});
test('다운로드 중 (일시정지 포함) 이면 받다 만 다운로드는 건너뛴다', () async {
final d = DownloadManager(backends: [_Backend()], settings: () => c.settings, readClipboard: () async => null);
addTearDown(d.dispose);
d.addPage('https://www.youtube.com/watch?v=abc');
await Future<void>.delayed(Duration.zero);
expect(d.activeCount, 1);
final groups = await Cleaner(c, downloads: d, dataDir: data, systemTemp: sysTmp).scan();
final dl = groups.firstWhere((g) => g.id == 'download');
expect(dl.skipped, isNotNull);
await Cleaner(c, downloads: d, dataDir: data, systemTemp: sysTmp).clean(groups);
expect(File(p.join(c.settings.ytDlpDir, '영상 [abc].mp4.part')).existsSync(), isTrue, reason: '이어받기용 조각은 그대로');
expect(Directory(p.join(tmp, 'ai_123')).existsSync(), isFalse, reason: '다른 묶음은 정리');
});
test('크기 글', () {
expect(Cleaner.sizeText(512), '512 B');
expect(Cleaner.sizeText(2048), '2 KB');
expect(Cleaner.sizeText(5 * 1024 * 1024), '5.0 MB');
expect(Cleaner.sizeText(3 * 1024 * 1024 * 1024), '3.0 GB');
});
}