Compare commits

..

5 Commits

9 changed files with 256 additions and 12 deletions

View File

@ -51,8 +51,8 @@ public class Api
app.MapControllers();
_tracker = app.Services.GetRequiredService<Tracker>();
app.Services.CreateScope().ServiceProvider.GetRequiredService<Context>().Database.Migrate();
_tracker = app.Services.GetRequiredService<Tracker>();
app.Run();
}

View File

@ -10,16 +10,17 @@ public class TimeTrackController(Context databaseContext) : ApiController(typeof
{
[HttpGet("{steamId}")]
[ProducesResponseType<Dictionary<ulong, TrackedTime[]>>(Status200OK)]
[ProducesResponseType<KeyValuePair<ulong, TrackedTime[]>[]>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetTrackedTime(ulong steamId)
{
if (databaseContext.Players.Find(steamId) is not { } player)
return NotFound();
databaseContext.Entry(player).Collection(p => p.TrackedTimes).Load();
Dictionary<ulong, TrackedTime[]> ret = player.TrackedTimes
KeyValuePair<ulong, TrackedTime[]>[] ret = player.TrackedTimes
.GroupBy(t => t.Game)
.ToDictionary(t => t.Key.AppId, t => t.ToArray());
.Select(t => new KeyValuePair<ulong, TrackedTime[]>(t.Key.AppId, t.ToArray()))
.ToArray();
return Ok(ret);
}
@ -53,17 +54,33 @@ public class TimeTrackController(Context databaseContext) : ApiController(typeof
return Ok(sum);
}
[HttpGet("{steamId}/Total/{appId}")]
[ProducesResponseType<ulong>(Status200OK)]
[ProducesResponseType(Status404NotFound)]
public IActionResult GetTrackedTimeAll(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();
ulong? maxTime = player.TrackedTimes.Where(t => t.Game == game).MaxBy(t => t.TimePlayed)?.TimePlayed;
return maxTime is not null ? Ok(maxTime) : NoContent();
}
[HttpGet("{steamId}/Total/PerGame")]
[ProducesResponseType<Dictionary<ulong, ulong>>(Status200OK)]
[ProducesResponseType<KeyValuePair<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
KeyValuePair<ulong, ulong>[] trackedTimes = player.TrackedTimes
.GroupBy(t => t.Game)
.ToDictionary(t => t.Key.AppId, t => t.MaxBy(time => time.TimePlayed)?.TimePlayed??0);
.Select(t => new KeyValuePair<ulong, ulong>(t.Key.AppId, t.MaxBy(time => time.TimePlayed)?.TimePlayed ?? 0))
.ToArray();
return Ok(trackedTimes);
}

View File

@ -0,0 +1,154 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SQLiteEF;
#nullable disable
namespace API.Migrations
{
[DbContext(typeof(Context))]
[Migration("20250526144942_Game-Logo-and-Icon")]
partial class GameLogoandIcon
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.5");
modelBuilder.Entity("GamePlayer", b =>
{
b.Property<ulong>("GamesAppId")
.HasColumnType("INTEGER");
b.Property<ulong>("PlayedBySteamId")
.HasColumnType("INTEGER");
b.HasKey("GamesAppId", "PlayedBySteamId");
b.HasIndex("PlayedBySteamId");
b.ToTable("GamePlayer");
});
modelBuilder.Entity("SQLiteEF.Game", b =>
{
b.Property<ulong>("AppId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("IconUrl")
.HasColumnType("TEXT");
b.Property<string>("LogoUrl")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("AppId");
b.ToTable("Games");
});
modelBuilder.Entity("SQLiteEF.Player", b =>
{
b.Property<ulong>("SteamId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AvatarUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ProfileUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("SteamId");
b.ToTable("Players");
});
modelBuilder.Entity("SQLiteEF.TrackedTime", b =>
{
b.Property<DateTime>("TimeStamp")
.HasColumnType("TEXT");
b.Property<ulong>("GameAppId")
.HasColumnType("INTEGER");
b.Property<ulong>("PlayerSteamId")
.HasColumnType("INTEGER");
b.Property<ulong>("TimePlayed")
.HasColumnType("INTEGER");
b.HasKey("TimeStamp");
b.HasIndex("GameAppId");
b.HasIndex("PlayerSteamId");
b.ToTable("TrackedTime");
});
modelBuilder.Entity("GamePlayer", b =>
{
b.HasOne("SQLiteEF.Game", null)
.WithMany()
.HasForeignKey("GamesAppId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SQLiteEF.Player", null)
.WithMany()
.HasForeignKey("PlayedBySteamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SQLiteEF.TrackedTime", b =>
{
b.HasOne("SQLiteEF.Game", "Game")
.WithMany("TrackedTimes")
.HasForeignKey("GameAppId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SQLiteEF.Player", "Player")
.WithMany("TrackedTimes")
.HasForeignKey("PlayerSteamId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Game");
b.Navigation("Player");
});
modelBuilder.Entity("SQLiteEF.Game", b =>
{
b.Navigation("TrackedTimes");
});
modelBuilder.Entity("SQLiteEF.Player", b =>
{
b.Navigation("TrackedTimes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace API.Migrations
{
/// <inheritdoc />
public partial class GameLogoandIcon : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "IconUrl",
table: "Games",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LogoUrl",
table: "Games",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IconUrl",
table: "Games");
migrationBuilder.DropColumn(
name: "LogoUrl",
table: "Games");
}
}
}

View File

@ -38,6 +38,12 @@ namespace API.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("IconUrl")
.HasColumnType("TEXT");
b.Property<string>("LogoUrl")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");

View File

@ -86,11 +86,23 @@ public class Tracker : IDisposable
SteamGame[] ownedGames = Steam.GetOwnedGames(player.SteamId);
foreach (SteamGame ownedGame in ownedGames)
{
string? iconUrlStr = ownedGame.img_icon_url is not null
? $"http://media.steampowered.com/steamcommunity/public/images/apps/{ownedGame.appid}/{ownedGame.img_icon_url}.jpg"
: null;
string? logoUrlStr = ownedGame.img_logo_url is not null
? $"http://media.steampowered.com/steamcommunity/public/images/apps/{ownedGame.appid}/{ownedGame.img_logo_url}.jpg"
: null;
if (context.Games.Find(ownedGame.appid) is not { } game)
{
game = new(ownedGame.appid, ownedGame.name);
game = new(ownedGame.appid, ownedGame.name, iconUrlStr, logoUrlStr);
context.Games.Add(game);
}
else
{
game.Name = ownedGame.name;
game.IconUrl = iconUrlStr;
game.LogoUrl = logoUrlStr;
}
if (!player.Games.Contains(game))
{
@ -127,13 +139,27 @@ public class Tracker : IDisposable
{
Log.Debug($"Updating game times for player {player}");
GetRecentlyPlayedGames recentlyPlayed = Steam.GetRecentlyPlayedGames(player.SteamId);
if (recentlyPlayed.total_count < 1)
return;
foreach (SteamGame recentlyPlayedGame in recentlyPlayed.games)
{
string? iconUrlStr = recentlyPlayedGame.img_icon_url is not null
? $"http://media.steampowered.com/steamcommunity/public/images/apps/{recentlyPlayedGame.appid}/{recentlyPlayedGame.img_icon_url}.jpg"
: null;
string? logoUrlStr = recentlyPlayedGame.img_logo_url is not null
? $"http://media.steampowered.com/steamcommunity/public/images/apps/{recentlyPlayedGame.appid}/{recentlyPlayedGame.img_logo_url}.jpg"
: null;
if (context.Games.Find(recentlyPlayedGame.appid) is not { } game)
{
game = new(recentlyPlayedGame.appid, recentlyPlayedGame.name);
game = new(recentlyPlayedGame.appid, recentlyPlayedGame.name, iconUrlStr, logoUrlStr);
context.Games.Add(game);
}
else
{
game.Name = recentlyPlayedGame.name;
game.IconUrl = iconUrlStr;
game.LogoUrl = logoUrlStr;
}
if (!player.Games.Contains(game))
{

View File

@ -4,10 +4,12 @@ using Newtonsoft.Json;
namespace SQLiteEF;
[PrimaryKey("AppId")]
public class Game(ulong appId, string name)
public class Game(ulong appId, string name, string? iconUrl, string? logoUrl)
{
public ulong AppId { get; init; } = appId;
public string Name { get; init; } = name;
public string Name { get; set; } = name;
public string? IconUrl { get; set; } = iconUrl;
public string? LogoUrl { get; set; } = logoUrl;
[JsonIgnore] public ICollection<Player> PlayedBy { get; init; } = null!;
[JsonIgnore] public ICollection<TrackedTime> TrackedTimes { get; init; } = null!;
}

View File

@ -1,3 +1,3 @@
namespace SteamApiWrapper.ReturnTypes;
public record Game(ulong appid, ulong playtime_forever, string name);
public record Game(ulong appid, ulong playtime_forever, string name, string? img_icon_url, string? img_logo_url);

View File

@ -6,4 +6,5 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMigrationCommandExecutor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fb43021565ba7f13262dd95827ae378ea6015db2c146979398f92c7356d98488_003FMigrationCommandExecutor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARelationalConnection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F696ea149e41ddc60bdd9238370d01ba5f417a4a91ce8ac42c17d4eee0afa038_003FRelationalConnection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASingleQueryingEnumerable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F413341ec7da6e42cb630e52ba9208edacb2e7267da1d9296f51628fcd35e81d9_003FSingleQueryingEnumerable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fbf07be36dfaa4f47b843d44a6617c8b9d19e00_003Fa5_003F6dee0ce4_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AXmlTextReaderImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff3fbda37b1e0423781e129b1ac69751d7a1a00_003F84_003F4c900766_003FXmlTextReaderImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>