5 Commits

Author SHA1 Message Date
Sweetbread d98bd7d6d8 fixup! Rework my page and add info from listenbrainz
Docker Build and Push / build-and-push (push) Successful in 38s
2026-02-04 15:04:22 +03:00
Sweetbread 8c1ffdcdfe Add OG meta 2026-02-04 14:44:11 +03:00
Sweetbread b78e4185d1 Add Steam info 2026-02-04 14:44:11 +03:00
Sweetbread bf05be13ea Delete otoring >: 2026-02-04 14:43:10 +03:00
Sweetbread b030991905 Rework my page and add info from listenbrainz 2026-02-04 14:43:10 +03:00
13 changed files with 128 additions and 534 deletions
-7
View File
@@ -11,12 +11,6 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 'Docker Tags'
id: tags
uses: cssnr/docker-tags-action@v2
with:
images: 'g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe'
- name: Login to Docker Registry - name: Login to Docker Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@@ -33,6 +27,5 @@ jobs:
context: . context: .
push: ${{ github.event_name == 'push' }} push: ${{ github.event_name == 'push' }}
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
labels: ${{ steps.tags.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
+12 -33
View File
@@ -1,46 +1,25 @@
FROM node:18-alpine AS sass-builder FROM node:18-alpine as sass
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass@latest --omit=dev --no-fund --no-audit RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass
WORKDIR /build WORKDIR /build
COPY ./blueprints ./blueprints COPY ./blueprints ./blueprints
RUN sass ./blueprints:./blueprints \ RUN sass ./blueprints:./blueprints \
--no-source-map \ --no-source-map \
--style=compressed \ --style=compressed
--quiet
FROM python:3.11-slim FROM python:3.11-slim
RUN apt-get update && \
apt-get install --no-install-recommends -y \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY . .
COPY --from=sass /build/blueprints/ ./blueprints/
RUN apt update && apt upgrade
RUN apt install libmagic1 -y
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
COPY --from=sass-builder /build/blueprints/ ./blueprints/ CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "4"]
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser
ENV FLASK_ENV=production \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
CMD ["gunicorn", "app:app", \
"-b", "0.0.0.0:80", \
"--workers", "4", \
"--worker-class", "sync", \
"--worker-tmp-dir", "/dev/shm", \
"--access-logfile", "-", \
"--error-logfile", "-", \
"--log-level", "info"]
+28 -57
View File
@@ -1,40 +1,33 @@
import os import os
from datetime import datetime, timedelta import magic
from pathlib import Path from pathlib import Path
from time import time from htmlmin import minify
from datetime import datetime, timedelta
from musicbrainzngs import get_image_front
from flask import ( from flask import (
Blueprint, Blueprint,
abort,
make_response,
render_template, render_template,
request,
send_file, send_file,
send_from_directory,
make_response,
abort,
) )
import magic
from htmlmin import minify
from musicbrainzngs import get_image_front
from .modules.api.lb import data as lb_data from .modules.api.lb import listens, listening
from .modules.api.steam import data as steam_data from .modules.api.steam import recent, owned
def tmsmp(sec: int) -> str: def tmsmp(min: int) -> str:
if sec == 0: if min < 60:
return 0 return f"{min} m"
elif sec < 60: elif min < 60*24:
return f"{sec} s" return f"{min/60:.1f} h"
elif sec < 60*60:
minutes = round(sec / 60, 1)
return f"{minutes:.0f} m" if minutes.is_integer() else f"{minutes:.1f} m"
elif sec < 60*60*24:
hours = round(sec / 3600, 1)
return f"{hours:.0f} h" if hours.is_integer() else f"{hours:.1f} h"
else: else:
days = round(sec / 86400, 1) return f"{min/60/24:.1f} d"
return f"{days:.0f} d" if days.is_integer() else f"{days:.1f} d"
def rtmsmp(unix: int) -> str: def rtmsmp(unix: int) -> str:
return tmsmp(int(time() - unix)) return datetime \
.utcfromtimestamp(unix) \
.strftime('%Y-%m-%d %H:%M:%S')
bp = Blueprint( bp = Blueprint(
"risdeveau", "risdeveau",
@@ -48,8 +41,7 @@ def render_tmpl(filename: str, **kwargs) -> str:
template_path = os.path.join("risdeveau/templates", filename) template_path = os.path.join("risdeveau/templates", filename)
return minify( return minify(
render_template(template_path, **kwargs), render_template(template_path, **kwargs),
remove_empty_space=True, remove_empty_space=True
remove_all_empty_space=True
) )
@bp.route("/static/<path:filename>") @bp.route("/static/<path:filename>")
@@ -69,35 +61,14 @@ def mb_cover(mbid):
.strftime('%a, %d %b %Y %H:%M:%S GMT') .strftime('%a, %d %b %Y %H:%M:%S GMT')
return r return r
args = {
"lb": lb_data,
"steam": steam_data,
"tmsmp": tmsmp,
"rtmsmp": rtmsmp
}
@bp.route("/") @bp.route("/")
def index(): def index():
return render_tmpl('index.html', **args) return render_tmpl(
'index.html',
@bp.route("/m/<module>") lb=listens,
def module(module): lb_now=listening,
if modified_since := request.headers.get('if-modified-since'): recent=recent.get('data', {}),
modified_since = int(modified_since) owned=owned.get('data', {}),
none_match = request.headers.get('if-none-match') tmsmp=tmsmp,
rtmsmp=rtmsmp
if any((modified_since, none_match)): )
match module:
case "listenbrainz":
if modified_since >= int(lb_data['last_updated']):
return '', 304
if none_match == lb_data['etag']:
return '', 304
case "steam":
if modified_since >= int(steam_data['last_updated']):
return '', 304
if none_match == steam_data['etag']:
return '', 304
return render_tmpl(f'{module}.htm', **args)
+26 -60
View File
@@ -1,31 +1,14 @@
import atexit from flask import Flask, jsonify
import re
from dataclasses import dataclass
from hashlib import md5
from json import dumps
from time import time
from urllib.parse import parse_qs, urlparse
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
import requests import requests
from flask import Flask, jsonify from datetime import datetime
import atexit
import re
from urllib.parse import urlparse, parse_qs
listens = {}
@dataclass listening = {}
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"now": Cache(),
"listens": Cache()
},
"last_updated": time(),
"etag": ""
}
def yt_cover(youtube_url): def yt_cover(youtube_url):
parsed_url = urlparse(youtube_url) parsed_url = urlparse(youtube_url)
@@ -40,36 +23,24 @@ def yt_cover(youtube_url):
if not video_id: if not video_id:
return return
return f"https://img.youtube.com/vi/{video_id}/2.jpg" return f"http://img.youtube.com/vi/{video_id}/sddefault.jpg"
def parse_listens(json: dict) -> dict: def parse_listens(data: dict) -> dict:
cover_replacing = { new_data = {
"1e699948-c7c8-4bb2-9f8b-62e14b882a5d": "ca464c1d-5848-45bb-b92d-b1e4b00f9d65", "count": data["count"],
"0d516a93-061e-4a27-9cf7-f36e3a96f888": "5cc0c0c7-22f9-4a4b-a24c-f1a6732f813b",
"92ea5cc8-80e0-4da0-a10b-1bc2f8e8781e": "e8f3e14a-4794-4bab-b403-d562cdad4c2f",
}
new_json = {
"count": json["count"],
"listens": [] "listens": []
} }
for track in json["listens"]: for track in data["listens"]:
listened_at = track.get("listened_at", 0)
track = track["track_metadata"] track = track["track_metadata"]
new_track = { new_track = {
"artist_name": track["artist_name"], "artist_name": track["artist_name"],
"track_name": track["track_name"], "track_name": track["track_name"]
"listened_at": listened_at
} }
if mb := track.get("mbid_mapping"): if mb := track.get("mbid_mapping"):
new_track["id"] = \ new_track["id"] = mb.get("caa_release_mbid", mb["release_mbid"])
cover_replacing.get(mb["release_mbid"],
mb.get("caa_release_mbid",
mb["release_mbid"]
))
new_track["artist_name"] = mb["artists"][0]["artist_credit_name"] new_track["artist_name"] = mb["artists"][0]["artist_credit_name"]
new_track["track_name"] = mb["recording_name"] new_track["track_name"] = mb["recording_name"]
elif info := track.get("additional_info"): elif info := track.get("additional_info"):
@@ -82,38 +53,33 @@ def parse_listens(json: dict) -> dict:
if "cover_url" not in new_track.keys() and "id" in new_track.keys(): if "cover_url" not in new_track.keys() and "id" in new_track.keys():
new_track["cover_url"] = "/asset/mb/" + new_track["id"] new_track["cover_url"] = "/asset/mb/" + new_track["id"]
new_json["listens"].append(new_track) new_data["listens"].append(new_track)
return new_json return new_data
def api_request(url: str, cache: Cache): def api_request(url: str, cache):
try: try:
response = requests.get(url, timeout=10) response = requests.get(url, timeout=10)
if response.status_code == 200: if response.status_code == 200:
json = parse_listens(response.json().get("payload")) cache.update({
cache.status = 'success' 'data': parse_listens(response.json().get("payload")),
'last_updated': datetime.now().isoformat(),
if cache.data != json: 'status': 'success'
cache.data = json })
cache.last_updated = time()
data['last_updated'] = time()
data['etag'] = md5(''.join(
( dumps(data['caches'][x].data) for x in data['caches'] )
).encode()).hexdigest()
else: else:
cache.status = f'error: {response.status_code}' cache['status'] = f'error: {response.status_code}'
except Exception as e: except Exception as e:
cache.status = f'error: {str(e)}' cache['status'] = f'error: {str(e)}'
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
scheduler.add_job( scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/listens?count=5", data['caches']['listens']), func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/listens?count=5", listens),
trigger=IntervalTrigger(minutes=1), trigger=IntervalTrigger(minutes=1),
id='risdeveau.listenbrainz.listens', id='risdeveau.listenbrainz.listens',
replace_existing=True replace_existing=True
) )
scheduler.add_job( scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/playing-now", data['caches']['now']), func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/playing-now", listening),
trigger=IntervalTrigger(seconds=15), trigger=IntervalTrigger(seconds=15),
id='risdeveau.listenbrainz.playing-now', id='risdeveau.listenbrainz.playing-now',
replace_existing=True replace_existing=True
+34 -66
View File
@@ -1,47 +1,27 @@
import atexit
import re
from dataclasses import dataclass
from hashlib import md5
from json import dumps
from os import environ from os import environ
from time import time from flask import Flask, jsonify
from urllib.parse import parse_qs, urlparse
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
import requests import requests
from flask import Flask from datetime import datetime
import atexit
import re
from urllib.parse import urlparse, parse_qs
TOKEN = environ.get("STEAM_TOKEN") TOKEN = environ.get("STEAM_TOKEN")
MY_ID = 76561198826355942 MY_ID = 76561198826355942
@dataclass recent = {}
class Cache: owned = {}
data = {}
last_updated = time()
status = None
data = { def get_cover_url(appid: int) -> str:
"caches": { return f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{appid}/header.jpg"
"recent": Cache(),
"owned": Cache()
},
"last_updated": time(),
"etag": ""
}
def modify_game_list(json: dict) -> dict: def inject_cover_url(json: dict) -> dict:
if 'games' in json.keys(): if 'games' in json.keys():
apps = (3301060, 404790, 1281930, 1920960, 1325960, 431960)
new_games = {}
for i, g in enumerate(json['games']): for i, g in enumerate(json['games']):
if g['appid'] not in apps: json['games'][i]['h_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/header.jpg"
json['games'][i]['h_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/header.jpg" json['games'][i]['v_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/library_600x900.jpg"
json['games'][i]['v_cover'] = f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{g['appid']}/library_600x900.jpg"
new_games[g['appid']] = json['games'][i]
json['games'] = new_games
return json return json
def steam_request(interface: str, method: str, v: int = 1, **kwargs) -> requests.Response: def steam_request(interface: str, method: str, v: int = 1, **kwargs) -> requests.Response:
@@ -55,41 +35,29 @@ def api_request(cache, *args, **kwargs):
try: try:
response = steam_request(*args, **kwargs) response = steam_request(*args, **kwargs)
if response.status_code == 200: if response.status_code == 200:
json = modify_game_list(response.json().get("response")) cache.update({
cache.status = 'success' 'data': inject_cover_url(response.json().get("response")),
'last_updated': datetime.now().isoformat(),
if cache.data != json: 'status': 'success'
cache.data = json })
cache.last_updated = time()
data['last_updated'] = time()
data['etag'] = md5(''.join(
( dumps(data['caches'][x].data) for x in data['caches'] )
).encode()).hexdigest()
else: else:
cache.status = f'error: {response.status_code}' cache['status'] = f'error: {response.status_code}'
print("x")
except Exception as e: except Exception as e:
cache.status = f'error: {str(e)}' cache['status'] = f'error: {str(e)}'
if TOKEN: scheduler = BackgroundScheduler()
scheduler = BackgroundScheduler() scheduler.add_job(
scheduler.add_job( func=lambda: api_request(recent, "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942),
func=lambda: api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942), trigger=IntervalTrigger(minutes=15),
trigger=IntervalTrigger(minutes=15), id='risdeveau.steam.recent',
id='risdeveau.steam.recent', replace_existing=True
replace_existing=True )
) scheduler.add_job(
scheduler.add_job( func=lambda: api_request(owned, "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1),
func=lambda: api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1), trigger=IntervalTrigger(minutes=15),
trigger=IntervalTrigger(minutes=60), id='risdeveau.steam.recent',
id='risdeveau.steam.owned', replace_existing=True
replace_existing=True )
) scheduler.start()
scheduler.start()
api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942) atexit.register(lambda: scheduler.shutdown())
api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1)
atexit.register(lambda: scheduler.shutdown())
else:
print("STEAM_TOKEN is not defined")
-136
View File
@@ -1,136 +0,0 @@
document.addEventListener('alpine:init', () => {
Alpine.data('rtime', (unixTimestamp) => ({
targetDate: new Date(unixTimestamp * 1000),
timeString: '',
timer: null,
interval: 1000,
colorClasses: {
green: 't-green',
yellow: 't-yellow',
orange: 't-orange',
red: 't-red'
},
currentColor: 'green',
get textColorClass() {
return this.colorClasses[this.currentColor];
},
init() {
this.updateTime();
this.timer = setInterval(() => this.updateTime(), this.interval);
this.$el.addEventListener('alpine:removing', () => {
if (this.interval) clearInterval(this.interval);
});
},
updateTime() {
const now = new Date();
const diffInSeconds = Math.floor((now - this.targetDate) / 1000);
const diffInMinutes = Math.floor(diffInSeconds / 60);
const diffInHours = Math.floor(diffInSeconds / 3600);
const diffInDays = Math.floor(diffInSeconds / 86400);
let newInterval = this.interval;
if (diffInSeconds < 60) {
this.timeString = 'a moment ago';
} else if (diffInMinutes < 60) {
this.timeString = `${diffInMinutes} m ago`;
newInterval = 10000;
} else if (diffInHours < 24) {
this.timeString = `${diffInHours} h ago`;
newInterval = 60000;
} else {
this.timeString = `${diffInDays} d ago`;
clearInterval(this.timer);
}
if (diffInMinutes <= 15) {
this.currentColor = 'green';
} else if (diffInHours < 1) {
this.currentColor = 'yellow';
} else if (diffInDays < 1) {
this.currentColor = 'orange';
} else {
this.currentColor = 'red';
}
if (this.interval != newInterval) {
clearInterval(this.timer)
this.timer = setInterval(() => this.updateTime(), newInterval);
}
},
}));
Alpine.data('steam_rtime', (unixTimestamp) => ({
targetDate: new Date(unixTimestamp * 1000),
timeString: '',
timer: null,
interval: 1000,
colorClasses: {
green: 't-green',
yellow: 't-yellow',
orange: 't-orange',
red: 't-red'
},
currentColor: 'green',
get textColorClass() {
return this.colorClasses[this.currentColor];
},
init() {
this.updateTime();
this.timer = setInterval(() => this.updateTime(), this.interval);
this.$el.addEventListener('alpine:removing', () => {
if (this.interval) clearInterval(this.interval);
});
},
updateTime() {
const now = new Date();
const diffInMinutes = Math.floor((now - this.targetDate) / 60000);
const diffInHours = Math.floor(diffInMinutes / 60);
const diffInDays = Math.floor(diffInHours / 24);
const diffInMonths = Math.floor(diffInDays / 30);
let newInterval = this.interval;
if (diffInMinutes < 60) {
this.timeString = `${diffInMinutes} m ago`;
newInterval = 10000;
} else if (diffInHours < 24) {
this.timeString = `${diffInHours} h ago`;
newInterval = 60000;
} else if (diffInDays < 30) {
this.timeString = `${diffInDays} d ago`;
clearInterval(this.timer);
} else {
this.timeString = `${diffInMonths} mth ago`;
clearInterval(this.timer);
}
if (diffInHours < 12) {
this.currentColor = 'green';
} else if (diffInDays < 1) {
this.currentColor = 'yellow';
} else if (diffInMonths < 6) {
this.currentColor = 'orange'
} else {
this.currentColor = 'red';
}
if (this.interval != newInterval) {
clearInterval(this.timer)
this.timer = setInterval(() => this.updateTime(), newInterval);
}
},
}));
});
@@ -44,34 +44,13 @@ h3 {
img { img {
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
object-fit: cover;
border-radius: .5rem; border-radius: .5rem;
} }
} }
.steam { .steam {
.block { .block {
&:not(.popup) { display: flex;
display: flex;
position: relative;
z-index: 1;
}
&.popup {
margin-top: -.5rem;
padding-top: 1rem;
background-color: theme.$mantle;
transition: all ease-out 300ms;
&.enter {}
&.off {
margin-bottom: -.5rem;
padding: 0 .5rem;
opacity: 0;
transform: translateY(-100%);
}
}
img { img {
height: 7rem; height: 7rem;
+1 -23
View File
@@ -3,38 +3,16 @@
<head> <head>
<title>Sweet Bread</title> <title>Sweet Bread</title>
<link rel="stylesheet" href="/static/style/tw.css">
<link rel="stylesheet" href="/static/style/main.css"> <link rel="stylesheet" href="/static/style/main.css">
<link rel="stylesheet" href="/static/style/risdeveau.css"> <link rel="stylesheet" href="/static/style/risdeveau.css">
<link rel="stylesheet" href="/static/style/track.css">
<link rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp" /> <link rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp" />
<script src="/static/script/rtime.js"></script>
<script <script
src="https://track.lair.moe/api/script.js" src="https://track.lair.moe/api/script.js"
data-site-id="1" data-site-id="1"
defer defer
></script> ></script>
<script
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta
name="htmx-config"
content='{
"responseHandling":[
{"code":"204", "swap": false},
{"code":"304", "swap": false},
{"code":"[23]..", "swap": true},
{"code":"422", "swap": true},
{"code":"[45]..", "swap": false, "error":true},
{"code":"...", "swap": true}
]
}'
/>
</head> </head>
<body> <body>
<header> <header>
@@ -6,33 +6,17 @@
<div> <div>
<p><b>{{ track.artist_name }}</b></p> <p><b>{{ track.artist_name }}</b></p>
<p>{{ track.track_name }}</p> <p>{{ track.track_name }}</p>
{% if not is_active %}
<p
x-data="rtime({{ track.listened_at }})"
x-text="`Listened ${timeString}`"
:class="textColorClass"
></p>
{% endif %}
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
<div <div class="block">
class="block"
hx-get="/m/listenbrainz"
hx-trigger="every 15s"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ lb.last_updated | int }},
"If-None-Match": "{{ lb.etag }}"
}'
>
<h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2> <h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2>
{% if lb.caches.now.data and lb.caches.now.data.listens.0 %} {% if lb_now.data and lb_now.data.listens.0 %}
{{ track_block(lb.caches.now.data.listens.0, is_active=true) }} {{ track_block(lb_now.data.listens.0, is_active=true) }}
{% endif %} {% endif %}
{% if lb.caches.listens.data and lb.caches.listens.data.listens %} {% if lb.data and lb.data.listens %}
{% for track in lb.caches.listens.data.listens %} {% for track in lb.data.listens %}
{{ track_block(track) }} {{ track_block(track) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
+20 -93
View File
@@ -1,25 +1,10 @@
{% if request.headers.get('hx-request') != "true" %} <div class="block steam">
<div x-data='{ "current": null, "total_mode": "T" }' class="mt-1">
{% endif %}
<div
class="block steam"
hx-get="/m/steam"
hx-trigger="every 1m"
hx-swap="outerHTML"
hx-headers='{
"If-Modified-Since": {{ steam.last_updated | int }},
"If-None-Match": "{{ steam.etag }}"
}'
>
<h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2> <h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2>
{{ steam.caches.recent.status }} {% if recent.games %}
{{ steam.caches.owned.status }}
{% if steam.caches.recent.data.games %}
<h3>Recently played:</h3> <h3>Recently played:</h3>
{% for g in steam.caches.recent.data.games.values() %} {% for g in recent.games %}
<div href="https://store.steampowered.com/app/{{ g.appid }}" class="block"> <a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<picture> <picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}"> <source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}"> <img src="{{ g.h_cover }}">
@@ -27,52 +12,23 @@
<div> <div>
<strong>{{ g.name }}</strong> <strong>{{ g.name }}</strong>
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks*60) }} <p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks) }}
<p>
<div> Total played:
<p {{ tmsmp(g.playtime_linux_forever) }} (<abbr title="On Linux">L</abbr>) +
x-data='{ playtime: { L: "{{ tmsmp(g.playtime_linux_forever*60) }}", W: "{{ tmsmp(g.playtime_windows_forever*60) }}", T: "{{ tmsmp(g.playtime_forever*60) }}" }}' {{ tmsmp(g.playtime_windows_forever) }} (<abbr title="On Windows">W</abbr>) =
x-text="`Total played: ${playtime[total_mode]}`" {{ tmsmp(g.playtime_forever) }} (<abbr title="Total">T</abbr>)
> </p>
</p>
<div>
<button @click="total_mode = 'L'" :class="total_mode == 'L' && 't-green'">L</button>
<button @click="total_mode = 'W'" :class="total_mode == 'W' && 't-green'">W</button>
<button @click="total_mode = 'T'" :class="total_mode == 'T' && 't-green'">T</button>
</div>
</div>
{% if steam.caches.owned.data.games %}
{% if steam.caches.owned.data.games[g.appid] %}
<p
x-data="steam_rtime({{ steam.caches.owned.data.games[g.appid].rtime_last_played }})"
x-text="`Last played: ${timeString}`"
:class="textColorClass"
></p>
{% else %}
<p class="t-red">Last played: Unknown</p>
{% endif %}
{% endif %}
</div> </div>
</div> </a>
{% endfor %} {% endfor %}
<p
x-data="rtime({{steam.caches.recent.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %} {% endif %}
{% if steam.caches.owned.data.games %} {% if owned.games %}
<h3>Top played games:</h3> <h3>Top played games:</h3>
{% set owned_games = steam.caches.owned.data.games.values() | sort(attribute="playtime_forever", reverse=true) %} {% set owned_games = owned.games | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %} {% for g in owned_games[:5] %}
<div <a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
@click='current == {{ g.appid }} ? current = null : current = {{ g.appid }}'
href="https://store.steampowered.com/app/{{ g.appid }}"
class="block"
>
<picture> <picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}"> <source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}"> <img src="{{ g.h_cover }}">
@@ -82,44 +38,15 @@
<strong>{{ g.name }}</strong> <strong>{{ g.name }}</strong>
<p> <p>
Total played: Total played:
{{ tmsmp(g.playtime_linux_forever*60) }} (<abbr title="On Linux">L</abbr>) + {{ tmsmp(g.playtime_linux_forever) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever*60) }} (<abbr title="On Windows">W</abbr>) = {{ tmsmp(g.playtime_windows_forever) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever*60) }} (<abbr title="Total">T</abbr>) {{ tmsmp(g.playtime_forever) }} (<abbr title="Total">T</abbr>)
</p> </p>
{% if g.rtime_last_played != 0 %} {% if g.rtime_last_played != 0 %}
<p <p>Last played: {{ rtmsmp(g.rtime_last_played) }}</p>
x-data="steam_rtime({{ g.rtime_last_played }})"
x-text="`Last played: ${timeString}`"
:class="textColorClass"
></p>
{% endif %} {% endif %}
</div> </div>
</div> </a>
<div
class="block popup"
x-show="current == {{ g.appid }}"
x-transition:enter-start="off"
x-transition:leave-end="off"
>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
<p>Some info</p>
</div>
{% endfor %} {% endfor %}
<p
x-data="rtime({{steam.caches.owned.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %} {% endif %}
</div> </div>
{% if request.headers.get('hx-request') != "true" %}
</div>
{% endif %}
+1 -1
View File
@@ -3,7 +3,7 @@ from os.path import join
def compile_styles(): def compile_styles():
dir = "blueprints/root/static/style" dir = "blueprints/root/static/style"
files = ("main", "tw") files = ("main",)
for file in files: for file in files:
console(f"sass {join(dir, file+'.scss')} {join(dir, file+'.css')}") console(f"sass {join(dir, file+'.scss')} {join(dir, file+'.css')}")
@@ -18,7 +18,6 @@ $subtext0: #a6adc8;
$subtext1: #bac2de; $subtext1: #bac2de;
$red: #f38ba8; $red: #f38ba8;
$yellow: #f9e2af;
$green: #a6e3a1; $green: #a6e3a1;
$peach: #fab387; $peach: #fab387;
$blue: #89b4fa; $blue: #89b4fa;
-14
View File
@@ -1,14 +0,0 @@
@use "catppuccin" as theme;
.m {
&t {
&-1 { margin-top: .5rem; }
}
}
.t {
&-red { color: theme.$red; }
&-orange { color: theme.$peach; }
&-yellow { color: theme.$yellow; }
&-green { color: theme.$green; }
}