So I love it when people have a list of recently listened to music or recently watched movies or books and wanted to add something similar to this site. The only problem is that Hugo is a static website generator. Every page is created and stored as is, not pulled from a database. That left me with only a few options if I wanted to include this file within my hugo blog (rather than host the page outside of it and just link to it from hugo). What I decided on in the end was to create a github workflow that in combination with a small python script would keep a regular check on my recently listened to list from last.fm via their API. From that list it would then update a music page if it notices new content, running every few hours to check to see if there’s been an update. I added all of this to a github repo if you want to skip the long version. You can find that here . Otherwise here is how I created Brentter.com/music/ :

How do you track what you listen to? What is Scrobbling?

Scrobbling is the term for sharing what content you are watching/listening to across other services. There are a bunch of sites like Last.fm which can directly interact with your media apps like Youtube, Spotify and Itunes to record what you are playing. The purpose is to get a better idea of what type of music you have enjoyed over time as well as help introduce you to new music based on your listening habits. You can enable this feature in the settings or options area of most media apps. While there are a bunch of sites that you can use to keep track of this data, I chose last.fm because of how easy it is to use (and free). So you will need a last.fm account for this as well as an API key.

Create a last.fm account and get an API key

Head on over to Last.fm and create an account. You will then need to head over to here to create the API key. Just put anything in for the application description. You can even use localhost as the callback url. The next screen will show you your application API key. Write this down somewhere safe.

Enable scrobbling in your media apps

This is just going to be for spotify but you can do the same for any other media application that you prefer. Log into your last.fm account, go to Settings then click Applications. Click on the connect to Spotify button and it will have you verify your credentials with spotify and that you are allowing last.fm to connect to your account. Once approved last.fm will keep track of what you listen to!

You Will Now Need To Add A New Shortcode

So in order for the list of recently listened to music to show up properly you’ll need to show a little bit of HTML code on the page. Hugo typically will remove any HTML tags on any pages unless you add a shortcode for displaying raw HTML. To do this create new shortcode template in layouts/shortcode/rawhtml.html

Then place the following inside it:

layouts/shortcode/rawhtml.html

<!-- raw html -->
{{.Inner}}

Now on your markdown page you’ll be able to use it like this;

{{< rawhtml >}} HTML GOES HERE {{< /rawhtml >}}

Here is the python script to update the page

You will need to go into your github repo, click settings then actions and add two new secrets - LASTFM_API_KEY and LASTFM_USERNAME with the appropriate info.

generate_markdown.py

import requests
import xml.etree.ElementTree as ET
from datetime import datetime
import os
import sys
import json

# Load track data from JSON file instead of fetching it directly from API.
try:
    with open("lastfm_tracks.json", "r") as f:
         response = json.load(f)

except FileNotFoundError as e:
    print(f"Error loading track data file {e}")
    sys.exit(1)  # Exit the script with a non-zero status to indicate failure.

# Extract track information from loaded JSON data.
tracks = []
for track in response['recenttracks']['track']:
    artist = track['artist']['#text'] if 'artist' in track else ""
    name = track['name'] if 'name' in track else ""
    album = track['album']['#text'] if 'album' in track else ""
    url = track['url'] if 'url' in track else ""

    # Correctly extract large image URL
    image_url = next((img['#text'] for img in track.get('image', []) if img.get('size') == 'large'), "")

    date_listened_full = track['date']['#text'] if 'date' in track else ""

    date_listened_date_only = date_listened_full.split(',')[0] if date_listened_full else ""

    tracks.append({
         'artist': artist,
         'name': name,
         'album': album,
         'url': url,
         'image_url': image_url,
         'date_listened': date_listened_date_only,
     })


# Get current date and time in ISO 8601 format with timezone offset
current_datetime = datetime.now().astimezone().isoformat()

# Generate Markdown content with Hugo front matter and embedded HTML for styling
markdown_content=f'''---
title: "Music"
date: "{current_datetime}"
#YOUR FRONTPLATE INFO GOES HERE
#I included how it changes the date to the current one every time it runs with {current_datetime}
#If you prefer to use the lastupdated option instead go for it
---

## Last Ten Tracks Listened To From Last.fm

{{{{< rawhtml >}}}}

<ul class="track-list">
'''

for t in tracks :
   markdown_content+= f'''
<li class="track-item">
<div class="track-image"><img src="{t['image_url']}" alt="{t['name']}"></div>
<div class="track-details">
<h3><a href="{t['url']}" target="_blank">{t['name']}</a></h3>
<p><strong>Artist:</strong> {t['artist']}<br>
<strong>Album:</strong> {t['album']}<br>
<strong>Date Listened To:</strong> {t['date_listened']}</p>
</div>
</li>'''

markdown_content += '''
</ul >
{{< /rawhtml >}}
'''

# Write to a markdown file- Change to whatever folder or filename you want the results placed in. 
with open("content/music.md", "w") as f:
     f.write(markdown_content)

Now what this does is it uses the lastfm_tracks.json file that your workflow will create from the last.fm API and generates a music.md file in your content folder. It currently is set to show the last 15 tracks but if you’d like to change that just scroll down and in the workflow change the appropriate line. You will also want to add all your frontplate information where noted as well as double-check where you’d like the generated file to go. When pushed live this page would be at yourwebsite.com/music

Now to the CSS to make it look decent

Depending on what theme you are using you are going to have to find what css file works for the type of page or post you are placing this in. For me that was post-single.css. All you have to do is copy that css file from your theme folder to your own assets/css/common/ folder and at the end add the following:

.css

.track-item {
    border-radius :8px; box-shadow :0 2px 4px rgba(0 ,0 ,0 ,.1); margin-bottom :20px; overflow:hidden; display:flex; align-items:center;
}

.track-image {
width:auto;height:auto;margin-right:.5em;
}

.track-image img{
max-width :100%;height:auto;border-radius:.5em;
}

.track-details{
padding:.5em;width:auto;
}

.track-details h3{
margin-top :0;margin-bottom :.25em;font-size :
1.25em;} p{margin :.25em;font-size :.9em;color:d3d3d3;}

Now to keep this updated!

In order to keep this regularly updated we need to create a github workflow. What this will do is grab the latest list of songs recorded by last.fm’s API and save it as a json file named lastfm_tracks.json. That’s what our python script will use to fill out the music.md page as well as what the workflow will check against to see if there has been an update to our recently listened to list. More information on workflows can be found here . In your base directory create two new folders .github and then inside that create a folder named workflows (so /.github/workflows/) In the workflows folder we are going to create a file named update_music_page.yml with the following code:

update_music_page.yml

name: Update Last 15 Tracks

on:
  schedule:
    - cron: '0 */4 * * *' # Runs every 4 hours (adjust as needed)
  push:
    branches:
      - main # or your default branch

jobs:
  update-tracks:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11.2'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install requests jq          
      - name: Check for new tracks on Last.fm
        id: check_tracks
        env:
          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}
          LASTFM_USERNAME: ${{ secrets.LASTFM_USERNAME }}
        run: |
          # Load last fetched tracks from file or set default value if not exists
          if [ -f ./lastfm_tracks.json ]; then  # Look in the current directory (root)
            LAST_TRACKS=$(cat ./lastfm_tracks.json)
            LAST_TIMESTAMP=$(echo "$LAST_TRACKS" | jq '.recenttracks.track[-1].date["uts"]' | tr -d '"')
          else
            LAST_TRACKS="{}"
            LAST_TIMESTAMP=0 # Default value if no previous data exists.
          fi

          # Fetch recent tracks from Last.fm API and filter by timestamp. Change # after limit if you want something other than the latest 15 tracks. 
          RESPONSE=$(curl -s "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${LASTFM_USERNAME}&limit=15&api_key=${LASTFM_API_KEY}&format=json")

          # Print response for debugging purposes
          echo "API Response: $RESPONSE"

          # Check for valid response before processing
          if [[ "$RESPONSE" == *"error"* ]]; then
            echo "Error fetching data from Last.fm API."
            exit 1  # Exit with error code.
          fi

          NEW_TRACKS=$(echo "$RESPONSE" | jq --arg LAST_TIMESTAMP "$LAST_TIMESTAMP" '.recenttracks.track | map(select(.date.uts != null and (.date.uts | tonumber > ($LAST_TIMESTAMP | tonumber))))')

          echo "New Tracks Data: $NEW_TRACKS"

          NEW_TRACKS_FOUND=false

          if [ "$(echo "$NEW_TRACKS" | wc -l)" -gt 0 ]; then 
            echo "New tracks found."
            NEW_TRACKS_FOUND=true
            
            # Save the latest track information to a JSON file in root directory.
            TRACK_DATA=$(echo "$RESPONSE" | jq '.recenttracks.track')
            
            echo "Writing new track data to lastfm_tracks.json..."
            
            echo "{\"recenttracks\":{\"track\":$TRACK_DATA}}" > ./lastfm_tracks.json
            
            if [ $? -eq 0 ]; then
              echo "Successfully wrote to lastfm_tracks.json."
              # Get latest timestamp from new tracks for next run comparison.
              LATEST_TIMESTAMP=$(echo "$NEW_TRACKS" | jq '.[-1].date.uts // empty' | tr -d '"')
              echo "Latest timestamp updated to $LATEST_TIMESTAMP."
              
              # Set output as environment variable
              echo "has_new_tracks=true" >> $GITHUB_ENV

              # Check for changes before committing
              git config --global user.name "${{ github.actor }}"
              git config --global user.email "${{ github.actor }}@users.noreply.github.com"
              
              git add ./lastfm_tracks.json 
              
              # Only commit if there are changes in lastfm_tracks.json.
              git diff-index --quiet HEAD || git commit -m "Update lastfm_tracks.json with new tracks"
              
           else 
             echo "Failed to write to lastfm_tracks.json."
             exit 1  # Exit with error code on failure.
           fi
            
          else
            echo "No new tracks found."
            # Set output as environment variable
            echo "has_new_tracks=false" >> $GITHUB_ENV
          fi          


      # Check for changes in git files (including pushes)
      - name: Check for new commits or changes in lastfm_tracks.json
        id: check_git_changes
        run: |
          git fetch origin main 
          
          # Check if there are any changes between HEAD and origin/main (the last commit)
          if ! git diff --quiet HEAD origin/main; then
            echo "has_new_git_changes=true" >> $GITHUB_ENV
          else
            echo "has_new_git_changes=false" >> $GITHUB_ENV
          fi
                          
      # Conditional execution based on whether new tracks were found or there are new commits/pushes.
      - name: Run script to generate markdown file
        if: env.has_new_tracks == 'true' || env.has_new_git_changes == 'true'
        env:
          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}
          LASTFM_USERNAME: ${{ secrets.LASTFM_USERNAME }}
        run: |
           python generate_music_markdown.py           

      - name: Commit changes
        if: env.has_new_tracks == 'true' || env.has_new_git_changes == 'true'
        run : |
           git config --global user.name "${{ github.actor }}"
           git config --global user.email "${{ github.actor }}@users.noreply.github.com"
           
           git add content/music.md lastfm_tracks.json 
           
           # Only commit if there are changes in music.md or timestamps.
           git diff-index --quiet HEAD || git commit -m "Update music.md and track list"           

      - name: Push changes back to repository
        if: env.has_new_tracks == 'true' || env.has_new_git_changes == 'true'
        run: |
           git config --global user.name "${{ github.actor }}"
           git config --global user.email "${{ github.actor }}@users.noreply.github.com"

           # Push changes to main branch
           git checkout main
           git push origin main           
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}EN }}
	  branch : main

This is set to run every 4 hours but you can change that to however frequent you would prefer by changing the line under cron. You should also change the folder and file that it pushes to your git repository unless you want to keep it at contents/music.md.

Git add, commit and push to your repo!

Hopefully there won’t be any errors along the way and it should run the python script, create your new music page and stay dormant until it is ready to update again. What this will not do is auto-push to your hosting service unless your repo is already setup with either another workflow or service like cloudflare where it will automatically update anytime there is a new build. Pushing it to your host of choice though only needs adding another job entry in your yml file though so it should be simple enough. If there are errors you can see them under Actions in your github repo. I currently have it giving updates as it goes through each process so finding where the problem is shouldn’t be too hard.

Hopefully this helps some of you, please use it and create something much cooler as this is pretty basic but let me know what you have made so I can check it out!

Enjoy!

~Brent