> GetFeedAsync(
+ RecommendationFeedQuery query,
+ CancellationToken cancellationToken = default);
+
+ void ClearCache();
+}
diff --git a/LanMontainDesktop.RecommendationBackend/Services/RecommendationDataService.cs b/LanMontainDesktop.RecommendationBackend/Services/RecommendationDataService.cs
new file mode 100644
index 0000000..7a3e9b0
--- /dev/null
+++ b/LanMontainDesktop.RecommendationBackend/Services/RecommendationDataService.cs
@@ -0,0 +1,729 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using LanMontainDesktop.RecommendationBackend.Models;
+
+namespace LanMontainDesktop.RecommendationBackend.Services;
+
+public sealed record RecommendationApiOptions
+{
+ public string DailyQuoteUrl { get; init; } = "https://v1.hitokoto.cn/?encode=json&charset=utf-8";
+
+ public string DailyPoetryUrl { get; init; } = "https://v1.jinrishici.com/all.json";
+
+ public string DoubanHotMovieUrlTemplate { get; init; } =
+ "https://movie.douban.com/j/search_subjects?type=movie&tag=%E7%83%AD%E9%97%A8&page_limit={0}&page_start=0";
+
+ public string BaiduHotSearchUrl { get; init; } = "https://top.baidu.com/board?tab=realtime";
+
+ public string ArtInstituteArtworkApiTemplate { get; init; } =
+ "https://api.artic.edu/api/v1/artworks?page={0}&limit={1}&fields=id,title,artist_title,artist_display,date_display,image_id,api_link";
+
+ public string ArtInstituteImageUrlTemplate { get; init; } =
+ "https://www.artic.edu/iiif/2/{0}/full/843,/0/default.jpg";
+
+ public TimeSpan CacheDuration { get; init; } = TimeSpan.FromMinutes(15);
+
+ public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromSeconds(8);
+
+ public int DefaultMovieCandidateCount { get; init; } = 20;
+
+ public int DefaultHotSearchLimit { get; init; } = 10;
+
+ public int DefaultArtworkCandidateCount { get; init; } = 50;
+}
+
+public sealed class RecommendationDataService : IRecommendationDataService, IDisposable
+{
+ private sealed record CacheEntry(object Value, DateTimeOffset ExpireAt);
+
+ private sealed record MovieCandidate(
+ string Title,
+ string? Rating,
+ string? Url,
+ string? CoverUrl);
+
+ private sealed record ArtworkCandidate(
+ string Title,
+ string? Artist,
+ string? Year,
+ string? ArtworkUrl,
+ string? ImageId);
+
+ private static readonly Regex HtmlTagRegex = new("<[^>]+>", RegexOptions.Compiled);
+ private static readonly Regex HotSearchSplitRegex = new("