releases/docs/BACKLOG.md
Yun Chan 02995f4153 MEDIABROWSER 개명·업데이트 채널 전환·아이콘 tofu 근본 수정 (BACKLOG 13차) — ①개명: installer.iss AppName/출력 MediaBrowser-Setup-*(AppId·exe·데이터 경로 유지로 업그레이드 호환), 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser-<ver>-arm64.apk), 창 타이틀·모바일 표시명·obtainium.json·README. ②UpdateChecker: apiBase 주입(기본=자체 Forgejo /api/v1, GitHub 는 apiBase 로 호환), osx 자산 tar.gz 허용(기존 dmg 만 → mac 영원히 자산 없음 결함 수정), AppConfig 기본 UpdateRepo=Video-Downloader/releases+UpdateApiBase — 테스트 4건. ③아이콘 tofu(사용자 보고 "절대 안고쳐지네") 근본 원인: Shared.xaml 암시적 TextBlock 스타일의 FontFamily=UiFont 강제가 버튼 ContentPresenter 자동 TextBlock 의 IconFont 상속을 덮어 모든 PUA 글리프가 notdef — 폰트 설치/글리프 존재/HW·SW 렌더/RTB/DPI/런타임 8·10/자체포함·공유 전부 배제 후 실창 픽셀 비교로 격리, 콘텐츠 TextBlock 의 실폰트가 Pretendard 체인임을 테스트로 실증. 수정: 암시 스타일 FontFamily 제거(창 루트 상속으로 통일, PairingWindow/PlayerWindow 는 SetResourceReference), PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳 정리, 순수 글리프 2곳 IconFont 명시. 회귀 방어: IconGlyphXamlTests(정적 스캔)·IconContentFontTests(실창 폰트 단정)·IconGlyphRenderTests(픽셀 비교) — 462/462 GREEN, 수정 전후 실창 캡처로 글리프 렌더 확인
2026-08-18 07:13:18 +09:00

37 KiB
Raw Blame History

Video Downloader — 개선 백로그

출처: 2026-07-26 다차원 코드 감사(9개 차원, 원시 63개 → 상위 15 선정). 모두 실제 소스 기반(file:line 검증). 사용법: 완료한 항목은 - [ ] → - [x] 로 체크.

🏆 우선 구현 추천 (TOP 3)

  1. #1 다운로드 취소 버튼 — 멈출 수 없는 치명 UX 결함, 코드에 Cts만 있어 한 줄이면 끝.
  2. #2 ffmpeg stderr 드레인 + Kill — hang/데드락/좀비 프로세스 안전 결함 + 리먹스 실패 노출.
  3. #5 동시 다운로드 전역 제한(SemaphoreSlim) — 소켓 고갈(#11)과 직결, 정적 필드 하나짜리 퀵윈.

S 공수 (빠른 승리 — #1~#10)

[x] 1. 다운로드 취소 버튼이 UI에 없음

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/MainWindow.xaml, Core/DownloadManager.cs
  • 문제: 카드에 받기/재시도/폴더만 있고 취소 없음. item.Cts 토큰은 만들지만 .Cancel() 호출 0건 → 다운로드를 멈출 방법 자체가 없음.
  • 해결: 카드에 '취소' 버튼 → item.Cts?.Cancel(). 진행 중(다운로드/병합/리먹스)일 때만 활성화.

[x] 2. ffmpeg stderr 미읽기 → 데드락 + 좀비 프로세스 + 리먹스 실패 조용 폭백

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.Core/Hls/HlsDownloader.cs (RunFfmpegAsync, RemuxAsync)
  • 문제: stderr 리다이렉트만 켜두고 안 읽음 → 파이프 버퍼 차면 ffmpeg hang. 취소 시 Kill 없이 좀비. 실패해도 .ts로 조용히 넘어감.
  • 해결: WaitForExitAsync와 StandardError.ReadToEndAsync 병렬 대기(로그 저장), finally에서 Kill(entireProcessTree), exit≠0이면 예외 throw → item.Error 표시. partial mp4 정리.

[x] 3. explorer.exe 인자 인젝션

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/MainWindow.xaml.cs (CardFolder/OpenFolder)
  • 문제: Process.Start("explorer.exe", $"...{path}...") 문자열 인자 직접 전달. OutputFolder(config 평문) 변조 시 /root, 스위치로 인자 조작 가능.
  • 해결: ProcessStartInfo 명시 + Path.GetFullPath 정규화 + OutputFolder 하위 검증(또는 UseShellExecute=true).

[x] 4. NewWindowRequested 미처리 → 팝업이 앱 통제 이탈

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/MainWindow.xaml.cs (InitializeBrowserAsync)
  • 문제: target=_blank/window.open() 팝업이 별도 창 → 감지/쿠키 안 타서 팝업 안 영상은 대기열에 안 잡힘.
  • 해결: cv.NewWindowRequested에서 현재 WebView2로 강제 라우팅(e.NewWindow = (CoreWebView2)s; e.Handled = true).

[x] 5. 동시 다운로드 수 전역 제한 없음

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/Core/DownloadManager.cs (StartDownload)
  • 문제: StartDownload가 fire-and-forget. N개 누르면 N × Concurrency개 TCP 동시 요청 폭증 → 소켓 고갈(#11) 가속.
  • 해결: static SemaphoreSlim(MaxConcurrentDownloads) 도입, StartDownload에서 WaitAsync/Release. 세그먼트 단위 Concurrency와 분리해 AppConfig에 각각 설정.

[x] 6. .parts 디렉토리 정리 누락 → 디스크 누수 + 이름 cascade

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.Core/Hls/HlsDownloader.cs, Core/DownloadManager.cs (MakeUnique)
  • 문제: 완료/실패/취소 어느 경로든 partsDir 안 지움 → 수천 세그먼트 영구 축적. 잔류 .parts가 MakeUnique 밀어내 (1)(2)... 접미 연쇄.
  • 해결: 성공 시 Directory.Delete(partsDir, recursive). 실패/취소 시에도 정리(이어받기 토글 옵션).

[x] 7. 전역 키보드 단축키 전무

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/MainWindow.xaml
  • 문제: Ctrl+L(주소), F5/Ctrl+R(새로고침), Esc(정지), Alt+←/→(뒤로/앞으로), Ctrl+F(검색), Ctrl+±/0(줌) 어느 것도 안 됨.
  • 해결: Window.InputBindings에 KeyBinding 추가.

[x] 8. requests.log 무한 증가 (로테이션 전무)

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/Core/DownloadManager.cs (AppendAll, OnResponseReceived)
  • 문제: 모든 응답(이미지/광고/분석 포함) 기록, 크기 제한 없음 → 세션당 GB 가능, OutputFolder 디스크 채움.
  • 해결: 10MB 임계 롤오버 + truncate. 관심 없는 content-type(status≥300/image/font/css/js/beacon) 스킵. 설정 토글로 끄기.

[x] 9. 직링크 Content-Length 미상 시 진행률 0% 고정 (멈춤 오인)

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.Core/Hls/DirectDownloader.cs, Models/DownloadProgress.cs, MainWindow.xaml
  • 문제: chunked 응답에서 pct=0 → ProgressBar 내내 0%. Segments엔 '1.2 MB'만 떠 멈춘 것처럼 보임.
  • 해결: 길이 미상(OverridePercent=-1) 시 IsIndeterminate=true, Detail(바이트)은 계속 표시.

[x] 10. 다운로드 실패 원인(Error)이 UI에 안 보임

  • 임팩트: 높 · 공수: S · 파일: VideoDownloader.App/MainWindow.xaml (카드 템플릿), ViewModels/DownloadItemViewModel.cs
  • 문제: item.Error는 채워지는데 카드엔 Status만 바인딩 → '오류' 단어만. '404'/'디스크 가득' 등 원인은 app.log 열어야 앎. ErrorBrush도 미사용.
  • 해결: 카드에 {Binding Error} TextBlock(ErrorBrush, Status=='오류'일 때만 노출) + 실패 카드 보더/전경색 DataTrigger.

M 공수 (#11~#15)

[x] 11. 호출마다 new HttpClient + Dispose 안 함 (소켓 고갈)

  • 임팩트: 높 · 공수: M · 파일: VideoDownloader.App/Core/DownloadManager.cs (BuildHttpClientAsync)
  • 문제: 매 호출마다 새 HttpClient(handler+client). 항목당 2개. TIME_WAIT ~240초 점유 → 포트 고갈.
  • 해결: 앱 수명 단일 공유 HttpClient(SocketsHttpHandler + PooledConnectionLifetime). 요청별 헤더(쿠키/Referer/UA)는 HttpRequestMessage로.

[x] 12. 직링크에 재시도·이어받기·임시파일 전무

  • 임팩트: 높 · 공수: M · 파일: VideoDownloader.Core/Hls/DirectDownloader.cs
  • 문제: GetAsync 1회로 끝. 드롭 시 0바이트부터 재시작. File.Create(최종이름) 직접 쓰기 → 중단 시 손상 파일이 최종명으로 남음.
  • 해결: .part 임시파일 + Range: bytes=written- 이어받기 + 재시도 루프(4xx 영구 오류 제외, Retry-After) + 완료 시 File.Move.

[x] 13. WebView2 기본 다운로드가 대기열/OutputFolder 우회

  • 임팩트: 높 · 공수: M · 파일: VideoDownloader.App/MainWindow.xaml.cs (InitializeBrowserAsync)
  • 문제: 페이지 내 a[download] 링크 클릭 시 WebView2 자체 다운로더 → 사용자 기본 폴더. DownloadStarting 구독 없음 → 앱 대기열/카드에 안 잡힘.
  • 해결: cv.DownloadStarting에서 ResultFilePath를 OutputFolder로 강제 또는 Cancel 후 대기열로 라우팅.

[x] 14. 글로벌 예외 핸들러/크래시 리포트 부재

  • 임팩트: 높 · 공수: M · 파일: VideoDownloader.App/App.xaml.cs
  • 문제: App.xaml.cs 빈 클래스. DispatcherUnhandledException/TaskScheduler.UnobservedTaskException 없음 → fire-and-forget 예외 조용 삼켜짐, 크래시 시 '작동 중지'만.
  • 해결: 세 핸들러 연결 → app.log에 스택 기록 + 요약 메시지. fire-and-forget에 ContinueWith(OnlyOnFaulted) 관찰.

[x] 15. 감지 신뢰성: image/* 위장·청크·후보 미등록이 미디어 누락으로

  • 임팩트: 높 · 공수: M · 파일: VideoDownloader.App/Detection/MediaDetector.cs, MainWindow.xaml.cs (OnResponseReceived)
  • 문제: (a) MatchFormat에 image/* 분기 없음 → tvchak식 play.png+image/png 위장 mp4/TS 100% 누락. (b) chunked 응답은 크기 가드 통과 → 토큰 만료 HTML 에러 페이지도 mp4로 오탐. (c) .mpd/.m4s/octet-stream 후보는 로그만 찍고 항목 등록 안 됨.
  • 해결: 다운로드 첫 GET 시 magic byte 검증(MP4 ftyp / TS 0x47 / WebM 0x1A45DFA3 / MP3), MatchFormat에 image/*+대형 분기, 후보도 OnDetected 전달, chunked는 사후 길이 재검증.

메모

  • S 공수 10개(#1~#10)는 반나절 안에 일괄 처리 가능.
  • #5와 #11은 함께 잡으면 시너지(소켓 고갈 근본 해결).
  • #15는 감지 신뢰성의 핵심 — 이 사이트(tvchak 계열 image/png 위장)에 직결.

팔로업 (2026-07-26 완료)

  • 라이트 테마 토글 — Themes/Shared.xaml(스타일) + Dark/Light.xaml(색). 설정 체크박스 + 변경 시 재시작. DWM 타이틀바 색 연동.
  • MaxConcurrentDownloads UI 노출 — 설정에 "최대 동시 항목" 필드. (다음 시작부터 적용)
  • DASH(.mpd) 지원 — Core/Dash/ (DashParser/DashDownloader). 클리어 콘텐츠만. 샘플 MPD 파싱 검증 통과.
  • 보더리스 윈도우 — WindowChrome + Fluent 캡션버튼 + 툴바 전체 드래그 + Win11 둥근 모서리.

2차 팔로업 (2026-07-27 완료)

  • DASH A/V 통합 + ffmpeg-native — 수동 세그먼트 병합(fMP4)이 깨지기 쉬워 ffmpeg -i <mpd> 로 위임. DashParser는 DRM 사전검증/진행률(duration)용. BBB 공개스트림 종단간 검증(h264+aac mp4 생성).
  • 감지 오탐 감소 — GIF 트래킹 픽셀이 TS(0x47='G')로 오탐되던 것 수정. ProbeFormat이 이미지 매직바이트(GIF/PNG/JPEG/WebP) 제외.
  • MaxConcurrentDownloads 런타임 반영 — 동적 게이트(증가=즉시 Release, 감소=완료 시 미충전으로 자연 수축). 설정 저장 시 즉시 적용.
  • 라이트 테마 시각 점검 — 팔레트 대비 건전(어두운 텍스트 #1B1C24 / 밝은 배경).
  • 주소창 자동완성/제안 — 히스토리 접두사/부분일치 드롭다운 + 인라인 자동완성(Tab/→ 확정, ↑↓ 탐색, Enter 이동, 클릭 이동).
  • 인스타/blob·Range 영상 감지 — 206 응답을 Content-Range 전체크기로 판정(청크가 작아 skip되던 것). IG 실계정 테스트는 불가(로그인/지역).
  • IndeterminateBar 스토리보드 예외 수정 — TranslateTransform 의 x:Name(템플릿 namescope 미등록) → Border 이름 + RenderTransform 속성경로로 타겟.

3차 팔로업 (2026-07-29 완료) — 쿠키/YouTube + 감사 결함 일괄 수정

9개 차원 다차원 감사(24 에이전트 워크플로우, 원시 42개 → 확정 11개 결함) 기반. 사용자 제보(YouTube 다운로드 시 yt-dlp 가 쿠키 파일 거부)에서 시작.

치명 — YouTube/쿠키 (사용자 제보 직결)

  • 쿠키 파일 UTF-8 BOM → yt-dlp 거부 — CookieJarWriter 가 Encoding.UTF8(BOM 포함) 로 써 첫 줄 헤더 # Netscape HTTP Cookie File 인식이 깨짐 → "does not look like a Netscape format cookies file" + "invalid length 1". new UTF8Encoding(false) 로 BOM 제거. HttpOnly 쿠키에 #HttpOnly_ 접두 추가, 값 내 tab/CRLF sanitize. (Core/Social/CookieJarWriter.cs)
  • YouTube player_client 오버라이드가 yt-dlp 기본값보다 못했음 — 하드코딩 android_vr,android,web_sns,web 이 2026년엔 대부분 무효: android/ios 는 GVS/Player PO 토큰 필수 + 쿠키 미지원, web 도 PO 토큰 없으면 SABR 만, web_sns 는 yt-dlp 에 존재하지 않는 클라이언트. → 오버라이드를 비우면 yt-dlp 공식 기본값(visionos,android_vr,web, 유지보수자 갱신) 사용. 고급 설정 필드 YoutubePlayerClients(설정 UI)로 보존. (Core/Social/YtDlpDownloader.cs, Core/Config/AppConfig.cs, SettingsWindow)

백로그 "완료" 표기 중 실제 미구현이었던 것

  • #12 Retry-After 미적용 — 백로그엔 "Retry-After 적용" 이라 적었으나 소스엔 한 줄도 없었음. DirectDownloader 가 429/503 시 서버 Retry-After(최대 60s 캡) 를 존중하도록 수정. (Core/Hls/DirectDownloader.cs)
  • #15(b) chunked video/ 사후 길이 재검증 누락* — Content-Length 없는 video/* 응답이 크기 가드를 통과해 토큰 만료 HTML 오류 페이지가 mp4 로 오탐. chunked video/* 를 magic-byte probe 로 재검증(정상 MP4 는 ftyp box 로 통과). (MainWindow.xaml.cs)

코드 헬스 (감사 확정)

  • 공유 CookieContainer 프로세스 수준 누적 — SharedHandler 의 컨테이너에 매 다운로드마다 쿠키가 누적/타사이트 누출. UseCookies=false + 요청(다운로드) 단위 Cookie 헤더로 스코프. (Core/DownloadManager.cs)
  • DASH ffmpeg 헤더 누락 — ffmpeg -i <mpd> 가 UA/Referer/쿠키 없이 기본 UA 로 받아 토큰·Referer 인증 CDN(maccms 계열) 에서 403. -user_agent/-referer/-headers 를 -i 앞에 전달. (Core/Dash/DashDownloader.cs)
  • DashParser Time → 세그먼트 번호 오탐 — $Time$ 를 번호로 치환하던 것(ISO/IEC 23009-1 위반) 을 미디어 시작시각(t/d 누적) 으로 수정. (Core/Dash/DashParser.cs)
  • DashParser r=-1 → 세그먼트 0개 — SegmentTimeline r="-1"(Period 끝까지 반복) 을 count=r+1=0 으로 처리해 표현이 통째로 사라지던 것 수정. (Core/Dash/DashParser.cs)
  • HlsDownloader stdout 드레인 태스크 미관측 — 취소 시 _ = 폐기 태스크가 미관측 상태로 남던 것을 finally 에서 await. (Core/Hls/HlsDownloader.cs)
  • BrowserStore 조용 예외 삼킴 — 즐겨찾기/히스토리 저장·로드 실패를 완전 무시(조용 데이터 손실) 하던 것을 browserstore.log 기록 + 손상 파일 .corrupt.bak 백업. (Browser/BrowserStore.cs)

검증

  • dotnet build 0 오류 / 0 경고.
  • 셀프테스트(임시 --selftest): 쿠키파일 BOM 없음·HttpOnly 접두·Netscape 헤더 + DashParser r=-1(5 세그먼트)/$Time$(0,8000) 8/8 PASS.
  • 실제 공개 DASH(BBB) 매니페스트 파싱 정상(VIDEO/AUDIO 각 159 세그먼트) — $Number$ 경로 회귀 없음.

4차 팔로업 (2026-07-29 완료) — 브라우저 프로필 + Private 모드 + 범용 패널

브라우저 데이터 보존/관리 (사용자 요청)

  • WebView2 영속 프로필 — 기본(exe 디렉터리 내) user data 폴더는 빌드/재배포 시 날아가 로그인이 풀렸음. %AppData%/VideoDownloader/webview 로 명시(CoreWebView2Environment.CreateAsync) → 쿠키·세션·localStorage·캐시가 앱/빌드 재시작에도 보존. (MainWindow.xaml.cs)
  • Private(InPrivate) 모드 — CoreWebView2ControllerOptions.IsInPrivateModeEnabled 로 Edge InPrivate 세션(디스크에 아무것도 저장 안 함). 툴바 토글 버튼(활성 시 강조) → 확인 → AppConfig.PrivateMode 저장 → 재시작 적용(테마와 동일 패턴).
  • 브라우저 데이터 관리 — 설정창에 프로필 경로 표시 + "삭제" 버튼(CoreWebView2Profile.ClearBrowsingDataAsync → 쿠키·DOM 저장소·캐시 정리). (SettingsWindow)

UI (사용자 요청)

  • 범용 우측 패널 — 다운로드 컬럼을 접기/펼치기(헤더 토글, 접힘 시 좁은 스트립) + GridSplitter 폭 리사이즈(280~900) + RightPanelWidth/RightPanelCollapsed 저장·복원. (MainWindow.xaml/.cs)
  • 진행률 바 미작동 수정 — AccentProgressBar 템플릿에 PART_Track 이름이 없어 WPF 가 인디케이터 폭을 계산 못 함(바가 안 채워짐). x:Name="PART_Track" 추가. (Themes/Shared.xaml)

검증

  • dotnet build 0 오류 / 0 경고. 앱 시작 스모크 테스트(새 WebView2 초기화 경로) 7s 생존 확인 — 크래시 없음.

5차 팔로업 (2026-07-29 완료) — YouTube n 챌린지(EJS) 다운로드 실패

사용자 제보: YouTube 영상 다운로드 시 yt-dlp 가 n challenge solving failed + Only images are available for download 로 MP4 실패(exit 1).

치명 — YouTube (사용자 제보 직결)

  • YouTube n 챌린지 해석 미설정 → 포맷 드랍/스로틀 — YouTube 가 플레이어 응답의 "n" 서명 스로틀 파라미터를 JS 로 난독화. yt-dlp 는 이를 풀기 위해 JS 런타임(Deno) 으로 실행되는 EJS(External JS Solver) 챌린지 솔버가 필요하며, 없으면 비디오 포맷이 통째로 드랍(이미지만 남) 되거나 살아남아도 수백 KiB/s 로 심하게 스로틀됨. BuildArgs(다운로드)·FetchFormatsAsync(정밀 화질 드롭다운) 양쪽에 --remote-components ejs:github(yt-dlp 공식 권장, https://github.com/yt-dlp/yt-dlp/wiki/EJS) 추가. Deno 는 PATH 에 있으면 자동 감지(환경에 2.8.1 설치됨). 솔버 스크립트는 최초 1회만 GitHub 에서 받아 캐싱. (Core/Social/YtDlpDownloader.cs)
    • 정합성 주의: --remote-components 는 yt-dlp 2025년 중반(EJS 도입) 이후 버전에서만 인식. 본 프로젝트는 yt-dlp 를 releases/latest 에서 자동 설치하므로 사실상 항상 지원; 단 사용자가 YtDlpPath(설정) 로 구버전을 수동 지정한 경우에만 "unrecognized arguments" 회귀 가능.

검증

  • 실제 동영상(EjSqcVC1qwk) 종단간 테스트: --remote-components 없으면 다운로드는 시작되나 992KiB/s → Unknown B/s 스로틀; 플래그 추가 시 13~18 MiB/s 정상 속도 + mp4 병합(EXIT=0). -F 포맷 정상 노출.
  • dotnet build 0 오류 / 0 경고.

6차 팔로업 (2026-07-29 완료) — YouTube 안티봇 실패 시 yt-dlp 자동 갱신/재시도

사용자 요청: YouTube 가 난독화를 바꿀 때마다 수동 대응하지 않고 "자동으로 맞춰지게". yt-dlp 유지보수자가 보통 몇 시간~며칠 내 대응 릴리스를 내보내므로, 최신화만으로 대부분 복구됨에 착안.

기능

  • 안티봇 실패 감지 → yt-dlp 최신화 → 1회 재시도 — DownloadAsync 에서 yt-dlp 종료코드≠0 일 때 stderr 에서 안티봇 징후(n challenge/nsig/Only images are available/PO Token/po_token/Requested format is not available/Unable to extract) 를 감지하면: (1) 설치 버전 vs GitHub Releases 최신 태그 비교, (2) 새 버전이 있을 때만 releases/latest 에서 강제 재다운로드(번들 경로) 후 동일 인자로 1회 재시도. 진행 중 카드에 "yt-dlp 갱신 확인 중 / x → y 갱신 후 재시도" 표시. (Core/Social/YtDlpDownloader.cs)
  • 버전 비교로 불필요 재다운로드 방지 — 이미 최신인데도 실패하면 갱신 생략하고 에러 메시지에 "YouTube가 방금 바뀌었을 수 있음(잠시 후 재시도 권장)" 부연. (Core/Social/YtDlpRunner.cs — GetVersionAsync/GetLatestVersionAsync)
  • 설정 토글 — AppConfig.AutoUpdateYtDlp(기본 true). 설정창 "yt-dlp 자동 다운로드" 옆 체크박스로 노출.

검증

  • 버전 형식 일치 확인: 설치 2026.03.17 vs GitHub 최신 2026.07.04 (둘 다 YYYY.MM.DD → 문자열 비교 정확). 현재 설치본이 최신보다 구버전이므로, 실패 시 2026.07.04 로 자동 갱신 후 재시도 동작 확인 가능.
  • 재시도 인자는 --remote-components ejs:github 포함(5차) 그대로 재사용 → EJS 솔버 + 바이너리 갱신 이중 방어.
  • dotnet build(전체 솔루션) 0 오류 / 0 경고.

7차 팔로업 (2026-07-29 완료) — Deno(JS 런타임) 자동 설치로 배포 환경 EJS 보장

사용자 요청(연장): "Deno도 없으면 자동 설치". 5차의 --remote-components ejs:github 는 JS 런타임(Deno) 이 있어야 동작 — 개발 머신엔 Deno 가 있지만 배포 PC엔 없으면 EJS 가 안 풀려 YouTube 가 막힘.

기능

  • DenoRunner 자동 설치 — Resolve(설정→번들→PATH)·EnsureInstalledAsync·DownloadAsync(GitHub denoland/deno Releases zip → deno.exe 단일 엔트리 추출). yt-dlp 와 동일 %AppData%/VideoDownloader/tools 공유. OS/아키텍처별 에셋(Windows x64 / macOS·Linux arm64+x64). (Core/Social/DenoRunner.cs)
  • PATH 주입(플래그 의존 제거) — YtDlpRunner.RunAsync/RunCaptureAsync 가 yt-dlp 자식 프로세스 PATH 앞에 ToolsDir 를 추가 → 번들 deno.exe 를 yt-dlp 가 자동 발견. --js-runtimes deno:<경로> 플래그의 Windows 경로 콜론 파싱 엣지케이스를 피하는 가장 견고한 방식(시스템 PATH 불변, 자식 프로세스에만 적용). (Core/Social/YtDlpRunner.cs)
  • 연결 — DownloadAsync 시작 시 Deno 를 ensure(best-effort, 진행률 표시). Deno 누락은 치명 오류가 아니므로(yt-dlp 가 경고 후 진행) 결과 무시. AppConfig.DenoPath·AutoInstallDeno(기본 true) + 설정 토글.
  • 3중 방어 완성: (1) EJS 솔버 --remote-components ejs:github(5차) + (2) yt-dlp 자동 갱신(6차) + (3) Deno 자동 설치(7차).

검증

  • Deno 없는 환경 시뮬레이션 종단 테스트: 시스템 deno(npm) 를 PATH에서 제외하고 번들 deno.exe(/tmp/vdtools) 만 노출한 상태에서 yt-dlp --remote-components ejs:github 실행 → which deno = 번들만, 다운로드 15~124 MiB/s 풀스피드 + 경고 0건 + EXIT=0(mp4 병합). PATH 주입으로 번들 Deno 가 n 챌린지 해석 확인.
  • Deno 다운로드/추출: zip(42MB) → deno.exe(97MB) 단일 파일, v2.9.4 정상 동작 확인.
  • dotnet build(전체 솔루션) 0 오류 / 0 경고.

8차 팔로업 (2026-07-29 완료) — 배포 파이프라인 + Avalonia 크로스플랫폼 이관 착수

사용자 요청: "배포도 신경써보자" — Windows 다중 아키텍처 정식 설치자 + Avalonia UI 재작성 착수(맥/리눅스 지원 목표). 계획 승인 후 2단계로 실행.

Phase 1 — Windows Inno Setup 다중 아키텍처 설치자 (완전 완료)

  • VideoDownloader.App.csproj 멀티아키텍처/self-contained — <Platforms>x64;arm64</Platforms>, <RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>, SelfContained=true(폴더, single-file/trim 아님 — WPF+트리밍 위험 회피). PlatformTarget 하드코딩 제거(RID에서 파생).
  • build/publish.ps1 — x64/arm64 루프: dotnet publish -c Release -r <rid> -p:Platform=<arch> --self-contained 후 ISCC(/DARCH=)로 out/VideoDownloader-Setup-<arch>.exe 생성. ISCC 위치 자동 해결(기본경로/PATH/LocalAppData 후보).
  • build/installer.iss — 단일 스크립트 2회 컴파일. 아키텍처별 고유 AppId GUID; ArchitecturesAllowed 게이트(arm64 전용 / x64=x64compatible+arm64폴백); per-user 설치(PrivilegesRequired=lowest); 시작메뉴+데스크톱 바로가기+제거자; 한국어 기본(Korean.isl)+영어(Default.isl).
  • Inno Setup: winget JRSoftware.InnoSetup(6.7.3, per-user) 설치. 영어는 Languages/English.isl이 아니라 루트 Default.isl이 기본(트랩).

Phase 1 검증

  • 두 설치자 생성: out/VideoDownloader-Setup-x64.exe(49.1 MB), out/VideoDownloader-Setup-arm64.exe(43.8 MB). 둘 다 유효 PE(MZ).
  • arm64 publish 성공 — WPF arm64(self-contained) 첫 빌드, 이슈 없음(예상 리스크 미발현). yt-dlp 네이티브 arm64 빌드가 없어 arm64 Win11에선 x64 에뮬레이션(WoW64)으로 동작(릴리스 노트 명시 필요).
  • publish.ps1 exit 0(두 아키텍처 publish+ISCC 전 단계).

Phase 2 — Avalonia 크로스플랫폼 UI (기반 + Gate #0, 로드맵 포함)

  • VideoDownloader.Avalonia(신규, net8.0) — WPF 앱은 유지한 채 병행. Core 참조(엔진 공유). VideoDownloader.slnx 등록.
  • Gate #0 최소 동작 UI — MainWindow.axaml/MainViewModel(INPC) + RelayCommand. URL 붙여넣기 → 유형 판별(소셜/HLS/DASH/직링크) → Core 다운로더 직접 호출(DownloadManager의 WebView2 쿠키 결합 회피), 진행률/로그 표시. 소셜은 방금 만든 자동 복구(6차)·Deno 자동설치(7차) 로직도 그대로 작동.
  • Gate #1(관문) = 크로스플랫폼 브라우저 컴포넌트: 현재 앱의 심장은 WebView2의 응답 가로채기+쿠키. 추천 = CefGlue.Avalonia(OnResourceResponse↔WebResourceResponseReceived, CefCookieManager↔쿠키, 거의 1:1). 단, CEF가 자체 Chromium 번들(~150-200MB/OS) + 성숙도/Linux 리스크 → 3OS 프로토타입 통과 전 본 이관 착수 금지(로드맵 1순위). 차선 CefNet, 최후 DotNetBrowser(상용).

Phase 2 검증

  • dotnet build 0 오류/0 경고(Avalonia 12.1.0 + Core net8.0). (컴파일드 바인딩 → 코드비하인드 DataContext이므로 x:CompileBindings="False"; Items→ItemsSource; Watermark→PlaceholderText.)
  • 스모크 테스트: dotnet run → 창 정상 시작, 10초 생존(크래시 없음, 빈 로그) → 크로스플랫폼 빌드 + Core 재사용 + MVVM 바인딩 입증.

이관 로드맵(의존 순)

  1. Gate #1 브라우저 3OS 프로토타입(CEF) — 통과 전 3~8 불가.
  2. 공유 리팩터: App/Detection/*(순수C#) Core 이동; ICookieProvider 중립 추상화; DownloadManager 공유화(생성자→ICookieProvider); BindingOperations.EnableCollectionSynchronization→Avalonia 디스패처; IAppPaths OS별 경로. 3~8. 브라우저+감지, 설정, 테마(Avalonia Styles 재작성), 보더리스 윈도우(ExtendClientAreaToDecorationsHint), ViewModels, 북마크/히스토리.
  3. Mac/Linux 패키징(.app 노타라이제이션, .AppImage/.deb); YtDlpRunner/DenoRunner OS/arch별 바이너리 확인. 10. 패리티 후 WPF 은퇴.
  • 참고: 저장소가 git 아님 → git init 권장 → git 전환 완료.

9차 팔로업 (2026-08-16 완료) — 계획 문서 잔여 갭 소거 (SignalR·mDNS·자동 업데이트·CI)

목표 "만들어진 모든 계획 문서 달성"에 따라 MOBILE_PLAN·DEPLOYMENT_PLAN 의 미이행 항목 일괄 구현.

  • SignalR 큐 허브 /hubs/queue — MOBILE_PLAN §3 아키텍처 항목. QueueHub(GetQueue 스냅샷) + IQueueAdapter.Changed → queueUpdated 전 클라이언트 푸시. 인증 미들웨어에 /hubs 편입(t/access_token 쿼리 허용) + UseWebSockets. 실 Kestrel E2E 로 접속·푸시·401 검증. (Server/QueueHub.cs, ServerHost.cs)
  • mDNS 광고 _videodl._tcp — MOBILE_PLAN M0 항목. MdnsAdvertiser(Makaretu.Dns) + ServerOptions.EnableMdns(App 은 켬). 실패는 ServerHost.MdnsError 로 표면화(발견 보조 — 서버 기동 불가로 번지지 않게). Makaretu 가 TXT 에 ASCII 만 허용 → 한글 기기명은 퍼센트 인코딩(무손실), 인스턴스 라벨은 ASCII 접기. 광고→발견 실증: 별도 브라우저 인스턴스가 _videodl._tcp.local 수신(RESULT=DISCOVERED). (Server/MdnsAdvertiser.cs)
  • 자동 업데이트 v1 — DEPLOYMENT_PLAN §6/D6. Core/Update/UpdateChecker: GitHub Releases latest 태그 비교(관대한 v접두/4자리 파싱), RID별 설치 자산 선택(arm64 우선 배제 규칙), GitHub UA 규약. 설정창 "앱 업데이트" 섹션(AppConfig.UpdateRepo) — 새 버전이면 릴리스 페이지 열기(설치는 사람 관문). 가짜 핸들러 격리 테스트 + 실 창 UI 테스트.
  • CI 릴리스 파이프라인 스캐폴드 — DEPLOYMENT_PLAN §5/D1~D3. .github/workflows/release.yml: 태그 v* → test 관문(win) → Windows 설치자(x64/arm64, Inno) · Linux/macOS tar.gz(Cli+Avalonia) · Android APK(키스토어 시크릿 조건부 서명) → Draft Release. iOS 잡은 ENABLE_IOS 변수 게이트(Apple 계정 전제). build/obtainium.json + README 배포 절 추가.
  • 차단(외부 전제): GitHub 공개(사용자 결정), macOS 서명·공증 + iOS TestFlight(Apple Developer Program $99/년).

검증 (9차)

  • dotnet test 180/180 통과 (기준선 164 → +16: 허브 E2E 2, mDNS 4, UpdateChecker 9, 설정 UI 1).
  • 빌드 오류 0 / 신규 경고 0 (기존 CS8602 1건 유지).
  • mDNS 실증: ServerHost(EnableMdns) 기동 → 별도 ServiceDiscovery 로 15초 내 발견 성공.

10차 (2026-08-17) — UI 복잡 버전 유즈케이스 RED 스펙 (234건)

사용자 요청 "UI 복잡 버전 및 스타일 등의 유즈케이스를 200개 이상 생각해서 RED 를 만들고 테스트를 돌린다". TDD RED 단계 산출물 — 구현(GREEN) 전. 스펙 전문: docs/UI_USECASES.md.

  • 유즈케이스 234건 설계 — 10개 카탈로그(테마·대비 30 / 타이포그래피 22 / 레이아웃·밀도 26 / 탭 고급 24 / 다운로드 관리자 30 / 설정창 20 / 접근성 20 / 인터랙션 20 / 라이브러리·재생목록·기록 22 / 상태·알림 20). 각 유즈케이스 = 테스트 1건과 1:1 대응.
  • RED 테스트 234건 작성 — Tests/E2E/UiUsecase{Theme,Typography,Layout,Tabs,Downloads,Settings,Accessibility,Interaction,Library,Status}RedTests.cs. 검증 방식: 소스 XAML/코드비하인드 직접 읅기(신규 Infrastructure/SourceTree.cs — 루트 탐색·브러시 대비 계산·리플렉션 계약).
  • 실측 결함 발견(대비) — 다크 TextMuted/CardBg 4.42:1, CardBgHover 4.01:1, 라이트 TextMuted/CardBg 4.22:1·GlassBg 3.53:1, 다크 AccentBrush/CardBg 4.38:1 (모두 WCAG AA 4.5:1 미달), 라이트 Track/Panel 1.18:1·다크 Border/Card 1.38:1 (비텍스트 3:1 미달).
  • GREEN 구현 완료(2026-08-17) — 234/234 통과. 토큰·테마·설정 계약·대비 수정(라이브 테마 전환 포함)·다운로드 관리자·라이브러리·접근성·인터랙션 전 구현. 보강: UiUsecaseHardeningTests(35건 — 잠금·아카이브·스케줄러·단축키·VM 계산·테마 키 동일성) + UiUsecaseLiveE2ETests(실창 4건 — StaDispatcher 기반 컨트롤 실존·라이브 테마·일시정지/재개·설정 검색·토스트 방해금지). 총 453/453 GREEN.

검증 (10차)

  • dotnet test — 실패 234 / 통과 180 / 전체 414 (RED 의도 상태: 신규 234전부 실패, 기존 180 전부 통과, 기존 영향 실패 0건).
  • 빌드 오류 0 / 신규 경고 0.

11차 (2026-08-17) — Forgejo 자체 러너 릴리스 CI 가동 (전 플랫폼 빌드→배포 폐쇄루프)

사용자: "러너들이 생겼다 이제 우리도 빌드를 모두 다 배포할 수 있다" — 저장소를 자체 Forgejo (git.chanpaca.net/Video-Downloader/client)로 이관한 데 이어, org 러너 3대로 릴리스 CI 구축. §5-bis 참조.

  • .forgejo/workflows/release.yml — 태그 v* → test 관문(전체 xUnit) → windows(설치자 x64/arm64)·android(APK)·linux(tar.gz×2 RID)·macos(tar.gz×2 RID) 병렬 → 각 잡이 draft 릴리스에 자산 직접 업로드. 러너 3대(yunchan-win/kd-desktop/yunchan-mac) 모두 host 모드라 uses: 액션 전무 — 체크아웃도 git fetch+FETCH_HEAD 직접. workflow_dispatch = ci-rehearsal draft prerelease 리허설.
  • build/ci/upload-release-asset.{ps1,sh} — draft 릴리스 확보(병렬 잡 동시 생성 경합 시 재조회) + 동명 자산 교체 멱등. 실 API 검증 중 Windows Git Bash 에서 python 출력 CRLF 로 자산 id 가 5\r 로 오염돼 DELETE 가 조용히 실패하는 결함 발견·수정(tr -d '\r').
  • android MSB3030 수정 — CLI 전역 -p:RuntimeIdentifiers=android-arm64 가 참조 프로젝트(net8.0 Mobile.Core)까지 전파돼 RID 경로 dll 부재로 실패. 9차 GitHub 스캐폴드에 잠재하던 결함이 첫 실전 실행에서 실증 → Mobile csproj 의 CiPackArm64Only 게이트 조건부 RuntimeIdentifiers 로 이전.
  • 인프라 — RELEASE_TOKEN 시크릿(PAT write:repository) API 등록, ALPACA-HOME 유령 Inno Setup(레지스트리만 잔존) 재설치, .sh 실행 비트. Forgejo 는 .forgejo/workflows 만 인식 — .github/workflows(GitHub 공개 시 백업 채널)와 이중 실행 없음 확인.

검증 (11차)

  • 리허설 1차(b05e8bf): test 453/453 통과(실창 E2E 포함 — 러너가 사용자 세션이라 창 생성 가능) · windows/linux/macos 성공 · android 실패(MSB3030).
  • 리허설 2차(d98c734, 수정 후): 5/5 잡 전부 성공 — ci-rehearsal draft prerelease 자산 7개: Setup x64 57.2MB·arm64 51.5MB / linux tar.gz x64 71.1MB·arm64 68.1MB / osx tar.gz arm64 69.6MB·x64 72.7MB / APK(arm64-v8a 단독) 23.3MB.
  • 정식 배포: git tag v1.0.0 && git push origin v1.0.0 → 동일 파이프라인이 v1.0.0 draft 릴리스 생성 → QA 후 수동 Publish (§7.5 런북 그대로).

12차 (2026-08-18) — MEDIABROWSER 배포 사이트 + 공개 릴리스 저장소

사용자: "릴리즈 페이지 만들자 — 프로모션(패럴랙스·글래스모피즘·라이트) + 릴리스 + 사용 안내, wrangler 로 Cloudflare 배포, 파일은 NAS 호스팅, 배포명은 MEDIABROWSER". DEPLOYMENT_PLAN §5-ter 참조.

  • site/ 3페이지 — index(프로모션: 히어로 스플릿+벤토+기능 그리드+모바일+플랫폼 밴드, CSS scroll-driven 패럴랙스 + IntersectionObserver 등장, prefers-reduced-motion/transparency 존중, JS 실패 시에도 내용이 보이는 html.js 게이트) · releases(플랫폼 그룹·OS 자동 감지 권장·이전 버전 접힘·빈 상태) · guide(설치→첫 다운로드→관리→휴대폰 연결→FAQ). Wanted Sans Variable + Phosphor 스프라이트.
  • 실앱 자산 — 히어로/가이드의 다운로드 패널 이미지는 VD_DATA_DIR 격리 프로필로 띄운 실제 앱 창 캡처(밝기 보정). 가짜 목업 없음.
  • 공개 릴리스 저장소 — 소스(client)가 비공개라 자산 익명 다운로드 불가 → Video-Downloader/releases (공개) 신설, CI 업로드를 env RELEASE_REPO 로 전환. draft 는 익명에 안 보여 QA 후 Publish 흐름 유지. 생성 직후 저장소 설명이 인코딩 깨짐(EFBFBD)으로 올라간 문제(사용자 보고)는 UTF-8 파일 기반 PATCH 로 교정.
  • 배포 — build/deploy-site.ps1(releases.json 생성 → wrangler pages deploy). 라이브: https://mediabrowser-7r4.pages.dev (mediabrowser 서브도메인 선점 → 접미).

검증 (12차)

  • Playwright 로 3페이지 전체 스크린샷 검증(1440px): 히어로/벤토/기능/모바일/밴드 렌더, 릴리스 빈 상태, 샘플 데이터 주입 렌더(플랫폼 4그룹·권장 강조·프리뷰 필), 가이드 목차·FAQ. 라이브 URL 에서 재확인.
  • 도중 결함 2건 수정: fullPage 캡처로 드러난 reveal 무한 숨김 위험(no-JS 백지) → html.js 게이트, 패널 캡처의 사용자 세션 노출 위험 → 격리 프로필 재캡처로 교체.

13차 (2026-08-18) — 후속 과제 일괄: 개명·업데이트 채널·사이트 자동화 + 아이콘 tofu 근본 수정

사용자: "ㄱㄱ 다해" — 12차에서 남긴 후속 5건 진행. 아이콘은 "절대 안고쳐지네" 보고를 받아 근본 원인까지 추적.

  • MEDIABROWSER 개명(배포명) — installer.iss(AppName/출력 MediaBrowser-Setup-, AppId GUID·exe·데이터 경로는 유지해 업그레이드 호환), publish.ps1, 양쪽 릴리스 워크플로 산출물명(mediabrowser-.tar.gz, MediaBrowser--arm64.apk), 창 타이틀·환영 문구·모바일 ApplicationTitle, obtainium.json(공개 저장소 URL), README·release 스킬.
  • UpdateChecker 채널 전환 — CheckAsync(..., apiBase) 주입(기본 https://git.chanpaca.net/api/v1, GitHub 는 apiBase 지정으로 호환), osx 자산에 tar.gz 허용(기존 dmg 만 허용 → mac 영원히 자산 없음 결함), AppConfig 기본 UpdateRepo=Video-Downloader/releases + UpdateApiBase. 테스트 4건 추가.
  • 릴리스 Publish 시 사이트 자동 재배포 — releases 저장소에 site-deploy.yml(release published → CLIENT_TOKEN 으로 client site/ 체크아웃 → releases.json 생성 → wrangler 배포). 시크릿 3종 API 등록, workflow_dispatch 검증 성공(deploy success).
  • [◐] 커스텀 도메인 — Pages 프로젝트에 mediabrowser.chanpaca.net 등록 완료. DNS CNAME 은 CLOUDFLARE_API_TOKEN/CF_API_TOKEN 모두 Zone DNS 편집 권한이 없어 보류 — 사용자 액션: 대시보드에서 mediabrowser CNAME → mediabrowser-7r4.pages.dev(프록시 ON) 추가하면 즉시 활성.
  • 아이콘 tofu 근본 수정 — 원인: Shared.xaml 의 암시적 TextBlock 스타일이 FontFamily=UiFont 를 강제 → 버튼 ContentPresenter 자동 TextBlock 이 IconFont 상속을 잃고 모든 PUA 글리프가 notdef(□). 폰트 설치·글리프 존재· 렌더 경로(HW/SW·RTB·DPI·런타임 8/10·자체포함/공유) 전부 배제 후 실창 픽셀 비교로 격리. 수정: 암시 스타일의 FontFamily 강제 제거(창 루트 상속으로 통일), PairingWindow/PlayerWindow 에 SetResourceReference. 부수 결함: PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳(항상 tofu 로 렌더되던 것) 정리, 순수 글리프 2곳 IconFont 명시.
  • 미해결 관찰(후속): 첫 실행(빈 프로필)에서 초기 탭이 HomeUrl 로 자동 탐색되지 않고 빈 화면으로 남는 현상.

검증 (13차)

  • dotnet test — 462/462 통과 (기준선 453 → UpdateChecker 4·IconGlyphXaml 2·IconGlyphRender 2·IconContentFont 1 추가).
  • 아이콘: 수정 전 실창 캡처(□ tofu) → 수정 후 동일 조건 캡처에서 ‹ › ↻ ⌂ 글리프 정상 렌더 확인. 실창 회귀 테스트가 콘텐츠 TextBlock 의 FontFamily=Segoe Fluent Icons + 본문 Pretendard 유지를 단정.
  • android 로컬 재현·리허설은 12차와 동일 경로(변경 없음). 산출물 개명은 다음 리허설/태그에서 반영 확인 예정.