Files
AX-Copilot-Codex/src/AxCopilot/Services/FaviconService.cs

109 lines
3.6 KiB
C#

using System.Collections.Concurrent;
using System.IO;
using System.Net.Http;
using System.Windows.Media.Imaging;
namespace AxCopilot.Services;
/// <summary>
/// 외부 URL의 favicon을 다운로드하여 캐시합니다.
/// Google Favicon API (https://www.google.com/s2/favicons?domain=xxx&sz=32)를 사용합니다.
/// 캐시는 메모리(ConcurrentDictionary) + 디스크(%APPDATA%\AxCopilot\favicons\)에 저장됩니다.
/// </summary>
public static class FaviconService
{
private static readonly string CacheDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "favicons");
private static readonly ConcurrentDictionary<string, BitmapImage?> _memCache = new();
private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(5) };
/// <summary>
/// 도메인의 favicon BitmapImage를 반환합니다.
/// 캐시에 있으면 즉시 반환, 없으면 null 반환 후 백그라운드에서 다운로드합니다.
/// 다운로드 완료 시 콜백을 호출합니다.
/// </summary>
public static BitmapImage? GetFavicon(string url, Action? onLoaded = null)
{
var domain = ExtractDomain(url);
if (string.IsNullOrEmpty(domain)) return null;
// 메모리 캐시 확인
if (_memCache.TryGetValue(domain, out var cached))
return cached;
// 디스크 캐시 확인
var diskPath = Path.Combine(CacheDir, $"{domain}.png");
if (File.Exists(diskPath))
{
try
{
var bmp = LoadFromDisk(diskPath);
_memCache[domain] = bmp;
return bmp;
}
catch { }
}
// 백그라운드에서 다운로드
_memCache[domain] = null; // 중복 요청 방지
_ = DownloadAsync(domain, diskPath, onLoaded);
return null;
}
private static async Task DownloadAsync(string domain, string diskPath, Action? onLoaded)
{
try
{
var faviconUrl = $"https://www.google.com/s2/favicons?domain={Uri.EscapeDataString(domain)}&sz=32";
var bytes = await _http.GetByteArrayAsync(faviconUrl).ConfigureAwait(false);
if (bytes.Length < 100) return; // 너무 작으면 유효한 이미지 아님
// 디스크 저장
Directory.CreateDirectory(CacheDir);
await File.WriteAllBytesAsync(diskPath, bytes).ConfigureAwait(false);
// 메모리 캐시 갱신
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
try
{
var bmp = LoadFromDisk(diskPath);
_memCache[domain] = bmp;
onLoaded?.Invoke();
}
catch { }
});
}
catch (Exception ex)
{
LogService.Warn($"Favicon 다운로드 실패: {domain} — {ex.Message}");
}
}
private static BitmapImage LoadFromDisk(string path)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.UriSource = new Uri(path, UriKind.Absolute);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.DecodePixelWidth = 32;
bmp.EndInit();
bmp.Freeze();
return bmp;
}
private static string? ExtractDomain(string url)
{
try
{
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
url = "https://" + url;
return new Uri(url).Host.ToLowerInvariant();
}
catch { return null; }
}
}