8 Commits

Author SHA1 Message Date
6f693e58f7 fix: now hurl test and update Dockerfile
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m13s
Build Docker / BuildImage (push) Successful in 2m44s
2026-03-21 15:45:53 +11:00
ce956abe5c feat: Add args for host in testing
Some checks failed
Build Docker / BuildImage (push) Has been cancelled
Check Code Quality / RuffCheck (push) Has been cancelled
2026-03-20 21:15:45 +11:00
f044566c52 feat: Add AUDD to tokens
All checks were successful
Build Docker / BuildImage (push) Successful in 2m42s
Check Code Quality / RuffCheck (push) Successful in 2m55s
2026-03-20 03:58:10 +00:00
968d9587b6 feat: Add support for specifying cli spotify image size
All checks were successful
Build Docker / BuildImage (push) Successful in 1m9s
Check Code Quality / RuffCheck (push) Successful in 1m19s
2026-03-18 23:08:13 +11:00
1bc3d3e15d fix: Add ascii_art to Dockerfile
All checks were successful
Build Docker / BuildImage (push) Successful in 1m7s
Check Code Quality / RuffCheck (push) Successful in 1m23s
2026-03-18 22:45:14 +11:00
df4a8da5df feat: Add spotify ascii curl page
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m23s
Build Docker / BuildImage (push) Successful in 1m28s
2026-03-18 22:41:38 +11:00
d7dc5cd6f3 fix: CSIRO URL to include www
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 1m1s
Build Docker / BuildImage (push) Successful in 1m24s
2026-03-17 22:33:29 +11:00
962910f36b feat: Add curl handler for pgp route
All checks were successful
Build Docker / BuildImage (push) Successful in 1m20s
Check Code Quality / RuffCheck (push) Successful in 2m18s
2026-03-05 12:42:00 +11:00
11 changed files with 161 additions and 42 deletions

View File

@@ -84,5 +84,21 @@
"symbol": "stWDBRN", "symbol": "stWDBRN",
"name": "Woodburn Vault", "name": "Woodburn Vault",
"chain": "SOL" "chain": "SOL"
},
{
"symbol": "AUDD",
"name": "Australian Digital Dollar",
"chain": "ETH"
},
{
"symbol": "AUDD",
"name": "Australian Digital Dollar",
"chain": "SOL"
},
{
"symbol": "AUDD",
"name": "Australian Digital Dollar",
"chain": "BASE",
"address": "0x6cB4B39bEc23a921C9a20D061Bf17d4640B0d39e"
} }
] ]

View File

@@ -4,6 +4,7 @@
FROM python:3.13-alpine AS build FROM python:3.13-alpine AS build
# Install build dependencies for Pillow and other native wheels # Install build dependencies for Pillow and other native wheels
# Kept in case source builds are needed, though wheels are preferred
RUN apk add --no-cache \ RUN apk add --no-cache \
build-base \ build-base \
jpeg-dev zlib-dev freetype-dev jpeg-dev zlib-dev freetype-dev
@@ -12,52 +13,47 @@ RUN apk add --no-cache \
COPY --from=ghcr.io/astral-sh/uv:0.8.21 /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:0.8.21 /uv /uvx /bin/
WORKDIR /app WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
# Install dependencies into a virtual environment # Install dependencies into a virtual environment
# - --frozen: strict lockfile usage
# - --no-dev: exclude development dependencies
# - --no-install-project: avoid installing app as package
# - --compile-bytecode: ensuring .pyc files for startup speed (optional, omit if size is critical but usually worth it)
# We omit --compile-bytecode here to save space as requested
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
# Copy only app source files uv sync --frozen --no-dev --no-install-workspace
COPY blueprints blueprints
COPY main.py server.py curl.py tools.py mail.py cache_helper.py ./
COPY templates templates
COPY data data
COPY pwa pwa
COPY .well-known .well-known
# Clean up caches and pycache
RUN rm -rf /root/.cache/uv
RUN find . -type d -name "__pycache__" -exec rm -rf {} +
### Runtime stage ### ### Runtime stage ###
FROM python:3.13-alpine AS runtime FROM python:3.13-alpine AS runtime
ENV PATH="/app/.venv/bin:$PATH" ENV PATH="/app/.venv/bin:$PATH"
# Create non-root user # Create non-root user and install curl for healthchecks
RUN addgroup -g 1001 appgroup && \ RUN addgroup -g 1001 appgroup && \
adduser -D -u 1001 -G appgroup -h /app appuser adduser -D -u 1001 -G appgroup -h /app appuser && \
apk add --no-cache curl
WORKDIR /app WORKDIR /app
RUN apk add --no-cache curl
# Copy the virtual environment from build stage
# Copy only whats needed for runtime
COPY --from=build --chown=appuser:appgroup /app/.venv /app/.venv COPY --from=build --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=build --chown=appuser:appgroup /app/blueprints /app/blueprints
COPY --from=build --chown=appuser:appgroup /app/templates /app/templates # Copy all top-level Python files
COPY --from=build --chown=appuser:appgroup /app/data /app/data COPY --chown=appuser:appgroup *.py ./
COPY --from=build --chown=appuser:appgroup /app/pwa /app/pwa
COPY --from=build --chown=appuser:appgroup /app/.well-known /app/.well-known # Copy application directories
COPY --from=build --chown=appuser:appgroup /app/main.py /app/ COPY --chown=appuser:appgroup blueprints blueprints
COPY --from=build --chown=appuser:appgroup /app/server.py /app/ COPY --chown=appuser:appgroup templates templates
COPY --from=build --chown=appuser:appgroup /app/curl.py /app/ COPY --chown=appuser:appgroup data data
COPY --from=build --chown=appuser:appgroup /app/tools.py /app/ COPY --chown=appuser:appgroup pwa pwa
COPY --from=build --chown=appuser:appgroup /app/mail.py /app/ COPY --chown=appuser:appgroup .well-known .well-known
COPY --from=build --chown=appuser:appgroup /app/cache_helper.py /app/
USER appuser USER appuser
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["python3", "main.py"] ENTRYPOINT ["python3", "main.py"]

Binary file not shown.

70
ascii_art.py Normal file
View File

@@ -0,0 +1,70 @@
import requests
from PIL import Image
from io import BytesIO
ASCII_CHARS = ["@", "#", "S", "%", "?", "*", "+", ";", ":", ",", "."]
def resized_gray_image(image, new_width=40):
"""
Resize and convert image to grayscale.
"""
width, height = image.size
aspect_ratio = height / width
# 0.55 is a correction factor as terminal characters are taller than they are wide
new_height = int(aspect_ratio * new_width * 0.55)
img = image.resize((new_width, new_height))
return img.convert("L")
def pixels_to_ascii(image):
"""
Map grayscale pixels to ASCII characters.
"""
pixels = image.getdata()
# 255 / 11 (len(ASCII_CHARS)) ~= 23. Using 25 for safe integer division mapping.
characters = "".join([ASCII_CHARS[pixel // 25] for pixel in pixels])
return characters
def image_url_to_ascii(url, new_width=40):
"""
Convert an image URL to a colored ASCII string using ANSI escape codes.
"""
if not url:
return ""
try:
response = requests.get(url, timeout=5)
image = Image.open(BytesIO(response.content))
except Exception:
return ""
# Resize image
width, height = image.size
aspect_ratio = height / width
# Calculate new height to maintain aspect ratio, considering terminal character dimensions
# ASCII chars are taller than they are wide (approx ~2x)
# Since we are using '██' (double width), we effectively make each "cell" square.
# So we can just scale by aspect ratio directly without additional correction factor.
new_height = int(aspect_ratio * new_width)
if new_height > height:
new_height = height
new_width = int(new_height / aspect_ratio)
# Resize and ensure RGB mode
img = image.resize((new_width, new_height))
img = img.convert("RGB")
pixels = img.getdata()
ascii_str = ""
for i, pixel in enumerate(pixels):
r, g, b = pixel
ascii_str += f"\033[38;2;{r};{g};{b}m██\033[0m"
# Add newline at the end of each row
if (i + 1) % new_width == 0:
ascii_str += "\n"
return ascii_str

View File

@@ -1,5 +1,6 @@
from flask import redirect, render_template, request, Blueprint, url_for from flask import redirect, render_template, request, Blueprint, url_for
from tools import json_response, isCLI from tools import json_response, isCLI
from ascii_art import image_url_to_ascii
import os import os
import requests import requests
import time import time
@@ -105,8 +106,13 @@ def callback():
def currently_playing(): def currently_playing():
"""Public endpoint showing your current track.""" """Public endpoint showing your current track."""
track = get_playing_spotify_track() track = get_playing_spotify_track()
# Get terminal width for ASCII art scaling
width = request.args.get("width", default=40, type=int)
if isCLI(request): if isCLI(request):
return json_response(request, {"spotify": track}, 200) if "album_art" in track:
track["ascii_art"] = image_url_to_ascii(track["album_art"], new_width=width)
return render_template("spotify.ascii", track=track)
# Render a simple HTML page for browsers # Render a simple HTML page for browsers
return render_template("spotify.html", track=track) return render_template("spotify.html", track=track)

View File

@@ -1,4 +1,4 @@
from flask import render_template from flask import render_template, send_file
from tools import getAddress, get_tools_data, getClientIP from tools import getAddress, get_tools_data, getClientIP
import os import os
from functools import lru_cache from functools import lru_cache
@@ -125,13 +125,15 @@ def curl_response(request):
{"Content-Type": "text/plain; charset=utf-8"}, {"Content-Type": "text/plain; charset=utf-8"},
) )
if path == "pgp" or path == "gpg":
if os.path.exists("data/nathanwoodburn.asc"):
return send_file("data/nathanwoodburn.asc")
if os.path.exists(f"templates/{path}.ascii"): if os.path.exists(f"templates/{path}.ascii"):
return ( return (
render_template(f"{path}.ascii", header=get_header()), render_template(f"{path}.ascii", header=get_header()),
200, 200,
{"Content-Type": "text/plain; charset=utf-8"}, {"Content-Type": "text/plain; charset=utf-8"},
) )
# Fallback to html if it exists # Fallback to html if it exists
if os.path.exists(f"templates/{path}.html"): if os.path.exists(f"templates/{path}.html"):
return render_template(f"{path}.html") return render_template(f"{path}.html")

View File

@@ -19,6 +19,7 @@ from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
from ansi2html import Ansi2HTMLConverter from ansi2html import Ansi2HTMLConverter
from PIL import Image from PIL import Image
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
import argparse
# Import blueprints # Import blueprints
from blueprints import now, blog, wellknown, api, podcast, acme, spotify from blueprints import now, blog, wellknown, api, podcast, acme, spotify
@@ -678,4 +679,10 @@ def not_found(e):
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=5000, host="127.0.0.1") # If --host argument is passed, use that as host, otherwise use 127.0.0.1
parser = argparse.ArgumentParser(description="Run the Flask server.")
parser.add_argument("--host", type=str, default="127.0.0.1")
args = parser.parse_args()
app.run(debug=True, port=5000, host=args.host)

View File

@@ -108,7 +108,7 @@
</div> </div>
</div> </div>
</section> </section>
<p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy managing linux servers for my various projects<br>I code stuff with Python, Bash and tons of other languages<br>I'm currently working as a system admin at <a href="https://csiro.au" target="_blank">CSIRO</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i> <p style="margin-top: 1em;">Hi, I am Nathan Woodburn and I live in Canberra<br>I am currently studying at the Australian National University<br>I enjoy managing linux servers for my various projects<br>I code stuff with Python, Bash and tons of other languages<br>I'm currently working as a system admin at <a href="https://www.csiro.au" target="_blank">CSIRO</a><br><br></p><i class="fas fa-arrow-down" style="font-size: 50px;" onclick="slideout()"></i>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script> <script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script> <script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script> <script src="/assets/js/grayscale.min.js"></script>

View File

@@ -95,7 +95,7 @@ Check them out here!</blockquote><img class="img-fluid" src="/assets/img/pfront.
<div class="col-lg-8 mx-auto"> <div class="col-lg-8 mx-auto">
<h2>About ME</h2> <h2>About ME</h2>
<div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.webp" style="border-radius: 50%;" alt="My Profile"><img class="profile foreground" src="/assets/img/pfront.webp" alt=""></div> <div class="profile-container" style="margin-bottom: 2em;"><img class="profile background" src="/assets/img/profile.webp" style="border-radius: 50%;" alt="My Profile"><img class="profile foreground" src="/assets/img/pfront.webp" alt=""></div>
<p style="margin-bottom: 5px;">Hi, I'm Nathan Woodburn and I live in Canberra, Australia.<br>I've been home schooled all the way to Yr 12.<br>I'm currently studying a&nbsp;Bachelor of Computer Science.<br>I create tons of random projects so this site is often behind.<br>I'm currently working as a system admin at <a href="https://csiro.au" target="_blank">CSIRO</a></p> <p style="margin-bottom: 5px;">Hi, I'm Nathan Woodburn and I live in Canberra, Australia.<br>I've been home schooled all the way to Yr 12.<br>I'm currently studying a&nbsp;Bachelor of Computer Science.<br>I create tons of random projects so this site is often behind.<br>I'm currently working as a system admin at <a href="https://www.csiro.au" target="_blank">CSIRO</a></p>
<p title="{{repo_description}}" style="margin-bottom: 0px;display: inline-block;">I'm currently working on</p> <p title="{{repo_description}}" style="margin-bottom: 0px;display: inline-block;">I'm currently working on</p>
<p data-bs-toggle="tooltip" data-bss-tooltip="" title="{{repo_description}}" style="display: inline-block;">{{repo | safe}}</p> <p data-bs-toggle="tooltip" data-bss-tooltip="" title="{{repo_description}}" style="display: inline-block;">{{repo | safe}}</p>
</div> </div>

22
templates/spotify.ascii Normal file
View File

@@ -0,0 +1,22 @@
{% include 'header.ascii' %}
API [/api/v1]
───────────────────────────────────────────────
 CURRENTLY PLAYING 
───────────────────
{% if track.error %}
Error: {{ track.error }}
{% else %}
{% if track.ascii_art %}
{{ track.ascii_art }}
{% endif %}
Song: {{ track.song_name }}
Artist: {{ track.artist }}
Album: {{ track.album_name }}
URL: {{ track.url }}
Progress: {{ track.progress_ms // 60000 }}:{{ '%02d' % ((track.progress_ms // 1000) % 60) }} / {{ track.duration_ms // 60000 }}:{{ '%02d' % ((track.duration_ms // 1000) % 60) }}
Status: {{ 'Playing' if track.is_playing else 'Paused' }}
Note: Specify width with ?width=80 (default 40)
{% endif %}

View File

@@ -10,15 +10,15 @@ HTTP 200
GET http://127.0.0.1:5000/now/24_02_18 GET http://127.0.0.1:5000/now/24_02_18
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/now/now.json GET http://127.0.0.1:5000/now.json
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/now/now.xml GET http://127.0.0.1:5000/now.xml
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/now/now.rss GET http://127.0.0.1:5000/now.rss
HTTP 200 HTTP 200
GET http://127.0.0.1:5000/now/rss.xml GET http://127.0.0.1:5000/rss.xml
HTTP 200 HTTP 200