15 Commits

Author SHA1 Message Date
Sweetbread 3f67d21fc9 tmp 2026-04-03 00:34:08 +03:00
Sweetbread 297648d4a2 tmp 2026-03-31 19:42:43 +03:00
Sweetbread ceffe633fd tmp 2026-03-31 16:09:57 +03:00
Sweetbread afb13b6a03 Update info
Docker Build and Push / build-and-push (push) Successful in 2m56s
2026-03-06 22:49:03 +03:00
Sweetbread a0cfa765bd Cover replacing for LB
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-07 19:25:12 +03:00
Sweetbread 62b8f056fa Fix covers
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-07 18:39:58 +03:00
Sweetbread 0edf6e35d7 Reactive updating of related time 2026-02-07 18:39:58 +03:00
Sweetbread db11fabe1a Refactoring 2026-02-07 18:39:58 +03:00
Sweetbread 0f5d1b5221 Don't send data if not modified 2026-02-06 18:56:38 +03:00
Sweetbread 9935bc7f1f Add polling
Docker Build and Push / build-and-push (push) Successful in 20s
2026-02-05 23:04:19 +03:00
Sweetbread 42ffd887d2 Update docker image 2026-02-05 23:04:19 +03:00
Sweetbread 96b73e4751 Add OG meta 2026-02-05 23:04:19 +03:00
Sweetbread 0aa2477f35 Add Steam info 2026-02-05 23:04:19 +03:00
Sweetbread c17bf8ca83 Delete otoring >: 2026-02-04 15:31:04 +03:00
Sweetbread ea5156aa6c Rework my page and add info from listenbrainz 2026-02-04 15:31:04 +03:00
42 changed files with 920 additions and 442 deletions
+7
View File
@@ -11,6 +11,12 @@ jobs:
- name: Checkout code
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
uses: docker/login-action@v2
with:
@@ -27,5 +33,6 @@ jobs:
context: .
push: ${{ github.event_name == 'push' }}
tags: g.lair.moe/${{ vars.DOCKER_USERNAME }}/lair.moe:latest
labels: ${{ steps.tags.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+33 -10
View File
@@ -1,23 +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
COPY ./blueprints ./blueprints
RUN sass ./blueprints:./blueprints \
--no-source-map \
--style=compressed
--style=compressed \
--quiet
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
COPY . .
COPY --from=sass /build/blueprints/ ./blueprints/
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
COPY . .
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"]
+8 -1
View File
@@ -9,7 +9,14 @@ import blueprints.risdeveau.modules.style
from flask import Flask
app = Flask(__name__, static_folder=None, subdomain_matching=True)
app = Flask(
__name__,
static_folder=None,
subdomain_matching=True,
template_folder="blueprints"
)
app.jinja_env.add_extension('pypugjs.ext.jinja.PyPugJSExtension')
app.before_request(locale.before_request)
app.context_processor(locale.inject_translations)
+71 -10
View File
@@ -1,7 +1,45 @@
import os
from datetime import datetime, timedelta
from pathlib import Path
from time import time
from flask import (
Blueprint,
abort,
make_response,
render_template,
request,
send_file,
)
import magic
from htmlmin import minify
from flask import Blueprint, render_template, send_from_directory, send_file, abort
from musicbrainzngs import get_image_front
from .modules.api.lb import data as lb_data
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 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(
"risdeveau",
@@ -11,10 +49,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 +64,37 @@ def static(filename: str):
return send_file(path)
return abort(404)
@bp.route("/asset/mb/<mbid>")
def mb_cover(mbid):
r = make_response(image := 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['Expires'] = (datetime.now() + timedelta(days=1)) \
.strftime('%a, %d %b %Y %H:%M:%S GMT')
return r
args = {
"lb": lb_data,
"steam": steam_data,
"tmsmp": tmsmp,
"utmsmp": utmsmp,
"rtmsmp": rtmsmp
}
@bp.route("/")
def index():
return render_tmpl('index.html')
return render_tmpl('index.pug', **args)
@bp.route("/contacts")
def contacts():
return render_tmpl('contacts.html')
@bp.route("/m/<module>")
def module(module):
if none_match := request.headers.get('if-none-match'):
match module:
case "listenbrainz":
if none_match == lb_data['etag']:
return '', 304
@bp.route("/donate")
def donate():
return render_tmpl('donate.html')
case "steam":
if none_match == steam_data['etag']:
return '', 304
return render_tmpl(f'{module}.pug', **args)
+123
View File
@@ -0,0 +1,123 @@
import atexit
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.triggers.interval import IntervalTrigger
import requests
from flask import Flask, jsonify
@dataclass
class Cache:
data = {}
last_updated = time()
status = None
data = {
"caches": {
"now": Cache(),
"listens": Cache()
},
"last_updated": time(),
"etag": ""
}
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"https://img.youtube.com/vi/{video_id}/2.jpg"
def parse_listens(json: dict) -> dict:
cover_replacing = {
"1e699948-c7c8-4bb2-9f8b-62e14b882a5d": "ca464c1d-5848-45bb-b92d-b1e4b00f9d65",
"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": []
}
for track in json["listens"]:
listened_at = track.get("listened_at", 0)
track = track["track_metadata"]
new_track = {
"artist_name": track["artist_name"],
"track_name": track["track_name"],
"listened_at": listened_at
}
if mb := track.get("mbid_mapping"):
new_track["id"] = \
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["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_json["listens"].append(new_track)
return new_json
def api_request(url: str, cache: Cache):
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
json = parse_listens(response.json().get("payload"))
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}'
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", data['caches']['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", data['caches']['now']),
trigger=IntervalTrigger(seconds=15),
id='risdeveau.listenbrainz.playing-now',
replace_existing=True
)
scheduler.start()
atexit.register(lambda: scheduler.shutdown())
+94
View File
@@ -0,0 +1,94 @@
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.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:
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")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,68 @@
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);
}
},
}));
});
@@ -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,36 @@ h3 {
}
}
.track {
display: flex;
&.active {
box-shadow: theme.$green 0 0 5px 0;
}
img {
width: 5rem;
height: 5rem;
object-fit: cover;
border-radius: .5rem;
}
}
.steam {
.block {
display: flex;
img {
height: 7rem;
margin-right: .5rem;
}
p {
margin: .5rem 0;
}
}
}
table, tbody {
vertical-align: baseline;
border-collapse: collapse;
@@ -66,7 +72,7 @@ table, tbody {
border-radius: 10px;
&:hover {
background-color: color.change($surface1, $alpha:75%);
background-color: color.change(theme.$surface1, $alpha:75%);
}
th {
+13
View File
@@ -0,0 +1,13 @@
div
.88-31
a.disabled(href="https://chest.lair.moe")
img(src="/static/img/88x31/gf.png")
a#pie(href="https://preview.about.akarpov.ru")
img(src="/static/img/88x31/withpie.gif")
.88-31
a(href="https://g.lair.moe/Sweetbread/nixos-config")
img(src="/static/img/88x31/nixos.webp")
img(src="/static/img/88x31/teto.webp")
-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,58 +0,0 @@
{% extends 'risdeveau/templates/base.tmpl' %}
{% block head %}
<style>
main {
width: -webkit-fill-available;
}
</style>
{% endblock %}
{% block content %}
<h3>Development</h3>
<div class="blocks badges">
<a class="block" href="//g.lair.moe/Sweetbread">
<img class="icon" src="/static/icon/service/gitea.webp" />
Gitea
</a>
<a class="block" href="https://github.com/VerySweetBread">
<img class="icon" src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png" />
GitHub
</a>
<a class="block" href="https://git.kolibrios.org/Sweetbread">
<img class="icon" src="https://git.kolibrios.org/assets/img/logo.svg" />
KolibriOS Git
</a>
</div>
<h3>Contacts</h3>
<div class="blocks badges">
<a class="block" href="https://matrix.to/#/@risdeveau:codrs.ru">
<img class="icon" src="https://matrix.org/assets/favicon.ico" />
Matrix
</a>
<a class="block" href="//b.lair.moe/@risdeveau">
<img class="icon" src="/static/icon/service/sharkey.webp" />
Fediverse
</a>
<a class="block" href="https://discord.com/users/459823895256498186">
<img class="icon" src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg" />
Discord
</a>
<a class="block" href="mailto:risdeveau@lair.moe">
Mail
</a>
</div>
<h3>Game accounts</h3>
<div class="blocks badges">
<a class="block" href="https://steamcommunity.com/id/risdeveau">
<img class="icon" src="https://store.steampowered.com/favicon.ico" />
Steam
</a>
<a class="block" href="https://gamebanana.com/members/3899828">
<img class="icon" src="https://images.gamebanana.com/static/img/favicon/favicon.ico" />
GameBanana
</a>
</div>
{% endblock %}
@@ -0,0 +1,40 @@
.block
h3 Development
.blocks.badges
a.block(href="//g.lair.moe/Sweetbread")
img.icon(src="/static/icon/service/gitea.webp")
| Gitea
a.block(href="https://github.com/VerySweetBread")
img.icon(src="https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png")
| GitHub
a.block(href="https://git.kolibrios.org/Sweetbread")
img.icon(src="https://git.kolibrios.org/assets/img/logo.svg")
| KolibriOS Git
h3 Contacts
.blocks.badges
a.block(href="https://matrix.to/#/@risdeveau:lair.moe")
img.icon(src="https://matrix.org/assets/favicon.ico")
| Matrix
a.block(href="//b.lair.moe/@risdeveau")
img.icon(src="/static/icon/service/sharkey.webp")
| Fediverse
a.block(href="https://discord.com/users/459823895256498186")
img.icon(src="https://cdn.prod.website-files.com/6257adef93867e50d84d30e2/66e3d80db9971f10a9757c99_Symbol.svg")
| Discord
a.block(href="mailto:risdeveau@lair.moe") Mail
h3 Game accounts
.blocks.badges
a.block(href="https://steamcommunity.com/id/risdeveau")
img.icon(src="https://store.steampowered.com/favicon.ico")
| Steam
a.block(href="https://gamebanana.com/members/3899828")
img.icon(src="https://images.gamebanana.com/static/img/favicon/favicon.ico")
| GameBanana
@@ -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 %}
+14
View File
@@ -0,0 +1,14 @@
div
h3 Wallets
.blocks.qr
.block.qr
p POL, BNB
img(src="/static/img/wallets/evm.webp")
.block.qr
p TON
img(src="/static/img/wallets/ton.webp")
.block.qr
p XMR
img(src="/static/img/wallets/xmr.webp")
@@ -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>
-69
View File
@@ -1,69 +0,0 @@
{% extends 'risdeveau/templates/base.tmpl' %}
{% 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>
<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 %}
+50
View File
@@ -0,0 +1,50 @@
doctype html
html(lang="en")
head
title Sweet Bread
each f in ('tw', 'main', 'risdeveau')
link(rel="stylesheet", href="/static/style/#{f}.css")
link(rel="icon" type="image/webp" href="/static/icon/us/risdeveau.webp")
script(src="/static/script/rtime.js")
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
script(
src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"
)
script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
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}
]
}'
)
body
header
a(href=url_for('root.index')) Lair
main
include risdeveau/templates/info.pug
include risdeveau/templates/contacts.pug
include risdeveau/templates/listenbrainz.pug
include risdeveau/templates/steam.pug
include risdeveau/templates/donate.pug
include risdeveau/templates/88x31.pug
+35
View File
@@ -0,0 +1,35 @@
.block
table
tr
th DoB
td 2005-01-13
tr
th Languages
td
table
tr
td Russian
td Native
tr
td English
td B2
tr
td French
td A1?
tr
td German
td A2?
tr
td Japanese
td Beginner
tr
th Student
td
table
tr
td Programmer
td 2.5/4yr.
tr
td Translator
td 2.5/3yr.
@@ -0,0 +1,32 @@
mixin track_block(track, is_active=false)
- set act_cls = "active" if is_active else ""
.block.track(class=act_cls)
if track.cover_url
img(src=track.cover_url)
div
p
b= track.artist_name
p= track.track_name
if not is_active
p(
x-data="rtime(#{track.listened_at})",
x-text="`Listened ${timeString}`",
:class="textColorClass"
)
.block(
hx-get="/m/listenbrainz",
hx-trigger="every 15s",
hx-swap="outerHTML",
hx-headers='{"If-None-Match": "#{lb.etag}"}'
)
h2
a(href="https://listenbrainz.org/user/risdeveau/") Listenbrainz
if lb.caches.now.data and lb.caches.now.data.listens[0]
+track_block(lb.caches.now.data.listens[0], true)
if lb.caches.listens.data and lb.caches.listens.data.listens
each track in lb.caches.listens.data.listens
+track_block(track)
+52
View File
@@ -0,0 +1,52 @@
mixin game_block(g, show_last_played=false)
a.block(href="https://store.steampowered.com/app/#{g.appid}", target="_blank")
picture
source(media="(max-width: 45rem)", srcset=g.v_cover)
img(src=g.h_cover)
div
strong= g.name
if g.playtime_2weeks
p Played last 2 weeks: #{tmsmp(g.playtime_2weeks * 60)}
p
- var lin = g.playtime_linux_forever > 0;
- var win = g.playtime_windows_forever > 0;
| Total played:
if lin
= tmsmp(g.playtime_linux_forever * 60)
| (
abbr(title="On Linux") L
| )
if lin and win
= " + "
if win
= tmsmp(g.playtime_windows_forever * 60)
| (
abbr(title="On Windows") W
| )
if lin and win
= " = "
= tmsmp(g.playtime_forever * 60)
if show_last_played and g.rtime_last_played != 0
p Last played: #{utmsmp(g.rtime_last_played)}
.block.steam(
hx-get="/m/steam",
hx-trigger="every 1m",
hx-swap="outerHTML",
hx-headers='{ "If-None-Match": "#{steam.etag}" }'
)
h2
a(href="https://steamcommunity.com/id/risdeveau") Steam
if steam.caches.recent.data.games
h3 Recently played:
each g in steam.caches.recent.data.games
+game_block(g)
p(x-data="rtime(#{steam.caches.recent.last_updated})", x-text="`Last updated: ${timeString}`")
if steam.caches.owned.data.games
h3 Top played games:
- var owned_games = steam.caches.owned.data.games | sort(attribute="playtime_forever", reverse=true)
each g in owned_games[:5]
+game_block(g, true)
p(x-data="rtime(#{steam.caches.owned.last_updated})", x-text="`Last updated: ${timeString}`")
+3 -3
View File
@@ -17,12 +17,12 @@ def render_tmpl(filename: str) -> str:
@bp.route("/")
def index():
return render_tmpl('index.html')
return render_tmpl('index.pug')
@bp.route("/host")
def host():
return render_tmpl('host.html')
return render_tmpl('host.pug')
@bp.route("/us")
def us():
return render_tmpl('us.html')
return render_tmpl('us.pug')
+1 -1
View File
@@ -3,7 +3,7 @@ from os.path import join
def compile_styles():
dir = "blueprints/root/static/style"
files = ("main",)
files = ("main", "tw")
for file in files:
console(f"sass {join(dir, file+'.scss')} {join(dir, file+'.css')}")
@@ -0,0 +1,25 @@
// 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;
$yellow: #f9e2af;
$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;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
@use "catppuccin" as theme;
.t {
&-red { color: theme.$red; }
&-orange { color: theme.$peach; }
&-yellow { color: theme.$yellow; }
&-green { color: theme.$green; }
}
+33
View File
@@ -0,0 +1,33 @@
doctype html
html(lang=g.locale)
head
title Lair
link(rel="stylesheet" href="/static/style/main.css")
link(rel="icon" type="image/webp" href="/static/icon/lair.webp")
script(src="/static/script/copy-mono.js")
script(
src="https://track.lair.moe/api/script.js"
data-site-id="1"
defer
)
meta(name="viewport" content="width=device-width, initial-scale=1.0")
meta(name="mock-email" content="admin@example.com")
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"))
body
include root/templates/header.pug
h1
block title
main
block content
include root/templates/footer.pug
-28
View File
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Lair</title>
<link rel="stylesheet" href="/static/style/main.css">
<link rel="icon" type="image/webp" href="/static/icon/lair.webp" />
<script src="/static/script/copy-mono.js"> </script>
<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">
<meta name="mock-email" content="admin@example.com">
</head>
<body>
{% include 'header.tmpl' %}
<h1>{% block title %}{% endblock %}</h1>
<main>
{% block content %}{% endblock %}
</main>
{% include 'footer.tmpl' %}
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
footer
div
| lair.moe &#127279; 2025 - 2026</div>
div
a(href="https://g.lair.moe/Sweetbread/lair.moe")= _('site source')
div
= _('contact us')
= ": "
a(href="mailto:admin@lair.moe") admin@lair.moe
-5
View File
@@ -1,5 +0,0 @@
<footer>
<div>lair.moe &#127279; 2025 - 2026</div>
<div><a href="https://g.lair.moe/Sweetbread/lair.moe">{{ _('site source') }}</a></div>
<div>{{ _('contact us') }}: <a href="mailto:admin@lair.moe">admin@lair.moe</a></div>
</footer>
+17
View File
@@ -0,0 +1,17 @@
header
if request.path != url_for('.index')
a(href=url_for('.index')) Lair
else
div
.header-links
-
var links = (
('.us', _('about us')),
('.host', _('about host')),
)
each l, t in links
if url_for(l) == request.path
strong= t
else
a(href=url_for(l))= t
-20
View File
@@ -1,20 +0,0 @@
<header>
{%- if request.path != url_for('.index') %}
<a href="{{ url_for('.index') }}">Lair</a>
{%- else %}
<div></div>
{%- endif %}
<div class="header-links">
{%- for (l, t) in (
('.us', _('about us')),
('.host', _('about host'))
) %}
{%- if url_for(l) == request.path %}
<strong>{{ t }}</strong>
{%- else %}
<a href="{{ url_for(l) }}">{{ t }}</a>
{%- endif %}
{%- endfor %}
</div>
</header>
-21
View File
@@ -1,21 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}{{ _('about host') }}{% endblock %}
{% block content %}
<a href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank" class="block">
<strong>{{ _("host:hoster") }}</strong>: play2go
<p>{{ _('host:hoster_descr') }}</p>
</a>
<div class="block">
<strong>{{ _("host:specifications") }}</strong>:
<ul>
<li>CPU: Ryzen 9@3.4GHz (4 cores)</li>
<li>RAM: 8 GB</li>
<li>SSD: 150 GB</li>
<li>ETH: 500Mb/s</li>
<li>Loc: Deutschland, Frankfurt am Main</li>
</ul>
</div>
{% endblock %}
+20
View File
@@ -0,0 +1,20 @@
extends root/templates/base.pug
block title
= _('about host')
block content
a.block(href="https://play2go.cloud/?ref_id=4baFoOIp5QE" target="_blank")
strong= _("host:hoster")
| : play2go
p= _('host:hoster_descr')
.block
strong= _("host:specifications")
| :
ul
li CPU: Ryzen 9@3.4GHz (4 cores)
li RAM: 8 GB
li SSD: 150 GB
li ETH: 500Mb/s
li Loc: Deutschland, Frankfurt am Main
-58
View File
@@ -1,58 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}
<img src="/static/icon/lair.webp" class="icon" />
Lair
{% endblock %}
{% block content %}
<a href="https://b.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/sharkey.webp" class="icon"/>
<strong>Sharkey</strong>
</div>
<p>{{ _('index.descr:sharkey') }}</p>
</a>
<a href="https://g.lair.moe" target="_blank" class="block">
<div class="header">
<img src="/static/icon/service/gitea.webp" class="icon"/>
<strong>Gitea</strong>
</div>
<p>{{ _('index.descr:gitea') }}</p>
</a>
<div class="block">
<p><a href="https://m.codrs.ru" target="_blank"><strong>Matrix</strong></a> &mdash; {{ _('index.descr:matrix') }}</p>
<p><a href="https://c.lair.moe" target="_blank"><strong>Copyparty</strong></a> &mdash; {{ _('index.descr:copyparty') }}</p>
<p><a href="https://tools.lair.moe" target="_blank"><strong>IT-tools</strong></a> &mdash; {{ _('index.descr:tools') }}</p>
<p><a href="https://vert.lair.moe" target="_blank"><strong>Vert</strong></a> &mdash; {{ _('index.descr:vert') }}</p>
</div>
<div class="block">
<strong>{{ _('index:altfronts') }}</strong>
<p><a href="https://s.lair.moe" target="_blank"><strong>4get</strong></a> &mdash; {{ _('index.descr:4get') }}</p>
<p><a href="https://tl.lair.moe" target="_blank"><strong>TransLite</strong></a> &mdash; {{ _('index.descr:tl') }}</p>
<p><a href="https://lyr.lair.moe" target="_blank"><strong>Intellectual</strong></a> &mdash; {{ _('index.descr:lyr') }}</p>
</div>
<div class="block">
<div>
<strong>DNS</strong>:
<ul>
<li><span class="mono">64.188.64.176</span></li>
<li>DoT: <span class="mono">lair.moe:853</span></li>
<li>DoH: <span class="mono">dns.lair.moe</span></li>
</ul>
</div>
<p>
<strong>Yggdrasil</strong>:
<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 %}
+70
View File
@@ -0,0 +1,70 @@
extends root/templates/base.pug
block title
img.icon(src="/static/icon/lair.webp")
| Lair
block content
a.block(href="https://b.lair.moe" target="_blank")
.header
img.icon(src="/static/icon/service/sharkey.webp")
strong Sharkey
p= _('index.descr:sharkey')
a.block(href="https://g.lair.moe" target="_blank")
.header
img.icon(src="/static/icon/service/gitea.webp")
strong Gitea
p= _('index.descr:gitea')
.block
p
a(href="https://m.lair.moe" target="_blank")
strong Matrix
| &mdash; {{ _('index.descr:matrix') }}
p
a(href="//c.lair.moe" target="_blank")
strong Copyparty
| &mdash; {{ _('index.descr:copyparty') }}
p
a(href="https://tools.lair.moe" target="_blank")
strong IT-tools
| &mdash; {{ _('index.descr:tools') }}
p
a(href="https://vert.lair.moe" target="_blank")
strong Vert
| &mdash; {{ _('index.descr:vert') }}
.block
strong= _('index:altfronts')
p
a(href="https://s.lair.moe" target="_blank")
strong 4get
| &mdash; {{ _('index.descr:4get') }}
p
a(href="https://tl.lair.moe" target="_blank")
strong TransLite
| &mdash; {{ _('index.descr:tl') }}
p
a(href="https://lyr.lair.moe" target="_blank")
strong Intellectual
| &mdash; {{ _('index.descr:lyr') }}
.block
div
strong DNS
| :
ul
li
span.mono 64.188.64.176
li
| DoT:
span.mono lair.moe:853
li
| DoH:
span.mono dns.lair.moe
p
strong Yggdrasil
| :
span.mono 200:ee1:bad2:1732:4b91:c3e3:2f08:29b3
-21
View File
@@ -1,21 +0,0 @@
{% extends 'base.tmpl' %}
{% block title %}О нас{% endblock %}
{% block content %}
<a href="{{ url_for('risdeveau.index') }}" class="block green">
<div class="header">
<img src="/static/icon/us/risdeveau.webp" class="icon"/>
Sweetbread
</div>
Главный админ, занимается почти всеми сервисами. Создал этот сайт
</a>
<div class="block orange disabled">
<div class="header">
<img src="/static/icon/us/chest.webp" class="icon"/>
Chest
</div>
Должна была помогать делать этот сайт
</div>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
extends root/templates/base.pug
block title
| О нас
block content
a.block.green(href=url_for('risdeveau.index'))
.header
img.icon(src="/static/icon/us/risdeveau.webp")
| Sweetbread
| Главный админ, занимается почти всеми сервисами. Создал этот сайт
.block.orange.disabled
.header
img.icon(src="/static/icon/us/chest.webp")
| Chest
| Должна была помогать делать этот сайт
+2
View File
@@ -8,6 +8,8 @@ about host = About host
contacts = Contacts
donate = Donate
description = Small personal site
[index]
altfronts = Altfronts
+2
View File
@@ -8,6 +8,8 @@ about host = О хосте
contacts = Контакты
donate = Донат
description = Небольшой личный сайт
[index]
altfronts = Альтфронты
+5
View File
@@ -1,3 +1,8 @@
Flask==3.1.1
gunicorn
htmlmin2
requests
APScheduler
musicbrainzngs
python-magic
git+https://github.com/VerySweetBread/pypugjs
+2 -1
View File
@@ -7,8 +7,9 @@ pkgs.mkShell {
buildInputs = with pypkgs; [
python
python-magic
virtualenv
pkgs.nodePackages.sass
pkgs.dart-sass
];
shellHook = ''