Compare commits

...

4 Commits

Author SHA1 Message Date
41b50e352f detailed debug llog for file reader
All checks were successful
Build and Push Docker Image / build (push) Successful in 33s
2024-12-11 22:55:55 +00:00
1ff4f4e831 get total playtime from player 2024-12-11 22:55:39 +00:00
b2847ebe95 user log files only and handle task around build 2024-12-11 22:55:24 +00:00
825843ed86 remove that stupid package aerich, it's terrible! 2024-12-11 22:55:08 +00:00
8 changed files with 42 additions and 19 deletions

View File

@ -11,7 +11,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Debug environment variable flag. - Debug environment variable flag.
- Command to build player data from existing log files. - Command to build player data from existing log files.
- Database migration management with the aerich package.
### Changed ### Changed

View File

@ -13,4 +13,4 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . /app/ COPY . /app/
CMD ["sh", "-c", "aerich upgrade && python bot.py"] CMD ["python", "bot.py"]

2
bot.py
View File

@ -25,7 +25,7 @@ TORTOISE_ORM = {
"connections": { "default": f"sqlite://{str(DATA_DIR)}/db.sqlite" }, "connections": { "default": f"sqlite://{str(DATA_DIR)}/db.sqlite" },
"apps": { "apps": {
"models": { "models": {
"models": ["utils.models", "aerich.models"], "models": ["utils.models"],
"default_connection": "default" "default_connection": "default"
} }
} }

View File

@ -45,17 +45,19 @@ class PlayersCog(commands.Cog):
Build player data from existing and older log files. Build player data from existing and older log files.
""" """
await inter.response.defer() await inter.response.defer()
self.listen_for_changes.stop()
log.info("Building player data from logs.") log.info("Building player data from logs.")
# Delete the existing data, as we will reconstruct it. # Delete the existing data, as we will reconstruct it.
await Player.all().delete() await Player.all().delete()
for log_file in LOGS_FOLDER_PATH.glob("**/*.txt"): for log_file in LOGS_FOLDER_PATH.glob("**/*_user.txt"):
log.debug("building from log file: %s", str(log_file)) log.debug("building from log file: %s", str(log_file))
file_handler = LogFileReader(log_file, track_from_start=True) file_handler = LogFileReader(log_file, track_from_start=True)
for line in await file_handler.read(): for line in await file_handler.read():
await self.process_log_line(line, alert=False) await self.process_log_line(line, alert=False)
self.listen_for_changes.start()
await inter.followup.send("Completed") await inter.followup.send("Completed")
@tasks.loop(seconds=3) @tasks.loop(seconds=3)

View File

@ -3,21 +3,30 @@ from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str: async def upgrade(db: BaseDBAsyncClient) -> str:
return """ return """
CREATE TABLE IF NOT EXISTS "players" ( CREATE TABLE IF NOT EXISTS "ingame_coordinates" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"x" INT NOT NULL,
"y" INT NOT NULL,
"z" INT NOT NULL
);
CREATE TABLE IF NOT EXISTS "players" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"username" VARCHAR(20) NOT NULL UNIQUE, "username" VARCHAR(20) NOT NULL UNIQUE,
"last_connection" TIMESTAMP,
"last_disconnection" TIMESTAMP,
"play_time_seconds" INT NOT NULL DEFAULT 0,
"is_dead" INT NOT NULL DEFAULT 0 "is_dead" INT NOT NULL DEFAULT 0
) /* */; ) /* */;
CREATE TABLE IF NOT EXISTS "player_deaths" ( CREATE TABLE IF NOT EXISTS "player_deaths" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"coordinate_x" INT NOT NULL,
"coordinate_y" INT NOT NULL,
"coordinate_z" INT NOT NULL,
"cause" VARCHAR(32) NOT NULL, "cause" VARCHAR(32) NOT NULL,
"timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
"coordinates_id" INT NOT NULL REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
"player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "player_session" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"connected_at" TIMESTAMP NOT NULL,
"disconnected_at" TIMESTAMP NOT NULL,
"connected_coords_id" INT NOT NULL REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
"disconnected_coords_id" INT REFERENCES "ingame_coordinates" ("id") ON DELETE CASCADE,
"player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE "player_id" INT NOT NULL REFERENCES "players" ("id") ON DELETE CASCADE
); );
CREATE TABLE IF NOT EXISTS "steam_profile_summary" ( CREATE TABLE IF NOT EXISTS "steam_profile_summary" (

View File

@ -1,4 +1,3 @@
aerich==0.8.0
aiofiles==24.1.0 aiofiles==24.1.0
aiohappyeyeballs==2.4.4 aiohappyeyeballs==2.4.4
aiohttp==3.11.9 aiohttp==3.11.9

View File

@ -4,6 +4,7 @@ Database schemas.
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pytz import timezone
import httpx import httpx
from tortoise import fields from tortoise import fields
@ -61,13 +62,15 @@ class PlayerSession(Model):
on_delete=fields.CASCADE on_delete=fields.CASCADE
) )
connected_at = fields.DatetimeField() connected_at = fields.DatetimeField()
disconnected_at = fields.DatetimeField(null=False) disconnected_at = fields.DatetimeField(null=True)
connected_coords = fields.ForeignKeyField( connected_coords = fields.ForeignKeyField(
model_name="models.Coordinates", model_name="models.Coordinates",
related_name="connected_coords",
on_delete=fields.CASCADE on_delete=fields.CASCADE
) )
disconnected_coords = fields.ForeignKeyField( disconnected_coords = fields.ForeignKeyField(
model_name="models.Coordinates", model_name="models.Coordinates",
related_name="disconnected_coords",
on_delete=fields.CASCADE, on_delete=fields.CASCADE,
null=True null=True
) )
@ -93,10 +96,18 @@ class Player(Model):
table = "players" table = "players"
async def get_playtime(self) -> timedelta: async def get_playtime(self) -> timedelta:
playtime = await PlayerSession.filter(player=self).exclude(disconnected_at=None).annotate( sessions = await PlayerSession.filter(player=self)
total_playtime=Sum(F("disconnected_at") - F("connected_at")) total_playtime = timedelta()
).values() now = datetime.now()
return timedelta(seconds=playtime[0]["total_playtime"].total_seconds() if playtime else timedelta()) utc = timezone("UTC")
# I know this is terrible efficiency-wise, but the tortoise docs
# are so bad and the annotations don't work like Django's models. Deal with it!
for session in sessions:
disconnected_at = session.disconnected_at or now
total_playtime += disconnected_at.astimezone(utc) - session.connected_at.astimezone(utc)
return total_playtime
async def get_deaths(self) -> list[PlayerDeath]: async def get_deaths(self) -> list[PlayerDeath]:
return await PlayerDeath.filter(player=self) return await PlayerDeath.filter(player=self)
@ -139,7 +150,8 @@ class Player(Model):
log.debug("creating session for player: %s", self.username) log.debug("creating session for player: %s", self.username)
existing_session = await self.get_latest_session(ignore_closed_sessions=True) existing_session = await self.get_latest_session(ignore_closed_sessions=True)
if existing_session: if existing_session:
raise ValueError("Tried to open session while an open one exists.") log.debug("deleting an unfinished session to open a new one")
await existing_session.delete()
coordinates = await Coordinates.create(x=coord_x, y=coord_y, z=coord_z) coordinates = await Coordinates.create(x=coord_x, y=coord_y, z=coord_z)
return await PlayerSession.create( return await PlayerSession.create(
@ -213,7 +225,8 @@ class Player(Model):
raise ValueError("You must fetch the steam_profile_summary before creating an embed.") raise ValueError("You must fetch the steam_profile_summary before creating an embed.")
death_count = len(await self.get_deaths()) death_count = len(await self.get_deaths())
playtime = str(await self.get_playtime()) playtime = str(await self.get_playtime()).split(".")[0] # remove the miliseconds
log.debug("death count is: %s and playtime is: %s", death_count, playtime)
embed = Embed( embed = Embed(
title="Player", title="Player",

View File

@ -28,6 +28,7 @@ class LogFileReader:
raise FileNotFoundError(self.file_path) raise FileNotFoundError(self.file_path)
async with aiofiles.open(self.file_path, "r", encoding="utf-8") as file: async with aiofiles.open(self.file_path, "r", encoding="utf-8") as file:
log.debug("file open, and jumping to line: %s", self._last_line_number)
if self._last_line_number == 0 and not self.track_from_start: if self._last_line_number == 0 and not self.track_from_start:
await file.seek(0, 2) await file.seek(0, 2)
self._last_line_number = await file.tell() self._last_line_number = await file.tell()