4 Commits

Author SHA1 Message Date
Sweetbread 23bd539799 Add Steam info
Docker Build and Push / build-and-push (push) Successful in 55s
2026-02-04 00:34:16 +03:00
Sweetbread bc8947dfac fixup! Rework my page and add info from listenbrainz 2026-01-27 19:40:21 +03:00
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
20 changed files with 490 additions and 232 deletions
+46 -11
View File
@@ -1,7 +1,32 @@
import os
from pathlib import Path
from htmlmin import minify
from flask import Blueprint, render_template, send_from_directory, send_file, abort
from datetime import datetime, timedelta
from musicbrainzngs import get_image_front
from flask import (
Blueprint,
render_template,
send_file,
send_from_directory,
make_response,
abort,
)
from .modules.api.lb import listens, listening
from .modules.api.steam import recent, owned
def tmsmp(min: int) -> str:
if min < 60:
return f"{min} m"
elif min < 60*24:
return f"{min/60:.1f} h"
else:
return f"{min/60/24:.1f} d"
def rtmsmp(unix: int) -> str:
return datetime \
.utcfromtimestamp(unix) \
.strftime('%Y-%m-%d %H:%M:%S')
bp = Blueprint(
"risdeveau",
@@ -11,10 +36,10 @@ bp = Blueprint(
static_folder=None
)
def render_tmpl(filename: str) -> str:
def render_tmpl(filename: str, **kwargs) -> str:
template_path = os.path.join("risdeveau/templates", filename)
return minify(
render_template(template_path),
render_template(template_path, **kwargs),
remove_empty_space=True
)
@@ -26,14 +51,24 @@ def static(filename: str):
return send_file(path)
return abort(404)
@bp.route("/asset/mb/<mbid>")
def mb_cover(mbid):
r = make_response(get_image_front(mbid, "250"))
r.headers['Cache-Control'] = 'public, max-age=86400'
r.headers['Expires'] = (datetime.now() + timedelta(days=1)) \
.strftime('%a, %d %b %Y %H:%M:%S GMT')
return r
@bp.route("/")
def index():
return render_tmpl('index.html')
print(recent)
@bp.route("/contacts")
def contacts():
return render_tmpl('contacts.html')
@bp.route("/donate")
def donate():
return render_tmpl('donate.html')
return render_tmpl(
'index.html',
lb=listens,
lb_now=listening,
recent=recent.get('data', {}),
owned=owned.get('data', {}),
tmsmp=tmsmp,
rtmsmp=rtmsmp
)
+89
View File
@@ -0,0 +1,89 @@
from flask import Flask, jsonify
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from datetime import datetime
import atexit
import re
from urllib.parse import urlparse, parse_qs
listens = {}
listening = {}
def yt_cover(youtube_url):
parsed_url = urlparse(youtube_url)
if parsed_url.netloc in ("youtube.com", "music.youtube.com"):
query_params = parse_qs(parsed_url.query)
video_id = query_params.get('v', [None])[0]
elif parsed_url.netloc == 'youtu.be':
video_id = parsed_url.path[1:]
if not video_id:
return
return f"http://img.youtube.com/vi/{video_id}/sddefault.jpg"
def parse_listens(data: dict) -> dict:
new_data = {
"count": data["count"],
"listens": []
}
for track in data["listens"]:
track = track["track_metadata"]
new_track = {
"artist_name": track["artist_name"],
"track_name": track["track_name"]
}
if mb := track.get("mbid_mapping"):
new_track["id"] = mb.get("caa_release_mbid", mb["release_mbid"])
new_track["artist_name"] = mb["artists"][0]["artist_credit_name"]
new_track["track_name"] = mb["recording_name"]
elif info := track.get("additional_info"):
if info \
.get("music_service_name", "") \
.lower() in ("youtube", "youtube music"):
if cover := yt_cover(track["additional_info"]["origin_url"]):
new_track["cover_url"] = cover
if "cover_url" not in new_track.keys() and "id" in new_track.keys():
new_track["cover_url"] = "/asset/mb/" + new_track["id"]
new_data["listens"].append(new_track)
return new_data
def api_request(url: str, cache):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
cache.update({
'data': parse_listens(response.json().get("payload")),
'last_updated': datetime.now().isoformat(),
'status': 'success'
})
else:
cache['status'] = f'error: {response.status_code}'
except Exception as e:
cache['status'] = f'error: {str(e)}'
scheduler = BackgroundScheduler()
scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/listens?count=5", listens),
trigger=IntervalTrigger(minutes=1),
id='risdeveau.listenbrainz.listens',
replace_existing=True
)
scheduler.add_job(
func=lambda: api_request("https://api.listenbrainz.org/1/user/risdeveau/playing-now", listening),
trigger=IntervalTrigger(seconds=15),
id='risdeveau.listenbrainz.playing-now',
replace_existing=True
)
scheduler.start()
atexit.register(lambda: scheduler.shutdown())
+65
View File
@@ -0,0 +1,65 @@
from os import environ
from flask import Flask, jsonify
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
import requests
from datetime import datetime
import atexit
import re
from urllib.parse import urlparse, parse_qs
TOKEN = environ.get("STEAM_TOKEN")
MY_ID = 76561198826355942
recent = {}
owned = {}
def get_cover_url(appid: int) -> str:
return f"https://shared.fastly.steamstatic.com/store_item_assets//steam/apps/{appid}/header.jpg"
def inject_cover_url(json: dict) -> dict:
if 'games' in json.keys():
for i, g in enumerate(json['games']):
json['games'][i]['img_icon_url'] = get_cover_url(g['appid'])
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': inject_cover_url(response.json().get("response")),
'last_updated': datetime.now().isoformat(),
'status': 'success'
})
else:
cache['status'] = f'error: {response.status_code}'
except Exception as e:
cache['status'] = f'error: {str(e)}'
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=15),
id='risdeveau.steam.recent',
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())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -1,29 +1,5 @@
@use "sass:color";
// Palette: Catppuccin Mocha
// https://catppuccin.com/palette/
$base: #1e1e2e;
$text: #cdd6f4;
$mantle: #181825;
$crust: #11111b;
$overlay0: #6c7086;
$overlay1: #7f849c;
$overlay2: #9399b2;
$surface0: #313244;
$surface1: #45475a;
$surface2: #585b70;
$subtext0: #a6adc8;
$subtext1: #bac2de;
$red: #f38ba8;
$green: #a6e3a1;
$peach: #fab387;
$blue: #89b4fa;
$mauve: #8839ef;
@use "../../../root/static/style/catppuccin" as theme;
h3 {
margin-block-end: 0;
@@ -58,6 +34,38 @@ h3 {
}
}
.track {
display: flex;
&.active {
box-shadow: theme.$green 0 0 5px 0;
}
img {
width: 5rem;
height: 5rem;
border-radius: .5rem;
}
}
.steam {
.block {
display: flex;
align-items: stretch;
img {
height: 6rem;
flex: 0 0 auto;
margin-right: .5rem;
}
p {
flex: 1;
margin: .5rem 0;
}
}
}
table, tbody {
vertical-align: baseline;
border-collapse: collapse;
@@ -66,7 +74,7 @@ table, tbody {
border-radius: 10px;
&:hover {
background-color: color.change($surface1, $alpha:75%);
background-color: color.change(theme.$surface1, $alpha:75%);
}
th {
+16
View File
@@ -0,0 +1,16 @@
<div>
<div class="88-31">
<a href="https://chest.lair.moe" class="disabled">
<img src="/static/img/88x31/gf.png"/>
</a>
<a href="https://preview.about.akarpov.ru" id="pie">
<img src="/static/img/88x31/withpie.gif"/>
</a>
</div>
<div class="88-31">
<a href="https://g.lair.moe/Sweetbread/nixos-config">
<img src="/static/img/88x31/nixos.webp"/>
</a>
<img src="/static/img/88x31/teto.webp"/>
</div>
</div>
-24
View File
@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Sweet Bread</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="stylesheet" href="/static/style/risdeveau.css">
<link rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp" />
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% block head %}{% endblock %}
</head>
<body>
{% include 'risdeveau/templates/header.tmpl' %}
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
@@ -1,14 +1,4 @@
{% extends 'risdeveau/templates/base.tmpl' %}
{% block head %}
<style>
main {
width: -webkit-fill-available;
}
</style>
{% endblock %}
{% block content %}
<div class="block">
<h3>Development</h3>
<div class="blocks badges">
<a class="block" href="//g.lair.moe/Sweetbread">
@@ -55,4 +45,4 @@
GameBanana
</a>
</div>
{% endblock %}
</div>
+19
View File
@@ -0,0 +1,19 @@
<div>
<h3>Wallets</h3>
<div class="blocks qr">
<div class="block qr">
<p>POL, BNB</p>
<img src="/static/img/wallets/evm.webp">
</div>
<div class="block qr">
<p>TON</p>
<img src="/static/img/wallets/ton.webp">
</div>
<div class="block qr">
<p>XMR</p>
<img src="/static/img/wallets/xmr.webp">
</div>
</div>
</div>
@@ -1,21 +0,0 @@
{% extends 'risdeveau/templates/base.tmpl' %}
{% block content %}
<h3>Wallets</h3>
<div class="blocks qr">
<div class="block qr">
<p>POL, BNB</p>
<img src="/static/img/wallets/evm.webp">
</div>
<div class="block qr">
<p>TON</p>
<img src="/static/img/wallets/ton.webp">
</div>
<div class="block qr">
<p>XMR</p>
<img src="/static/img/wallets/xmr.webp">
</div>
</div>
{% endblock %}
@@ -1,20 +0,0 @@
<header>
{%- if request.path != url_for('.index') %}
<a href="{{ url_for('.index') }}">Main</a>
{%- else %}
<a href="{{ url_for('root.index') }}">Lair</a>
{%- endif %}
<div class="header-links">
{%- for (l, t) in (
('.contacts', _('contacts')),
('.donate', _('donate'))
) %}
{%- if url_for(l) == request.path %}
<strong>{{ t }}</strong>
{%- else %}
<a href="{{ url_for(l) }}">{{ t }}</a>
{%- endif %}
{%- endfor %}
</div>
</header>
+33 -67
View File
@@ -1,69 +1,35 @@
{% extends 'risdeveau/templates/base.tmpl' %}
<!DOCTYPE html>
<html>
<head>
<title>Sweet Bread</title>
{% block content %}
<div class="block">
<table>
<tr>
<th>DoB</th>
<td>2005-01-13</td>
</tr>
<tr>
<th>Languages</th>
<td>
<table>
<tr>
<td>Russian</td>
<td>Native</td>
</tr>
<tr>
<td>English</td>
<td>B2</td>
</tr>
<tr>
<td>French</td>
<td>A1?</td>
</tr>
<tr>
<td>German</td>
<td>A2?</td>
</tr>
<tr>
<td>Japanese</td>
<td>Beginner</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>Student</th>
<td>
<table>
<tr>
<td>Programmer</td>
<td>2/4yr.</td>
</tr>
<tr>
<td>Translator</td>
<td>2/3yr.</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<link rel="stylesheet" href="/static/style/main.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" />
<script
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header>
<a href="{{ url_for('root.index') }}">Lair</a>
</header>
<div class="88-31">
<a href="https://chest.lair.moe" class="disabled">
<img src="/static/img/88x31/gf.png"/>
</a>
<a href="https://preview.about.akarpov.ru" id="pie">
<img src="/static/img/88x31/withpie.gif"/>
</a>
</div>
<div class="88-31">
<a href="https://g.lair.moe/Sweetbread/nixos-config">
<img src="/static/img/88x31/nixos.webp"/>
</a>
<img src="/static/img/88x31/teto.webp"/>
</div>
{% endblock %}
<main>
{% for m in (
'info',
'contacts',
'listenbrainz',
'steam',
'donate',
'88x31'
) %}
{% include 'risdeveau/templates/%s.htm' % m %}
{% endfor %}
</main>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
<div class="block">
<table>
<tr>
<th>DoB</th>
<td>2005-01-13</td>
</tr>
<tr>
<th>Languages</th>
<td>
<table>
<tr>
<td>Russian</td>
<td>Native</td>
</tr>
<tr>
<td>English</td>
<td>B2</td>
</tr>
<tr>
<td>French</td>
<td>A1?</td>
</tr>
<tr>
<td>German</td>
<td>A2?</td>
</tr>
<tr>
<td>Japanese</td>
<td>Beginner</td>
</tr>
</table>
</td>
</tr>
<tr>
<th>Student</th>
<td>
<table>
<tr>
<td>Programmer</td>
<td>2/4yr.</td>
</tr>
<tr>
<td>Translator</td>
<td>2/3yr.</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
@@ -0,0 +1,23 @@
{% macro track_block(track, is_active=false) %}
<div class="block track{% if is_active %} active{% endif %}">
{% if track.cover_url %}
<img src="{{ track.cover_url }}"/>
{% endif %}
<div>
<p><b>{{ track.artist_name }}</b></p>
<p>{{ track.track_name }}</p>
</div>
</div>
{% endmacro %}
<div class="block">
<h2><a href="https://listenbrainz.org/user/risdeveau/">Listenbrainz</a></h2>
{% if lb_now.data and lb_now.data.listens.0 %}
{{ track_block(lb_now.data.listens.0, is_active=true) }}
{% endif %}
{% if lb.data and lb.data.listens %}
{% for track in lb.data.listens %}
{{ track_block(track) }}
{% endfor %}
{% endif %}
</div>
+45
View File
@@ -0,0 +1,45 @@
<div class="block steam">
<h2><a href="https://steamcommunity.com/id/risdeveau">Steam</a></h2>
<h3>Recently played:</h3>
{% for g in recent.games %}
<a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<img src="{{ g.img_icon_url }}"/>
<div>
<strong>{{ g.name }}</strong>
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks) }}
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever) }} (<abbr title="Total">T</abbr>)
</p>
</div>
</a>
{% endfor %}
<h3>Top played games:</h3>
{% set owned_games = owned.games | sort(attribute="playtime_forever", reverse=true) %}
{% for g in owned_games[:5] %}
<a href="https://store.steampowered.com/app/{{ g.appid }}" class="block">
<img src="{{ g.img_icon_url }}"/>
<div>
<strong>{{ g.name }}</strong>
{% if g.playtime_2weeks %}
<p>Played last 2 weeks: {{ tmsmp(g.playtime_2weeks) }}
{% endif %}
<p>
Total played:
{{ tmsmp(g.playtime_linux_forever) }} (<abbr title="On Linux">L</abbr>) +
{{ tmsmp(g.playtime_windows_forever) }} (<abbr title="On Windows">W</abbr>) =
{{ tmsmp(g.playtime_forever) }} (<abbr title="Total">T</abbr>)
</p>
{% if g.rtime_last_played != 0 %}
<p>Last played: {{ rtmsmp(g.rtime_last_played) }}</p>
{% endif %}
</div>
</a>
{% endfor %}
</div>
@@ -0,0 +1,24 @@
// Palette: Catppuccin Mocha
// https://catppuccin.com/palette/
$base: #1e1e2e;
$text: #cdd6f4;
$mantle: #181825;
$crust: #11111b;
$overlay0: #6c7086;
$overlay1: #7f849c;
$overlay2: #9399b2;
$surface0: #313244;
$surface1: #45475a;
$surface2: #585b70;
$subtext0: #a6adc8;
$subtext1: #bac2de;
$red: #f38ba8;
$green: #a6e3a1;
$peach: #fab387;
$blue: #89b4fa;
$mauve: #8839ef;
+34 -45
View File
@@ -1,29 +1,5 @@
@use "sass:color";
// Palette: Catppuccin Mocha
// https://catppuccin.com/palette/
$base: #1e1e2e;
$text: #cdd6f4;
$mantle: #181825;
$crust: #11111b;
$overlay0: #6c7086;
$overlay1: #7f849c;
$overlay2: #9399b2;
$surface0: #313244;
$surface1: #45475a;
$surface2: #585b70;
$subtext0: #a6adc8;
$subtext1: #bac2de;
$red: #f38ba8;
$green: #a6e3a1;
$peach: #fab387;
$blue: #89b4fa;
$mauve: #8839ef;
@use "catppuccin" as theme;
html {
@@ -33,9 +9,9 @@ html {
body {
display: flex;
flex-direction: column;
background-color: $base;
background-color: theme.$base;
font-family: Pixeloid, PixelMPlus;
color: $text;
color: theme.$text;
width: 100%;
height: 100%;
margin: 0;
@@ -55,7 +31,7 @@ h1 {
a {
color: unset;
text: {
decoration: underline {color: $blue};
decoration: underline {color: theme.$blue};
underline-offset: 1px;
}
transition: 0.3s ease;
@@ -68,7 +44,7 @@ a {
transition: none !important;
display: inline-block;
transform: scale(.98) !important;
background-color: $mantle !important;
background-color: theme.$mantle !important;
}
&.block {
@@ -76,7 +52,7 @@ a {
&:hover {
transform: scale(1.02) translateY(-.25rem);
background-color: $surface1;
background-color: theme.$surface1;
}
}
}
@@ -92,7 +68,7 @@ ul {
header {
display: flex;
justify-content: space-between;
background-color: $mantle;
background-color: theme.$mantle;
padding: .5rem;
font-size: larger;
@@ -105,7 +81,7 @@ footer {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
background-color: $mantle;
background-color: theme.$mantle;
margin-top: 2rem;
padding: 1rem;
column-gap: 4ch;
@@ -113,38 +89,48 @@ footer {
.mono {
font-family: Monocraft, monospace;
background-color: $mantle;
background-color: theme.$mantle;
border-radius: 2px;
padding: 0 .25rem;
color: $subtext0;
color: theme.$subtext0;
overflow-wrap: anywhere;
&:hover {
transition: .3s ease;
background-color: $crust;
background-color: theme.$crust;
}
}
.block {
display: block;
background-color: $surface0;
background-color: theme.$surface0;
border-radius: .5rem;
padding: .5rem;
h2 {
margin: -.5rem -.5rem 1rem;
padding: .5rem;
text-align: center;
}
.block {
background-color: theme.$surface1;
}
& + & {
margin-top: .5rem;
}
&.red {
background-color: color.mix($surface0, $red, 60%);
background-color: color.mix(theme.$surface0, theme.$red, 60%);
}
&.orange {
background-color: color.mix($surface0, $peach, 60%);
background-color: color.mix(theme.$surface0, theme.$peach, 60%);
}
&.green {
background-color: color.mix($surface0, $green, 60%);
&:hover { background-color: color.mix($surface1, $green, 60%); }
&:active { background-color: color.mix($mantle, $green, 60%) !important; }
background-color: color.mix(theme.$surface0, theme.$green, 60%);
&:hover { background-color: color.mix(theme.$surface1, theme.$green, 60%); }
&:active { background-color: color.mix(theme.$mantle, theme.$green, 60%) !important; }
}
& .header {
@@ -215,9 +201,12 @@ footer {
margin-top: 0;
}
img {
a, img {
width: 88px;
height: 31px;
}
img {
transition-timing-function: ease-out;
transition-duration: .2s;
@@ -244,15 +233,15 @@ footer {
}
&-track {
background-color: $base;
background-color: theme.$base;
}
&-thumb {
background-color: $overlay0;
background-color: theme.$overlay0;
border-radius: .25rem;
&:hover {
background-color: $overlay1;
background-color: theme.$overlay1;
}
}
}
+7
View File
@@ -13,6 +13,13 @@
></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<body>
{% include 'header.tmpl' %}
-6
View File
@@ -49,10 +49,4 @@
<span class="mono">200:ee1:bad2:1732:4b91:c3e3:2f08:29b3</span>
</p>
</div>
<div class="webring disabled">
<a class="block" href="https://otor.ing/lair/prev">&lt;</a>
<a class="block" href="https://otor.ing/">Otoring</a>
<a class="block" href="https://otor.ing/lair/next">&gt;</a>
</div>
{% endblock %}
+3
View File
@@ -1,3 +1,6 @@
Flask==3.1.1
gunicorn
htmlmin2
requests
APScheduler
musicbrainzngs