using System.Text; namespace VideoDownloader.Core.Social; /// /// yt-dlp 가 읽을 수 있는 Netscape cookies.txt 형식으로 쿠키를 내보낸다. /// 로그인/연령제한/멤버 콘텐츠(인스타·X 등) 를 위해 앱(WebView2) 의 세션 쿠키를 yt-dlp 에 전달. /// public static class CookieJarWriter { // ★ BOM 금지: Encoding.UTF8 은 EF BB BF(BOM) 를 붙이는데, yt-dlp 가 첫 줄 // "# Netscape HTTP Cookie File" 헤더를 인식하지 못해("does not look like a Netscape format // cookies file" + "invalid length 1") 쿠키 파일 전체를 거부한다. // → preamble 을 emit 하지 않는 UTF-8 인코딩 사용. private static readonly Encoding NoBomUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); public static void Write(string path, IEnumerable cookies) { var sb = new StringBuilder(); sb.AppendLine("# Netscape HTTP Cookie File"); sb.AppendLine("# This is generated by VideoDownloader. Do not edit."); foreach (var c in cookies) { // 필드: domain | flag(도메인 전체 적용) | path | secure | expiration | name | value string domain = Sanitize(c.Domain); if (string.IsNullOrEmpty(domain)) continue; // 도메인 없는 쿠키는 7-필드 레이아웃 보호를 위해 건너뜀 string name = Sanitize(c.Name); string value = Sanitize(c.Value); if (name.Length == 0 && value.Length == 0) continue; bool subdomains = domain.StartsWith('.'); // HttpOnly 쿠키는 줄 앞에 "#HttpOnly_" 접두 — yt-dlp 가 http_only 플래그로 인식. // (YouTube 의 __Secure-* / SID 계열 인증 쿠키가 대부분 HttpOnly.) if (c.HttpOnly) sb.Append("#HttpOnly_"); sb.Append(domain).Append('\t'); sb.Append(subdomains ? "TRUE" : "FALSE").Append('\t'); sb.Append(string.IsNullOrEmpty(c.Path) ? "/" : Sanitize(c.Path)).Append('\t'); sb.Append(c.Secure ? "TRUE" : "FALSE").Append('\t'); sb.Append(c.Expires > 0 ? c.Expires : 0).Append('\t'); sb.Append(name).Append('\t'); sb.Append(value); sb.AppendLine(); } File.WriteAllText(path, sb.ToString(), NoBomUtf8); } /// 필드 구분자(tab) 와 줄바꿈(CR/LF) 이 값 안에 들어가 7-필드 레이아웃이 깨지는 것을 방지. private static string Sanitize(string s) => string.IsNullOrEmpty(s) ? s : s.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); }