Working
This commit is contained in:
@ -8,15 +8,26 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.WebApi" Version="7.1.0" />
|
||||
<PackageReference Include="log4net" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="8.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="8.1.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="8.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SQLiteEF\SQLiteEF.csproj" />
|
||||
<ProjectReference Include="..\SteamApiWrapper\SteamApiWrapper.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
26
API/Api.cs
26
API/Api.cs
@ -1,20 +1,33 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.OpenApi;
|
||||
using SQLiteEF;
|
||||
|
||||
namespace API;
|
||||
|
||||
public class Api
|
||||
{
|
||||
private Tracker _tracker;
|
||||
public Api(string[] args)
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddOpenApi(options =>
|
||||
{
|
||||
options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
|
||||
});
|
||||
builder.Services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin().AllowAnyMethod()));
|
||||
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
builder.Services.AddDbContext<Context>();
|
||||
builder.Services.AddDbContext<Context>(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"), optionsBuilder => optionsBuilder.MigrationsAssembly(Assembly.GetAssembly(GetType())!)));
|
||||
|
||||
builder.Services.AddSingleton<Tracker>();
|
||||
SteamApiWrapper.SteamApiWrapper.SetApiKey(builder.Configuration.GetValue<string>("SteamWebApiKey")!);
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
@ -22,14 +35,17 @@ public class Api
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Services.CreateScope().ServiceProvider.GetRequiredService<Context>().Database.Migrate();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
84
API/Controllers/ActionsController.cs
Normal file
84
API/Controllers/ActionsController.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SQLiteEF;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class ActionsController(Context databaseContext) : ApiController(typeof(ActionsController))
|
||||
{
|
||||
[HttpPut("Player/{steamId}")]
|
||||
[ProducesResponseType<Player>(Status202Accepted)]
|
||||
[ProducesResponseType<string>(Status500InternalServerError)]
|
||||
public IActionResult AddPlayerToTrack(Tracker tracker, ulong steamId)
|
||||
{
|
||||
Player nPlayer = new (steamId, "", "", "");
|
||||
try
|
||||
{
|
||||
databaseContext.Players.Add(nPlayer);
|
||||
databaseContext.SaveChanges();
|
||||
tracker.ForceLoop();
|
||||
return Accepted(nPlayer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
return StatusCode(Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("Update/Player/{steamId}/All")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult UpdatePlayer(Tracker tracker, ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
tracker.UpdatePlayer(databaseContext, player);
|
||||
tracker.UpdateOwnedGamesPlayer(databaseContext, player);
|
||||
tracker.UpdateGameTimesPlayer(databaseContext, player);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("Update/Player/{steamId}/Info")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult UpdatePlayerInfo(Tracker tracker, ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
tracker.UpdatePlayer(databaseContext, player);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("Update/Player/{steamId}/OwnedGames")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult UpdatePlayerOwnedGames(Tracker tracker, ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
tracker.UpdateOwnedGamesPlayer(databaseContext, player);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("Update/Player/{steamId}/TimeTracked")]
|
||||
[ProducesResponseType(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult UpdatePlayerTimeTracked(Tracker tracker, ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
tracker.UpdateGameTimesPlayer(databaseContext, player);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("Update/All")]
|
||||
[ProducesResponseType(Status202Accepted)]
|
||||
public IActionResult Update(Tracker tracker)
|
||||
{
|
||||
tracker.ForceLoop();
|
||||
return Accepted();
|
||||
}
|
||||
}
|
9
API/Controllers/ApiController.cs
Normal file
9
API/Controllers/ApiController.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using log4net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
public abstract class ApiController(Type type) : Controller
|
||||
{
|
||||
protected ILog Log { get; init; } = LogManager.GetLogger(type);
|
||||
}
|
66
API/Controllers/DataController.cs
Normal file
66
API/Controllers/DataController.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SQLiteEF;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DataController(Context databaseContext) : ApiController(typeof(DataController))
|
||||
{
|
||||
[HttpGet("Players")]
|
||||
[ProducesResponseType<Player[]>(Status200OK)]
|
||||
public IActionResult GetPlayers()
|
||||
{
|
||||
return Ok(databaseContext.Players.ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("Player/{steamId}")]
|
||||
[ProducesResponseType<Player>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetPlayer(ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
return Ok(player);
|
||||
}
|
||||
|
||||
[HttpGet("Player/{steamId}/Games")]
|
||||
[ProducesResponseType<Game[]>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetGamesPlayedByPlayer(ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
databaseContext.Entry(player).Collection(p => p.Games).Load();
|
||||
return Ok(player.Games.ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("Games")]
|
||||
[ProducesResponseType<Game[]>(Status200OK)]
|
||||
public IActionResult GetGames()
|
||||
{
|
||||
return Ok(databaseContext.Games.ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("Game/{appId}")]
|
||||
[ProducesResponseType<Game>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetGame(ulong appId)
|
||||
{
|
||||
if (databaseContext.Games.Find(appId) is not { } game)
|
||||
return NotFound();
|
||||
return Ok(game);
|
||||
}
|
||||
|
||||
[HttpGet("Game/{appId}/Players")]
|
||||
[ProducesResponseType<Player[]>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetPlayersPlayingGame(ulong appId)
|
||||
{
|
||||
if (databaseContext.Games.Find(appId) is not { } game)
|
||||
return NotFound();
|
||||
databaseContext.Entry(game).Collection(g => g.PlayedBy).Load();
|
||||
return Ok(game.PlayedBy.ToArray());
|
||||
}
|
||||
}
|
55
API/Controllers/TimeTrackController.cs
Normal file
55
API/Controllers/TimeTrackController.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SQLiteEF;
|
||||
using static Microsoft.AspNetCore.Http.StatusCodes;
|
||||
|
||||
namespace API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class TimeTrackController(Context databaseContext) : ApiController(typeof(TimeTrackController))
|
||||
{
|
||||
[HttpGet("{steamId}/{appId}")]
|
||||
[ProducesResponseType<TrackedTime[]>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetTrackedTimeForApp(ulong steamId, ulong appId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
if (databaseContext.Games.Find(appId) is not { } game)
|
||||
return NotFound();
|
||||
databaseContext.Entry(player).Collection(p => p.TrackedTimes).Load();
|
||||
return Ok(player.TrackedTimes.Where(t => t.Game == game).ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("{steamId}/Total")]
|
||||
[ProducesResponseType<ulong>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetTrackedTimeTotal(ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
databaseContext.Entry(player).Collection(p => p.TrackedTimes).Load();
|
||||
ulong[] times = player.TrackedTimes.GroupBy(t => t.Game)
|
||||
.Select(t => t.MaxBy(time => time.TimePlayed)?.TimePlayed ?? 0)
|
||||
.ToArray();
|
||||
ulong sum = 0;
|
||||
foreach (ulong time in times)
|
||||
sum += time;
|
||||
return Ok(sum);
|
||||
}
|
||||
|
||||
[HttpGet("{steamId}/PerGame")]
|
||||
[ProducesResponseType<Dictionary<ulong, ulong>>(Status200OK)]
|
||||
[ProducesResponseType(Status404NotFound)]
|
||||
public IActionResult GetTrackedTimeAll(ulong steamId)
|
||||
{
|
||||
if (databaseContext.Players.Find(steamId) is not { } player)
|
||||
return NotFound();
|
||||
databaseContext.Entry(player).Collection(p => p.TrackedTimes).Load();
|
||||
Dictionary<ulong, ulong> trackedTimes = player.TrackedTimes
|
||||
.GroupBy(t => t.Game)
|
||||
.ToDictionary(t => t.Key.AppId, t => t.MaxBy(time => time.TimePlayed)?.TimePlayed??0);
|
||||
|
||||
return Ok(trackedTimes);
|
||||
}
|
||||
}
|
202
API/Tracker.cs
Normal file
202
API/Tracker.cs
Normal file
@ -0,0 +1,202 @@
|
||||
using System.Globalization;
|
||||
using log4net;
|
||||
using SQLiteEF;
|
||||
using SteamApiWrapper.ReturnTypes;
|
||||
using SteamGame = SteamApiWrapper.ReturnTypes.Game;
|
||||
using Player = SQLiteEF.Player;
|
||||
using SteamPlayer = SteamApiWrapper.ReturnTypes.Player;
|
||||
using Steam = SteamApiWrapper.SteamApiWrapper;
|
||||
|
||||
namespace API;
|
||||
|
||||
public class Tracker : IDisposable
|
||||
{
|
||||
private readonly Thread _trackerThread;
|
||||
private bool _running = true;
|
||||
private ILog Log { get; } = LogManager.GetLogger(typeof(Tracker));
|
||||
private TimeSpan Interval { get; init; }
|
||||
|
||||
public Tracker(IServiceProvider serviceProvider, IConfiguration configuration)
|
||||
{
|
||||
Interval = TimeSpan.Parse(configuration.GetValue<string>("UpdateInterval")!, CultureInfo.InvariantCulture);
|
||||
_trackerThread = new (TrackerLoop);
|
||||
_trackerThread.Start(serviceProvider);
|
||||
}
|
||||
|
||||
internal void UpdatePlayers(Context context)
|
||||
{
|
||||
Log.Info("Updating Players");
|
||||
Player[] dbPlayers = context.Players.ToArray();
|
||||
SteamPlayer[] summaries = Steam.GetPlayerSummaries(dbPlayers.Select(p => p.SteamId).ToArray());
|
||||
foreach (Player dbPlayer in dbPlayers)
|
||||
{
|
||||
if(summaries.All(summaryPlayer => summaryPlayer.steamid != dbPlayer.SteamId))
|
||||
continue;
|
||||
SteamPlayer summaryPlayer = summaries.First(summaryPlayer => summaryPlayer.steamid == dbPlayer.SteamId);
|
||||
|
||||
try
|
||||
{
|
||||
dbPlayer.Name = summaryPlayer.personaname;
|
||||
dbPlayer.AvatarUrl = summaryPlayer.avatar;
|
||||
dbPlayer.ProfileUrl = summaryPlayer.profileurl;
|
||||
dbPlayer.UpdatedAt = DateTime.UtcNow;
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdatePlayer(Context context, Player player)
|
||||
{
|
||||
SteamPlayer[] summaries = Steam.GetPlayerSummaries([player.SteamId]);
|
||||
if(summaries.All(summaryPlayer => summaryPlayer.steamid != player.SteamId))
|
||||
return;
|
||||
SteamPlayer summaryPlayer = summaries.First(summaryPlayer => summaryPlayer.steamid == player.SteamId);
|
||||
|
||||
try
|
||||
{
|
||||
player.Name = summaryPlayer.personaname;
|
||||
player.AvatarUrl = summaryPlayer.avatar;
|
||||
player.ProfileUrl = summaryPlayer.profileurl;
|
||||
player.UpdatedAt = DateTime.UtcNow;
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateOwnedGames(Context context)
|
||||
{
|
||||
Log.Info("Updating Owned Games");
|
||||
Player[] players = context.Players.ToArray();
|
||||
foreach (Player player in players)
|
||||
{
|
||||
UpdateOwnedGamesPlayer(context, player);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateOwnedGamesPlayer(Context context, Player player)
|
||||
{
|
||||
Log.Debug($"Updating owned games for player {player}");
|
||||
SteamGame[] ownedGames = Steam.GetOwnedGames(player.SteamId);
|
||||
foreach (SteamGame ownedGame in ownedGames)
|
||||
{
|
||||
if (context.Games.Find(ownedGame.appid) is not { } game)
|
||||
{
|
||||
game = new(ownedGame.appid, ownedGame.name);
|
||||
context.Games.Add(game);
|
||||
}
|
||||
|
||||
if (!player.Games.Contains(game))
|
||||
{
|
||||
context.Entry(player).Collection(p => p.Games).Load();
|
||||
player.Games.Add(game);
|
||||
context.Entry(game).Collection(g => g.PlayedBy).Load();
|
||||
game.PlayedBy.Add(player);
|
||||
context.Entry(player).Collection(p => p.TrackedTimes).Load();
|
||||
player.TrackedTimes.Add(new (game, player, ownedGame.playtime_forever));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateGameTimes(Context context)
|
||||
{
|
||||
Log.Info("Updating Game Times");
|
||||
Player[] players = context.Players.ToArray();
|
||||
foreach (Player player in players)
|
||||
{
|
||||
UpdateGameTimesPlayer(context, player);
|
||||
}
|
||||
}
|
||||
|
||||
internal void UpdateGameTimesPlayer(Context context, Player player)
|
||||
{
|
||||
Log.Debug($"Updating game times for player {player}");
|
||||
GetRecentlyPlayedGames recentlyPlayed = Steam.GetRecentlyPlayedGames(player.SteamId);
|
||||
foreach (SteamGame recentlyPlayedGame in recentlyPlayed.games)
|
||||
{
|
||||
if (context.Games.Find(recentlyPlayedGame.appid) is not { } game)
|
||||
{
|
||||
game = new(recentlyPlayedGame.appid, recentlyPlayedGame.name);
|
||||
context.Games.Add(game);
|
||||
}
|
||||
|
||||
if (!player.Games.Contains(game))
|
||||
{
|
||||
context.Entry(player).Collection(p => p.Games).Load();
|
||||
player.Games.Add(game);
|
||||
context.Entry(game).Collection(g => g.PlayedBy).Load();
|
||||
game.PlayedBy.Add(player);
|
||||
}
|
||||
|
||||
context.Entry(player).Collection(p => p.TrackedTimes).Load();
|
||||
player.TrackedTimes.Add(new (game, player, recentlyPlayedGame.playtime_forever));
|
||||
|
||||
try
|
||||
{
|
||||
context.SaveChanges();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackerLoop(object? obj)
|
||||
{
|
||||
if (obj is not IServiceProvider serviceProvider)
|
||||
{
|
||||
Log.Fatal("Tracker loop wasn't started.");
|
||||
return;
|
||||
}
|
||||
|
||||
while (_running)
|
||||
{
|
||||
IServiceScope scope = serviceProvider.CreateScope();
|
||||
Context context = scope.ServiceProvider.GetRequiredService<Context>();
|
||||
UpdatePlayers(context);
|
||||
UpdateOwnedGames(context);
|
||||
UpdateGameTimes(context);
|
||||
scope.Dispose();
|
||||
try
|
||||
{
|
||||
Thread.Sleep(Interval);
|
||||
}
|
||||
catch (ThreadInterruptedException)
|
||||
{
|
||||
Log.Debug("Thread interrupted");
|
||||
}
|
||||
catch (ThreadAbortException)
|
||||
{
|
||||
_running = false;
|
||||
}
|
||||
}
|
||||
Log.Info("Thread exited");
|
||||
}
|
||||
|
||||
public void ForceLoop()
|
||||
{
|
||||
_trackerThread.Interrupt();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_running = false;
|
||||
_trackerThread.Interrupt();
|
||||
}
|
||||
}
|
@ -2,5 +2,6 @@
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data Source=SteamGameTime.db;"
|
||||
},
|
||||
"UpdateInterval": "00:05:00",
|
||||
"SteamWebApiKey": ""
|
||||
}
|
||||
|
Reference in New Issue
Block a user