Making a Simple Lyrics Fetcher in Python

For the longest time I’ve used this script to fetch the lyrics of any currently playing song, modified to use rc(1) from plan9port:

#!/usr/bin/env rc
flagfmt='h,a artist,t title'
url='https://makeitpersonal.co/lyrics'

if(! ifs=() eval '{getflags $*} || ~ $#flagh 1){
	usage
	exit usage
}

if(~ $#flaga 1) artist=$flaga
if not artist='{playerctl metadata artist}
if(~ $#flagt 1) title=$flagt
if not title='{playerctl metadata title}

echo $"artist - $"title
curl -fs --get $url \
	--data-urlencode 'artist='$"artist \
	--data-urlencode 'title='$"title |
sed '$d'

It worked well, but I’d become increasingly frustrated with the quality coming from makeitpersonal, and decided to rewite it to use Genius instead. To my amazement I found they offer their lyrics through an API1, completely for free. I began seaching if there were any bindings for Go, but the most recently updated library I found returned quite poor output. I did come across the LyricsGenius Python library and⁠—after some tests⁠—found it to be perfect for my needs. Now, the last time I used Python was in 2012, where I gave up ⅓ of the way through Zed A. Shaw’s book. But with my previous knowledge and a bit of Google-fu, I made it work. So let’s see how we can use the LyricsGenius library to make an automated lyrics fetcher.

To start, let's test that the library works correctly. This requires registering an account with Genius, creating a new API client and generating a client access token. From here we can make a simple smoke-test:

#!/usr/bin/env python
import lyricsgenius

token = "your-token-here"
genius = lyricsgenius.Genius(token)
song = genius.search_song("Big Brother", "David Bowie")
print(song.lyrics)

This should spit out the following:

Searching for "Big Brother" by David Bowie … Done.
[Verse 1]
Don't talk of dust and roses
…

Everything works, but what’s up with that extraneous output? Looking at the library docs, the Genius constructor specifies the verbose boolean that controls these messages. For our purposes, we don’t need them:

genius = lyricsgenius.Genius(token, verbose=False)

Now lets add the option parsing. The easiest way of doing this is through the argparse library. A couple lines of code is all we need to get equivalent functionality:

#!/usr/bin/env python
import lyricsgenius
import argparse

token = "your-token-here"

argparser = argparse.ArgumentParser()
argparser.add_argument("-a", "--artist", help="song artist")
argparser.add_argument("-t", "--title", help="song title")
args = argparser.parse_args()

genius = lyricsgenius.Genius(token, verbose=False)
artist = args.artist
title = args.title

song = genius.search_song(title, artist)
print(song.lyrics)

Long options aren’t really my style, but they’re the only way to get argparse to print the proper option argument for the help text. In any case, we can test this new iteration with a different song:

$ python test-script.py -a "King Crimson" -t "One More Red Nightmare"
[Verse 1]
Pan American nightmare
…

Here comes the “hardest” part, detecting the currently playing song. My original script used playerctl, a great little program that can control MPRIS-enabled players. It’s functionality is also exported as a Glib GObject, which we can access through the PyGObject library. The playerctl repo has a couple examples for using it with Python, but I had some trouble simply getting the song artist and title. It wasn’t until I looked at the function definitions of the playerctl Player object that I needed to use the get_artist() and get_title() methods:

#!/usr/bin/env python
import argparse
import gi
import lyricsgenius
gi.require_version('Playerctl', '2.0')
from gi.repository import Playerctl

token = "your-token-here"
player = Playerctl.Player()

argparser = argparse.ArgumentParser()
argparser.add_argument("-a", "--artist", help="song artist")
argparser.add_argument("-t", "--title", help="song title")
args = argparser.parse_args()

artist = args.artist if args.artist else player.get_artist()
title = args.title if args.title else player.get_title()

genius = lyricsgenius.Genius(token, verbose=False)
song = genius.search_song(title, artist)

print(song.lyrics)

And now a simple test to see if it works:

$ playerctl metadata -f '{{artist}} - {{title}}'
Bruce Haack - Cherubic Hymn
$ python test-script.py
[Verse 1]
Come with me into the great winter
…

To finish off, let’s add the song title and artist at the top of the output. While we’re at it, we should check if search_song() succeeded. Looking at its definition shows it returns None on failure. Lets check for that and print a simple error message:

#!/usr/bin/env python
import argparse
import gi
import lyricsgenius
gi.require_version('Playerctl', '2.0')
from gi.repository import Playerctl

token = "your-token-here"
player = Playerctl.Player()

argparser = argparse.ArgumentParser()
argparser.add_argument("-a", "--artist", help="song artist")
argparser.add_argument("-t", "--title", help="song title")
args = argparser.parse_args()

artist = args.artist if args.artist else player.get_artist()
title = args.title if args.title else player.get_title()

genius = lyricsgenius.Genius(token, verbose=False)
song = genius.search_song(title, artist)

if not song:
 sys.exit(f"could not find lyrics for {artist} - {title}")

print(f"{artist} - {title}\n\n{song.lyrics}")

And we’re finished! The only thing missing is for the Genius token to be acquired securely, but I’ll leave that as an exercise for the reader.

Special thanks to John W. Miller for maintaining the LyricsGenius package, Tony Crisci for maintaing Playerctl, and everyone else who works on high-quality open-source software. You rock!

References:

  1. This isn’t entirely true. The API only gives you link to the lyrics page, but you can use a HTML parser to extract them. This is what LyricsGenius does behind the scenes.