Carte

LastFM Now Playing/Server Heartbeat


python

Disclaimer up front. This is 100% based on the Raspberry Pi now playing from chorus.fm. However, instead of making a single page PHP application, written in Python using the Flask framework. I don’t currently have a raspberry pi running this 24/7, but that is something I’d like to do for a future project

App Creation

last.fm API

Step 1 is to get an API key. This is normally very easy and the API for last.fm is free (as far as I have seen). The end goal is to have a very low request volume application. I am going to update my Now Playing screen every 15 seconds and the Stats screen every 30 or 60 seconds. This isn’t a real time playback system so a slower update rate is fine with me. I may do similar to the original project and set up some shortcuts for whatever display solution I end up with.

Basic Flask System

While this is perfectly acceptable as a single page system, I wanted to actually make a small Flask site so that I can extend functions beyond just the now playing screen if I want. Thankfully, the philosophy of flask is to add on items as needed so with an application this simple, it is very light weight. The basic setup for the project is:

app
├── app.py
├── classes
├── requirements.txt
├── static
└── templates
    └── Components

app.py

app.py is the main entry point of the project when running with a python command. it will contain the if __name__ == __main__ check that will determine if the file is being run directly or loaded from an external program.

The basic Flask setup is

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello!</h1>'

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080, debug=True)

The two most important parts of this basic program are app = Flask(name) which actually imports associates the Flask server with the program and app.run(host=“0.0.0.0”, port=8080, debug=True) the host=‘0.0.0.0’ allows external machines to access the flask test server. If you are only running Flask locally, you don’t need this value. I chose to run the program during development on a virtual machine running Ubuntu. This will end up being the operating system I run this project on (spoiler: I’m using it as a health check like Jason Tate as well)

Now Playing

The now playing portion is simple enough. To follow Don’t Repeat Yourself (DRY) code I made a few helper functions for working with last.fm. I made class LastFmApi that utilizes the requests module for HTTP get operations and contains utilities to simplify the returned JSON from last.fm into only the parts I care about for my now playing screen. I could have saved some complexity by just throwing all this data into a python dictionary, but then I would lose intellisense when utilizing the information elsewhere in my project. Because of this I decided to make a class with methods for getting/setting artist info and images and return an array of these objects from my last.fm calls. I am still learning Front-End Development so for the styling I just stole the CSS from the inspiration project here I like the general look of it good enough for now and can modify/extend as I slowly improve upon this implementation

Stats

One thing I found missing from the original project was the idle screen was a little lacking from what last.fm can give you through the API. The largest deviation I have from the original is I modified the stats screen so that I retrieve a random set of stats when a song isn’t currently playing. To accomplish this I made to lists of sets, one for a time frame and one for the stats. I decided to use two lists because then I can operate the stat pulled and the time frame with their own randomness as well as keep how many records to pull for each stat (e.g. it is easier to fit more top albums on the stats page that top artist info). My goal is that I can also extend this to display top tracks and top tags from my account as well.

time_frames = [("All Time", "overall"), ("7 Days", "7day"), ("3 Months", "3month"), ("Month", "1month"), ("6 Months", "6month"), ("12 Months", "12month")]

stats = [("Top Albums", 12), ("Top Artists", 6)]

def get_top_section() -> tuple:
    data = None 
    time = random.randint(0, (len(time_frames) - 1))
    stat = random.randint(0, (len(stats) - 1))
    stat_type = stats[stat][0], f' - Last {time_frames[time][0]}' if time > 0 else ' - All Time'
    
    logger.write_log(message=f'Retrieving {"".join(stat_type)}')

    if stat == 0:
        data = last.process_albums(last.user_get_top_albums(period=time_frames[time][1], limit=stats[stat][1]))
    elif stat == 1:
        data = last.process_artists(last.user_get_top_artists(period=time_frames[time][1], limit=stats[stat][1]))
    else:
        raise ValueError(stat)

    return stat_type, data

For the MVP I am only handling Artist and Album data, but the logic will be easy enough to extend to tags and tracks once I get a chance. After I pull the stats I had to modify the logic for how the data is displayed depending on the stat pulled which was easy enough with flask’s if/else logic

Project Screen while song is playing

Screen shown when a song is playing

LastFM stats overview showing top artists

LastFM Top Albums stats page

LastFM Stats overview showing top artists

LastFM Top Artists stats page

App Deployment

Now came the fuzzier bit for me. I know how to complete simple deployments of sites and how to run backend services. So I figured now would be a great time to challenge myself and learn how to deploy my app as a custom docker image to Azure

Docker

Docker containers have become one of the largest skills in software in recent years. The basic idea is to make an image that is self contained for a service and use orchestration through Kubernetes or Docker Swarm to make an application that is resilient and can auto scale for the needs of your application. I am not good enough (yet) at Docker to go into the details of how docker works and the lifecycle. I can make an image of my current app and it is stored in a public docker hub I haven’t made a readme file yet but it can be pulled with docker pull corycarte/nowplaying. Right now I have the image with my API key and username, this is easy enough to remedy with a bind mount to where the image is run. I would want to document that in the README before completing that change though. One last annoyance of note. When I graduated last April with my Bachelor’s, I treated myself and bought a fancy new M1 Macbook Pro, when building my docker images with it it builds the ARM version. This version doesn’t run on the server I want to deploy it to so I have to make sure I tag the version as ARM and build a separate version for other machines. I am sure there is a remedy to this, I just haven’t researched it yet (topic for the future).

Deployment Option 1: Azure Web App Service

The deployment option for the future is to run this as a scalable microservice in the cloud. I chose to attempt Azure since I use it at work anyway so let’s try and get it running in their Web App Service.

Step One: Azure Resource Group

Azure resources are contained within resource groups, since this is a test only I made a specific test-rg resource group to hold my application. The steps are very straight-forward and are detailed here

Step Two: Azure container registry

Azure like all the cloud services has their own private container repos. These can be used as a part of a CI/CD pipeline for docker containers so let’s go ahead and set one up. Similar to the resource group. Straightforward to set up, detailed here

Step Three: Web App

The final step is to actually get your container up and running. I followed all the steps here and… well it never worked for me. I also went through with the demo and it still gave me a 500 status when attempting to navigate to the deployed app. This wasn’t the way I wanted to deploy this since I want to keep it as a heartbeat on a server so the Azure Web App not working isn’t the end of the world.

Deployment Option 2: Nginx

Keeping in fully ripping off chrous.fm, I can deploy this container image onto an existing server and use it as a health check. This for sure maintains more of a pet than cattle relationship with the given server. I am not building a Highly Available service and caring over a couple of servers that run various personal projects isn’t that much of a hassle. This approach (at least the way I want to do it) does require some updates to my webserver. Currently Nginx is the server of choice for small scale projects in lieu of the Apache Http Server. This isn’t to say that one is superior, I just want to us Nginx for this. Since I am deploying a Docker container, I will expose a port on the server to get my HTTP Request into the container. I will accomplish the routing of this request from the open web through Nginx by setting up proxy pass and a rewrite rule. Something fun I dealt with (and it may be due to being new with Nginx) is though the proxy worked properly for my flask app and css files, the base.js file I made wasn’t being properly retrieved. Not too big of a deal since all I am using that file for is to refresh the page periodically so instead of solving this issue, I just added a script tag in my base.html template with the refresh function. Is this the best solution, no. However, it works.

Conclusion

Overall, simple to get up and running on my server to keep an eye on it. The main thing I want to do in the future is to modify the CSS to make it more of my own project vs a (mostly) complete ripoff project. I’ve used Flask minimally in the past though I was still able to learn about Jinja macros and Nginx reverse proxy into Docker containers. The project code is up on my github now