4 Commits

Author SHA1 Message Date
Sweetbread ae035a3721 Add polling
Docker Build and Push / build-and-push (push) Successful in 19s
2026-02-05 17:37:11 +03:00
Sweetbread 8e3fc28fc5 Update docker image 2026-02-05 17:37:11 +03:00
Sweetbread c08a893b14 Add OG meta 2026-02-05 17:37:11 +03:00
Sweetbread dd077ec06a Add Steam info 2026-02-05 17:37:10 +03:00
11 changed files with 243 additions and 14 deletions
+7
View File
@@ -11,6 +11,12 @@ 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:
@@ -27,5 +33,6 @@ 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
+33 -12
View File
@@ -1,25 +1,46 @@
FROM node:18-alpine as sass FROM node:18-alpine AS sass-builder
RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass RUN NODE_OPTIONS=--dns-result-order=ipv4first npm install -g sass@latest --omit=dev --no-fund --no-audit
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 . . COPY requirements.txt .
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
ENV FLASK_ENV=production COPY . .
ENV PYTHONUNBUFFERED=1
CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "4"] COPY --from=sass-builder /build/blueprints/ ./blueprints/
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"]
+35 -1
View File
@@ -2,6 +2,7 @@ import os
import magic import magic
from pathlib import Path from pathlib import Path
from htmlmin import minify from htmlmin import minify
from time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from musicbrainzngs import get_image_front from musicbrainzngs import get_image_front
from flask import ( from flask import (
@@ -14,6 +15,25 @@ from flask import (
) )
from .modules.api.lb import listens, listening from .modules.api.lb import listens, listening
from .modules.api.steam import recent, owned
def tmsmp(sec: int) -> str:
if sec < 60:
return f"{sec} s"
elif sec < 60*60:
return f"{sec/60:.1f} m"
elif sec < 60*60*24:
return f"{sec/60/60:.1f} h"
else:
return f"{sec/60/60/24:.1f} d"
def utmsmp(unix: int) -> str:
return datetime \
.utcfromtimestamp(unix) \
.strftime('%Y-%m-%d %H:%M:%S')
def rtmsmp(unix: int) -> str:
return tmsmp(int(time() - unix))
bp = Blueprint( bp = Blueprint(
"risdeveau", "risdeveau",
@@ -47,6 +67,20 @@ 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": listens,
"lb_now": listening,
"recent": recent,
"owned": owned,
"tmsmp": tmsmp,
"utmsmp": utmsmp,
"rtmsmp": rtmsmp
}
@bp.route("/") @bp.route("/")
def index(): def index():
return render_tmpl('index.html', lb=listens, lb_now=listening) return render_tmpl('index.html', **args)
@bp.route("/m/<module>")
def module(module):
return render_tmpl(f'{module}.htm', **args)
+71
View File
@@ -0,0 +1,71 @@
from os import environ
from flask import Flask, jsonify
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from time import time
import atexit
import re
from urllib.parse import urlparse, parse_qs
TOKEN = environ.get("STEAM_TOKEN")
MY_ID = 76561198826355942
recent = {}
owned = {}
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.append(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:
cache.update({
'data': modify_game_list(response.json().get("response")),
'last_updated': time(),
'status': 'success'
})
else:
cache['status'] = f'error: {response.status_code}'
except Exception as e:
cache['status'] = f'error: {str(e)}'
if TOKEN:
scheduler = BackgroundScheduler()
scheduler.add_job(
func=lambda: api_request(recent, "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942),
trigger=IntervalTrigger(minutes=15),
id='risdeveau.steam.recent',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request(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(recent, "IPlayerService", "GetRecentlyPlayedGames", steamid=76561198826355942)
api_request(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")
@@ -48,6 +48,21 @@ h3 {
} }
} }
.steam {
.block {
display: flex;
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;
@@ -11,6 +11,11 @@
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>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body> <body>
@@ -23,6 +28,7 @@
'info', 'info',
'contacts', 'contacts',
'listenbrainz', 'listenbrainz',
'steam',
'donate', 'donate',
'88x31' '88x31'
) %} ) %}
@@ -10,7 +10,12 @@
</div> </div>
{% endmacro %} {% endmacro %}
<div class="block"> <div
class="block"
hx-get="/m/listenbrainz"
hx-trigger="every 15s"
hx-swap="outerHTML"
>
<h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2> <h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2>
{% if lb_now.data and lb_now.data.listens.0 %} {% if lb_now.data and lb_now.data.listens.0 %}
{{ track_block(lb_now.data.listens.0, is_active=true) }} {{ track_block(lb_now.data.listens.0, is_active=true) }}
+59
View File
@@ -0,0 +1,59 @@
<div
class="block steam"
hx-get="/m/steam"
hx-trigger="every 1m"
hx-swap="outerHTML"
>
<h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2>
{% if recent.data.games %}
<h3>Recently played:</h3>
{% for g in recent.data.games %}
<a 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) }}
<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>
</div>
</a>
{% endfor %}
<p>Last updated: {{ rtmsmp(recent.last_updated) }} ago</p>
{% endif %}
{% if owned.data.games %}
<h3>Top played games:</h3>
{% set owned_games = owned.data.games | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %}
<a 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>Last played: {{ utmsmp(g.rtime_last_played) }}</p>
{% endif %}
</div>
</a>
{% endfor %}
<p>Last updated: {{ rtmsmp(owned.last_updated) }} ago</p>
{% endif %}
</div>
+7
View File
@@ -13,6 +13,13 @@
></script> ></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="mock-email" content="admin@example.com"> <meta name="mock-email" content="admin@example.com">
<!-- og meta -->
<meta property="og:type" value="website" />
<meta property="og:url" value="https://lair.moe" />
<meta property="og:title" value="Lair.moe" />
<meta property="og:image" value="https://lair.moe/static/icon/lair.webp" />
<meta property="og:description" value="{{ _("description") }}" />
</head> </head>
<body> <body>
{% include 'header.tmpl' %} {% include 'header.tmpl' %}
+2
View File
@@ -8,6 +8,8 @@ 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,6 +8,8 @@ about host = О хосте
contacts = Контакты contacts = Контакты
donate = Донат donate = Донат
description = Небольшой личный сайт
[index] [index]
altfronts = Альтфронты altfronts = Альтфронты