From 1a08f932afd9fb9d23674be28ffd66e14b795b6a Mon Sep 17 00:00:00 2001 From: glax Date: Mon, 26 May 2025 01:44:26 +0200 Subject: [PATCH] Working --- .gitignore | 4 +- API/API.csproj | 13 +- API/Api.cs | 26 ++- API/Controllers/ActionsController.cs | 84 ++++++++ API/Controllers/ApiController.cs | 9 + API/Controllers/DataController.cs | 66 ++++++ API/Controllers/TimeTrackController.cs | 55 +++++ API/Tracker.cs | 202 ++++++++++++++++++ API/appsettings.json | 1 + Run/Program.cs | 12 -- Run/Run.csproj | 16 -- SQLiteEF/Context.cs | 30 ++- SQLiteEF/Game.cs | 5 +- SQLiteEF/Player.cs | 15 +- SQLiteEF/SQLiteEF.csproj | 6 + SQLiteEF/TrackedTime.cs | 7 +- SteamApiWrapper/ReturnTypes/GetOwnedGames.cs | 3 - .../ReturnTypes/GetPlayerSummaries.cs | 3 - SteamApiWrapper/SteamApiWrapper.cs | 36 +++- SteamApiWrapper/SteamApiWrapper.csproj | 1 + SteamGameTimeTrack.sln | 12 -- Tracker/Tracker.cs | 155 -------------- Tracker/Tracker.csproj | 21 -- 23 files changed, 526 insertions(+), 256 deletions(-) create mode 100644 API/Controllers/ActionsController.cs create mode 100644 API/Controllers/ApiController.cs create mode 100644 API/Controllers/DataController.cs create mode 100644 API/Controllers/TimeTrackController.cs create mode 100644 API/Tracker.cs delete mode 100644 Run/Program.cs delete mode 100644 Run/Run.csproj delete mode 100644 SteamApiWrapper/ReturnTypes/GetOwnedGames.cs delete mode 100644 SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs delete mode 100644 Tracker/Tracker.cs delete mode 100644 Tracker/Tracker.csproj diff --git a/.gitignore b/.gitignore index f400df3..57ac3f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ obj/ /packages/ riderModule.iml /_ReSharper.Caches/ -.idea/ \ No newline at end of file +.idea/ +db/ +SteamGameTime.db* \ No newline at end of file diff --git a/API/API.csproj b/API/API.csproj index dacc57b..6e6cc3b 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -8,15 +8,26 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - + + diff --git a/API/Api.cs b/API/Api.cs index 809fc9e..459f60f 100644 --- a/API/Api.cs +++ b/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(); + builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"), optionsBuilder => optionsBuilder.MigrationsAssembly(Assembly.GetAssembly(GetType())!))); + + builder.Services.AddSingleton(); + SteamApiWrapper.SteamApiWrapper.SetApiKey(builder.Configuration.GetValue("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().Database.Migrate(); app.Run(); } diff --git a/API/Controllers/ActionsController.cs b/API/Controllers/ActionsController.cs new file mode 100644 index 0000000..e21f0b7 --- /dev/null +++ b/API/Controllers/ActionsController.cs @@ -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(Status202Accepted)] + [ProducesResponseType(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(); + } +} \ No newline at end of file diff --git a/API/Controllers/ApiController.cs b/API/Controllers/ApiController.cs new file mode 100644 index 0000000..683ca38 --- /dev/null +++ b/API/Controllers/ApiController.cs @@ -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); +} \ No newline at end of file diff --git a/API/Controllers/DataController.cs b/API/Controllers/DataController.cs new file mode 100644 index 0000000..bfcfaac --- /dev/null +++ b/API/Controllers/DataController.cs @@ -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(Status200OK)] + public IActionResult GetPlayers() + { + return Ok(databaseContext.Players.ToArray()); + } + + [HttpGet("Player/{steamId}")] + [ProducesResponseType(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(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(Status200OK)] + public IActionResult GetGames() + { + return Ok(databaseContext.Games.ToArray()); + } + + [HttpGet("Game/{appId}")] + [ProducesResponseType(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(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()); + } +} \ No newline at end of file diff --git a/API/Controllers/TimeTrackController.cs b/API/Controllers/TimeTrackController.cs new file mode 100644 index 0000000..3bcfde9 --- /dev/null +++ b/API/Controllers/TimeTrackController.cs @@ -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(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(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>(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 trackedTimes = player.TrackedTimes + .GroupBy(t => t.Game) + .ToDictionary(t => t.Key.AppId, t => t.MaxBy(time => time.TimePlayed)?.TimePlayed??0); + + return Ok(trackedTimes); + } +} \ No newline at end of file diff --git a/API/Tracker.cs b/API/Tracker.cs new file mode 100644 index 0000000..c62d022 --- /dev/null +++ b/API/Tracker.cs @@ -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("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(); + 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(); + } +} \ No newline at end of file diff --git a/API/appsettings.json b/API/appsettings.json index 993f2e4..3e3eb0a 100644 --- a/API/appsettings.json +++ b/API/appsettings.json @@ -2,5 +2,6 @@ "ConnectionStrings": { "DefaultConnection": "Data Source=SteamGameTime.db;" }, + "UpdateInterval": "00:05:00", "SteamWebApiKey": "" } diff --git a/Run/Program.cs b/Run/Program.cs deleted file mode 100644 index 546589b..0000000 --- a/Run/Program.cs +++ /dev/null @@ -1,12 +0,0 @@ -using API; - -namespace Run; - -class Program -{ - static void Main(string[] args) - { - Api a = new (args); - Tracker.Tracker t = new (); - } -} \ No newline at end of file diff --git a/Run/Run.csproj b/Run/Run.csproj deleted file mode 100644 index 1c8f52c..0000000 --- a/Run/Run.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - net9.0 - latest - enable - enable - - - - - - - - diff --git a/SQLiteEF/Context.cs b/SQLiteEF/Context.cs index d7aeff1..cb6f14d 100644 --- a/SQLiteEF/Context.cs +++ b/SQLiteEF/Context.cs @@ -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 contextOptions) : DbContext(contextOptions) { public DbSet Players { get; set; } public DbSet 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() .Navigation(p => p.Games) - .AutoInclude(); + .AutoInclude(false); + modelBuilder.Entity() + .Navigation(p => p.TrackedTimes) + .AutoInclude(false); + modelBuilder.Entity() + .Navigation(g => g.PlayedBy) + .AutoInclude(false); + modelBuilder.Entity() + .Navigation(g => g.TrackedTimes) + .AutoInclude(false); modelBuilder.Entity() .HasOne(p => p.Player) .WithMany(p => p.TrackedTimes) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(t => t.Player) + .AutoInclude(); modelBuilder.Entity() .HasOne(p => p.Game) .WithMany(g => g.TrackedTimes) .OnDelete(DeleteBehavior.Cascade); + modelBuilder.Entity() + .Navigation(t => t.Game) + .AutoInclude(); } } \ No newline at end of file diff --git a/SQLiteEF/Game.cs b/SQLiteEF/Game.cs index 04842c3..7c96587 100644 --- a/SQLiteEF/Game.cs +++ b/SQLiteEF/Game.cs @@ -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 PlayedBy { get; init; } = null!; - public ICollection TrackedTimes { get; init; } = null!; + [JsonIgnore] public ICollection PlayedBy { get; init; } = null!; + [JsonIgnore] public ICollection TrackedTimes { get; init; } = null!; } \ No newline at end of file diff --git a/SQLiteEF/Player.cs b/SQLiteEF/Player.cs index db048f3..7786d5f 100644 --- a/SQLiteEF/Player.cs +++ b/SQLiteEF/Player.cs @@ -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 Games { get; init; } = null!; - public ICollection TrackedTimes { get; init; } = null!; - public DateTime UpdatedAt { get; set; } = DateTime.Now; + [JsonIgnore] public ICollection Games { get; init; } = null!; + [JsonIgnore] public ICollection 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 /// /// EF CORE /// - 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; diff --git a/SQLiteEF/SQLiteEF.csproj b/SQLiteEF/SQLiteEF.csproj index 1211f9b..f2fe46c 100644 --- a/SQLiteEF/SQLiteEF.csproj +++ b/SQLiteEF/SQLiteEF.csproj @@ -7,8 +7,14 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/SQLiteEF/TrackedTime.cs b/SQLiteEF/TrackedTime.cs index c03095a..25f50b4 100644 --- a/SQLiteEF/TrackedTime.cs +++ b/SQLiteEF/TrackedTime.cs @@ -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; } diff --git a/SteamApiWrapper/ReturnTypes/GetOwnedGames.cs b/SteamApiWrapper/ReturnTypes/GetOwnedGames.cs deleted file mode 100644 index fa16ba6..0000000 --- a/SteamApiWrapper/ReturnTypes/GetOwnedGames.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SteamApiWrapper.ReturnTypes; - -public record GetOwnedGames(uint game_count, Game[] games) : IReturnType; \ No newline at end of file diff --git a/SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs b/SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs deleted file mode 100644 index 6686720..0000000 --- a/SteamApiWrapper/ReturnTypes/GetPlayerSummaries.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace SteamApiWrapper.ReturnTypes; - -public record GetPlayerSummaries(Player[] players) : IReturnType; \ No newline at end of file diff --git a/SteamApiWrapper/SteamApiWrapper.cs b/SteamApiWrapper/SteamApiWrapper.cs index 62c7fde..4220de4 100644 --- a/SteamApiWrapper/SteamApiWrapper.cs +++ b/SteamApiWrapper/SteamApiWrapper.cs @@ -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()??new(0, []); + Log.Debug(jObj); + return jObj["response"]?["games"]?.ToObject()??[]; } 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()??new([]); + Log.Debug(jObj); + return jObj["response"]?["players"]?.ToObject()??[]; } 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()??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()??new([]); } catch (Exception e) { - Console.WriteLine(e.Message); + Log.Error(e); return new([]); } } diff --git a/SteamApiWrapper/SteamApiWrapper.csproj b/SteamApiWrapper/SteamApiWrapper.csproj index 728fd98..b4ddbdb 100644 --- a/SteamApiWrapper/SteamApiWrapper.csproj +++ b/SteamApiWrapper/SteamApiWrapper.csproj @@ -8,6 +8,7 @@ + diff --git a/SteamGameTimeTrack.sln b/SteamGameTimeTrack.sln index 0698c66..771bc57 100644 --- a/SteamGameTimeTrack.sln +++ b/SteamGameTimeTrack.sln @@ -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 diff --git a/Tracker/Tracker.cs b/Tracker/Tracker.cs deleted file mode 100644 index b399ad3..0000000 --- a/Tracker/Tracker.cs +++ /dev/null @@ -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 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 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 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(); - } -} \ No newline at end of file diff --git a/Tracker/Tracker.csproj b/Tracker/Tracker.csproj deleted file mode 100644 index c46201b..0000000 --- a/Tracker/Tracker.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net9.0 - latest - enable - enable - - - - - - - - - - - - -