Added game last played, and total play time stats

* Added game last played, and total play time stats
This commit is contained in:
Michael Green
2024-02-07 16:19:30 +11:00
committed by GitHub
parent 3c451f5558
commit a2d863dc7a
10 changed files with 322 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,99 @@
using System.Data;
using gaseous_server.Models;
namespace gaseous_server.Classes
{
public class Statistics
{
public StatisticsModel RecordSession(Guid SessionId, long GameId, string UserId)
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql;
Dictionary<string, object> dbDict;
if (SessionId == Guid.Empty)
{
// new session required
SessionId = Guid.NewGuid();
sql = "INSERT INTO UserTimeTracking (GameId, UserId, SessionId, SessionTime, SessionLength) VALUES (@gameid, @userid, @sessionid, @sessiontime, @sessionlength);";
dbDict = new Dictionary<string, object>{
{ "gameid", GameId },
{ "userid", UserId },
{ "sessionid", SessionId },
{ "sessiontime", DateTime.UtcNow },
{ "sessionlength", 1 }
};
db.ExecuteNonQuery(sql, dbDict);
return new StatisticsModel{
GameId = GameId,
SessionId = SessionId,
SessionStart = (DateTime)dbDict["sessiontime"],
SessionLength = (int)dbDict["sessionlength"]
};
}
else
{
// update existing session
sql = "UPDATE UserTimeTracking SET SessionLength = SessionLength + @sessionlength WHERE GameId = @gameid AND UserId = @userid AND SessionId = @sessionid;";
dbDict = new Dictionary<string, object>{
{ "gameid", GameId },
{ "userid", UserId },
{ "sessionid", SessionId },
{ "sessionlength", 1 }
};
db.ExecuteNonQuery(sql, dbDict);
sql = "SELECT * FROM UserTimeTracking WHERE GameId = @gameid AND UserId = @userid AND SessionId = @sessionid;";
DataTable data = db.ExecuteCMD(sql, dbDict);
return new StatisticsModel{
GameId = (long)data.Rows[0]["GameId"],
SessionId = Guid.Parse(data.Rows[0]["SessionId"].ToString()),
SessionStart = (DateTime)data.Rows[0]["SessionTime"],
SessionLength = (int)data.Rows[0]["SessionLength"]
};
}
}
public StatisticsModel? GetSession(long GameId, string UserId)
{
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
string sql = "SELECT SUM(SessionLength) AS TotalLength FROM UserTimeTracking WHERE GameId = @gameid AND UserId = @userid;";
Dictionary<string, object> dbDict = new Dictionary<string, object>{
{ "gameid", GameId },
{ "userid", UserId }
};
DataTable data = db.ExecuteCMD(sql, dbDict);
if (data.Rows.Count == 0)
{
return null;
}
else
{
if (data.Rows[0]["TotalLength"] == DBNull.Value)
{
return null;
}
else
{
int TotalTime = int.Parse(data.Rows[0]["TotalLength"].ToString());
sql = "SELECT * FROM UserTimeTracking WHERE GameId = @gameid AND UserId = @userid ORDER BY SessionTime DESC LIMIT 1;";
data = db.ExecuteCMD(sql, dbDict);
return new StatisticsModel{
GameId = GameId,
SessionLength = TotalTime,
SessionStart = (DateTime)data.Rows[0]["SessionTime"]
};
}
}
}
}
}

View File

@@ -0,0 +1,104 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using gaseous_server.Models;
using gaseous_server.Classes;
using Authentication;
using Microsoft.AspNetCore.Identity;
using Asp.Versioning;
namespace gaseous_server.Controllers.v1_1
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[ApiController]
public class StatisticsController: ControllerBase
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public StatisticsController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager
)
{
_userManager = userManager;
_signInManager = signInManager;
}
[MapToApiVersion("1.0")]
[MapToApiVersion("1.1")]
[HttpPost]
[Authorize]
[ProducesResponseType(typeof(Models.StatisticsModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Route("Games/{GameId}/")]
public async Task<ActionResult> NewRecordStatistics(long GameId)
{
var user = await _userManager.GetUserAsync(User);
if (user != null)
{
Statistics statistics = new Statistics();
return Ok(statistics.RecordSession(Guid.Empty, GameId, user.Id));
}
else
{
return Unauthorized();
}
}
[MapToApiVersion("1.0")]
[MapToApiVersion("1.1")]
[HttpPut]
[Authorize]
[ProducesResponseType(typeof(Models.StatisticsModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Route("Games/{GameId}/{SessionId}")]
public async Task<ActionResult> SubsequentRecordStatistics(long GameId, Guid SessionId)
{
var user = await _userManager.GetUserAsync(User);
if (user != null)
{
Statistics statistics = new Statistics();
return Ok(statistics.RecordSession(SessionId, GameId, user.Id));
}
else
{
return Unauthorized();
}
}
[MapToApiVersion("1.0")]
[MapToApiVersion("1.1")]
[HttpGet]
[Authorize]
[ProducesResponseType(typeof(Models.StatisticsModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Route("Games/{GameId}")]
public async Task<ActionResult> GetStatistics(long GameId)
{
var user = await _userManager.GetUserAsync(User);
if (user != null)
{
Statistics statistics = new Statistics();
StatisticsModel? model = statistics.GetSession(GameId, user.Id);
if (model == null)
{
return NoContent();
}
else
{
return Ok(model);
}
}
else
{
return Unauthorized();
}
}
}
}

View File

@@ -0,0 +1,17 @@
namespace gaseous_server.Models
{
public class StatisticsModel
{
public Guid SessionId { get; set; } = Guid.Empty;
public long GameId { get; set; }
public DateTime SessionStart { get; set; }
public int SessionLength { get; set; }
public DateTime SessionEnd
{
get
{
return SessionStart.AddMinutes(SessionLength);
}
}
}
}

View File

@@ -0,0 +1,13 @@
CREATE TABLE `UserTimeTracking` (
`GameId` BIGINT NULL,
`UserId` VARCHAR(45) NULL,
`SessionId` VARCHAR(45) NULL,
`SessionTime` DATETIME NULL,
`SessionLength` INT NULL,
INDEX `UserId_idx` (`UserId` ASC) VISIBLE,
INDEX `SessionId_idx` (`SessionId` ASC) VISIBLE,
CONSTRAINT `UserId`
FOREIGN KEY (`UserId`)
REFERENCES `Users` (`Id`)
ON DELETE CASCADE
ON UPDATE NO ACTION);

View File

@@ -61,6 +61,8 @@
<None Remove="Support\Database\MySQL\gaseous-1016.sql" />
<None Remove="Support\Database\MySQL\gaseous-1017.sql" />
<None Remove="Support\Database\MySQL\gaseous-1018.sql" />
<None Remove="Support\Database\MySQL\gaseous-1019.sql" />
<None Remove="Support\Database\MySQL\gaseous-1020.sql" />
<None Remove="Classes\Metadata\" />
</ItemGroup>
<ItemGroup>
@@ -102,5 +104,7 @@
<EmbeddedResource Include="Support\Database\MySQL\gaseous-1016.sql" />
<EmbeddedResource Include="Support\Database\MySQL\gaseous-1017.sql" />
<EmbeddedResource Include="Support\Database\MySQL\gaseous-1018.sql" />
<EmbeddedResource Include="Support\Database\MySQL\gaseous-1019.sql" />
<EmbeddedResource Include="Support\Database\MySQL\gaseous-1020.sql" />
</ItemGroup>
</Project>

Binary file not shown.

View File

@@ -70,4 +70,30 @@
bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image/original/' + artworks[artworksPosition] + '.jpg"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);');
}
}
// statistics
var SessionId = undefined;
function SaveStatistics() {
var model;
if (SessionId == undefined) {
ajaxCall(
'/api/v1.1/Statistics/Games/' + gameId,
'POST',
function (success) {
SessionId = success.sessionId;
}
);
} else {
ajaxCall(
'/api/v1.1/Statistics/Games/' + gameId + '/' + SessionId,
'PUT',
function (success) {
}
);
}
}
setInterval(SaveStatistics, 60000);
</script>

View File

@@ -63,6 +63,16 @@
<div id="gamescreenshots_gallery_panel"></div>
</div>
</div>
<div id="gamestatistics">
<div id="gamestatistics_lastplayed" class="gamestatistics_box">
<span class="gamestatistics_label">Last Played</span>
<span id="gamestatistics_lastplayed_value" class="gamestatistics_value">-</span>
</div>
<div id="gamestatistics_timeplayed" class="gamestatistics_box">
<span class="gamestatistics_label">Total Play Time</span>
<span id="gamestatistics_timeplayed_value" class="gamestatistics_value">-</span>
</div>
</div>
<div id="gamesummarytext">
<span id="gamesummarytext_label" class="line-clamp-4"></span>
<p id="gamesummarytext_label_button_expand" class="text_link" style="display: none;" onclick="document.querySelector('#gamesummarytext_label').classList.remove('line-clamp-4'); document.querySelector('#gamesummarytext_label_button_expand').setAttribute('style', 'display: none;'); document.querySelector('#gamesummarytext_label_button_contract').setAttribute('style', '');">Read more...</p>
@@ -256,6 +266,30 @@
gamePublisherLabel.setAttribute('style', 'display: none;');
}
// load statistics
ajaxCall('/api/v1.1/Statistics/Games/' + gameId, 'GET', function (result) {
var gameStat_lastPlayed = document.getElementById('gamestatistics_lastplayed_value');
var gameStat_timePlayed = document.getElementById('gamestatistics_timeplayed_value');
if (result) {
// gameStat_lastPlayed.innerHTML = moment(result.sessionEnd).format("YYYY-MM-DD h:mm:ss a");
const dateOptions = {
//weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};
gameStat_lastPlayed.innerHTML = new Date(result.sessionEnd).toLocaleDateString(undefined, dateOptions);
if (result.sessionLength >= 60) {
gameStat_timePlayed.innerHTML = Number(result.sessionLength / 60) + " hours";
} else {
gameStat_timePlayed.innerHTML = Number(result.sessionLength) + " minutes";
}
} else {
gameStat_lastPlayed.innerHTML = '-';
gameStat_timePlayed.innerHTML = '-';
}
});
// load release date
var gameSummaryRelease = document.getElementById('gamesummary_firstrelease');
if (result.firstReleaseDate) {

View File

@@ -905,6 +905,31 @@ iframe {
border-color: white;
}
#gamestatistics {
display: block;
width: 100%;
margin-bottom: 10px;
background-color: rgba(56, 56, 56, 0.3);
}
.gamestatistics_box {
display: inline-block;
padding: 10px;
}
.gamestatistics_label {
display: block;
font-weight: bold;
font-size: 14px;
text-transform: uppercase;
padding: 3px;
}
.gamestatistics_value {
display: block;
padding: 3px;
}
#gamesummarytext_label {
}