29 Commits
0.2.2 ... 1.0.0

Author SHA1 Message Date
215374167b Version 1.0.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 19:58:40 +01:00
77268ae92f Add make loadall 2023-01-20 19:58:31 +01:00
c42687a072 Change ENTRYPOINT to CMD 2023-01-20 19:58:09 +01:00
ca16345374 Fix start session button starting different game than it says
Fixes #44
2023-01-20 19:57:45 +01:00
3a3045be91 Sort form fields alphabetically
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #39
Fixes #40
2023-01-20 18:27:30 +01:00
d40612af72 Remove Caddy
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 17:15:53 +01:00
18e8f93261 Additional fixes
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-20 15:06:42 +01:00
56e5dfaa03 Rename project, part 2 (#42)
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #42
2023-01-20 13:37:46 +00:00
2f00be455d Rename project (#41)
All checks were successful
continuous-integration/drone/push Build is passing
The old naming scheme was causing confusion and probably errors.

Reviewed-on: #41
2023-01-19 19:35:25 +00:00
c3c9ae0632 Don't take time from .git
All checks were successful
continuous-integration/drone/push Build is passing
.git is not included int the Dockerfile thus it cannot be used to get
time of last edit
2023-01-18 17:13:11 +01:00
55c2693f32 Bump version to 0.2.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-18 17:01:37 +01:00
972ff67050 Update CSS 2023-01-18 16:58:55 +01:00
8ae99faa8e Fix button taking up 100% width
Fixes #37
2023-01-18 16:58:25 +01:00
8e4086ce83 Remove reduntant property last
Fixes #38
2023-01-18 16:57:32 +01:00
2760068cde Add more to .gitignore
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-18 16:33:38 +01:00
cef797c333 Revert "Add date and time pickers to forms"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 4d91a76513.
2023-01-16 23:23:26 +01:00
4d91a76513 Add date and time pickers to forms
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 22:07:43 +01:00
e51d586255 Automatically select purchase when adding session 2023-01-16 21:19:20 +01:00
2553d6f9e6 Definitively disable pre-commit hook
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 19:49:41 +01:00
8cf6270d8f Bump version 2023-01-16 19:47:32 +01:00
0b1089b0f4 Disable pre-commit hook 2023-01-16 19:46:15 +01:00
9534492f17 Exclude manual times from graphs
Fixes #35
2023-01-16 19:39:24 +01:00
8b7ed90b49 Improve playtime graph date formatting 2023-01-16 19:27:52 +01:00
2ce4dd3a0e Fix graph timeline being backwards 2023-01-16 17:26:10 +01:00
a851b5329a Correctly display game that is used as filter 2023-01-16 17:24:34 +01:00
6fa049e1b1 Sort and clean up imports 2023-01-15 23:39:52 +01:00
6b7ed0dbb5 Order by timestamp_start from the newest
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 23:20:43 +01:00
dd50d6dd40 Allow filtering by platform and game
All checks were successful
continuous-integration/drone/push Build is passing
Fixes #32
2023-01-15 23:14:28 +01:00
162f4f3dbf Fix Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-15 19:16:34 +01:00
55 changed files with 1834 additions and 429 deletions

View File

@ -1,5 +1,8 @@
src/web/static/*
.venv
.git
.githooks
.mypy_cache
.pytest_cache
.venv
.vscode
node_modules
node_modules
src/timetracker/static/*

0
.githooks/pre-commit → .githooks/pre-commit.disabled Executable file → Normal file
View File

2
.gitignore vendored
View File

@ -5,4 +5,4 @@ __pycache__
node_modules
package-lock.json
db.sqlite3
src/web/static
static

View File

@ -1,3 +1,33 @@
## 1.0.0 / 2023-01-20 19:54+01:00
* Breaking
* Due to major re-arranging and re-naming of the folder structure, tables also had to be renamed.
* Fixed
* Sort form fields alphabetically (https://git.kucharczyk.xyz/lukas/timetracker/issues/39, https://git.kucharczyk.xyz/lukas/timetracker/issues/40)
* Start session button starts different game than it says (#44)
## 0.2.5 / 2023-01-18 17:01+01:00
* New
* When adding session, pre-select game with the last session
* Fixed
* Start session now button would take up 100% width, leading to accidental clicks (https://git.kucharczyk.xyz/lukas/timetracker/issues/37)
* Removed
* Session model property `last` is already implemented by Django method `last()`, thus it was removed (https://git.kucharczyk.xyz/lukas/timetracker/issues/38)
## 0.2.4 / 2023-01-16 19:39+01:00
* Fixed
* When filtering by game, the "Filtering by (...)" text would erroneously list an unrelated platform
* Playtime graph would display timeline backwards
* Playtime graph with many dates would overlap (https://git.kucharczyk.xyz/lukas/timetracker/issues/34)
* Manually added times (= without end timestamp) would make graphs look ugly and noisy (https://git.kucharczyk.xyz/lukas/timetracker/issues/35)
## 0.2.3 / 2023-01-15 23:13+01:00
* Allow filtering by platform and game on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/32)
* Order session by newest as preparation for https://git.kucharczyk.xyz/lukas/timetracker/issues/33
## 0.2.2 / 2023-01-15 17:59+01:00
* Display playtime graph on session list (https://git.kucharczyk.xyz/lukas/timetracker/issues/29)

View File

@ -5,10 +5,10 @@
:8000 {
handle_path /static/* {
root * src/web/static/
root * /usr/share/caddy
file_server
}
handle {
reverse_proxy :8001
reverse_proxy backend:8001
}
}

View File

@ -2,23 +2,26 @@ FROM node as css
WORKDIR /app
COPY . /app
RUN npm install && \
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --minify
npx tailwindcss -i ./common/input.css -o ./static/base.css --minify
FROM python:3.10.9-alpine
FROM python:3.10.9-slim-bullseye
ENV VERSION_NUMBER 0.2.2
ENV VERSION_NUMBER 1.0.0
ENV PROD 1
ENV PYTHONUNBUFFERED=1
RUN apk add \
RUN apt update && \
apt install -y \
bash \
vim \
curl \
caddy
RUN adduser -D -u 1000 timetracker
curl && \
rm -rf /var/lib/apt/lists/*
RUN useradd -m --uid 1000 timetracker
WORKDIR /home/timetracker/app
COPY . /home/timetracker/app/
RUN chown -R timetracker:timetracker /home/timetracker/app
COPY --from=css /app/src/web/tracker/static/base.css /home/timetracker/app/src/web/tracker/static/base.css
COPY --from=css ./app/static/base.css /home/timetracker/app/static/base.css
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
@ -28,4 +31,4 @@ RUN pip install --no-cache-dir poetry
RUN poetry install --without dev
EXPOSE 8000
ENTRYPOINT [ "/entrypoint.sh" ]
CMD [ "/entrypoint.sh" ]

View File

@ -2,49 +2,52 @@ all: css migrate
initialize: npm css migrate sethookdir loadplatforms
HTMLFILES := $(shell find src/web/tracker/templates -type f)
HTMLFILES := $(shell find games/templates -type f)
npm:
npm install
css: src/input.css
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css
css: input.css
npx tailwindcss -i ./input.css -o ./games/static/base.css
css-dev: css
npx tailwindcss -i ./src/input.css -o ./src/web/tracker/static/base.css --watch
npx tailwindcss -i ./input.css -o ./games/static/base.css --watch
makemigrations:
poetry run python src/web/manage.py makemigrations
poetry run python manage.py makemigrations
migrate: makemigrations
poetry run python src/web/manage.py migrate
poetry run python manage.py migrate
dev: migrate sethookdir
poetry run python src/web/manage.py runserver
dev: migrate
poetry run python manage.py runserver
caddy:
caddy run --watch
dev-prod: migrate collectstatic sethookdir
cd src/web/; PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 web.asgi:application -k uvicorn.workers.UvicornWorker
dev-prod: migrate collectstatic
PROD=1 poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker
dumptracker:
poetry run python src/web/manage.py dumpdata --format yaml tracker --output tracker_fixture.yaml
dumpgames:
poetry run python manage.py dumpdata --format yaml games --output tracker_fixture.yaml
loadplatforms:
poetry run python src/web/manage.py loaddata platforms.yaml
poetry run python manage.py loaddata platforms.yaml
loadall:
poetry run python manage.py loaddata data.yaml
loadsample:
poetry run python src/web/manage.py loaddata sample.yaml
poetry run python manage.py loaddata sample.yaml
createsuperuser:
poetry run python src/web/manage.py createsuperuser
poetry run python manage.py createsuperuser
shell:
poetry run python src/web/manage.py shell
poetry run python manage.py shell
collectstatic:
poetry run python src/web/manage.py collectstatic --clear --no-input
poetry run python manage.py collectstatic --clear --no-input
poetry.lock: pyproject.toml
poetry install
@ -52,13 +55,10 @@ poetry.lock: pyproject.toml
test: poetry.lock
poetry run pytest
sethookdir:
git config core.hooksPath .githooks
date:
poetry run python -c 'import datetime; from zoneinfo import ZoneInfo; print(datetime.datetime.isoformat(datetime.datetime.now(ZoneInfo("Europe/Prague")), timespec="minutes", sep=" "))'
cleanstatic:
rm -r src/web/static/*
rm -r static/*
clean: cleanstatic

30
common/import_data.py Normal file
View File

@ -0,0 +1,30 @@
import csv
from typing import TypeAlias
from games.models import Game
DataList: TypeAlias = list[dict[str, str]] | None
def read_csv(filename: str) -> DataList:
with open(filename, "r") as csvfile:
writer = csv.DictReader(csvfile)
return writer
def import_data(data: DataList):
matching_names = {}
for line in data:
name = line["name"]
if name not in matching_names:
# try exact match first
try:
game_id = Game.objects.get(name__iexact=name)
except:
pass
matching_names[name] = game_id
print(f"Exact matched {len(matching_names)} games.")
def import_from_file(filename: str):
import_data(read_csv(filename))

View File

@ -14,7 +14,7 @@ textarea {
#session-table {
display: grid;
grid-template-columns: repeat(3, 2fr) 0.5fr 1fr;
grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
}
#button-container button {

91
common/plots.py Normal file
View File

@ -0,0 +1,91 @@
import base64
from datetime import datetime
from io import BytesIO
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import pandas as pd
from django.db.models import F, IntegerField, QuerySet, Sum
from django.db.models.functions import TruncDay
from games.models import Session
def key_value_to_value_value(data):
return {data["date"]: data["hours"]}
def playtime_over_time_chart(queryset: QuerySet = Session.objects):
microsecond_in_second = 1000000
result = (
queryset.exclude(timestamp_end__exact=None)
.annotate(date=TruncDay("timestamp_start"))
.values("date")
.annotate(
hours=Sum(
F("duration_calculated"),
output_field=IntegerField(),
)
)
.values("date", "hours")
)
keys = []
values = []
running_total = int(0)
for item in result:
# date_value = datetime.strftime(item["date"], "%d-%m-%Y")
date_value = item["date"]
keys.append(date_value)
running_total += int(item["hours"] / (3600 * microsecond_in_second))
values.append(running_total)
data = [keys, values]
return get_chart(
data,
title="Playtime over time (manual excluded)",
xlabel="Date",
ylabel="Hours",
)
def get_graph():
buffer = BytesIO()
plt.savefig(buffer, format="svg", transparent=True)
buffer.seek(0)
image_png = buffer.getvalue()
graph = base64.b64encode(image_png)
graph = graph.decode("utf-8")
buffer.close()
return graph
def get_chart(data, title="", xlabel="", ylabel=""):
x = data[0]
y = data[1]
plt.style.use("dark_background")
plt.switch_backend("SVG")
fig, ax = plt.subplots()
fig.set_size_inches(10, 4)
ax.plot(x, y)
first = x[0]
last = x[-1]
difference = last - first
if difference.days <= 14:
ax.xaxis.set_major_locator(mdates.DayLocator())
elif difference.days < 60 or len(x) < 60:
ax.xaxis.set_major_locator(mdates.WeekdayLocator())
ax.xaxis.set_minor_locator(mdates.DayLocator())
elif difference.days < 720:
ax.xaxis.set_major_locator(mdates.MonthLocator())
ax.xaxis.set_minor_locator(mdates.WeekdayLocator())
else:
ax.xaxis.set_major_locator(mdates.YearLocator())
ax.xaxis.set_minor_locator(mdates.MonthLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))
for label in ax.get_xticklabels(which="major"):
label.set(rotation=30, horizontalalignment="right")
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_title(title)
fig.tight_layout()
chart = get_graph()
return chart

View File

@ -1,7 +1,8 @@
from datetime import datetime, timedelta
from django.conf import settings
from zoneinfo import ZoneInfo
import re
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from django.conf import settings
def now() -> datetime:

View File

@ -0,0 +1,17 @@
---
services:
timetracker:
image: registry.kucharczyk.xyz/timetracker
build:
context: .
dockerfile: Dockerfile
container_name: timetracker
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
# volumes:
# - "db:/home/timetracker/app/src/timetracker/db.sqlite3"
ports:
- "8000:8000"
restart: unless-stopped

View File

@ -1,17 +1,28 @@
---
services:
timetracker:
backend:
image: registry.kucharczyk.xyz/timetracker
build:
context: .
dockerfile: Dockerfile
container_name: timetracker
environment:
- TZ=Europe/Prague
- CSRF_TRUSTED_ORIGINS="https://tracker.kucharczyk.xyz"
user: "1000"
# volumes:
# - "db:/home/timetracker/app/src/web/db.sqlite3"
volumes:
- "static-files:/home/timetracker/app/static"
restart: unless-stopped
frontend:
image: caddy
volumes:
- "static-files:/usr/share/caddy"
- "$PWD/Caddyfile:/etc/caddy/Caddyfile"
ports:
- "8000:8000"
restart: unless-stopped
depends_on:
- backend
volumes:
static-files:

View File

@ -2,12 +2,10 @@
# Apply database migrations
set -euo pipefail
echo "Apply database migrations"
poetry run python src/web/manage.py migrate
poetry run python manage.py migrate
echo "Collect static files"
poetry run python src/web/manage.py collectstatic --clear --no-input
poetry run python manage.py collectstatic --clear --no-input
echo "Starting server"
caddy start
cd src/web || exit
poetry run python -m gunicorn --bind 0.0.0.0:8001 web.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -
echo "Starting app"
poetry run python -m gunicorn --bind 0.0.0.0:8001 timetracker.asgi:application -k uvicorn.workers.UvicornWorker --access-logfile - --error-logfile -

View File

@ -1,5 +1,6 @@
from django.contrib import admin
from .models import Game, Purchase, Platform, Session
from games.models import Game, Platform, Purchase, Session
# Register your models here.
admin.site.register(Game)

View File

@ -1,6 +1,6 @@
from django.apps import AppConfig
class TrackerConfig(AppConfig):
class GamesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tracker"
name = "games"

1091
games/fixtures/data.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,28 @@
- model: tracker.Platform
- model: games.Platform
fields:
name: Steam
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Xbox Gamepass
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Epic Games Store
group: PC
- model: tracker.Platform
- model: games.Platform
fields:
name: Playstation 5
group: Playstation
- model: tracker.Platform
- model: games.Platform
fields:
name: Playstation 4
group: Playstation
- model: tracker.Platform
- model: games.Platform
fields:
name: Nintendo Switch
group: Nintendo
- model: tracker.Platform
- model: games.Platform
fields:
name: Nintendo 3DS
group: Nintendo

View File

@ -0,0 +1,225 @@
name,platform,start,end
Nioh 2,PS5,2022-12-17 19:34,2022-12-17 22:53
Nioh 2,PS5,2022-12-15 02:25,2022-12-15 03:57
Nioh 2,PS5,2022-12-13 02:41,2022-12-13 04:34
Nioh 2,PS5,2022-12-11 21:01,2022-12-11 23:21
VALKYRIE ELYSIUM,PS5,2022-12-11 06:07,2022-12-11 06:20
Metal: Hellsinger,PS5,2022-12-11 05:50,2022-12-11 06:07
Nioh 2,PS5,2022-12-11 04:31,2022-12-11 05:50
Nioh 2,PS5,2022-12-11 04:11,2022-12-11 04:26
Forspoken,PS5,2022-12-10 22:29,2022-12-10 23:10
Nioh 2,PS5,2022-12-10 19:44,2022-12-10 22:29
Nioh 2,PS5,2022-12-09 02:14,2022-12-09 04:16
Nioh 2,PS5,2022-12-08 01:03,2022-12-08 01:57
Nioh 2,PS5,2022-12-07 00:43,2022-12-07 04:16
Nioh 2,PS5,2022-12-04 20:48,2022-12-04 23:31
Nioh 2,PS5,2022-12-04 04:26,2022-12-04 07:01
Nioh 2,PS5,2022-12-04 04:20,2022-12-04 04:22
Nioh 2,PS5,2022-11-26 19:18,2022-11-26 21:28
Nioh 2,PS5,2022-11-26 19:16,2022-11-26 19:18
Nioh 2,PS5,2022-11-26 02:46,2022-11-26 03:56
Nioh 2,PS5,2022-11-26 02:01,2022-11-26 02:43
God of War Ragnarök,PS5,2022-11-24 23:03,2022-11-25 01:32
God of War Ragnarök,PS5,2022-11-23 00:41,2022-11-23 07:52
God of War Ragnarök,PS5,2022-11-21 22:52,2022-11-22 04:51
God of War Ragnarök,PS5,2022-11-21 02:11,2022-11-21 05:13
God of War Ragnarök,PS5,2022-11-20 21:34,2022-11-20 22:50
God of War Ragnarök,PS5,2022-11-20 03:46,2022-11-20 05:52
God of War Ragnarök,PS5,2022-11-19 14:30,2022-11-19 16:14
God of War Ragnarök,PS5,2022-11-18 23:15,2022-11-19 04:16
God of War Ragnarök,PS5,2022-11-18 19:58,2022-11-18 20:40
God of War Ragnarök,PS5,2022-11-18 03:50,2022-11-18 06:25
God of War Ragnarök,PS5,2022-11-17 19:36,2022-11-18 00:26
God of War Ragnarök,PS5,2022-11-17 13:16,2022-11-17 16:13
God of War Ragnarök,PS5,2022-11-16 21:45,2022-11-16 22:38
God of War Ragnarök,PS5,2022-11-16 00:14,2022-11-16 04:28
God of War Ragnarök,PS5,2022-11-15 01:33,2022-11-15 05:06
God of War Ragnarök,PS5,2022-11-14 00:37,2022-11-14 04:43
God of War Ragnarök,PS5,2022-11-12 23:32,2022-11-13 03:45
God of War Ragnarök,PS5,2022-11-12 03:17,2022-11-12 05:00
Grand Theft Auto V (PlayStation®5),PS5,2022-10-03 02:01,2022-10-03 02:23
Grand Theft Auto V (PlayStation®5),PS5,2022-10-02 13:59,2022-10-02 15:54
Grand Theft Auto V (PlayStation®5),PS5,2022-09-30 22:40,2022-10-01 02:50
Grand Theft Auto V (PlayStation®5),PS5,2022-09-27 22:38,2022-09-28 00:16
Grand Theft Auto V (PlayStation®5),PS5,2022-09-27 19:27,2022-09-27 21:09
Grand Theft Auto V (PlayStation®5),PS5,2022-09-26 20:58,2022-09-26 23:38
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 23:56,2022-09-26 02:36
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 14:57,2022-09-25 16:38
Grand Theft Auto V (PlayStation®5),PS5,2022-09-25 02:04,2022-09-25 02:12
Grand Theft Auto V (PlayStation®5),PS5,2022-09-23 20:33,2022-09-23 23:38
Wo Long: Fallen Dynasty,PS5,2022-09-18 15:26,2022-09-18 16:58
Grand Theft Auto V (PlayStation®5),PS5,2022-08-31 00:42,2022-08-31 01:15
Grand Theft Auto V (PlayStation®5),PS5,2022-08-18 13:43,2022-08-18 15:12
Grand Theft Auto V (PlayStation®5),PS5,2022-08-18 00:42,2022-08-18 01:58
Tony Hawk's™ Pro Skater™ 1 + 2,PS5,2022-08-18 00:40,2022-08-18 00:42
Tony Hawk's™ Pro Skater™ 1 + 2,PS5,2022-08-14 15:46,2022-08-14 16:10
FINAL FANTASY VII,PS5,2022-07-26 18:56,2022-07-26 20:22
FINAL FANTASY VII REMAKE,PS5,2022-07-26 17:39,2022-07-26 18:53
FINAL FANTASY VII REMAKE,PS5,2022-07-25 22:12,2022-07-26 04:37
FINAL FANTASY VII REMAKE,PS5,2022-07-24 00:09,2022-07-24 05:33
FINAL FANTASY VII REMAKE,PS5,2022-07-23 23:34,2022-07-23 23:48
FINAL FANTASY VII REMAKE,PS5,2022-07-23 18:05,2022-07-23 19:44
FINAL FANTASY VII REMAKE,PS5,2022-07-22 17:07,2022-07-23 01:48
FINAL FANTASY VII REMAKE,PS5,2022-07-22 15:26,2022-07-22 15:59
FINAL FANTASY VII REMAKE,PS5,2022-07-21 21:27,2022-07-21 22:43
FINAL FANTASY VII REMAKE,PS5,2022-07-21 20:48,2022-07-21 20:58
FINAL FANTASY VII REMAKE,PS5,2022-07-21 18:35,2022-07-21 18:36
Stray,PS5,2022-07-21 01:24,2022-07-21 02:34
Stray,PS5,2022-07-19 23:49,2022-07-20 03:24
Atelier Ayesha ~The Alchemist of Dusk~,PS3,2022-07-04 03:26,2022-07-04 03:34
Red Dead Redemption,PS3,2022-07-04 02:36,2022-07-04 03:14
Ghost of Tsushima,PS5,2022-07-04 01:38,2022-07-04 02:34
Dark Cloud™,PS4,2022-07-01 23:48,2022-07-02 00:04
Atelier Ayesha ~The Alchemist of Dusk~,PS3,2022-07-01 23:20,2022-07-01 23:46
Resident Evil Directors Cut,PS5,2022-07-01 23:14,2022-07-01 23:20
ELEX II,PS5,2022-07-01 22:48,2022-07-01 23:13
OlliOlli World,PS5,2022-07-01 21:30,2022-07-01 22:30
Deep Rock Galactic,PS5,2022-06-16 05:30,2022-06-16 06:14
Curse of the Dead Gods,PS5,2022-06-16 05:00,2022-06-16 05:22
Persona 5: Dancing in Starlight,PS5,2022-04-29 20:14,2022-04-29 20:15
Persona 5: Dancing in Starlight,PS5,2022-04-29 00:18,2022-04-29 00:44
Dying Light 2,PS5,2022-04-14 01:26,2022-04-14 01:27
Grand Theft Auto V (PlayStation®5),PS5,2022-03-24 16:26,2022-03-24 16:27
Grand Theft Auto V (PlayStation®5),PS5,2022-03-21 15:52,2022-03-21 15:59
Horizon Forbidden West,PS5,2022-02-23 19:37,2022-02-24 00:24
Horizon Forbidden West,PS5,2022-02-23 13:57,2022-02-23 17:44
Horizon Forbidden West,PS5,2022-02-22 18:05,2022-02-23 05:26
Horizon Forbidden West,PS5,2022-02-22 15:39,2022-02-22 17:02
Horizon Forbidden West,PS5,2022-02-22 00:05,2022-02-22 04:08
Horizon Forbidden West,PS5,2022-02-20 15:39,2022-02-20 23:08
Horizon Forbidden West,PS5,2022-02-20 14:54,2022-02-20 15:09
Horizon Forbidden West,PS5,2022-02-19 23:37,2022-02-20 04:45
Horizon Forbidden West,PS5,2022-02-18 23:15,2022-02-19 03:27
Assassin's Creed® Origins,PS5,2022-02-18 21:49,2022-02-18 23:15
Assassin's Creed® Origins,PS5,2022-01-17 02:38,2022-01-17 02:50
Deep Rock Galactic,PS5,2022-01-17 00:57,2022-01-17 02:35
HITMAN 3,PS5,2021-11-17 00:35,2021-11-17 01:17
HITMAN 3,PS5,2021-11-08 01:59,2021-11-08 06:17
HITMAN 3,PS5,2021-11-07 03:10,2021-11-07 05:23
HITMAN 3,PS5,2021-11-06 04:23,2021-11-06 08:49
HITMAN 3,PS5,2021-11-06 02:17,2021-11-06 03:31
HITMAN 3,PS5,2021-11-05 21:33,2021-11-05 23:24
HITMAN 3,PS5,2021-11-05 03:09,2021-11-05 03:34
HITMAN 3,PS5,2021-11-05 00:47,2021-11-05 02:26
HITMAN 3,PS5,2021-11-04 20:27,2021-11-04 23:32
HITMAN 3,PS5,2021-11-04 01:34,2021-11-04 05:33
RESIDENT EVIL 3,PS5,2021-11-03 23:14,2021-11-03 23:56
RESIDENT EVIL 3,PS5,2021-11-02 23:56,2021-11-03 05:10
RESIDENT EVIL 3,PS5,2021-11-02 21:22,2021-11-02 23:23
RESIDENT EVIL 3,PS5,2021-11-02 05:36,2021-11-02 06:56
HITMAN 3,PS5,2021-11-02 03:00,2021-11-02 05:36
HITMAN 3,PS5,2021-11-02 01:19,2021-11-02 01:25
HITMAN™ 2,PS5,2021-11-02 01:09,2021-11-02 01:19
HITMAN 3,PS5,2021-11-01 23:45,2021-11-02 01:09
RESIDENT EVIL 3,PS5,2021-11-01 19:32,2021-11-01 19:47
Marvel's Spider-Man: Miles Morales,PS5,2021-10-17 01:06,2021-10-17 03:27
Marvel's Spider-Man: Miles Morales,PS5,2021-10-16 20:58,2021-10-16 22:00
Marvel's Spider-Man: Miles Morales,PS5,2021-10-05 02:30,2021-10-05 03:27
Marvel's Spider-Man: Miles Morales,PS5,2021-10-03 23:12,2021-10-04 01:21
Marvel's Spider-Man: Miles Morales,PS5,2021-10-03 03:02,2021-10-03 04:42
Marvel's Spider-Man: Miles Morales,PS5,2021-10-02 20:12,2021-10-02 21:10
Marvel's Spider-Man: Miles Morales,PS5,2021-10-02 01:40,2021-10-02 03:36
Marvel's Spider-Man: Miles Morales,PS5,2021-10-01 04:34,2021-10-01 05:30
DEATHLOOP,PS5,2021-10-01 01:12,2021-10-01 04:27
DEATHLOOP,PS5,2021-09-30 03:04,2021-09-30 06:30
DEATHLOOP,PS5,2021-09-29 00:28,2021-09-29 05:08
Persona 5 Royal,PS5,2021-09-28 00:36,2021-09-28 03:08
Persona 5 Royal,PS5,2021-09-27 02:16,2021-09-27 05:56
Persona 5 Royal,PS5,2021-09-26 14:54,2021-09-26 16:32
Persona 5 Royal,PS5,2021-09-25 18:43,2021-09-25 23:26
Persona 5 Royal,PS5,2021-09-24 21:41,2021-09-25 03:40
Persona 5 Royal,PS5,2021-09-23 00:18,2021-09-23 06:26
Persona 5 Royal,PS5,2021-09-21 20:27,2021-09-22 05:43
Persona 5 Royal,PS5,2021-09-21 01:07,2021-09-21 06:06
Borderlands: The Handsome Collection,PS5,2021-09-20 23:59,2021-09-21 01:07
Persona 5 Royal,PS5,2021-09-20 23:53,2021-09-20 23:59
DEATHLOOP,PS5,2021-09-20 02:03,2021-09-20 06:29
DEATHLOOP,PS5,2021-09-19 19:49,2021-09-20 01:16
Borderlands: The Handsome Collection,PS5,2021-09-19 00:51,2021-09-19 03:41
Borderlands: The Handsome Collection,PS5,2021-09-17 23:45,2021-09-18 01:48
Borderlands: The Handsome Collection,PS5,2021-09-17 23:40,2021-09-17 23:41
DEATHLOOP,PS5,2021-09-17 16:48,2021-09-17 18:56
DEATHLOOP,PS5,2021-09-17 03:02,2021-09-17 04:39
DEATHLOOP,PS5,2021-09-17 00:03,2021-09-17 02:53
DEATHLOOP,PS5,2021-09-16 18:39,2021-09-16 21:12
Persona 5 Royal,PS5,2021-09-16 18:29,2021-09-16 18:30
Persona 5 Royal,PS5,2021-09-16 02:26,2021-09-16 06:13
Persona 5 Royal,PS5,2021-09-16 02:20,2021-09-16 02:21
Persona 5 Royal,PS5,2021-09-15 01:48,2021-09-15 06:07
Persona 5 Royal,PS5,2021-09-14 22:21,2021-09-15 01:22
Persona 5 Royal,PS5,2021-09-14 02:01,2021-09-14 05:48
Persona 5 Royal,PS5,2021-09-14 00:24,2021-09-14 01:46
Persona 5 Royal,PS5,2021-08-12 05:04,2021-08-12 07:05
Persona 5 Royal,PS5,2021-08-11 05:02,2021-08-11 06:48
Persona 5 Royal,PS5,2021-08-09 00:37,2021-08-09 06:15
Persona 5 Royal,PS5,2021-08-08 00:31,2021-08-08 08:01
Persona 5 Royal,PS5,2021-08-07 19:51,2021-08-07 22:50
Persona 5 Royal,PS5,2021-08-06 23:51,2021-08-07 01:35
Persona 5 Royal,PS5,2021-08-06 19:26,2021-08-06 22:26
Persona 5 Royal,PS5,2021-08-06 02:42,2021-08-06 06:51
Persona 5 Royal,PS5,2021-08-06 00:37,2021-08-06 01:54
Far Cry® 5,PS5,2021-08-01 23:27,2021-08-02 02:09
Far Cry® 5,PS5,2021-08-01 18:10,2021-08-01 19:40
STAR WARS™: Squadrons,PS5,2021-08-01 18:02,2021-08-01 18:10
STAR WARS™: Squadrons,PS5,2021-08-01 00:24,2021-08-01 00:30
STEEP,PS5,2021-08-01 00:15,2021-08-01 00:24
Red Dead Redemption 2,PS5,2021-07-31 23:48,2021-08-01 00:13
Persona 5 Royal,PS5,2021-07-30 19:09,2021-07-30 19:10
Persona 5 Royal,PS5,2021-07-29 03:41,2021-07-29 04:59
Persona 5 Royal,PS5,2021-07-28 02:32,2021-07-28 03:07
Demon's Souls,PS5,2021-07-28 00:12,2021-07-28 02:32
Red Dead Redemption 2,PS5,2021-07-27 23:20,2021-07-27 23:23
Red Dead Redemption 2,PS5,2021-07-26 00:42,2021-07-26 01:13
Ghost of Tsushima,PS5,2021-07-25 19:03,2021-07-25 22:12
Ghost of Tsushima,PS5,2021-07-25 18:52,2021-07-25 18:55
Rez Infinite,PS5,2021-07-25 18:32,2021-07-25 18:52
Returnal,PS5,2021-07-25 05:24,2021-07-25 05:26
Tom Clancy's The Division® 2,PS5,2021-07-25 02:12,2021-07-25 05:24
Returnal,PS5,2021-07-25 00:00,2021-07-25 02:12
Returnal,PS5,2021-07-24 16:13,2021-07-24 17:39
Returnal,PS5,2021-07-24 03:02,2021-07-24 07:02
Returnal,PS5,2021-07-23 18:08,2021-07-23 20:39
Returnal,PS5,2021-07-23 14:36,2021-07-23 15:38
Titanfall™ 2,PS5,2021-07-22 02:42,2021-07-22 03:20
Returnal,PS5,2021-07-20 02:12,2021-07-20 05:40
Returnal,PS5,2021-07-19 03:37,2021-07-19 05:24
Concrete Genie,PS5,2021-07-19 03:35,2021-07-19 03:37
Concrete Genie,PS5,2021-07-18 05:04,2021-07-18 05:30
Stranded Deep,PS5,2021-07-18 04:32,2021-07-18 04:58
Sniper Elite 4,PS5,2021-07-18 04:16,2021-07-18 04:32
Oddworld: Soulstorm,PS5,2021-07-18 04:00,2021-07-18 04:14
Zombie Army 4: Dead War,PS5,2021-07-18 03:48,2021-07-18 03:58
Sekiro™: Shadows Die Twice,PS5,2021-07-18 03:00,2021-07-18 03:48
Returnal,PS5,2021-07-17 17:41,2021-07-17 23:32
Returnal,PS5,2021-07-17 01:35,2021-07-17 06:07
Returnal,PS5,2021-07-17 00:23,2021-07-17 01:21
Another World - 20th Anniversary Edition,PS5,2021-07-15 22:09,2021-07-15 23:33
Sekiro™: Shadows Die Twice,PS5,2021-07-15 21:52,2021-07-15 22:09
Sekiro™: Shadows Die Twice,PS5,2021-07-15 19:07,2021-07-15 20:13
Sekiro™: Shadows Die Twice,PS5,2021-07-15 01:50,2021-07-15 03:31
Sekiro™: Shadows Die Twice,PS5,2021-07-15 00:12,2021-07-15 01:14
Sekiro™: Shadows Die Twice,PS5,2021-07-14 03:19,2021-07-14 05:13
Persona 5,PS5,2021-07-13 21:56,2021-07-13 21:58
Persona 5,PS5,2021-07-13 01:32,2021-07-13 02:59
Maquette,PS5,2021-07-13 01:30,2021-07-13 01:32
Maquette,PS5,2021-07-12 23:59,2021-07-13 00:34
ASTRO's PLAYROOM,PS5,2021-07-12 23:06,2021-07-12 23:59
Crash Bandicoot N. Sane Trilogy,PS5,2021-07-12 23:01,2021-07-12 23:06
Virtua Fighter 5 Ultimate Showdown,PS4,2021-07-02 19:46,2021-07-02 20:57
Bloodborne™,PS4,2021-04-03 19:50,2021-04-03 23:52
Tom Clancy's The Division® 2,PS4,2021-04-03 02:10,2021-04-03 05:22
Bloodborne™,PS4,2021-04-02 21:34,2021-04-03 02:10
Bloodborne™,PS4,2021-04-02 06:01,2021-04-02 08:11
Tom Clancy's The Division® 2,PS4,2021-04-02 04:24,2021-04-02 06:01
Tom Clancy's The Division® 2,PS4,2021-03-31 02:55,2021-03-31 05:50
Tom Clancy's The Division® 2,PS4,2021-03-29 02:00,2021-03-29 02:02
Tom Clancy's The Division® 2,PS4,2021-03-27 03:31,2021-03-27 06:29
Tom Clancy's The Division® 2,PS4,2021-03-26 05:10,2021-03-26 06:03
Remnant: From the Ashes,PS4,2021-03-23 03:20,2021-03-23 05:58
DARK SOULS™ II: Scholar of the First Sin,PS4,2021-03-20 01:31,2021-03-20 01:34
Remnant: From the Ashes,PS4,2021-03-12 01:29,2021-03-12 01:30
Remnant: From the Ashes,PS4,2021-03-08 02:41,2021-03-08 05:38
Remnant: From the Ashes,PS4,2021-03-07 03:21,2021-03-07 06:49
13 Sentinels: Aegis Rim,PS4,2021-03-07 03:20,2021-03-07 03:21
DARK SOULS™ II: Scholar of the First Sin,PS4,2020-10-24 23:43,2020-10-25 01:18
Ghost of Tsushima,PS4,2020-10-24 23:14,2020-10-24 23:22
1 name platform start end
2 Nioh 2 PS5 2022-12-17 19:34 2022-12-17 22:53
3 Nioh 2 PS5 2022-12-15 02:25 2022-12-15 03:57
4 Nioh 2 PS5 2022-12-13 02:41 2022-12-13 04:34
5 Nioh 2 PS5 2022-12-11 21:01 2022-12-11 23:21
6 VALKYRIE ELYSIUM PS5 2022-12-11 06:07 2022-12-11 06:20
7 Metal: Hellsinger PS5 2022-12-11 05:50 2022-12-11 06:07
8 Nioh 2 PS5 2022-12-11 04:31 2022-12-11 05:50
9 Nioh 2 PS5 2022-12-11 04:11 2022-12-11 04:26
10 Forspoken PS5 2022-12-10 22:29 2022-12-10 23:10
11 Nioh 2 PS5 2022-12-10 19:44 2022-12-10 22:29
12 Nioh 2 PS5 2022-12-09 02:14 2022-12-09 04:16
13 Nioh 2 PS5 2022-12-08 01:03 2022-12-08 01:57
14 Nioh 2 PS5 2022-12-07 00:43 2022-12-07 04:16
15 Nioh 2 PS5 2022-12-04 20:48 2022-12-04 23:31
16 Nioh 2 PS5 2022-12-04 04:26 2022-12-04 07:01
17 Nioh 2 PS5 2022-12-04 04:20 2022-12-04 04:22
18 Nioh 2 PS5 2022-11-26 19:18 2022-11-26 21:28
19 Nioh 2 PS5 2022-11-26 19:16 2022-11-26 19:18
20 Nioh 2 PS5 2022-11-26 02:46 2022-11-26 03:56
21 Nioh 2 PS5 2022-11-26 02:01 2022-11-26 02:43
22 God of War Ragnarök PS5 2022-11-24 23:03 2022-11-25 01:32
23 God of War Ragnarök PS5 2022-11-23 00:41 2022-11-23 07:52
24 God of War Ragnarök PS5 2022-11-21 22:52 2022-11-22 04:51
25 God of War Ragnarök PS5 2022-11-21 02:11 2022-11-21 05:13
26 God of War Ragnarök PS5 2022-11-20 21:34 2022-11-20 22:50
27 God of War Ragnarök PS5 2022-11-20 03:46 2022-11-20 05:52
28 God of War Ragnarök PS5 2022-11-19 14:30 2022-11-19 16:14
29 God of War Ragnarök PS5 2022-11-18 23:15 2022-11-19 04:16
30 God of War Ragnarök PS5 2022-11-18 19:58 2022-11-18 20:40
31 God of War Ragnarök PS5 2022-11-18 03:50 2022-11-18 06:25
32 God of War Ragnarök PS5 2022-11-17 19:36 2022-11-18 00:26
33 God of War Ragnarök PS5 2022-11-17 13:16 2022-11-17 16:13
34 God of War Ragnarök PS5 2022-11-16 21:45 2022-11-16 22:38
35 God of War Ragnarök PS5 2022-11-16 00:14 2022-11-16 04:28
36 God of War Ragnarök PS5 2022-11-15 01:33 2022-11-15 05:06
37 God of War Ragnarök PS5 2022-11-14 00:37 2022-11-14 04:43
38 God of War Ragnarök PS5 2022-11-12 23:32 2022-11-13 03:45
39 God of War Ragnarök PS5 2022-11-12 03:17 2022-11-12 05:00
40 Grand Theft Auto V (PlayStation®5) PS5 2022-10-03 02:01 2022-10-03 02:23
41 Grand Theft Auto V (PlayStation®5) PS5 2022-10-02 13:59 2022-10-02 15:54
42 Grand Theft Auto V (PlayStation®5) PS5 2022-09-30 22:40 2022-10-01 02:50
43 Grand Theft Auto V (PlayStation®5) PS5 2022-09-27 22:38 2022-09-28 00:16
44 Grand Theft Auto V (PlayStation®5) PS5 2022-09-27 19:27 2022-09-27 21:09
45 Grand Theft Auto V (PlayStation®5) PS5 2022-09-26 20:58 2022-09-26 23:38
46 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 23:56 2022-09-26 02:36
47 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 14:57 2022-09-25 16:38
48 Grand Theft Auto V (PlayStation®5) PS5 2022-09-25 02:04 2022-09-25 02:12
49 Grand Theft Auto V (PlayStation®5) PS5 2022-09-23 20:33 2022-09-23 23:38
50 Wo Long: Fallen Dynasty PS5 2022-09-18 15:26 2022-09-18 16:58
51 Grand Theft Auto V (PlayStation®5) PS5 2022-08-31 00:42 2022-08-31 01:15
52 Grand Theft Auto V (PlayStation®5) PS5 2022-08-18 13:43 2022-08-18 15:12
53 Grand Theft Auto V (PlayStation®5) PS5 2022-08-18 00:42 2022-08-18 01:58
54 Tony Hawk's™ Pro Skater™ 1 + 2 PS5 2022-08-18 00:40 2022-08-18 00:42
55 Tony Hawk's™ Pro Skater™ 1 + 2 PS5 2022-08-14 15:46 2022-08-14 16:10
56 FINAL FANTASY VII PS5 2022-07-26 18:56 2022-07-26 20:22
57 FINAL FANTASY VII REMAKE PS5 2022-07-26 17:39 2022-07-26 18:53
58 FINAL FANTASY VII REMAKE PS5 2022-07-25 22:12 2022-07-26 04:37
59 FINAL FANTASY VII REMAKE PS5 2022-07-24 00:09 2022-07-24 05:33
60 FINAL FANTASY VII REMAKE PS5 2022-07-23 23:34 2022-07-23 23:48
61 FINAL FANTASY VII REMAKE PS5 2022-07-23 18:05 2022-07-23 19:44
62 FINAL FANTASY VII REMAKE PS5 2022-07-22 17:07 2022-07-23 01:48
63 FINAL FANTASY VII REMAKE PS5 2022-07-22 15:26 2022-07-22 15:59
64 FINAL FANTASY VII REMAKE PS5 2022-07-21 21:27 2022-07-21 22:43
65 FINAL FANTASY VII REMAKE PS5 2022-07-21 20:48 2022-07-21 20:58
66 FINAL FANTASY VII REMAKE PS5 2022-07-21 18:35 2022-07-21 18:36
67 Stray PS5 2022-07-21 01:24 2022-07-21 02:34
68 Stray PS5 2022-07-19 23:49 2022-07-20 03:24
69 Atelier Ayesha ~The Alchemist of Dusk~ PS3 2022-07-04 03:26 2022-07-04 03:34
70 Red Dead Redemption PS3 2022-07-04 02:36 2022-07-04 03:14
71 Ghost of Tsushima PS5 2022-07-04 01:38 2022-07-04 02:34
72 Dark Cloud™ PS4 2022-07-01 23:48 2022-07-02 00:04
73 Atelier Ayesha ~The Alchemist of Dusk~ PS3 2022-07-01 23:20 2022-07-01 23:46
74 Resident Evil Director’s Cut PS5 2022-07-01 23:14 2022-07-01 23:20
75 ELEX II PS5 2022-07-01 22:48 2022-07-01 23:13
76 OlliOlli World PS5 2022-07-01 21:30 2022-07-01 22:30
77 Deep Rock Galactic PS5 2022-06-16 05:30 2022-06-16 06:14
78 Curse of the Dead Gods PS5 2022-06-16 05:00 2022-06-16 05:22
79 Persona 5: Dancing in Starlight PS5 2022-04-29 20:14 2022-04-29 20:15
80 Persona 5: Dancing in Starlight PS5 2022-04-29 00:18 2022-04-29 00:44
81 Dying Light 2 PS5 2022-04-14 01:26 2022-04-14 01:27
82 Grand Theft Auto V (PlayStation®5) PS5 2022-03-24 16:26 2022-03-24 16:27
83 Grand Theft Auto V (PlayStation®5) PS5 2022-03-21 15:52 2022-03-21 15:59
84 Horizon Forbidden West PS5 2022-02-23 19:37 2022-02-24 00:24
85 Horizon Forbidden West PS5 2022-02-23 13:57 2022-02-23 17:44
86 Horizon Forbidden West PS5 2022-02-22 18:05 2022-02-23 05:26
87 Horizon Forbidden West PS5 2022-02-22 15:39 2022-02-22 17:02
88 Horizon Forbidden West PS5 2022-02-22 00:05 2022-02-22 04:08
89 Horizon Forbidden West PS5 2022-02-20 15:39 2022-02-20 23:08
90 Horizon Forbidden West PS5 2022-02-20 14:54 2022-02-20 15:09
91 Horizon Forbidden West PS5 2022-02-19 23:37 2022-02-20 04:45
92 Horizon Forbidden West PS5 2022-02-18 23:15 2022-02-19 03:27
93 Assassin's Creed® Origins PS5 2022-02-18 21:49 2022-02-18 23:15
94 Assassin's Creed® Origins PS5 2022-01-17 02:38 2022-01-17 02:50
95 Deep Rock Galactic PS5 2022-01-17 00:57 2022-01-17 02:35
96 HITMAN 3 PS5 2021-11-17 00:35 2021-11-17 01:17
97 HITMAN 3 PS5 2021-11-08 01:59 2021-11-08 06:17
98 HITMAN 3 PS5 2021-11-07 03:10 2021-11-07 05:23
99 HITMAN 3 PS5 2021-11-06 04:23 2021-11-06 08:49
100 HITMAN 3 PS5 2021-11-06 02:17 2021-11-06 03:31
101 HITMAN 3 PS5 2021-11-05 21:33 2021-11-05 23:24
102 HITMAN 3 PS5 2021-11-05 03:09 2021-11-05 03:34
103 HITMAN 3 PS5 2021-11-05 00:47 2021-11-05 02:26
104 HITMAN 3 PS5 2021-11-04 20:27 2021-11-04 23:32
105 HITMAN 3 PS5 2021-11-04 01:34 2021-11-04 05:33
106 RESIDENT EVIL 3 PS5 2021-11-03 23:14 2021-11-03 23:56
107 RESIDENT EVIL 3 PS5 2021-11-02 23:56 2021-11-03 05:10
108 RESIDENT EVIL 3 PS5 2021-11-02 21:22 2021-11-02 23:23
109 RESIDENT EVIL 3 PS5 2021-11-02 05:36 2021-11-02 06:56
110 HITMAN 3 PS5 2021-11-02 03:00 2021-11-02 05:36
111 HITMAN 3 PS5 2021-11-02 01:19 2021-11-02 01:25
112 HITMAN™ 2 PS5 2021-11-02 01:09 2021-11-02 01:19
113 HITMAN 3 PS5 2021-11-01 23:45 2021-11-02 01:09
114 RESIDENT EVIL 3 PS5 2021-11-01 19:32 2021-11-01 19:47
115 Marvel's Spider-Man: Miles Morales PS5 2021-10-17 01:06 2021-10-17 03:27
116 Marvel's Spider-Man: Miles Morales PS5 2021-10-16 20:58 2021-10-16 22:00
117 Marvel's Spider-Man: Miles Morales PS5 2021-10-05 02:30 2021-10-05 03:27
118 Marvel's Spider-Man: Miles Morales PS5 2021-10-03 23:12 2021-10-04 01:21
119 Marvel's Spider-Man: Miles Morales PS5 2021-10-03 03:02 2021-10-03 04:42
120 Marvel's Spider-Man: Miles Morales PS5 2021-10-02 20:12 2021-10-02 21:10
121 Marvel's Spider-Man: Miles Morales PS5 2021-10-02 01:40 2021-10-02 03:36
122 Marvel's Spider-Man: Miles Morales PS5 2021-10-01 04:34 2021-10-01 05:30
123 DEATHLOOP PS5 2021-10-01 01:12 2021-10-01 04:27
124 DEATHLOOP PS5 2021-09-30 03:04 2021-09-30 06:30
125 DEATHLOOP PS5 2021-09-29 00:28 2021-09-29 05:08
126 Persona 5 Royal PS5 2021-09-28 00:36 2021-09-28 03:08
127 Persona 5 Royal PS5 2021-09-27 02:16 2021-09-27 05:56
128 Persona 5 Royal PS5 2021-09-26 14:54 2021-09-26 16:32
129 Persona 5 Royal PS5 2021-09-25 18:43 2021-09-25 23:26
130 Persona 5 Royal PS5 2021-09-24 21:41 2021-09-25 03:40
131 Persona 5 Royal PS5 2021-09-23 00:18 2021-09-23 06:26
132 Persona 5 Royal PS5 2021-09-21 20:27 2021-09-22 05:43
133 Persona 5 Royal PS5 2021-09-21 01:07 2021-09-21 06:06
134 Borderlands: The Handsome Collection PS5 2021-09-20 23:59 2021-09-21 01:07
135 Persona 5 Royal PS5 2021-09-20 23:53 2021-09-20 23:59
136 DEATHLOOP PS5 2021-09-20 02:03 2021-09-20 06:29
137 DEATHLOOP PS5 2021-09-19 19:49 2021-09-20 01:16
138 Borderlands: The Handsome Collection PS5 2021-09-19 00:51 2021-09-19 03:41
139 Borderlands: The Handsome Collection PS5 2021-09-17 23:45 2021-09-18 01:48
140 Borderlands: The Handsome Collection PS5 2021-09-17 23:40 2021-09-17 23:41
141 DEATHLOOP PS5 2021-09-17 16:48 2021-09-17 18:56
142 DEATHLOOP PS5 2021-09-17 03:02 2021-09-17 04:39
143 DEATHLOOP PS5 2021-09-17 00:03 2021-09-17 02:53
144 DEATHLOOP PS5 2021-09-16 18:39 2021-09-16 21:12
145 Persona 5 Royal PS5 2021-09-16 18:29 2021-09-16 18:30
146 Persona 5 Royal PS5 2021-09-16 02:26 2021-09-16 06:13
147 Persona 5 Royal PS5 2021-09-16 02:20 2021-09-16 02:21
148 Persona 5 Royal PS5 2021-09-15 01:48 2021-09-15 06:07
149 Persona 5 Royal PS5 2021-09-14 22:21 2021-09-15 01:22
150 Persona 5 Royal PS5 2021-09-14 02:01 2021-09-14 05:48
151 Persona 5 Royal PS5 2021-09-14 00:24 2021-09-14 01:46
152 Persona 5 Royal PS5 2021-08-12 05:04 2021-08-12 07:05
153 Persona 5 Royal PS5 2021-08-11 05:02 2021-08-11 06:48
154 Persona 5 Royal PS5 2021-08-09 00:37 2021-08-09 06:15
155 Persona 5 Royal PS5 2021-08-08 00:31 2021-08-08 08:01
156 Persona 5 Royal PS5 2021-08-07 19:51 2021-08-07 22:50
157 Persona 5 Royal PS5 2021-08-06 23:51 2021-08-07 01:35
158 Persona 5 Royal PS5 2021-08-06 19:26 2021-08-06 22:26
159 Persona 5 Royal PS5 2021-08-06 02:42 2021-08-06 06:51
160 Persona 5 Royal PS5 2021-08-06 00:37 2021-08-06 01:54
161 Far Cry® 5 PS5 2021-08-01 23:27 2021-08-02 02:09
162 Far Cry® 5 PS5 2021-08-01 18:10 2021-08-01 19:40
163 STAR WARS™: Squadrons PS5 2021-08-01 18:02 2021-08-01 18:10
164 STAR WARS™: Squadrons PS5 2021-08-01 00:24 2021-08-01 00:30
165 STEEP PS5 2021-08-01 00:15 2021-08-01 00:24
166 Red Dead Redemption 2 PS5 2021-07-31 23:48 2021-08-01 00:13
167 Persona 5 Royal PS5 2021-07-30 19:09 2021-07-30 19:10
168 Persona 5 Royal PS5 2021-07-29 03:41 2021-07-29 04:59
169 Persona 5 Royal PS5 2021-07-28 02:32 2021-07-28 03:07
170 Demon's Souls PS5 2021-07-28 00:12 2021-07-28 02:32
171 Red Dead Redemption 2 PS5 2021-07-27 23:20 2021-07-27 23:23
172 Red Dead Redemption 2 PS5 2021-07-26 00:42 2021-07-26 01:13
173 Ghost of Tsushima PS5 2021-07-25 19:03 2021-07-25 22:12
174 Ghost of Tsushima PS5 2021-07-25 18:52 2021-07-25 18:55
175 Rez Infinite PS5 2021-07-25 18:32 2021-07-25 18:52
176 Returnal PS5 2021-07-25 05:24 2021-07-25 05:26
177 Tom Clancy's The Division® 2 PS5 2021-07-25 02:12 2021-07-25 05:24
178 Returnal PS5 2021-07-25 00:00 2021-07-25 02:12
179 Returnal PS5 2021-07-24 16:13 2021-07-24 17:39
180 Returnal PS5 2021-07-24 03:02 2021-07-24 07:02
181 Returnal PS5 2021-07-23 18:08 2021-07-23 20:39
182 Returnal PS5 2021-07-23 14:36 2021-07-23 15:38
183 Titanfall™ 2 PS5 2021-07-22 02:42 2021-07-22 03:20
184 Returnal PS5 2021-07-20 02:12 2021-07-20 05:40
185 Returnal PS5 2021-07-19 03:37 2021-07-19 05:24
186 Concrete Genie PS5 2021-07-19 03:35 2021-07-19 03:37
187 Concrete Genie PS5 2021-07-18 05:04 2021-07-18 05:30
188 Stranded Deep PS5 2021-07-18 04:32 2021-07-18 04:58
189 Sniper Elite 4 PS5 2021-07-18 04:16 2021-07-18 04:32
190 Oddworld: Soulstorm PS5 2021-07-18 04:00 2021-07-18 04:14
191 Zombie Army 4: Dead War PS5 2021-07-18 03:48 2021-07-18 03:58
192 Sekiro™: Shadows Die Twice PS5 2021-07-18 03:00 2021-07-18 03:48
193 Returnal PS5 2021-07-17 17:41 2021-07-17 23:32
194 Returnal PS5 2021-07-17 01:35 2021-07-17 06:07
195 Returnal PS5 2021-07-17 00:23 2021-07-17 01:21
196 Another World - 20th Anniversary Edition PS5 2021-07-15 22:09 2021-07-15 23:33
197 Sekiro™: Shadows Die Twice PS5 2021-07-15 21:52 2021-07-15 22:09
198 Sekiro™: Shadows Die Twice PS5 2021-07-15 19:07 2021-07-15 20:13
199 Sekiro™: Shadows Die Twice PS5 2021-07-15 01:50 2021-07-15 03:31
200 Sekiro™: Shadows Die Twice PS5 2021-07-15 00:12 2021-07-15 01:14
201 Sekiro™: Shadows Die Twice PS5 2021-07-14 03:19 2021-07-14 05:13
202 Persona 5 PS5 2021-07-13 21:56 2021-07-13 21:58
203 Persona 5 PS5 2021-07-13 01:32 2021-07-13 02:59
204 Maquette PS5 2021-07-13 01:30 2021-07-13 01:32
205 Maquette PS5 2021-07-12 23:59 2021-07-13 00:34
206 ASTRO's PLAYROOM PS5 2021-07-12 23:06 2021-07-12 23:59
207 Crash Bandicoot N. Sane Trilogy PS5 2021-07-12 23:01 2021-07-12 23:06
208 Virtua Fighter 5 Ultimate Showdown PS4 2021-07-02 19:46 2021-07-02 20:57
209 Bloodborne™ PS4 2021-04-03 19:50 2021-04-03 23:52
210 Tom Clancy's The Division® 2 PS4 2021-04-03 02:10 2021-04-03 05:22
211 Bloodborne™ PS4 2021-04-02 21:34 2021-04-03 02:10
212 Bloodborne™ PS4 2021-04-02 06:01 2021-04-02 08:11
213 Tom Clancy's The Division® 2 PS4 2021-04-02 04:24 2021-04-02 06:01
214 Tom Clancy's The Division® 2 PS4 2021-03-31 02:55 2021-03-31 05:50
215 Tom Clancy's The Division® 2 PS4 2021-03-29 02:00 2021-03-29 02:02
216 Tom Clancy's The Division® 2 PS4 2021-03-27 03:31 2021-03-27 06:29
217 Tom Clancy's The Division® 2 PS4 2021-03-26 05:10 2021-03-26 06:03
218 Remnant: From the Ashes PS4 2021-03-23 03:20 2021-03-23 05:58
219 DARK SOULS™ II: Scholar of the First Sin PS4 2021-03-20 01:31 2021-03-20 01:34
220 Remnant: From the Ashes PS4 2021-03-12 01:29 2021-03-12 01:30
221 Remnant: From the Ashes PS4 2021-03-08 02:41 2021-03-08 05:38
222 Remnant: From the Ashes PS4 2021-03-07 03:21 2021-03-07 06:49
223 13 Sentinels: Aegis Rim PS4 2021-03-07 03:20 2021-03-07 03:21
224 DARK SOULS™ II: Scholar of the First Sin PS4 2020-10-24 23:43 2020-10-25 01:18
225 Ghost of Tsushima PS4 2020-10-24 23:14 2020-10-24 23:22

View File

@ -0,0 +1,71 @@
- model: games.game
pk: 1
fields:
name: Nioh 2
wikidata: Q67482292
- model: games.game
pk: 2
fields:
name: Elden Ring
wikidata: Q64826862
- model: games.game
pk: 3
fields:
name: Cyberpunk 2077
wikidata: Q3182559
- model: games.purchase
pk: 1
fields:
game: 1
platform: 1
date_purchased: 2021-02-13
date_refunded: null
- model: games.purchase
pk: 2
fields:
game: 2
platform: 1
date_purchased: 2022-02-24
date_refunded: null
- model: games.purchase
pk: 3
fields:
game: 3
platform: 1
date_purchased: 2020-12-07
date_refunded: null
- model: games.platform
pk: 1
fields:
name: Steam
group: PC
- model: games.platform
pk: 3
fields:
name: Xbox Gamepass
group: PC
- model: games.platform
pk: 4
fields:
name: Epic Games Store
group: PC
- model: games.platform
pk: 5
fields:
name: Playstation 5
group: Playstation
- model: games.platform
pk: 6
fields:
name: Playstation 4
group: Playstation
- model: games.platform
pk: 7
fields:
name: Nintendo Switch
group: Nintendo
- model: games.platform
pk: 8
fields:
name: Nintendo 3DS
group: Nintendo

View File

@ -1,8 +1,11 @@
from django import forms
from .models import Session, Purchase, Game, Platform
from games.models import Game, Platform, Purchase, Session
class SessionForm(forms.ModelForm):
purchase = forms.ModelChoiceField(queryset=Purchase.objects.order_by("game__name"))
class Meta:
model = Session
fields = [
@ -15,6 +18,9 @@ class SessionForm(forms.ModelForm):
class PurchaseForm(forms.ModelForm):
game = forms.ModelChoiceField(queryset=Game.objects.order_by("name"))
platform = forms.ModelChoiceField(queryset=Platform.objects.order_by("name"))
class Meta:
model = Purchase
fields = ["game", "platform", "date_purchased", "date_refunded"]

View File

@ -1,7 +1,7 @@
# Generated by Django 4.1.4 on 2023-01-02 18:27
from django.db import migrations, models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
@ -60,14 +60,14 @@ class Migration(migrations.Migration):
(
"game",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="tracker.game"
on_delete=django.db.models.deletion.CASCADE, to="games.game"
),
),
(
"platform",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="tracker.platform",
to="games.platform",
),
),
],
@ -93,7 +93,7 @@ class Migration(migrations.Migration):
"purchase",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="tracker.purchase",
to="games.purchase",
),
),
],

View File

@ -1,13 +1,14 @@
# Generated by Django 4.1.4 on 2023-01-02 18:55
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0001_initial"),
("games", "0001_initial"),
]
operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0002_alter_session_duration_manual"),
("games", "0002_alter_session_duration_manual"),
]
operations = [

View File

@ -1,13 +1,14 @@
# Generated by Django 4.1.5 on 2023-01-09 14:49
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("tracker", "0003_alter_session_duration_manual_and_more"),
("games", "0003_alter_session_duration_manual_and_more"),
]
operations = [

View File

@ -1,11 +1,12 @@
# Generated by Django 4.1.5 on 2023-01-09 17:43
from django.db import migrations
from datetime import timedelta
from django.db import migrations
def set_duration_calculated_none_to_zero(apps, schema_editor):
Session = apps.get_model("tracker", "Session")
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_calculated == None:
session.duration_calculated = timedelta(0)
@ -13,7 +14,7 @@ def set_duration_calculated_none_to_zero(apps, schema_editor):
def revert_set_duration_calculated_none_to_zero(apps, schema_editor):
Session = apps.get_model("tracker", "Session")
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_calculated == timedelta(0):
session.duration_calculated = None
@ -23,7 +24,7 @@ def revert_set_duration_calculated_none_to_zero(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("tracker", "0004_alter_session_duration_manual"),
("games", "0004_alter_session_duration_manual"),
]
operations = [

View File

@ -1,11 +1,12 @@
# Generated by Django 4.1.5 on 2023-01-09 18:04
from django.db import migrations
from datetime import timedelta
from django.db import migrations
def set_duration_manual_none_to_zero(apps, schema_editor):
Session = apps.get_model("tracker", "Session")
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_manual == None:
session.duration_manual = timedelta(0)
@ -13,7 +14,7 @@ def set_duration_manual_none_to_zero(apps, schema_editor):
def revert_set_duration_manual_none_to_zero(apps, schema_editor):
Session = apps.get_model("tracker", "Session")
Session = apps.get_model("games", "Session")
for session in Session.objects.all():
if session.duration_manual == timedelta(0):
session.duration_manual = None
@ -23,7 +24,7 @@ def revert_set_duration_manual_none_to_zero(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
("tracker", "0005_auto_20230109_1843"),
("games", "0005_auto_20230109_1843"),
]
operations = [

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-01-19 18:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("games", "0006_auto_20230109_1904"),
]
operations = [
migrations.AlterField(
model_name="purchase",
name="game",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.game"
),
),
migrations.AlterField(
model_name="purchase",
name="platform",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.platform"
),
),
migrations.AlterField(
model_name="session",
name="purchase",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="games.purchase"
),
),
]

View File

@ -1,11 +1,11 @@
from django.db import models
from datetime import datetime, timedelta
from django.conf import settings
from zoneinfo import ZoneInfo
from common.util.time import format_duration
from django.db.models import Sum, F
from django.db.models import Manager
from typing import Any
from zoneinfo import ZoneInfo
from common.time import format_duration
from django.conf import settings
from django.db import models
from django.db.models import F, Manager, Sum
class Game(models.Model):
@ -79,10 +79,6 @@ class Session(models.Model):
def duration_sum(self) -> str:
return Session.objects.all().total_duration()
@property
def last(self) -> Manager[Any]:
return Session.objects.all().order_by("timestamp_start")[:-1]
def save(self, *args, **kwargs):
if self.timestamp_start != None and self.timestamp_end != None:
self.duration_calculated = self.timestamp_end - self.timestamp_start

View File

@ -683,66 +683,34 @@ select {
width: 100%;
}
.\!container {
width: 100% !important;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
.\!container {
max-width: 640px !important;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
.\!container {
max-width: 768px !important;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
.\!container {
max-width: 1024px !important;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
.\!container {
max-width: 1280px !important;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
.\!container {
max-width: 1536px !important;
}
}
.visible {
visibility: visible;
}
.collapse {
visibility: collapse;
}
.static {
@ -753,26 +721,6 @@ select {
position: fixed;
}
.\!fixed {
position: fixed !important;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.sticky {
position: sticky;
}
.\!sticky {
position: sticky !important;
}
.left-2 {
left: 0.5rem;
}
@ -781,6 +729,10 @@ select {
bottom: 0.5rem;
}
.clear-both {
clear: both;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@ -794,8 +746,12 @@ select {
margin-top: 1rem;
}
.mb-5 {
margin-bottom: 1.25rem;
.mb-3 {
margin-bottom: 0.75rem;
}
.mt-10 {
margin-top: 2.5rem;
}
.ml-1 {
@ -806,10 +762,6 @@ select {
display: block;
}
.inline-block {
display: inline-block;
}
.inline {
display: inline;
}
@ -818,28 +770,8 @@ select {
display: flex;
}
.table {
display: table;
}
.table-caption {
display: table-caption;
}
.table-cell {
display: table-cell;
}
.contents {
display: contents;
}
.hidden {
display: none;
}
.\!hidden {
display: none !important;
.h-6 {
height: 1.5rem;
}
.h-5 {
@ -858,6 +790,10 @@ select {
width: 100%;
}
.w-6 {
width: 1.5rem;
}
.w-5 {
width: 1.25rem;
}
@ -874,10 +810,6 @@ select {
max-width: 1024px;
}
.resize {
resize: both;
}
.flex-col {
flex-direction: column;
}
@ -910,10 +842,12 @@ select {
align-self: center;
}
.truncate {
.overflow-hidden {
overflow: hidden;
}
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap {
@ -932,10 +866,6 @@ select {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@ -1029,14 +959,6 @@ select {
font-weight: 600;
}
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
@ -1064,29 +986,6 @@ select {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.invert {
--tw-invert: invert(100%);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.\!invert {
--tw-invert: invert(100%) !important;
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.\!filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow) !important;
}
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
@ -1122,7 +1021,7 @@ textarea {
#session-table {
display: grid;
grid-template-columns: repeat(3, 2fr) 0.5fr 1fr;
grid-template-columns: 3fr 1fr repeat(2, 2fr) 0.5fr 1fr;
}
#button-container button {

View File

@ -5,29 +5,39 @@
{% block content %}
<div class="text-center text-xl mb-4 dark:text-slate-400">
{% if dataset.count >= 2 %}
<img src="data:image/svg+xml;base64,{{ chart|safe }}" class="mx-auto mb-5" />
<a href="{% url 'start_session' dataset.last.purchase.id %}">
<button type="button" title="Track last tracked" class="py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
New session of {{ dataset.last.purchase }}
<img src="data:image/svg+xml;base64,{{ chart|safe }}" class="mx-auto mb-3" />
{% endif %}
{% if dataset.count >= 1 %}
<div class="mb-4">Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</div>
{% endif %}
{% if purchase or platform or game %}
<a class="text-red-400 inline" href="{% url 'list_sessions' %}"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a><span>Filtering by "{% firstof purchase platform game %}"</span>
{% if purchase %}<a class="dark:text-white hover:underline block" href="{% url 'list_sessions_by_game' purchase.game.id %}">See all platforms</a>{% endif %}
{% endif %}
{% if dataset.count >= 1 %}
<a class="clear-both" href="{% url 'start_session' last.purchase.id %}">
<button type="button" title="Track last tracked" class="mt-10 py-1 px-2 bg-green-600 hover:bg-green-700 focus:ring-green-500 focus:ring-offset-blue-200 text-white transition ease-in duration-200 text-center text-base font-semibold shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg ">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="self-center w-6 h-6 inline">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
</svg>
{{ last.purchase }}
</button>
</a>
{% else %}
Playtime chart will be displayed when there are 2 or more sessions.
{% endif %}
{% if purchase %}
<h1>Listing sessions only for purchase "{{ purchase }}"</h1>
<h2>Total playtime: {{ total_duration }} over {{ dataset.count }} sessions.</h2>
<a class="dark:text-white hover:underline" href="{% url 'list_sessions' %}">View all sessions</a>
{% endif %}
</div>
<div id="session-table" class="gap-4 shadow rounded-xl max-w-screen-lg mx-auto dark:bg-slate-700 p-2 justify-center">
<div class="dark:border-white dark:text-slate-300 text-lg">Name</div>
<div class="dark:border-white dark:text-slate-300 text-lg">Platform</div>
<div class="dark:border-white dark:text-slate-300 text-lg text-center">Start</div>
<div class="dark:border-white dark:text-slate-300 text-lg text-center">End</div>
<div class="dark:border-white dark:text-slate-300 text-lg">Duration</div>
<div class="dark:border-white dark:text-slate-300 text-lg text-right">Manage</div>
{% for data in dataset %}
<div><a class="dark:text-white hover:underline" href="{% url 'list_sessions' data.purchase.id %}">{{ data.purchase }}</a></div>
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_purchase' data.purchase.id %}">{{ data.purchase.game }}</a></div>
<div class="dark:text-white overflow-hidden text-ellipsis whitespace-nowrap"><a class="hover:underline" href="{% url 'list_sessions_by_platform' data.purchase.platform.id %}">{{ data.purchase.platform }}</a></div>
<div class="dark:text-slate-400 text-center">{{ data.timestamp_start | date:"d/m/Y H:i" }}</div>
<div class="dark:text-slate-400 text-center">
{% if data.unfinished %}

View File

@ -1,7 +1,8 @@
import os
import time
from django import template
from django.conf import settings
import time
import os
register = template.Library()
@ -12,7 +13,7 @@ def version_date():
"%d-%b-%Y %H:%m",
time.gmtime(
os.path.getmtime(
os.path.abspath(os.path.join(settings.BASE_DIR, "..", "..", ".git"))
os.path.abspath(os.path.join(settings.BASE_DIR, "pyproject.toml"))
)
),
)

View File

@ -1,6 +1,6 @@
from django.urls import path
from . import views
from games import views
urlpatterns = [
path("", views.index, name="index"),
@ -27,6 +27,19 @@ urlpatterns = [
path(
"list-sessions/by-purchase/<int:purchase_id>",
views.list_sessions,
name="list_sessions",
{"filter": "purchase"},
name="list_sessions_by_purchase",
),
path(
"list-sessions/by-platform/<int:platform_id>",
views.list_sessions,
{"filter": "platform"},
name="list_sessions_by_platform",
),
path(
"list-sessions/by-game/<int:game_id>",
views.list_sessions,
{"filter": "game"},
name="list_sessions_by_game",
),
]

View File

@ -1,14 +1,13 @@
from django.shortcuts import render, redirect
from .models import Game, Platform, Purchase, Session
from .forms import SessionForm, PurchaseForm, GameForm, PlatformForm
from datetime import datetime, timedelta
from datetime import datetime
from zoneinfo import ZoneInfo
from common.plots import playtime_over_time_chart
from common.time import now as now_with_tz
from django.conf import settings
from common.util.time import now as now_with_tz, format_duration
from django.db.models import Sum
import logging
from common.util.plots import playtime_over_time_chart
from django.shortcuts import redirect, render
from .forms import GameForm, PlatformForm, PurchaseForm, SessionForm
from .models import Game, Platform, Purchase, Session
def model_counts(request):
@ -22,8 +21,15 @@ def model_counts(request):
def add_session(request):
context = {}
initial = {}
now = now_with_tz()
initial = {"timestamp_start": now}
initial["timestamp_start"] = now
last = Session.objects.all().last()
if last != None:
initial["purchase"] = last.purchase
form = SessionForm(request.POST or None, initial=initial)
if form.is_valid():
form.save()
@ -53,14 +59,21 @@ def delete_session(request, session_id=None):
return redirect("list_sessions")
def list_sessions(request, purchase_id=None):
def list_sessions(request, filter="", purchase_id="", platform_id="", game_id=""):
context = {}
if purchase_id != None:
if filter == "purchase":
dataset = Session.objects.filter(purchase=purchase_id)
context["purchase"] = Purchase.objects.get(id=purchase_id)
elif filter == "platform":
dataset = Session.objects.filter(purchase__platform=platform_id)
context["platform"] = Platform.objects.get(id=platform_id)
elif filter == "game":
dataset = Session.objects.filter(purchase__game=game_id)
context["game"] = Game.objects.get(id=game_id)
else:
dataset = Session.objects.all().order_by("timestamp_start")
# by default, sort from newest to oldest
dataset = Session.objects.all().order_by("-timestamp_start")
for session in dataset:
if session.timestamp_end == None and session.duration_manual.seconds == 0:
@ -69,7 +82,11 @@ def list_sessions(request, purchase_id=None):
context["total_duration"] = dataset.total_duration()
context["dataset"] = dataset
context["chart"] = playtime_over_time_chart(dataset)
# cannot use dataset[0] here because that might be only partial QuerySet
context["last"] = Session.objects.all().order_by("timestamp_start").last()
# charts are always oldest->newest
if Session.objects.count() >= 2:
context["chart"] = playtime_over_time_chart(dataset.order_by("timestamp_start"))
return render(request, "list_sessions.html", context)

2
src/web/manage.py → manage.py Normal file → Executable file
View File

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

20
poetry.lock generated
View File

@ -417,6 +417,24 @@ files = [
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
[[package]]
name = "isort"
version = "5.11.4"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"},
{file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"},
]
[package.extras]
colors = ["colorama (>=0.4.3,<0.5.0)"]
pipfile-deprecated-finder = ["pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "jsbeautifier"
version = "1.14.7"
@ -1279,4 +1297,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "01d5c9b89b638c993f8540298dedfa79321b3aac1b2af70da58ef77706d0a113"
content-hash = "bf02e951b8c14fbe6f2709b6df7f9664f99ad5fbf7195d8e7b18c8574d00e683"

View File

@ -1,10 +1,11 @@
[tool.poetry]
name = "timetracker"
version = "0.2.2"
version = "1.0.0"
description = "A simple time tracker."
authors = ["Lukáš Kucharczyk <lukas@kucharczyk.xyz>"]
license = "GPL"
readme = "README.md"
packages = [{include = "timetracker"}]
[tool.poetry.dependencies]
python = "^3.10"
@ -23,12 +24,11 @@ django-extensions = "^3.2.1"
werkzeug = "^2.2.2"
djhtml = "^1.5.2"
djlint = "^1.19.11"
isort = "^5.11.4"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.pytest.ini_options]
pythonpath = [
"src"
]
[tool.poetry.scripts]
timetracker-import = "common.import_data:import_from_file"

View File

@ -1,3 +0,0 @@
import logging
logging.basicConfig(level=logging.ERROR, filename="timelogger.log")

View File

@ -1,64 +0,0 @@
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.dates import date2num
import base64
from io import BytesIO
from tracker.models import Session
from django.db.models import Sum, IntegerField, F
from django.db.models.functions import TruncDay
import logging
from datetime import datetime
from django.db.models import QuerySet
def key_value_to_value_value(data):
return {data["date"]: data["hours"]}
def playtime_over_time_chart(queryset: QuerySet = Session.objects):
microsecond_in_second = 1000000
result = (
queryset.annotate(date=TruncDay("timestamp_start"))
.values("date")
.annotate(
hours=Sum(
F("duration_calculated") + F("duration_manual"),
output_field=IntegerField(),
)
)
.values("date", "hours")
)
keys = []
values = []
running_total = int(0)
for item in result:
date_value = datetime.strftime(item["date"], "%d-%m-%Y")
keys.append(date_value)
running_total += int(item["hours"] / (3600 * microsecond_in_second))
values.append(running_total)
data = [keys, values]
return get_chart(data, title="Playtime over time", xlabel="Date", ylabel="Hours")
def get_graph():
buffer = BytesIO()
plt.savefig(buffer, format="svg", transparent=True)
buffer.seek(0)
image_png = buffer.getvalue()
graph = base64.b64encode(image_png)
graph = graph.decode("utf-8")
buffer.close()
return graph
def get_chart(data, title="", xlabel="", ylabel=""):
plt.style.use("dark_background")
plt.switch_backend("SVG")
fig = plt.figure(figsize=(10, 4))
plt.plot(data[0], data[1])
plt.title(title)
plt.xlabel(xlabel)
plt.ylabel(ylabel)
plt.tight_layout()
chart = get_graph()
return chart

View File

@ -1,98 +0,0 @@
- model: tracker.game
pk: 1
fields:
name: Nioh 2
wikidata: Q67482292
- model: tracker.game
pk: 2
fields:
name: Elden Ring
wikidata: Q64826862
- model: tracker.game
pk: 3
fields:
name: Cyberpunk 2077
wikidata: Q3182559
- model: tracker.purchase
pk: 1
fields:
game: 1
platform: 1
date_purchased: 2021-02-13
date_refunded: null
- model: tracker.purchase
pk: 2
fields:
game: 2
platform: 1
date_purchased: 2022-02-24
date_refunded: null
- model: tracker.purchase
pk: 3
fields:
game: 3
platform: 1
date_purchased: 2020-12-07
date_refunded: null
- model: tracker.platform
pk: 1
fields:
name: Steam
group: PC
- model: tracker.platform
pk: 3
fields:
name: Xbox Gamepass
group: PC
- model: tracker.platform
pk: 4
fields:
name: Epic Games Store
group: PC
- model: tracker.platform
pk: 5
fields:
name: Playstation 5
group: Playstation
- model: tracker.platform
pk: 6
fields:
name: Playstation 4
group: Playstation
- model: tracker.platform
pk: 7
fields:
name: Nintendo Switch
group: Nintendo
- model: tracker.platform
pk: 8
fields:
name: Nintendo 3DS
group: Nintendo
- model: tracker.session
pk: 1
fields:
purchase: 2
timestamp_start: 2022-12-31 14:25:58+00:00
timestamp_end: 2022-12-31 16:25:22+00:00
duration_manual: null
duration_calculated: null
note: ''
- model: tracker.session
pk: 3
fields:
purchase: 3
timestamp_start: 2023-01-01 22:00:23+00:00
timestamp_end: 2023-01-01 23:28:23+00:00
duration_manual: null
duration_calculated: null
note: ''
- model: tracker.session
pk: 4
fields:
purchase: 3
timestamp_start: 2020-01-01 23:29:17+00:00
timestamp_end: 2020-01-01 23:29:17+00:00
duration_manual: '12:00:00'
duration_calculated: null
note: ''

View File

@ -1,6 +1,6 @@
module.exports = {
darkMode: 'class',
content: ["./src/**/*.{html,js}"],
content: ["./**/*.{html,js}"],
theme: {
fontFamily: {
sans: ['Inter', 'sans-serif'],

View File

@ -1,7 +1,8 @@
import unittest
from web.common.util.time import format_duration
from datetime import timedelta
from common.time import format_duration
class FormatDurationTest(unittest.TestCase):
def setUp(self) -> None:

View File

@ -1,5 +1,5 @@
"""
ASGI config for web project.
ASGI config for timetracker project.
It exposes the ASGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
application = get_asgi_application()

View File

@ -1,5 +1,5 @@
"""
Django settings for web project.
Django settings for timetracker project.
Generated by 'django-admin startproject' using Django 4.1.4.
@ -10,9 +10,8 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.1/ref/settings/
"""
from pathlib import Path
import logging
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -33,7 +32,7 @@ ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
"tracker.apps.TrackerConfig",
"games.apps.GamesConfig",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
@ -55,7 +54,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "web.urls"
ROOT_URLCONF = "timetracker.urls"
TEMPLATES = [
{
@ -68,13 +67,13 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"tracker.views.model_counts",
"games.views.model_counts",
],
},
},
]
WSGI_APPLICATION = "web.wsgi.application"
WSGI_APPLICATION = "timetracker.wsgi.application"
# Database

View File

@ -1,4 +1,4 @@
"""web URL Configuration
"""timetracker URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.1/topics/http/urls/
@ -13,15 +13,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from django.conf import settings
urlpatterns = [
path("", RedirectView.as_view(url="/tracker/list-sessions")),
path("tracker/", include("tracker.urls")),
path("tracker/", include("games.urls")),
]
if settings.DEBUG:

View File

@ -1,5 +1,5 @@
"""
WSGI config for web project.
WSGI config for timetracker project.
It exposes the WSGI callable as a module-level variable named ``application``.
@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "timetracker.settings")
application = get_wsgi_application()