using System.Net.Http;
using System.Text.Json.Nodes;
using Dpz.Core.Public.Entity.Steam;
using Dpz.Core.Public.ViewModel.Steam;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
namespace Dpz.Core.Service.RepositoryServiceImpl;
[method: ActivatorUtilitiesConstructor]
public class SteamGameService(
IRepository<SteamGame> repository,
IMapper mapper,
HttpClient httpClient,
IConfiguration configuration,
ILogger<SteamGameService> logger,
IHybridCachingProvider hybridCachingProvider)
: ISteamGameService
{
private readonly string _prefixKey = "Dpz.Core.Service.RepositoryServiceImpl.SteamGameService";
public event LogoDownload? OnLogoDownloadComplete;
public event AchievementIconGrayDownload? OnAchievementIconGrayDownloadComplete;
public event AchievementIconDownload? OnAchievementIconDownloadComplete;
public async Task UpdateGamesAsync()
{
var dbGames = await repository.SearchFor(x => true).ToListAsync();
var games = await FetchGamesAsync(dbGames);
// 新增游戏
var newGames = games.ExceptBy(dbGames.Select(x => x.Id), x => x.Id).ToArray();
if (newGames.Any())
await repository.InsertAsync(newGames);
// DB已有游戏,待更新
var updateGames = games.IntersectBy(dbGames.Select(x => x.Id), x => x.Id).ToList();
await Parallel.ForEachAsync(updateGames,
async (game, _) => { await UpdateGameAsync(game.Id, game.PlayTime, game.Achievements); });
await hybridCachingProvider.RemoveByPrefixAsync(_prefixKey);
// foreach (var game in updateGames)
// {
// await UpdateGameAsync(game.Id, game.PlayTime, game.Achievements);
// }
}
/// <summary>
/// 更新DB中已有的游戏信息,一般变动只有游戏时长、成就有变动,所以只修改这两项和修改时间
/// </summary>
/// <param name="id"></param>
/// <param name="playTime">游戏时长</param>
/// <param name="achievements">成就</param>
private async Task UpdateGameAsync(int id, uint playTime, IReadOnlyCollection<AchievementDetail> achievements)
{
var update = Builders<SteamGame>.Update
.Set(x => x.PlayTime, playTime)
.Set(x => x.LastUpdateTime, DateTime.Now);
if (achievements.Any())
{
update = update.Set(x => x.Achievements, achievements.ToList());
}
await repository.UpdateAsync(x => x.Id == id, update);
}
/// <summary>
/// 获取游戏成就
/// </summary>
/// <param name="id"></param>
/// <param name="dbGames"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
private async Task<List<AchievementDetail>> FetchAchievementsAsync(int id, IReadOnlyCollection<SteamGame> dbGames)
{
var achievements = new List<AchievementDetail>();
var appKey = configuration.GetSection("Steam")["AppKey"] ?? "";
var steamId = configuration.GetSection("Steam")["SteamId"] ?? "";
var queryString = new Dictionary<string, string?>
{
{ "steamid", steamId },
{ "key", appKey },
{ "format", "json" },
{ "appid", id.ToString() },
{ "l", "schinese" }
};
var requestUri = QueryHelpers.AddQueryString("/ISteamUserStats/GetSchemaForGame/v2/", queryString);
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
try
{
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var root = JsonNode.Parse(jsonContent);
achievements =
root?["game"]?["availableGameStats"]?["achievements"]?.Deserialize<List<AchievementDetail>>() ??
new List<AchievementDetail>();
}
}
catch (Exception e)
{
logger.LogError(e, "fetch game achievements fail");
}
var unlockAchievements = await FetchUnlockAchievementAsync(queryString);
await Parallel.ForEachAsync(achievements, async (achievement, _) =>
{
if (unlockAchievements.ContainsKey(achievement.Name) && unlockAchievements[achievement.Name] > 0)
{
achievement.UnlockTime = unlockAchievements[achievement.Name].ToDateTime();
}
await DownloadAchievementAsync(dbGames, id, achievement);
await DownloadAchievementGrayAsync(dbGames, id, achievement);
});
return achievements;
}
/// <summary>
/// 获取已解锁成就
/// </summary>
/// <param name="queryString"></param>
/// <returns></returns>
private async Task<Dictionary<string, long>> FetchUnlockAchievementAsync(IDictionary<string, string?> queryString)
{
var requestUri = QueryHelpers.AddQueryString("/ISteamUserStats/GetPlayerAchievements/v0001/", queryString);
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
try
{
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var root = JsonNode.Parse(jsonContent);
return root?["playerstats"]?["achievements"]?.AsArray()
.Select(x =>
{
var name = x?["apiname"]?.GetValue<string>();
if (name == null)
return (KeyValuePair<string, long>?)null;
var unlockTime = x?["unlocktime"]?.GetValue<long>() ?? 0;
if (x?["achieved"]?.GetValue<int>() != 1)
{
unlockTime = 0;
}
return new KeyValuePair<string, long>(name, unlockTime);
})
.Where(x => x != null)
.Select(x => x!.Value)
.ToDictionary(x => x.Key, x => x.Value)
?? new Dictionary<string, long>();
}
}
catch (Exception e)
{
logger.LogError(e, "fetch game unlock achievements fail");
}
return new Dictionary<string, long>();
}
/// <summary>
/// 从Steam API获取游戏库列表
/// </summary>
/// <returns></returns>
private async Task<List<SteamGame>> FetchGamesAsync(IReadOnlyCollection<SteamGame> dbGames)
{
var games = new List<SteamGame>();
var appKey = configuration.GetSection("Steam")["AppKey"] ?? "";
var steamId = configuration.GetSection("Steam")["SteamId"] ?? "";
var queryString = new Dictionary<string, string?>
{
{ "steamid", steamId },
{ "key", appKey },
{ "include_appinfo", "1" },
{ "format", "json" }
};
var requestUri = QueryHelpers.AddQueryString("/IPlayerService/GetOwnedGames/v0001/", queryString);
var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
try
{
var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var jsonContent = await response.Content.ReadAsStringAsync();
var root = JsonNode.Parse(jsonContent);
games = root?["response"]?["games"]?.Deserialize<List<SteamGame>>() ?? new List<SteamGame>();
}
}
catch (Exception e)
{
logger.LogError(e, "fetch games fail");
}
await Parallel.ForEachAsync(games, async (game, _) =>
{
game.LastUpdateTime = DateTime.Now;
game.Achievements = await FetchAchievementsAsync(game.Id, dbGames);
if (!IsLogoDownload(dbGames, game.Id))
{
var stream =
await HttpDownloadAsync(
$"https://media.st.dl.eccdnx.com/steam/apps/{game.Id}/capsule_231x87.jpg");
if (stream == null) return;
if (OnLogoDownloadComplete != null)
{
var url = await OnLogoDownloadComplete(stream, game.Id);
game.ImageLogo = url;
game.ImageIcon = url;
}
}
});
return games;
}
/// <summary>
/// 下载游戏图片或者成就图标
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
private async Task<Stream?> HttpDownloadAsync(string uri)
{
var request = new HttpRequestMessage(HttpMethod.Get, uri);
try
{
var response = await httpClient.SendAsync(request);
return await response.Content.ReadAsStreamAsync();
}
catch (Exception e)
{
logger.LogError(e, "download fail");
}
return null;
}
bool IsLogoDownload(IReadOnlyCollection<SteamGame> games, int id)
{
var game = games.FirstOrDefault(x => x.Id == id);
return game != null &&
game.ImageIcon.StartsWith("https://cdn.dpangzi.com", StringComparison.CurrentCultureIgnoreCase) &&
game.ImageLogo.StartsWith("https://cdn.dpangzi.com", StringComparison.CurrentCultureIgnoreCase);
}
async Task DownloadAchievementAsync(IEnumerable<SteamGame> dbGames, int id, AchievementDetail achievement)
{
var dbAchievement = dbGames.FirstOrDefault(x => x.Id == id)?.Achievements
.FirstOrDefault(x => x.Name == achievement.Name);
if (dbAchievement == null ||
!dbAchievement.Icon.StartsWith("https://cdn.dpangzi.com", StringComparison.CurrentCultureIgnoreCase))
{
var icon = await HttpDownloadAsync(achievement.Icon);
if (icon == null) return;
if (OnAchievementIconDownloadComplete != null)
{
var url = await OnAchievementIconDownloadComplete(icon, id, achievement.Name);
achievement.Icon = url;
}
}
else
{
achievement.Icon = dbAchievement.Icon;
}
}
async Task DownloadAchievementGrayAsync(IEnumerable<SteamGame> dbGames, int id,
AchievementDetail achievement)
{
var dbAchievement = dbGames.FirstOrDefault(x => x.Id == id)?.Achievements
.FirstOrDefault(x => x.Name == achievement.Name);
if (dbAchievement == null ||
!dbAchievement.IconGray.StartsWith("https://cdn.dpangzi.com", StringComparison.CurrentCultureIgnoreCase))
{
var icon = await HttpDownloadAsync(achievement.IconGray);
if (icon == null) return;
if (OnAchievementIconGrayDownloadComplete != null)
{
var url = await OnAchievementIconGrayDownloadComplete(icon, id, achievement.Name);
achievement.IconGray = url;
}
}
else
{
achievement.IconGray = dbAchievement.IconGray;
}
}
public async Task<List<VmSteamGame>> GetGamesAsync()
{
var cacheKey = $"{_prefixKey}:GetGamesAsync()";
var cache = await hybridCachingProvider.GetAsync<List<VmSteamGame>>(cacheKey);
if (!cache.IsNull && cache.HasValue)
{
return cache.Value;
}
var list = await repository.SearchFor(x => true).ToListAsync();
var result = mapper.Map<List<VmSteamGame>>(list);
await hybridCachingProvider.SetAsync(cacheKey, result, TimeSpan.FromDays(1));
return result;
}
public async Task<VmSteamGame?> GetGameAsync(int id)
{
var cacheKey = $"{_prefixKey}:GetGameAsync({id})";
var cache = await hybridCachingProvider.GetAsync<VmSteamGame>(cacheKey);
if (!cache.IsNull && cache.HasValue)
{
return cache.Value;
}
var game = await repository.SearchFor(x => x.Id == id).FirstOrDefaultAsync();
if (game == null) return null;
var result = mapper.Map<VmSteamGame>(game);
await hybridCachingProvider.SetAsync(cacheKey, result, TimeSpan.FromDays(1));
return result;
}
public void Test()
{
var serializerRegistry = BsonSerializer.SerializerRegistry;
var documentSerializer = serializerRegistry.GetSerializer<SteamGame>();
var update = Builders<SteamGame>.Update
.Set(x => x.PlayTime, 100u);
var updateArgs = new RenderArgs<SteamGame>(documentSerializer,serializerRegistry);
Console.WriteLine(update.Render(updateArgs));
update = update.Set(x => x.LastUpdateTime, DateTime.Now);
Console.WriteLine(update.Render(updateArgs));
}
}