Rework my page and add info from listenbrainz

This commit is contained in:
2026-01-22 23:55:58 +03:00
parent e97985b624
commit ea5156aa6c
18 changed files with 336 additions and 227 deletions
+2
View File
@@ -15,6 +15,8 @@ WORKDIR /app
COPY . .
COPY --from=sass /build/blueprints/ ./blueprints/
RUN apt update && apt upgrade
RUN apt install libmagic1 -y
RUN pip install --no-cache-dir -r requirements.txt
ENV FLASK_ENV=production
+25 -12
View File
@@ -1,7 +1,19 @@
import os
import magic
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
bp = Blueprint(
"risdeveau",
@@ -11,10 +23,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 +38,15 @@ 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
@bp.route("/")
def index():
return render_tmpl('index.html')
@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)
+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())
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,20 @@ h3 {
}
}
.track {
display: flex;
&.active {
box-shadow: theme.$green 0 0 5px 0;
}
img {
width: 5rem;
height: 5rem;
border-radius: .5rem;
}
}
table, tbody {
vertical-align: baseline;
border-collapse: collapse;
@@ -66,7 +56,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>
+31 -67
View File
@@ -1,69 +1,33 @@
{% 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="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',
'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>
@@ -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;
}
}
}
+4
View File
@@ -1,3 +1,7 @@
Flask==3.1.1
gunicorn
htmlmin2
requests
APScheduler
musicbrainzngs
python-magic
+1
View File
@@ -7,6 +7,7 @@ pkgs.mkShell {
buildInputs = with pypkgs; [
python
python-magic
virtualenv
pkgs.nodePackages.sass
];