diff --git a/Dockerfile b/Dockerfile index bfc6d69..583d00b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/blueprints/risdeveau/__init__.py b/blueprints/risdeveau/__init__.py index 6282937..9b991cb 100644 --- a/blueprints/risdeveau/__init__.py +++ b/blueprints/risdeveau/__init__.py @@ -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/") +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) diff --git a/blueprints/risdeveau/modules/api/lb.py b/blueprints/risdeveau/modules/api/lb.py new file mode 100644 index 0000000..02fc8f9 --- /dev/null +++ b/blueprints/risdeveau/modules/api/lb.py @@ -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()) diff --git a/blueprints/risdeveau/static/img/88x31/teto.webp b/blueprints/risdeveau/static/img/88x31/teto.webp index 88854db..66744f2 100644 Binary files a/blueprints/risdeveau/static/img/88x31/teto.webp and b/blueprints/risdeveau/static/img/88x31/teto.webp differ diff --git a/blueprints/risdeveau/static/style/risdeveau.scss b/blueprints/risdeveau/static/style/risdeveau.scss index 146d956..cec5142 100644 --- a/blueprints/risdeveau/static/style/risdeveau.scss +++ b/blueprints/risdeveau/static/style/risdeveau.scss @@ -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 { diff --git a/blueprints/risdeveau/templates/88x31.htm b/blueprints/risdeveau/templates/88x31.htm new file mode 100644 index 0000000..6beebe3 --- /dev/null +++ b/blueprints/risdeveau/templates/88x31.htm @@ -0,0 +1,16 @@ +
+ +
+ + + + +
+
\ No newline at end of file diff --git a/blueprints/risdeveau/templates/base.tmpl b/blueprints/risdeveau/templates/base.tmpl deleted file mode 100644 index 28a5fbf..0000000 --- a/blueprints/risdeveau/templates/base.tmpl +++ /dev/null @@ -1,24 +0,0 @@ - - - - Sweet Bread - - - - - - - {% block head %}{% endblock %} - - - {% include 'risdeveau/templates/header.tmpl' %} - -
- {% block content %}{% endblock %} -
- - \ No newline at end of file diff --git a/blueprints/risdeveau/templates/contacts.html b/blueprints/risdeveau/templates/contacts.htm similarity index 89% rename from blueprints/risdeveau/templates/contacts.html rename to blueprints/risdeveau/templates/contacts.htm index 3f6c66e..0f87e88 100644 --- a/blueprints/risdeveau/templates/contacts.html +++ b/blueprints/risdeveau/templates/contacts.htm @@ -1,14 +1,4 @@ -{% extends 'risdeveau/templates/base.tmpl' %} - -{% block head %} - -{% endblock %} - -{% block content %} +

Development

-{% endblock %} +
\ No newline at end of file diff --git a/blueprints/risdeveau/templates/donate.htm b/blueprints/risdeveau/templates/donate.htm new file mode 100644 index 0000000..7e7b12b --- /dev/null +++ b/blueprints/risdeveau/templates/donate.htm @@ -0,0 +1,19 @@ +
+

Wallets

+
+
+

POL, BNB

+ +
+ +
+

TON

+ +
+ +
+

XMR

+ +
+
+
diff --git a/blueprints/risdeveau/templates/donate.html b/blueprints/risdeveau/templates/donate.html deleted file mode 100644 index de7ca5f..0000000 --- a/blueprints/risdeveau/templates/donate.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'risdeveau/templates/base.tmpl' %} - -{% block content %} -

Wallets

-
-
-

POL, BNB

- -
- -
-

TON

- -
- -
-

XMR

- -
-
-{% endblock %} diff --git a/blueprints/risdeveau/templates/header.tmpl b/blueprints/risdeveau/templates/header.tmpl deleted file mode 100644 index 5c81ba4..0000000 --- a/blueprints/risdeveau/templates/header.tmpl +++ /dev/null @@ -1,20 +0,0 @@ -
- {%- if request.path != url_for('.index') %} - Main - {%- else %} - Lair - {%- endif %} - - -
\ No newline at end of file diff --git a/blueprints/risdeveau/templates/index.html b/blueprints/risdeveau/templates/index.html index 99e0e05..f7b839c 100644 --- a/blueprints/risdeveau/templates/index.html +++ b/blueprints/risdeveau/templates/index.html @@ -1,69 +1,33 @@ -{% extends 'risdeveau/templates/base.tmpl' %} + + + + Sweet Bread -{% block content %} -
- - - - - - - - - - - - - -
DoB2005-01-13
Languages - - - - - - - - - - - - - - - - - - - - - -
RussianNative
EnglishB2
FrenchA1?
GermanA2?
JapaneseBeginner
-
Student - - - - - - - - - -
Programmer2/4yr.
Translator2/3yr.
-
-
+ + + + + + + +
+ Lair +
-
- - - - - - -
-
- - - - -
-{% endblock %} \ No newline at end of file +
+ {% for m in ( + 'info', + 'contacts', + 'listenbrainz', + 'donate', + '88x31' + ) %} + {% include 'risdeveau/templates/%s.htm' % m %} + {% endfor %} +
+ + diff --git a/blueprints/risdeveau/templates/info.htm b/blueprints/risdeveau/templates/info.htm new file mode 100644 index 0000000..81547a3 --- /dev/null +++ b/blueprints/risdeveau/templates/info.htm @@ -0,0 +1,50 @@ +
+ + + + + + + + + + + + + +
DoB2005-01-13
Languages + + + + + + + + + + + + + + + + + + + + + +
RussianNative
EnglishB2
FrenchA1?
GermanA2?
JapaneseBeginner
+
Student + + + + + + + + + +
Programmer2/4yr.
Translator2/3yr.
+
+
diff --git a/blueprints/risdeveau/templates/listenbrainz.htm b/blueprints/risdeveau/templates/listenbrainz.htm new file mode 100644 index 0000000..fe7e9ef --- /dev/null +++ b/blueprints/risdeveau/templates/listenbrainz.htm @@ -0,0 +1,23 @@ +{% macro track_block(track, is_active=false) %} +
+ {% if track.cover_url %} + + {% endif %} +
+

{{ track.artist_name }}

+

{{ track.track_name }}

+
+
+{% endmacro %} + +
+

Listenbrainz

+ {% 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 %} +
diff --git a/blueprints/root/static/style/_catppuccin.scss b/blueprints/root/static/style/_catppuccin.scss new file mode 100644 index 0000000..3ecbcdc --- /dev/null +++ b/blueprints/root/static/style/_catppuccin.scss @@ -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; diff --git a/blueprints/root/static/style/main.scss b/blueprints/root/static/style/main.scss index 3cf662d..648de98 100644 --- a/blueprints/root/static/style/main.scss +++ b/blueprints/root/static/style/main.scss @@ -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; } } } diff --git a/requirements.txt b/requirements.txt index 9bf8758..75d13ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ Flask==3.1.1 gunicorn htmlmin2 +requests +APScheduler +musicbrainzngs +python-magic diff --git a/shell.nix b/shell.nix index a798ec6..c814697 100644 --- a/shell.nix +++ b/shell.nix @@ -7,6 +7,7 @@ pkgs.mkShell { buildInputs = with pypkgs; [ python + python-magic virtualenv pkgs.nodePackages.sass ];