12 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
90264c0024 fix: Test manual merge
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
Check Code Quality / RuffCheck (push) Successful in 1m17s
2026-03-04 23:06:39 +11:00
6286ff1a80 feat: Add spotify page
All checks were successful
Build Docker / BuildImage (push) Successful in 54s
Check Code Quality / RuffCheck (push) Successful in 1m13s
2026-03-04 22:14:50 +11:00
bfaa83021f fix: Ignore some types from broken linting
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
Check Code Quality / RuffCheck (push) Successful in 53s
2026-03-04 20:13:10 +11:00
8ba8962cb8 fix: Remove loading page
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 59s
Build Docker / BuildImage (push) Successful in 5m18s
2026-03-04 20:06:31 +11:00
15 changed files with 302 additions and 95 deletions

View File

@@ -84,5 +84,21 @@
"symbol": "stWDBRN",
"name": "Woodburn Vault",
"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
# 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 \
build-base \
jpeg-dev zlib-dev freetype-dev
@@ -12,50 +13,45 @@ RUN apk add --no-cache \
COPY --from=ghcr.io/astral-sh/uv:0.8.21 /uv /uvx /bin/
WORKDIR /app
# Copy dependency files
COPY pyproject.toml uv.lock ./
# 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 \
uv sync --locked
# Copy only app source files
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 {} +
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-dev --no-install-workspace
### Runtime stage ###
FROM python:3.13-alpine AS runtime
ENV PATH="/app/.venv/bin:$PATH"
# Create non-root user
# Create non-root user and install curl for healthchecks
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
RUN apk add --no-cache curl
# Copy only whats needed for runtime
# Copy the virtual environment from build stage
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 --from=build --chown=appuser:appgroup /app/data /app/data
COPY --from=build --chown=appuser:appgroup /app/pwa /app/pwa
COPY --from=build --chown=appuser:appgroup /app/.well-known /app/.well-known
COPY --from=build --chown=appuser:appgroup /app/main.py /app/
COPY --from=build --chown=appuser:appgroup /app/server.py /app/
COPY --from=build --chown=appuser:appgroup /app/curl.py /app/
COPY --from=build --chown=appuser:appgroup /app/tools.py /app/
COPY --from=build --chown=appuser:appgroup /app/mail.py /app/
COPY --from=build --chown=appuser:appgroup /app/cache_helper.py /app/
# Copy all top-level Python files
COPY --chown=appuser:appgroup *.py ./
# Copy application directories
COPY --chown=appuser:appgroup blueprints blueprints
COPY --chown=appuser:appgroup templates templates
COPY --chown=appuser:appgroup data data
COPY --chown=appuser:appgroup pwa pwa
COPY --chown=appuser:appgroup .well-known .well-known
USER appuser
EXPOSE 5000

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

@@ -90,7 +90,7 @@ def timezone():
current_time = datetime.datetime.now(tz)
return jsonify(
{
"timezone": tz.utcoffset(current_time).total_seconds() / 3600,
"timezone": tz.utcoffset(current_time).total_seconds() / 3600, # type: ignore
"timezone_name": tz.tzname(current_time),
"ip": getClientIP(request),
"status": HTTP_OK,

View File

@@ -1,5 +1,6 @@
from flask import redirect, request, Blueprint, url_for
from tools import json_response
from flask import redirect, render_template, request, Blueprint, url_for
from tools import json_response, isCLI
from ascii_art import image_url_to_ascii
import os
import requests
import time
@@ -105,7 +106,16 @@ def callback():
def currently_playing():
"""Public endpoint showing your current track."""
track = get_playing_spotify_track()
return json_response(request, {"spotify": track}, 200)
# Get terminal width for ASCII art scaling
width = request.args.get("width", default=40, type=int)
if isCLI(request):
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
return render_template("spotify.html", track=track)
def get_playing_spotify_track():
@@ -117,7 +127,6 @@ def get_playing_spotify_track():
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(SPOTIFY_CURRENTLY_PLAYING_URL, headers=headers)
if response.status_code == 204:
# return {"error": "Nothing is currently playing."}
return get_last_spotify_track()
elif response.status_code != 200:
return {"error": "Spotify API error", "status": response.status_code}
@@ -125,7 +134,6 @@ def get_playing_spotify_track():
data = response.json()
if not data.get("item"):
return {"error": "Nothing is currently playing."}
track = {
"song_name": data["item"]["name"],
"artist": ", ".join([artist["name"] for artist in data["item"]["artists"]]),
@@ -134,6 +142,8 @@ def get_playing_spotify_track():
"is_playing": data["is_playing"],
"progress_ms": data.get("progress_ms", 0),
"duration_ms": data["item"].get("duration_ms", 1),
"url": data["item"]["external_urls"]["spotify"],
"id": data["item"]["id"],
}
return track
@@ -160,6 +170,18 @@ def get_last_spotify_track():
"artist": ", ".join([artist["name"] for artist in last_track_info["artists"]]),
"album_name": last_track_info["album"]["name"],
"album_art": last_track_info["album"]["images"][0]["url"],
"is_playing": False,
"progress_ms": 0,
"duration_ms": last_track_info.get("duration_ms", 1),
"played_at": data["items"][0]["played_at"],
"url": last_track_info["external_urls"]["spotify"],
"id": last_track_info["id"],
}
return track
@app.route("/last")
def last_played():
"""Public endpoint showing your last played track."""
track = get_last_spotify_track()
return json_response(request, {"spotify": track}, 200)

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
import os
from functools import lru_cache
@@ -125,13 +125,15 @@ def curl_response(request):
{"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"):
return (
render_template(f"{path}.ascii", header=get_header()),
200,
{"Content-Type": "text/plain; charset=utf-8"},
)
# Fallback to html if it exists
if os.path.exists(f"templates/{path}.html"):
return render_template(f"{path}.html")

View File

@@ -19,12 +19,12 @@ from qrcode.constants import ERROR_CORRECT_L, ERROR_CORRECT_H
from ansi2html import Ansi2HTMLConverter
from PIL import Image
from zoneinfo import ZoneInfo
import argparse
# Import blueprints
from blueprints import now, blog, wellknown, api, podcast, acme, spotify
from tools import (
isCLI,
isCrawler,
getAddress,
getFilePath,
error_response,
@@ -233,32 +233,9 @@ def index():
if "podcast.woodburn.au" in request.host:
return render_template("podcast.html")
loaded = False
if request.referrer:
# Check if referrer includes nathan.woodburn.au
if "nathan.woodburn.au" in request.referrer:
loaded = True
if request.cookies.get("loaded"):
loaded = True
# Always load if load is in the query string
if request.args.get("load"):
loaded = False
if isCLI(request):
return curl_response(request)
if not loaded and not isCrawler(request):
# Set cookie
resp = make_response(
render_template("loading.html").replace(
"https://nathan.woodburn.au/loading", "https://nathan.woodburn.au/"
),
200,
{"Content-Type": "text/html"},
)
resp.set_cookie("loaded", "true", max_age=604800)
return resp
# Use cached git data
git = get_git_latest_activity()
repo_name = git["repo"]["name"].lower()
@@ -282,7 +259,7 @@ def index():
html_url = git["repo"]["html_url"]
repo = '<a href="' + html_url + '" target="_blank">' + repo_name + "</a>"
timezone_offset = TZ.utcoffset(datetime.datetime.now()).total_seconds() / 3600
timezone_offset = TZ.utcoffset(datetime.datetime.now()).total_seconds() / 3600 # type: ignore
time = datetime.datetime.now().strftime("%B %d")
time += """
<span id=\"time\"></span>
@@ -309,9 +286,7 @@ def index():
SOLaddress = getAddress("SOL")
BTCaddress = getAddress("BTC")
ETHaddress = getAddress("ETH")
# Set cookie
resp = make_response(
render_template(
return render_template(
"index.html",
handshake_scripts=getHandshakeScript(request.host),
HNS=HNSaddress,
@@ -325,13 +300,7 @@ def index():
projects=projects,
time=time,
message="",
),
200,
{"Content-Type": "text/html"},
)
resp.set_cookie("loaded", "true", max_age=604800)
return resp
# region Donate
@@ -710,4 +679,10 @@ def not_found(e):
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>
</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/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>

View File

@@ -1 +1 @@
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}
:root,[data-bs-theme=light]{--bs-primary:#6E0E9C;--bs-primary-rgb:110,14,156;--bs-primary-text-emphasis:#2C063E;--bs-primary-bg-subtle:#E2CFEB;--bs-primary-border-subtle:#C59FD7;--bs-link-color:#6E0E9C;--bs-link-color-rgb:110,14,156;--bs-link-hover-color:#a41685;--bs-link-hover-color-rgb:164,22,133}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5E0C85;--bs-btn-hover-border-color:#580B7D;--bs-btn-focus-shadow-rgb:233,219,240;--bs-btn-active-color:#fff;--bs-btn-active-bg:#580B7D;--bs-btn-active-border-color:#530B75;--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6E0E9C;--bs-btn-disabled-border-color:#6E0E9C}.btn-outline-primary{--bs-btn-color:#6E0E9C;--bs-btn-border-color:#6E0E9C;--bs-btn-focus-shadow-rgb:110,14,156;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6E0E9C;--bs-btn-hover-border-color:#6E0E9C;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6E0E9C;--bs-btn-active-border-color:#6E0E9C;--bs-btn-disabled-color:#6E0E9C;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6E0E9C}.no-title{text-transform:none!important}.spotify-icon{cursor:pointer}

1
templates/assets/js/spotify.min.js vendored Normal file
View File

@@ -0,0 +1 @@
let progressInterval=null,progressSpeed=0,lastUpdateTime=Date.now(),currentProgress=0,targetProgress=0,trackDuration=0,currentTrackId=null,trackurl=null;async function updateSpotifyWidget(){try{const e=await fetch("/api/v1/playing");if(!e.ok)return;const n=await e.json();if(n.error||n.message){if(document.getElementById("spotify-song").textContent)return;return document.getElementById("spotify-album-art").src="/assets/img/external/spotify.png",document.getElementById("spotify-album-art").style.cursor="default",document.getElementById("spotify-song").textContent="Not Playing",document.getElementById("spotify-artist").textContent="",document.getElementById("spotify-album").textContent="",document.getElementById("spotify-icon-playing").style.display="none",document.getElementById("spotify-icon-paused").style.display="none",document.getElementById("spotify-icon-stopped").style.display="inline",updateProgressBar(0,1),clearInterval(progressInterval),progressInterval=null,currentProgress=0,currentTrackId=null,void(trackurl=null)}const r=n.spotify;var t=!1;document.getElementById("spotify-song").textContent||(t=!0);const o=r.song_name+r.artist;null!==currentTrackId&&currentTrackId!==o&&(currentProgress=0,document.getElementById("spotify-progress").style.transition="none",document.getElementById("spotify-progress").style.width="0%",document.getElementById("spotify-progress").offsetHeight,document.getElementById("spotify-progress").style.transition="width 0.1s linear"),currentTrackId=o,trackurl=r.url,document.getElementById("spotify-album-art").src=r.album_art,document.getElementById("spotify-album-art").style.cursor="pointer",document.getElementById("spotify-song").textContent=r.song_name,document.getElementById("spotify-artist").textContent=r.artist,document.getElementById("spotify-album").textContent=r.album_name,r.is_playing?(currentProgress=r.progress_ms,trackDuration=r.duration_ms,lastUpdateTime=Date.now(),updateProgressBar(r.progress_ms,r.duration_ms),progressInterval&&clearInterval(progressInterval),progressInterval=setInterval(animateProgressBar,10),document.getElementById("spotify-icon-playing").style.display="inline",document.getElementById("spotify-icon-paused").style.display="none",document.getElementById("spotify-icon-stopped").style.display="none"):(updateProgressBar(r.progress_ms,r.duration_ms),clearInterval(progressInterval),progressInterval=null,currentProgress=r.progress_ms,trackDuration=r.duration_ms,document.getElementById("spotify-icon-playing").style.display="none",document.getElementById("spotify-icon-paused").style.display="inline",document.getElementById("spotify-icon-stopped").style.display="none"),t&&updateVisibility()}catch(t){console.error("Failed to fetch Spotify data",t)}}function updateProgressBar(t,e){if(0===e)return;const n=t/e*100,r=document.getElementById("spotify-progress");r.style.width=n+"%",r.setAttribute("aria-valuenow",t),r.setAttribute("aria-valuemax",e),r.setAttribute("aria-valuemin",0)}function animateProgressBar(){if(!trackDuration)return;const t=Date.now(),e=t-lastUpdateTime;if(lastUpdateTime=t,currentProgress+=e,currentProgress>trackDuration)return clearInterval(progressInterval),void updateSpotifyWidget();updateProgressBar(currentProgress,trackDuration)}updateSpotifyWidget(),setInterval(updateSpotifyWidget,5e3),document.getElementById("spotify-album-art").onclick=()=>{trackurl&&window.open(trackurl,"_blank")},document.getElementById("spotify-icon-playing").onclick=()=>{trackurl&&window.open(trackurl,"_blank")},document.getElementById("spotify-icon-paused").onclick=()=>{trackurl&&window.open(trackurl,"_blank")},document.getElementById("spotify-icon-stopped").onclick=()=>{trackurl&&window.open(trackurl,"_blank")};

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">
<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>
<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 data-bs-toggle="tooltip" data-bss-tooltip="" title="{{repo_description}}" style="display: inline-block;">{{repo | safe}}</p>
</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 %}

103
templates/spotify.html Normal file
View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en-au">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Currently Listening | Nathan.Woodburn/</title>
<meta name="theme-color" content="#000000">
<link rel="canonical" href="https://nathan.woodburn.au/spotify">
<meta property="og:url" content="https://nathan.woodburn.au/spotify">
<meta name="fediverse:creator" content="@nathanwoodburn@mastodon.woodburn.au">
<meta name="twitter:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta property="og:type" content="website">
<meta property="og:image" content="https://nathan.woodburn.au/assets/img/profile.jpg">
<meta name="twitter:description" content="See what I'm currently listening to">
<meta property="og:title" content="Currently Listening | Nathan.Woodburn/">
<meta name="description" content="See what I'm currently listening to">
<meta name="twitter:title" content="Currently Listening | Nathan.Woodburn/">
<meta name="twitter:card" content="summary">
<meta property="og:description" content="See what I'm currently listening to">
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/img/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="180x180" href="/assets/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/img/favicon/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="512x512" href="/assets/img/favicon/android-chrome-512x512.png">
<link rel="stylesheet" href="/assets/bootstrap/css/bootstrap.min.css">
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Cabin:700&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Anonymous+Pro&amp;display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap">
<link rel="stylesheet" href="/assets/fonts/font-awesome.min.css">
<link rel="stylesheet" href="/assets/css/styles.min.css">
<link rel="stylesheet" href="/assets/css/brand-reveal.min.css">
<link rel="stylesheet" href="/assets/css/profile.min.css">
<link rel="stylesheet" href="/assets/css/Social-Icons.min.css">
<link rel="me" href="https://mastodon.woodburn.au/@nathanwoodburn" />
<script async src="https://umami.woodburn.au/script.js" data-website-id="6a55028e-aad3-481c-9a37-3e096ff75589"></script>
</head>
<body id="page-top" data-bs-spy="scroll" data-bs-target="#mainNav" data-bs-offset="77">
<nav class="navbar navbar-expand-md fixed-top navbar-light" id="mainNav" style="background: var(--bs-navbar-hover-color);">
<div class="container-fluid"><a class="navbar-brand" href="/#">
<div style="padding-right: 1em;display: inline-flex;">
<div class="slider"><span>/</span></div><span class="brand">Nathan.Woodburn</span>
</div>
</a><button class="navbar-toggler navbar-toggler-right" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" type="button" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation" value="Menu"><i class="fa fa-bars"></i></button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item nav-link"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/hosting">Hosting</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/tools">Tools</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/blog">Blog</a></li>
<li class="nav-item nav-link"><a class="nav-link" href="/now">Now</a></li>
</ul>
</div>
</div>
</nav>
<header class="masthead" style="background: url(&quot;/assets/img/bg/projects.webp&quot;) bottom / cover no-repeat;height: auto;padding-top: 20px;">
<div style="margin-top: 150px;margin-bottom: 100px;">
<div class="container">
<div class="row">
<div class="col-lg-8 mx-auto">
<h1 class="brand-heading">Spotify</h1>
</div>
</div>
</div>
</div>
</header>
<section>
<div class="container" style="max-width: 500px;">
<div class="text-center" style="margin-top: 50px;margin-bottom: 50px;">
<div style="max-width: 500px;margin: auto;"><img class="img-fluid rounded-5 w-100 h-100" id="spotify-album-art" alt="Album Art" src="{{ track.album_art }}"></div>
</div>
<div style="position: relative;width: 100%;text-align: end;height: 0px;"><span id="join-btn" style="font-size: 4em;"><svg class="bi bi-play-circle-fill spotify-icon" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" data-bs-toggle="tooltip" data-bss-tooltip="" id="spotify-icon-playing" style="display: none;" title="Open in spotify">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.79 5.093A.5.5 0 0 0 6 5.5v5a.5.5 0 0 0 .79.407l3.5-2.5a.5.5 0 0 0 0-.814l-3.5-2.5"></path>
</svg><svg class="bi bi-pause-circle-fill spotify-icon" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" data-bs-toggle="tooltip" data-bss-tooltip="" id="spotify-icon-paused" style="display: none;" title="Open in spotify">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.25 5C5.56 5 5 5.56 5 6.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C7.5 5.56 6.94 5 6.25 5m3.5 0c-.69 0-1.25.56-1.25 1.25v3.5a1.25 1.25 0 1 0 2.5 0v-3.5C11 5.56 10.44 5 9.75 5"></path>
</svg><svg class="bi bi-stop-circle-fill spotify-icon" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" data-bs-toggle="tooltip" data-bss-tooltip="" id="spotify-icon-stopped" title="Open in spotify">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M6.5 5A1.5 1.5 0 0 0 5 6.5v3A1.5 1.5 0 0 0 6.5 11h3A1.5 1.5 0 0 0 11 9.5v-3A1.5 1.5 0 0 0 9.5 5z"></path>
</svg></span></div>
<h2 id="spotify-song" class="no-title" style="margin-bottom: 0px;">{{ track.song_name }}</h2>
<h3 id="spotify-artist" class="no-title" style="margin-bottom: 0px;"><strong>{{ track.artist }}</strong></h3>
<h4 id="spotify-album" class="no-title">{{ track.album_name }}</h4>
<div class="progress">
<div class="bg-primary progress-bar" id="spotify-progress" aria-valuenow="{{track.progress_ms}}" aria-valuemax="{{track.duration_ms}}" aria-valuemin="0" style="width: calc(({{ track.progress_ms }} / {{ track.duration_ms }}) * 100%);"></div>
</div>
</div>
</section>
<footer>
<div class="container text-center">
<p class="copyright">Copyright ©&nbsp;Nathan.Woodburn/ 2026</p>
</div>
</footer>
<script src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script src="/assets/js/script.min.js"></script>
<script src="/assets/js/grayscale.min.js"></script>
<script src="/assets/js/hacker.min.js"></script>
<script src="/assets/js/spotify.min.js"></script>
</body>
</html>

View File

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