Compare commits

...

2 Commits

Author SHA1 Message Date
1a08f932af Working 2025-05-26 01:44:26 +02:00
ebddd3c3ed SteamWebApiKey 2025-05-25 23:43:22 +02:00
24 changed files with 533 additions and 268 deletions

4
.gitignore vendored
View File

@ -3,4 +3,6 @@ obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
.idea/
db/
SteamGameTime.db*

View File

@ -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>

View File

@ -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();
}

View 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();
}
}

View 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);
}

View 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());
}
}

View 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
View 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();
}
}

View File

@ -1,8 +1,6 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
"ConnectionStrings": {
"DefaultConnection": "Data Source=SteamGameTime.db;"
},
"SteamWebApiKey": ""
}

View File

@ -1,9 +1,7 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
"ConnectionStrings": {
"DefaultConnection": "Data Source=SteamGameTime.db;"
},
"AllowedHosts": "*"
"UpdateInterval": "00:05:00",
"SteamWebApiKey": ""
}

View File

@ -1,12 +0,0 @@
using API;
namespace Run;
class Program
{
static void Main(string[] args)
{
Api a = new (args);
Tracker.Tracker t = new ();
}
}

View File

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\API\API.csproj" />
<ProjectReference Include="..\Tracker\Tracker.csproj" />
</ItemGroup>
</Project>

View File

@ -1,16 +1,23 @@
using log4net;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace SQLiteEF;
public class Context(IConfiguration configuration) : DbContext
public class Context(DbContextOptions<Context> contextOptions) : DbContext(contextOptions)
{
public DbSet<Player> Players { get; set; }
public DbSet<Game> Games { get; set; }
private ILog Log { get; } = LogManager.GetLogger(typeof(Context));
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(configuration.GetConnectionString("DefaultConnection"));
optionsBuilder.EnableDetailedErrors().EnableSensitiveDataLogging();
optionsBuilder.LogTo(s => Log.Debug(s), (_, level) => level <= LogLevel.Debug);
optionsBuilder.LogTo(s => Log.Info(s), (_, level) => level == LogLevel.Information);
optionsBuilder.LogTo(s => Log.Warn(s), (_, level) => level == LogLevel.Warning);
optionsBuilder.LogTo(s => Log.Error(s), (_, level) => level == LogLevel.Error);
optionsBuilder.LogTo(s => Log.Fatal(s), (_, level) => level >= LogLevel.Critical);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
@ -20,14 +27,29 @@ public class Context(IConfiguration configuration) : DbContext
.WithMany(g => g.PlayedBy);
modelBuilder.Entity<Player>()
.Navigation(p => p.Games)
.AutoInclude();
.AutoInclude(false);
modelBuilder.Entity<Player>()
.Navigation(p => p.TrackedTimes)
.AutoInclude(false);
modelBuilder.Entity<Game>()
.Navigation(g => g.PlayedBy)
.AutoInclude(false);
modelBuilder.Entity<Game>()
.Navigation(g => g.TrackedTimes)
.AutoInclude(false);
modelBuilder.Entity<TrackedTime>()
.HasOne<Player>(p => p.Player)
.WithMany(p => p.TrackedTimes)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<TrackedTime>()
.Navigation(t => t.Player)
.AutoInclude();
modelBuilder.Entity<TrackedTime>()
.HasOne<Game>(p => p.Game)
.WithMany(g => g.TrackedTimes)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<TrackedTime>()
.Navigation(t => t.Game)
.AutoInclude();
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace SQLiteEF;
@ -7,6 +8,6 @@ public class Game(ulong appId, string name)
{
public ulong AppId { get; init; } = appId;
public string Name { get; init; } = name;
public ICollection<Player> PlayedBy { get; init; } = null!;
public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
[JsonIgnore] public ICollection<Player> PlayedBy { get; init; } = null!;
[JsonIgnore] public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
}

View File

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace SQLiteEF;
@ -10,13 +11,13 @@ public class Player : IUpdateable
public string ProfileUrl { get; set; }
public string AvatarUrl { get; set; }
public ICollection<Game> Games { get; init; } = null!;
public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
public DateTime UpdatedAt { get; set; } = DateTime.Now;
[JsonIgnore] public ICollection<Game> Games { get; init; } = null!;
[JsonIgnore] public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public Player(ulong steamid, string name, string profileUrl, string avatarUrl)
public Player(ulong steamId, string name, string profileUrl, string avatarUrl)
{
this.SteamId = steamid;
this.SteamId = steamId;
this.Name = name;
this.ProfileUrl = profileUrl;
this.AvatarUrl = avatarUrl;
@ -27,9 +28,9 @@ public class Player : IUpdateable
/// <summary>
/// EF CORE
/// </summary>
internal Player(ulong steamid, string name, string profileUrl, string avatarUrl, DateTime updatedAt)
internal Player(ulong steamId, string name, string profileUrl, string avatarUrl, DateTime updatedAt)
{
this.SteamId = steamid;
this.SteamId = steamId;
this.Name = name;
this.ProfileUrl = profileUrl;
this.AvatarUrl = avatarUrl;

View File

@ -7,8 +7,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@ -1,12 +1,13 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace SQLiteEF;
[PrimaryKey("TimeStamp")]
public class TrackedTime
{
public Game Game { get; init; }
public Player Player { get; init; }
[JsonIgnore] public Game Game { get; init; }
[JsonIgnore] public Player Player { get; init; }
public DateTime TimeStamp { get; init; }
public ulong TimePlayed { get; init; }
@ -14,7 +15,7 @@ public class TrackedTime
{
this.Game = game;
this.Player = player;
this.TimeStamp = timeStamp??DateTime.Now;
this.TimeStamp = timeStamp??DateTime.UtcNow;
this.TimePlayed = timePlayed;
}

View File

@ -1,3 +0,0 @@
namespace SteamApiWrapper.ReturnTypes;
public record GetOwnedGames(uint game_count, Game[] games) : IReturnType;

View File

@ -1,3 +0,0 @@
namespace SteamApiWrapper.ReturnTypes;
public record GetPlayerSummaries(Player[] players) : IReturnType;

View File

@ -1,3 +1,4 @@
using log4net;
using Newtonsoft.Json.Linq;
using SteamApiWrapper.ReturnTypes;
@ -5,9 +6,10 @@ namespace SteamApiWrapper;
public static class SteamApiWrapper
{
public const string ApiUrl = "http://api.steampowered.com/";
public const string ApiUrl = "http://api.steampowered.com";
private static string _apiKey = string.Empty;
private static HttpClient client = new ();
private static ILog Log { get; } = LogManager.GetLogger(typeof(SteamApiWrapper));
public static void SetApiKey(string apiKey)
{
@ -25,81 +27,93 @@ public static class SteamApiWrapper
return ret;
}
public static GetOwnedGames GetOwnedGames(ulong steamid)
public static Game[] GetOwnedGames(ulong steamid)
{
Log.Debug("GetOwnedGames");
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.IPlayerService, "GetOwnedGames", opts: new()
{
{"steamid", steamid.ToString()},
{"include_appinfo", "true"},
{"include_played_free_games", "true"}
});
Log.Debug(request.ToString());
try
{
HttpResponseMessage response = client.Send(request);
response.EnsureSuccessStatusCode();
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
return jObj["response"]?.ToObject<GetOwnedGames>()??new(0, []);
Log.Debug(jObj);
return jObj["response"]?["games"]?.ToObject<Game[]>()??[];
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return new(0, []);
Log.Error(e);
return [];
}
}
public static GetPlayerSummaries GetPlayerSummaries(ulong[] steamids)
public static Player[] GetPlayerSummaries(ulong[] steamids)
{
Log.Debug("GetPlayerSummaries");
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.ISteamUser, "GetPlayerSummaries", "v0002", opts: new()
{
{"steamids", string.Join(',', steamids)}
});
Log.Debug(request.ToString());
try
{
HttpResponseMessage response = client.Send(request);
response.EnsureSuccessStatusCode();
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
return jObj["response"]?["players"]?.ToObject<GetPlayerSummaries>()??new([]);
Log.Debug(jObj);
return jObj["response"]?["players"]?.ToObject<Player[]>()??[];
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return new([]);
Log.Error(e);
return [];
}
}
public static GetRecentlyPlayedGames GetRecentlyPlayedGames(ulong steamid)
{
Log.Debug("GetRecentlyPlayedGames");
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.IPlayerService, "GetRecentlyPlayedGames", opts: new()
{
{"steamid", steamid.ToString()}
});
Log.Debug(request.ToString());
try
{
HttpResponseMessage response = client.Send(request);
response.EnsureSuccessStatusCode();
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
Log.Debug(jObj);
return jObj["response"]?.ToObject<GetRecentlyPlayedGames>()??new(0, []);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Log.Error(e);
return new(0, []);
}
}
public static GetSupportedAPIList GetSupportedAPIList()
{
Log.Debug("GetSupportedAPIList");
HttpRequestMessage request = BuildRequest(SupportedAPIInterface.ISteamWebAPIUtil, "GetSupportedAPIList");
Log.Debug(request.ToString());
try
{
HttpResponseMessage response = client.Send(request);
response.EnsureSuccessStatusCode();
JObject jObj = JObject.Parse(response.Content.ReadAsStringAsync().Result);
Log.Debug(jObj);
return jObj["apilist"]?.ToObject<GetSupportedAPIList>()??new([]);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Log.Error(e);
return new([]);
}
}

View File

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

View File

@ -4,10 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "API\API.csproj", "{9
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SteamApiWrapper", "SteamApiWrapper\SteamApiWrapper.csproj", "{53B416A7-E3B9-4F19-9A71-16CA115BB127}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Run", "Run\Run.csproj", "{959773AD-4671-4FC7-9729-0EE4A971918E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tracker", "Tracker\Tracker.csproj", "{59876ADC-5DF9-4672-8AA2-F6943966ABF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SQLiteEF", "SQLiteEF\SQLiteEF.csproj", "{A269EE55-A8E1-4421-9A70-D6F36D162C4D}"
EndProject
Global
@ -24,14 +20,6 @@ Global
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53B416A7-E3B9-4F19-9A71-16CA115BB127}.Release|Any CPU.Build.0 = Release|Any CPU
{959773AD-4671-4FC7-9729-0EE4A971918E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{959773AD-4671-4FC7-9729-0EE4A971918E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{959773AD-4671-4FC7-9729-0EE4A971918E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{959773AD-4671-4FC7-9729-0EE4A971918E}.Release|Any CPU.Build.0 = Release|Any CPU
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59876ADC-5DF9-4672-8AA2-F6943966ABF5}.Release|Any CPU.Build.0 = Release|Any CPU
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A269EE55-A8E1-4421-9A70-D6F36D162C4D}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -1,155 +0,0 @@
using Microsoft.Extensions.Configuration;
using SQLiteEF;
using SteamApiWrapper.ReturnTypes;
using SteamGame = SteamApiWrapper.ReturnTypes.Game;
using Player = SQLiteEF.Player;
using SteamPlayer = SteamApiWrapper.ReturnTypes.Player;
using Steam = SteamApiWrapper.SteamApiWrapper;
namespace Tracker;
public class Tracker : IDisposable
{
private IConfiguration Configuration { get; init; }
private readonly Thread _trackerThread;
private bool _running = true;
public Tracker()
{
Configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
_trackerThread = new (TrackerLoop);
_trackerThread.Start();
}
private void UpdatePlayers()
{
Context context = new (Configuration);
IQueryable<ulong> players = context.Players.Select(p => p.SteamId);
GetPlayerSummaries summaries = Steam.GetPlayerSummaries(players.ToArray());
foreach (SteamPlayer summariesPlayer in summaries.players)
{
if (context.Players.Find(summariesPlayer.steamid) is not { } player)
{
context.Players.Add(new (summariesPlayer.steamid, summariesPlayer.personaname,
summariesPlayer.profileurl, summariesPlayer.avatar));
continue;
}
player.Name = summariesPlayer.personaname;
player.ProfileUrl = summariesPlayer.profileurl;
player.AvatarUrl = summariesPlayer.avatar;
try
{
context.SaveChanges();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
private void UpdateOwnedGames()
{
Context context = new (Configuration);
IQueryable<Player> players = context.Players;
foreach (Player player in players)
{
GetOwnedGames ownedGames = Steam.GetOwnedGames(player.SteamId);
foreach (SteamGame ownedGame in ownedGames.games)
{
if (context.Games.Find(ownedGame.appid) is not { } game)
{
game = new(ownedGame.appid, ownedGame.name);
}
if (!player.Games.Contains(game))
{
player.Games.Add(game);
game.PlayedBy.Add(player);
player.TrackedTimes.Add(new (game, player, ownedGame.playtime_forever));
}
try
{
context.SaveChanges();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
private void UpdateGametimes()
{
Context context = new (Configuration);
IQueryable<Player> players = context.Players;
foreach (Player player in players)
{
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);
}
if (!player.Games.Contains(game))
{
player.Games.Add(game);
game.PlayedBy.Add(player);
}
player.TrackedTimes.Add(new (game, player, recentlyPlayedGame.playtime_forever));
try
{
context.SaveChanges();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
private void TrackerLoop()
{
while (_running)
{
try
{
Thread.Sleep(TimeSpan.FromHours(1));
}
catch (ThreadInterruptedException)
{
Console.WriteLine("Thread interrupted");
}
catch (ThreadAbortException)
{
_running = false;
}
}
Console.WriteLine("Thread exited");
}
public void ForceLoop()
{
_trackerThread.Interrupt();
}
static void Main(string[] args)
{
Tracker _ = new ();
}
public void Dispose()
{
_running = false;
_trackerThread.Interrupt();
}
}

View File

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SQLiteEF\SQLiteEF.csproj" />
<ProjectReference Include="..\SteamApiWrapper\SteamApiWrapper.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
</ItemGroup>
</Project>