Skip to content

Commit

Permalink
feat: Adding the page that will show the data for the stats (and the …
Browse files Browse the repository at this point in the history
…stats api endpoint)
  • Loading branch information
itssimple committed Dec 26, 2023
1 parent ff8e72d commit 884ab98
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 24 deletions.
118 changes: 118 additions & 0 deletions CFLookup/Pages/MinecraftModStatsOverTime.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
@page
@model CFLookup.Pages.MinecraftModStatsOverTimeModel
@{
ViewData["Title"] = "Minecraft mod stats over time";
}
@section Head {
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script>
google.charts.load('current', { packages: ['linechart'] });
google.charts.setOnLoadCallback(drawMinecraftChart);
function drawMinecraftChart() {
var data = new google.visualization.DataTable();
data.addColumn('datetime', 'Time');
data.addColumn('string', 'Game version');
data.addColumn('number', 'Forge');
data.addColumn('number', 'Fabric');
data.addColumn('number', 'Quilt');
data.addColumn('number', 'NeoForge');
const jsonData = @Html.Raw(Json.Serialize(Model.Stats));
for(var stat in jsonData)
{
let rowData = jsonData[stat];
for(var gameVersion in rowData)
{
let row = [];
row.push(new Date(stat));
row.push(gameVersion);
row.push(rowData[gameVersion]["Forge"]);
row.push(rowData[gameVersion]["Fabric"]);
row.push(rowData[gameVersion]["Quilt"]);
row.push(rowData[gameVersion]["NeoForge"]);
console.log(row);
data.addRow(row);
}
}
var options = {
title: 'Number of Minecraft mods per game version, per mod loader',
height: '700',
width: '100%',
isStacked: true,
legend: {
position: 'bottom',
textStyle: { color: '#6c757d' }
},
vAxis: {
title: 'Minecraft game version',
titleTextStyle: { color: '#6c757d' },
textStyle: { color: '#6c757d' },
gridlines: { color: '#787878' }
},
backgroundColor: { fill: 'transparent' },
titleTextStyle: {
color: '#fff'
},
hAxis: {
textStyle: { color: '#6c757d' },
titleTextStyle: { color: '#6c757d' },
gridlines: { color: '#787878' },
title: 'Time'
}
};
var chart = new google.visualization.LineChart(document.getElementById('minecraft-version-modloader'));
chart.draw(data, options);
}
</script>
}

<div id="minecraft-version-modloader"></div>

<table class="table table-dark table-striped table-condensed">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col" class="text-end">VersionInfo</th>
</tr>
</thead>
<tbody>
@foreach(var stat in Model.Stats)
{
<tr>
<td scope="row">@stat.Key</td>
<td>
<table class="table table-dark table-striped table-condensed">
<thead>
<tr>
<th scope="col">Game version</th>
<th scope="col">Forge</th>
<th scope="col">Fabric</th>
<th scope="col">Quilt</th>
<th scope="col">NeoForge</th>
</tr>
</thead>
<tbody>
@foreach(var gameVersion in stat.Value)
{
<tr>
<td scope="row">@gameVersion.Key</td>
<td>@gameVersion.Value["Forge"].ToString("n0")</td>
<td>@gameVersion.Value["Fabric"].ToString("n0")</td>
<td>@gameVersion.Value["Quilt"].ToString("n0")</td>
<td>@gameVersion.Value["NeoForge"].ToString("n0")</td>
</tr>
}
</tbody>
</table>
</td>
</tr>
}
</tbody>
</table>
24 changes: 24 additions & 0 deletions CFLookup/Pages/MinecraftModStatsOverTime.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Data;
using System.Text.Json;

namespace CFLookup.Pages
{
public class MinecraftModStatsOverTimeModel : PageModel
{
private readonly MSSQLDB _db;

public Dictionary<DateTimeOffset, Dictionary<string, Dictionary<string, long>>> Stats { get; set; } = new Dictionary<DateTimeOffset, Dictionary<string, Dictionary<string, long>>>();

public MinecraftModStatsOverTimeModel(MSSQLDB db)
{
_db = db;
}

public async Task OnGetAsync()
{
Stats = await SharedMethods.GetMinecraftStatsOverTime(_db);
}
}
}
61 changes: 38 additions & 23 deletions CFLookup/SharedMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
using CurseForge.APIClient.Models.Files;
using CurseForge.APIClient.Models.Games;
using CurseForge.APIClient.Models.Mods;
using Newtonsoft.Json;
using StackExchange.Redis;
using System.Collections.Concurrent;
using System.Data;
using System.Text.Json;
using System.Text.RegularExpressions;

namespace CFLookup
Expand All @@ -18,11 +19,11 @@ public static async Task<List<Game>> GetGameInfo(IDatabaseAsync _redis, ApiClien

if (!cachedGames.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<List<Game>>(cachedGames);
return JsonSerializer.Deserialize<List<Game>>(cachedGames);

Check warning on line 22 in CFLookup/SharedMethods.cs

View workflow job for this annotation

GitHub Actions / generate

Possible null reference argument for parameter 'json' in 'List<Game>? JsonSerializer.Deserialize<List<Game>>(string json, JsonSerializerOptions? options = null)'.

Check warning on line 22 in CFLookup/SharedMethods.cs

View workflow job for this annotation

GitHub Actions / generate

Possible null reference return.
}

var games = await _cfApiClient.GetGamesAsync();
await _redis.StringSetAsync("cf-games", JsonConvert.SerializeObject(games.Data), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync("cf-games", JsonSerializer.Serialize(games.Data), TimeSpan.FromMinutes(5));
return games.Data;
}

Expand All @@ -32,11 +33,11 @@ public static async Task<Game> GetGameInfo(IDatabaseAsync _redis, ApiClient _cfA

if (!cachedGame.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<Game>(cachedGame);
return JsonSerializer.Deserialize<Game>(cachedGame);

Check warning on line 36 in CFLookup/SharedMethods.cs

View workflow job for this annotation

GitHub Actions / generate

Possible null reference argument for parameter 'json' in 'Game? JsonSerializer.Deserialize<Game>(string json, JsonSerializerOptions? options = null)'.

Check warning on line 36 in CFLookup/SharedMethods.cs

View workflow job for this annotation

GitHub Actions / generate

Possible null reference return.
}

var games = await _cfApiClient.GetGameAsync(gameId);
await _redis.StringSetAsync($"cf-games-{gameId}", JsonConvert.SerializeObject(games.Data), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-games-{gameId}", JsonSerializer.Serialize(games.Data), TimeSpan.FromMinutes(5));
return games.Data;
}

Expand All @@ -46,13 +47,13 @@ public static async Task<List<Category>> GetCategoryInfo(IDatabaseAsync _redis,

if (!cachedCategories.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<List<Category>>(cachedCategories);
return JsonSerializer.Deserialize<List<Category>>(cachedCategories);

Check warning on line 50 in CFLookup/SharedMethods.cs

View workflow job for this annotation

GitHub Actions / generate

Possible null reference argument for parameter 'json' in 'List<Category>? JsonSerializer.Deserialize<List<Category>>(string json, JsonSerializerOptions? options = null)'.
}

var gameId = gameInfo.FirstOrDefault(x => x.Slug.Equals(game, StringComparison.InvariantCultureIgnoreCase))?.Id;

var categories = await _cfApiClient.GetCategoriesAsync(gameId);
await _redis.StringSetAsync($"cf-categories-{game}", JsonConvert.SerializeObject(categories.Data), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-categories-{game}", JsonSerializer.Serialize(categories.Data), TimeSpan.FromMinutes(5));

return categories.Data;
}
Expand All @@ -63,7 +64,7 @@ public static async Task<List<Category>> GetCategoryInfo(IDatabaseAsync _redis,

if (!cachedFile.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<(Mod mod, CurseForge.APIClient.Models.Files.File file, string changelog)>(cachedFile);
return JsonSerializer.Deserialize<(Mod mod, CurseForge.APIClient.Models.Files.File file, string changelog)>(cachedFile);
}

var file = await _cfApiClient.GetFilesAsync(new GetModFilesRequestBody
Expand All @@ -78,7 +79,7 @@ public static async Task<List<Category>> GetCategoryInfo(IDatabaseAsync _redis,

var changelog = await _cfApiClient.GetModFileChangelogAsync(file.Data[0].ModId, fileId);

await _redis.StringSetAsync($"cf-fileinfo-{fileId}", JsonConvert.SerializeObject((mod.Data, file.Data[0], changelog.Data)), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-fileinfo-{fileId}", JsonSerializer.Serialize((mod.Data, file.Data[0], changelog.Data)), TimeSpan.FromMinutes(5));

return (mod.Data, file.Data[0], changelog.Data);
}
Expand All @@ -89,11 +90,11 @@ public static async Task<List<Category>> GetCategoryInfo(IDatabaseAsync _redis,

if (!cachedCategories.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<List<Category>>(cachedCategories);
return JsonSerializer.Deserialize<List<Category>>(cachedCategories);
}

var categories = await _cfApiClient.GetCategoriesAsync(gameId);
await _redis.StringSetAsync($"cf-categories-id-{gameId}", JsonConvert.SerializeObject(categories.Data), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-categories-id-{gameId}", JsonSerializer.Serialize(categories.Data), TimeSpan.FromMinutes(5));
await Task.Delay(25);
return categories.Data;
}
Expand All @@ -103,12 +104,12 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl
var cachedMods = await _redis.StringGetAsync($"cf-mods-{string.Join('-', modIds)}");
if (!cachedMods.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<List<Mod>>(cachedMods);
return JsonSerializer.Deserialize<List<Mod>>(cachedMods);
}

var mods = await _cfApiClient.GetModsByIdListAsync(new GetModsByIdsListRequestBody { ModIds = modIds });

await _redis.StringSetAsync($"cf-mods-{string.Join('-', modIds)}", JsonConvert.SerializeObject(mods.Data), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-mods-{string.Join('-', modIds)}", JsonSerializer.Serialize(mods.Data), TimeSpan.FromMinutes(5));

return mods.Data;
}
Expand All @@ -123,7 +124,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl
return null;
}

return JsonConvert.DeserializeObject<Mod>(modResultCache); ;
return JsonSerializer.Deserialize<Mod>(modResultCache); ;
}

try
Expand All @@ -142,7 +143,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl
modResult.Data.Name = projectName;
}*/

var modJson = JsonConvert.SerializeObject(modResult.Data);
var modJson = JsonSerializer.Serialize(modResult.Data);

await _redis.StringSetAsync($"cf-mod-{projectId}", modJson, TimeSpan.FromMinutes(5));

Expand All @@ -164,7 +165,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl
return null;
}

var obj = JsonConvert.DeserializeObject<GenericListResponse<CurseForge.APIClient.Models.Files.File>>(modResultCache);
var obj = JsonSerializer.Deserialize<GenericListResponse<CurseForge.APIClient.Models.Files.File>>(modResultCache);

if (obj?.Data.Count > 0)
{
Expand All @@ -181,7 +182,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl

if (modResult.Data.Count > 0)
{
var modJson = JsonConvert.SerializeObject(modResult);
var modJson = JsonSerializer.Serialize(modResult);
await _redis.StringSetAsync($"cf-file-{fileId}", modJson, TimeSpan.FromMinutes(5));

return modResult.Data[0].ModId;
Expand All @@ -205,7 +206,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl
return null;
}

var cachedMod = JsonConvert.DeserializeObject<Mod>(cachedResponse);
var cachedMod = JsonSerializer.Deserialize<Mod>(cachedResponse);

return cachedMod;
}
Expand All @@ -227,7 +228,7 @@ public static async Task<List<Mod>> SearchModsAsync(IDatabaseAsync _redis, ApiCl

if (mod.Data.Count == 1)
{
await _redis.StringSetAsync($"cf-mod-{game}-{category}-{slug}", JsonConvert.SerializeObject(mod.Data[0]), TimeSpan.FromMinutes(5));
await _redis.StringSetAsync($"cf-mod-{game}-{category}-{slug}", JsonSerializer.Serialize(mod.Data[0]), TimeSpan.FromMinutes(5));
return mod.Data[0];
}

Expand All @@ -244,7 +245,7 @@ public static async Task<ConcurrentDictionary<string, ConcurrentDictionary<ModLo
return null;
}

var cachedMod = JsonConvert.DeserializeObject<ConcurrentDictionary<string, ConcurrentDictionary<ModLoaderType, long>>>(cachedResponse);
var cachedMod = JsonSerializer.Deserialize<ConcurrentDictionary<string, ConcurrentDictionary<ModLoaderType, long>>>(cachedResponse);

return cachedMod;
}
Expand Down Expand Up @@ -297,7 +298,7 @@ public static async Task<ConcurrentDictionary<string, ConcurrentDictionary<ModLo

await Task.WhenAll(versionTasks);

await _redis.StringSetAsync("cf-mcmod-stats", JsonConvert.SerializeObject(mcVersionModCount), TimeSpan.FromHours(1));
await _redis.StringSetAsync("cf-mcmod-stats", JsonSerializer.Serialize(mcVersionModCount), TimeSpan.FromHours(1));

return mcVersionModCount;
}
Expand All @@ -312,7 +313,7 @@ public static async Task<ConcurrentDictionary<string, long>> GetMinecraftModpack
return null;
}

var cachedMod = JsonConvert.DeserializeObject<ConcurrentDictionary<string, long>>(cachedResponse);
var cachedMod = JsonSerializer.Deserialize<ConcurrentDictionary<string, long>>(cachedResponse);

return cachedMod;
}
Expand Down Expand Up @@ -348,11 +349,25 @@ public static async Task<ConcurrentDictionary<string, long>> GetMinecraftModpack

await Task.WhenAll(versionTasks);

await _redis.StringSetAsync("cf-mcmodpack-stats", JsonConvert.SerializeObject(mcVersionModCount), TimeSpan.FromHours(1));
await _redis.StringSetAsync("cf-mcmodpack-stats", JsonSerializer.Serialize(mcVersionModCount), TimeSpan.FromHours(1));

return mcVersionModCount;
}

public static async Task<Dictionary<DateTimeOffset, Dictionary<string, Dictionary<string, long>>>> GetMinecraftStatsOverTime(MSSQLDB _db, int datapoints = 1000)
{
var stats = await _db.ExecuteDataTableAsync($"SELECT TOP {datapoints} * FROM MinecraftModStatsOverTime ORDER BY statId DESC");
var Stats = new Dictionary<DateTimeOffset, Dictionary<string, Dictionary<string, long>>>();
foreach (DataRow row in stats.Rows)
{
var timestamp = row.Field<DateTimeOffset>("timestamp_utc");
var gameStats = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, long>>>(row.Field<string>("stats")!)!;
Stats.Add(timestamp, gameStats);
}

return Stats;
}

public static string GetProjectNameFromFile(string url)
{
return Path.GetFileName(url);
Expand Down
12 changes: 11 additions & 1 deletion CFLookup/StatsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ public class StatsController : ControllerBase
{
private readonly ApiClient _cfApiClient;
private readonly IDatabaseAsync _redis;
private readonly MSSQLDB _db;

public ConcurrentDictionary<string, ConcurrentDictionary<ModLoaderType, long>> MinecraftStats = new();
public TimeSpan? CacheExpiration { get; set; }

public StatsController(ApiClient cfApiClient, ConnectionMultiplexer connectionMultiplexer)
public StatsController(ApiClient cfApiClient, ConnectionMultiplexer connectionMultiplexer, MSSQLDB db)
{
_cfApiClient = cfApiClient;
_redis = connectionMultiplexer.GetDatabase(5);
_db = db;
}

[HttpGet("Minecraft/ModStats.json")]
Expand Down Expand Up @@ -51,6 +53,14 @@ public async Task<IActionResult> MinecraftModpackStats()
});
}

[HttpGet("Minecraft/ModStatsOverTime.json")]
public async Task<IActionResult> MinecraftModStatsOverTime()
{
var stats = await SharedMethods.GetMinecraftStatsOverTime(_db);

return new JsonResult(stats);
}

private static DateTimeOffset GetTruncatedTime(TimeSpan timeSpan)
{
var now = DateTimeOffset.UtcNow;
Expand Down

0 comments on commit 884ab98

Please sign in to comment.