2 Commits

Author SHA1 Message Date
Sweetbread fb9d6590cd Delete otoring >:
Docker Build and Push / build-and-push (push) Successful in 38s
2026-01-22 23:56:29 +03:00
Sweetbread 0ca97f2444 Rework my page and add info from listenbrainz 2026-01-22 23:55:58 +03:00
17 changed files with 53 additions and 622 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
+10 -33
View File
@@ -1,46 +1,23 @@
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 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"]
+10 -63
View File
@@ -1,40 +1,18 @@
import os import os
from datetime import datetime, timedelta
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
def tmsmp(sec: int) -> str:
if sec == 0:
return 0
elif sec < 60:
return f"{sec} s"
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:
days = round(sec / 86400, 1)
return f"{days:.0f} d" if days.is_integer() else f"{days:.1f} d"
def rtmsmp(unix: int) -> str:
return tmsmp(int(time() - unix))
bp = Blueprint( bp = Blueprint(
"risdeveau", "risdeveau",
@@ -48,8 +26,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>")
@@ -62,42 +39,12 @@ def static(filename: str):
@bp.route("/asset/mb/<mbid>") @bp.route("/asset/mb/<mbid>")
def mb_cover(mbid): def mb_cover(mbid):
r = make_response(image := get_image_front(mbid, "250")) r = make_response(get_image_front(mbid, "250"))
r.headers['Content-Type'] = magic.from_buffer(image[:2048], mime=True)
r.headers['Cache-Control'] = 'public, max-age=86400' r.headers['Cache-Control'] = 'public, max-age=86400'
r.headers['Expires'] = (datetime.now() + timedelta(days=1)) \ r.headers['Expires'] = (datetime.now() + timedelta(days=1)) \
.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', lb=listens, lb_now=listening)
@bp.route("/m/<module>")
def module(module):
if modified_since := request.headers.get('if-modified-since'):
modified_since = int(modified_since)
none_match = request.headers.get('if-none-match')
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["caa_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
-95
View File
@@ -1,95 +0,0 @@
import atexit
import re
from dataclasses import dataclass
from hashlib import md5
from json import dumps
from os import environ
from time import time
from urllib.parse import parse_qs, urlparse
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from flask import Flask
TOKEN = environ.get("STEAM_TOKEN")
MY_ID = 76561198826355942
@dataclass
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"recent": Cache(),
"owned": Cache()
},
"last_updated": time(),
"etag": ""
}
def modify_game_list(json: dict) -> dict:
if 'games' in json.keys():
apps = (3301060, 404790, 1281930, 1920960, 1325960, 431960)
new_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]['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
def steam_request(interface: str, method: str, v: int = 1, **kwargs) -> requests.Response:
return requests.get(
f"https://api.steampowered.com/{interface}/{method}/v{v:04}/",
params=dict({"key": TOKEN}, **kwargs),
timeout=10
)
def api_request(cache, *args, **kwargs):
try:
response = steam_request(*args, **kwargs)
if response.status_code == 200:
json = modify_game_list(response.json().get("response"))
cache.status = 'success'
if cache.data != json:
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:
cache.status = f'error: {response.status_code}'
print("x")
except Exception as e:
cache.status = f'error: {str(e)}'
if TOKEN:
scheduler = BackgroundScheduler()
scheduler.add_job(
func=lambda: api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942),
trigger=IntervalTrigger(minutes=15),
id='risdeveau.steam.recent',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request(data['caches']['owned'], "IPlayerService", "GetOwnedGames", steamid=76561198826355942, include_appinfo=1, include_played_free_games=1),
trigger=IntervalTrigger(minutes=60),
id='risdeveau.steam.owned',
replace_existing=True
)
scheduler.start()
api_request(data['caches']['recent'], "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942)
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,46 +44,10 @@ h3 {
img { img {
width: 5rem; width: 5rem;
height: 5rem; height: 5rem;
object-fit: cover;
border-radius: .5rem; border-radius: .5rem;
} }
} }
.steam {
.block {
&:not(.popup) {
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 {
height: 7rem;
margin-right: .5rem;
}
p {
margin: .5rem 0;
}
}
}
table, tbody { table, tbody {
vertical-align: baseline; vertical-align: baseline;
border-collapse: collapse; border-collapse: collapse;
+1 -24
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>
@@ -46,7 +24,6 @@
'info', 'info',
'contacts', 'contacts',
'listenbrainz', 'listenbrainz',
'steam',
'donate', 'donate',
'88x31' '88x31'
) %} ) %}
@@ -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 %}
-125
View File
@@ -1,125 +0,0 @@
{% if request.headers.get('hx-request') != "true" %}
<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>
{{ steam.caches.recent.status }}
{{ steam.caches.owned.status }}
{% if steam.caches.recent.data.games %}
<h3>Recently played:</h3>
{% for g in steam.caches.recent.data.games.values() %}
<div href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks*60) }}
<div>
<p
x-data='{ playtime: { L: "{{ tmsmp(g.playtime_linux_forever*60) }}", W: "{{ tmsmp(g.playtime_windows_forever*60) }}", T: "{{ tmsmp(g.playtime_forever*60) }}" }}'
x-text="`Total played: ${playtime[total_mode]}`"
>
</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>
{% endfor %}
<p
x-data="rtime({{steam.caches.recent.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
{% if steam.caches.owned.data.games %}
<h3>Top played games:</h3>
{% set owned_games = steam.caches.owned.data.games.values() | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %}
<div
@click='current == {{ g.appid }} ? current = null : current = {{ g.appid }}'
href="https://store.steampowered.com/app/{{ g.appid }}"
class="block"
>
<picture>
<source media="(max-width: 45rem)" srcset="{{ g.v_cover }}">
<img src="{{ g.h_cover }}">
</picture>
<div>
<strong>{{ g.name }}</strong>
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever*60) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever*60) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever*60) }} (<abbr title="Total">T</abbr>)
</p>
{% if g.rtime_last_played != 0 %}
<p
x-data="steam_rtime({{ g.rtime_last_played }})"
x-text="`Last played: ${timeString}`"
:class="textColorClass"
></p>
{% endif %}
</div>
</div>
<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 %}
<p
x-data="rtime({{steam.caches.owned.last_updated}})"
x-text="`Last updated: ${timeString}`"
></p>
{% endif %}
</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; }
}
-2
View File
@@ -8,8 +8,6 @@ about host = About host
contacts = Contacts contacts = Contacts
donate = Donate donate = Donate
description = Small personal site
[index] [index]
altfronts = Altfronts altfronts = Altfronts
-2
View File
@@ -8,8 +8,6 @@ about host = О хосте
contacts = Контакты contacts = Контакты
donate = Донат donate = Донат
description = Небольшой личный сайт
[index] [index]
altfronts = Альтфронты altfronts = Альтфронты
-1
View File
@@ -4,4 +4,3 @@ htmlmin2
requests requests
APScheduler APScheduler
musicbrainzngs musicbrainzngs
python-magic
-1
View File
@@ -7,7 +7,6 @@ pkgs.mkShell {
buildInputs = with pypkgs; [ buildInputs = with pypkgs; [
python python
python-magic
virtualenv virtualenv
pkgs.nodePackages.sass pkgs.nodePackages.sass
]; ];