mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 17:24:27 +08:00
0.3.12
修复
This commit is contained in:
@@ -12,6 +12,8 @@ namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class RecommendationDataService : IRecommendationInfoService, IDisposable
|
||||
{
|
||||
private const string UserAgent = "Mozilla/5.0";
|
||||
|
||||
private sealed record DailyArtworkCacheEntry(DailyArtworkSnapshot Snapshot, DateTimeOffset ExpireAt);
|
||||
private sealed record DailyPoetryCacheEntry(DailyPoetrySnapshot Snapshot, DateTimeOffset ExpireAt);
|
||||
private sealed record ArtworkCandidate(
|
||||
@@ -19,13 +21,16 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
string? Artist,
|
||||
string? Year,
|
||||
string? ArtworkUrl,
|
||||
string? ImageId);
|
||||
string? ImageId,
|
||||
string? ThumbnailDataUrl);
|
||||
|
||||
private readonly RecommendationApiOptions _options;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsHttpClient;
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly object _cacheGate = new();
|
||||
private DailyArtworkCacheEntry? _dailyArtworkCache;
|
||||
private readonly Dictionary<string, DailyArtworkCacheEntry> _dailyArtworkCacheBySource =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
private DailyPoetryCacheEntry? _dailyPoetryCache;
|
||||
|
||||
public RecommendationDataService(
|
||||
@@ -60,7 +65,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
{
|
||||
lock (_cacheGate)
|
||||
{
|
||||
_dailyArtworkCache = null;
|
||||
_dailyArtworkCacheBySource.Clear();
|
||||
_dailyPoetryCache = null;
|
||||
}
|
||||
}
|
||||
@@ -79,7 +84,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, _options.JinriShiciPoetryUrl);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -132,45 +137,25 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedQuery = query ?? new DailyArtworkQuery();
|
||||
if (!normalizedQuery.ForceRefresh && TryGetDailyArtworkFromCache(out var cached))
|
||||
var mirrorSource = ResolveArtworkMirrorSource(normalizedQuery);
|
||||
if (!normalizedQuery.ForceRefresh && TryGetDailyArtworkFromCache(mirrorSource, out var cached))
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Ok(cached);
|
||||
}
|
||||
|
||||
var candidateCount = Math.Clamp(_options.DefaultArtworkCandidateCount, 10, 100);
|
||||
return string.Equals(mirrorSource, DailyArtworkMirrorSources.Domestic, StringComparison.OrdinalIgnoreCase)
|
||||
? await GetDailyArtworkFromDomesticSourceAsync(mirrorSource, cancellationToken)
|
||||
: await GetDailyArtworkFromOverseasSourceAsync(mirrorSource, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<RecommendationQueryResult<DailyArtworkSnapshot>> GetDailyArtworkFromOverseasSourceAsync(
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var localDate = GetChinaLocalDate();
|
||||
var page = Math.Clamp((localDate.DayOfYear % 100) + 1, 1, 100);
|
||||
var requestUrl = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_options.ArtInstituteArtworkApiTemplate,
|
||||
page,
|
||||
candidateCount);
|
||||
|
||||
string responseText;
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0");
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail(
|
||||
"upstream_http_error",
|
||||
$"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_network_error", ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var responseText = await FetchOverseasArtworkPayloadAsync(localDate, cancellationToken);
|
||||
using var document = JsonDocument.Parse(responseText);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("data", out var dataArray) || dataArray.ValueKind != JsonValueKind.Array)
|
||||
@@ -183,7 +168,9 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
{
|
||||
var title = ReadString(item, "title");
|
||||
var imageId = ReadString(item, "image_id");
|
||||
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(imageId))
|
||||
var thumbnailDataUrl = ReadString(item, "thumbnail", "lqip");
|
||||
if (string.IsNullOrWhiteSpace(title) ||
|
||||
(string.IsNullOrWhiteSpace(imageId) && string.IsNullOrWhiteSpace(thumbnailDataUrl)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -199,7 +186,8 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
artist,
|
||||
ReadString(item, "date_display"),
|
||||
ReadString(item, "api_link"),
|
||||
imageId.Trim()));
|
||||
string.IsNullOrWhiteSpace(imageId) ? null : imageId.Trim(),
|
||||
string.IsNullOrWhiteSpace(thumbnailDataUrl) ? null : thumbnailDataUrl.Trim()));
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
@@ -217,24 +205,121 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
Museum: "The Art Institute of Chicago",
|
||||
ArtworkUrl: selected.ArtworkUrl,
|
||||
ImageUrl: BuildArtworkImageUrl(selected.ImageId),
|
||||
ThumbnailDataUrl: selected.ThumbnailDataUrl,
|
||||
FetchedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
SetDailyArtworkCache(snapshot);
|
||||
SetDailyArtworkCache(mirrorSource, snapshot);
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Ok(snapshot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_network_error", ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_parse_error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetDailyArtworkFromCache(out DailyArtworkSnapshot snapshot)
|
||||
private async Task<RecommendationQueryResult<DailyArtworkSnapshot>> GetDailyArtworkFromDomesticSourceAsync(
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, _options.DomesticArtworkApiUrl);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail(
|
||||
"upstream_http_error",
|
||||
$"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(responseText);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("images", out var images) || images.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_parse_error", "Daily image list is missing.");
|
||||
}
|
||||
|
||||
var candidates = images.EnumerateArray().ToArray();
|
||||
if (candidates.Length == 0)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_empty_result", "No daily image candidates were returned.");
|
||||
}
|
||||
|
||||
var localDate = GetChinaLocalDate();
|
||||
var indexSeed = localDate.Year * 1000 + localDate.DayOfYear;
|
||||
var selected = candidates[Math.Abs(indexSeed) % candidates.Length];
|
||||
|
||||
var imageUrl = BuildDomesticImageUrl(
|
||||
ReadString(selected, "url"),
|
||||
_options.DomesticArtworkHost);
|
||||
if (string.IsNullOrWhiteSpace(imageUrl))
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_parse_error", "Daily image URL is missing.");
|
||||
}
|
||||
|
||||
var title = ReadString(selected, "title");
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = ExtractDomesticTitle(ReadString(selected, "copyright"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = "Bing Daily Image";
|
||||
}
|
||||
|
||||
var dateText = ParseDomesticDateText(ReadString(selected, "startdate"));
|
||||
var artworkUrl = BuildDomesticImageUrl(
|
||||
ReadString(selected, "copyrightlink"),
|
||||
_options.DomesticArtworkHost);
|
||||
if (string.IsNullOrWhiteSpace(artworkUrl) ||
|
||||
artworkUrl.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
artworkUrl = null;
|
||||
}
|
||||
|
||||
var snapshot = new DailyArtworkSnapshot(
|
||||
Provider: "BingCN",
|
||||
Title: title.Trim(),
|
||||
Artist: "Bing China",
|
||||
Year: dateText,
|
||||
Museum: "Bing China",
|
||||
ArtworkUrl: artworkUrl,
|
||||
ImageUrl: imageUrl,
|
||||
ThumbnailDataUrl: null,
|
||||
FetchedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
SetDailyArtworkCache(mirrorSource, snapshot);
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Ok(snapshot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RecommendationQueryResult<DailyArtworkSnapshot>.Fail("upstream_network_error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetDailyArtworkFromCache(string mirrorSource, out DailyArtworkSnapshot snapshot)
|
||||
{
|
||||
lock (_cacheGate)
|
||||
{
|
||||
if (_dailyArtworkCache is not null && _dailyArtworkCache.ExpireAt > DateTimeOffset.UtcNow)
|
||||
if (_dailyArtworkCacheBySource.TryGetValue(mirrorSource, out var cacheEntry) &&
|
||||
cacheEntry.ExpireAt > DateTimeOffset.UtcNow)
|
||||
{
|
||||
snapshot = _dailyArtworkCache.Snapshot;
|
||||
snapshot = cacheEntry.Snapshot;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -243,11 +328,11 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SetDailyArtworkCache(DailyArtworkSnapshot snapshot)
|
||||
private void SetDailyArtworkCache(string mirrorSource, DailyArtworkSnapshot snapshot)
|
||||
{
|
||||
lock (_cacheGate)
|
||||
{
|
||||
_dailyArtworkCache = new DailyArtworkCacheEntry(
|
||||
_dailyArtworkCacheBySource[mirrorSource] = new DailyArtworkCacheEntry(
|
||||
snapshot,
|
||||
DateTimeOffset.UtcNow.Add(_options.CacheDuration));
|
||||
}
|
||||
@@ -325,6 +410,105 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
imageId.Trim());
|
||||
}
|
||||
|
||||
private string ResolveArtworkMirrorSource(DailyArtworkQuery query)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(query.MirrorSource))
|
||||
{
|
||||
return DailyArtworkMirrorSources.Normalize(query.MirrorSource);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DailyArtworkMirrorSources.Overseas;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> FetchOverseasArtworkPayloadAsync(DateOnly localDate, CancellationToken cancellationToken)
|
||||
{
|
||||
var candidateCount = Math.Clamp(_options.DefaultArtworkCandidateCount, 10, 100);
|
||||
var page = Math.Clamp((localDate.DayOfYear % 100) + 1, 1, 100);
|
||||
var requestUrl = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_options.ArtInstituteArtworkApiTemplate,
|
||||
page,
|
||||
candidateCount);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
|
||||
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {Truncate(responseText, 180)}");
|
||||
}
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
private static string? BuildDomesticImageUrl(string? rawValue, string fallbackHost)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidate = rawValue.Trim();
|
||||
if (Uri.TryCreate(candidate, UriKind.Absolute, out var absoluteUri))
|
||||
{
|
||||
return absoluteUri.ToString();
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(fallbackHost, UriKind.Absolute, out var hostUri))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalizedPath = candidate.StartsWith("/", StringComparison.Ordinal) ? candidate : $"/{candidate}";
|
||||
return new Uri(hostUri, normalizedPath).ToString();
|
||||
}
|
||||
|
||||
private static string ExtractDomesticTitle(string? copyrightText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(copyrightText))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var compact = copyrightText.Trim();
|
||||
var bracketIndex = compact.IndexOf('(');
|
||||
if (bracketIndex <= 0)
|
||||
{
|
||||
return compact;
|
||||
}
|
||||
|
||||
return compact[..bracketIndex].Trim();
|
||||
}
|
||||
|
||||
private static string? ParseDomesticDateText(string? rawDate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawDate) || rawDate.Length < 8)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (DateTime.TryParseExact(
|
||||
rawDate[..8],
|
||||
"yyyyMMdd",
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
out var date))
|
||||
{
|
||||
return date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? ReadFirstNonEmptyLine(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
Reference in New Issue
Block a user