Initial commit to new repository
This commit is contained in:
108
src/AxCopilot/Services/FaviconService.cs
Normal file
108
src/AxCopilot/Services/FaviconService.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user