sgregoratto.me

source files for www.sgregoratto.me
git clone git://git.sgregoratto.me/sgregoratto.me
Log | Files | Refs

simple-lyrics-script-python.xml (16667B)


      1 <?xml version="1.0" encoding="utf-8"?>
      2 <article data-sblg-article="1">
      3  <header>
      4  <h1>Making a Simple Lyrics Fetcher in Python</h1>
      5  <time datetime="2019-11-13T20:18:39Z">November 13, 2019</time>
      6 	</header>
      7 	<p>For the longest time I’ve used <a href="https://gist.github.com/febuiles/1549991">this script</a> to fetch the lyrics of any currently playing song, modified to use <code>rc(1)</code> from <a href="https://9fans.github.io/plan9port/">plan9port</a>:</p>
      8 	<code><pre>#!/usr/bin/env rc
      9 flagfmt='h,a artist,t title'
     10 url='https://makeitpersonal.co/lyrics'
     11 
     12 if(! ifs=() eval '{getflags $*} || ~ $#flagh 1){
     13 	usage
     14 	exit usage
     15 }
     16 
     17 if(~ $#flaga 1) artist=$flaga
     18 if not artist='{playerctl metadata artist}
     19 if(~ $#flagt 1) title=$flagt
     20 if not title='{playerctl metadata title}
     21 
     22 echo $"artist - $"title
     23 curl -fs --get $url \
     24 	--data-urlencode 'artist='$"artist \
     25 	--data-urlencode 'title='$"title |
     26 sed '$d'</pre></code>
     27 <p>It worked well, but I’d become increasingly frustrated with the quality coming from <a href="https://makeitpersonal.co">makeitpersonal</a>, and decided to rewite it to use <a href="https://genius.com">Genius</a> instead. To my amazement I found they offer their lyrics through an <a href="https://docs.genius.com/">API</a><sup><a class="footnote" href="#fn1" id="fnref1">1</a></sup>, completely for free. I began seaching if there were any bindings for Go, but the most <a href="https://github.com/rhnvrm/lyric-api-go">recently updated library</a> I found returned quite poor output. I did come across the <a href="https://github.com/johnwmillr/LyricsGenius">LyricsGenius</a> 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 <a href="https://learnpythonthehardway.org/">Zed A. Shaw’s book</a>. 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.</p>
     28 <p>To start, let's test that the library works correctly. This requires registering an account with Genius, creating a new <a href="https://genius.com/api-clients">API client</a> and generating a client access token. From here we can make a simple smoke-test:</p>
     29 <code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span>
     30 <span class="kn">import</span> <span class="nn">lyricsgenius</span>
     31 
     32 <span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span>
     33 <span class="n">genius</span> <span class="o">=</span> <span class="n">lyricsgenius</span><span class="o">.</span><span class="n">Genius</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
     34 <span class="n">song</span> <span class="o">=</span> <span class="n">genius</span><span class="o">.</span><span class="n">search_song</span><span class="p">(</span><span class="s2">"Big Brother"</span><span class="p">,</span> <span class="s2">"David Bowie"</span><span class="p">)</span>
     35 <span class="k">print</span><span class="p">(</span><span class="n">song</span><span class="o">.</span><span class="n">lyrics</span><span class="p">)</span></pre></code>
     36 <p>This should spit out the following:</p>
     37 <pre>
     38 Searching for "Big Brother" by David Bowie … Done.
     39 [Verse 1]
     40 Don't talk of dust and roses
     41     42 </pre>
     43 <p>Everything works, but what’s up with that extraneous output? Looking at the library docs, the <code>Genius</code> constructor specifies the <code>verbose</code> boolean that controls these messages. For our purposes, we don’t need them:</p>
     44 <code><pre class="chroma"><span class="n">genius</span> <span class="o">=</span> <span class="n">lyricsgenius</span><span class="o">.</span><span class="n">Genius</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span></pre></code>
     45 <p>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:</p>
     46 <code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span>
     47 <span class="kn">import</span> <span class="nn">lyricsgenius</span>
     48 <span class="kn">import</span> <span class="nn">argparse</span>
     49 
     50 <span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span>
     51 
     52 <span class="n">argparser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
     53 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-a"</span><span class="p">,</span> <span class="s2">"--artist"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song artist"</span><span class="p">)</span>
     54 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-t"</span><span class="p">,</span> <span class="s2">"--title"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song title"</span><span class="p">)</span>
     55 <span class="n">args</span> <span class="o">=</span> <span class="n">argparser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
     56 
     57 <span class="n">genius</span> <span class="o">=</span> <span class="n">lyricsgenius</span><span class="o">.</span><span class="n">Genius</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
     58 <span class="n">artist</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">artist</span>
     59 <span class="n">title</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span>
     60 
     61 <span class="n">song</span> <span class="o">=</span> <span class="n">genius</span><span class="o">.</span><span class="n">search_song</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">artist</span><span class="p">)</span>
     62 <span class="k">print</span><span class="p">(</span><span class="n">song</span><span class="o">.</span><span class="n">lyrics</span><span class="p">)</span></pre></code>
     63 	<p>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:</p>
     64 <code><pre class="chroma">$ python test-script.py -a <span class="s2">"King Crimson"</span> -t <span class="s2">"One More Red Nightmare"</span>
     65 [Verse 1]
     66 Pan American nightmare
     67 …</pre></code>
     68 <p>Here comes the “hardest” part, detecting the currently playing song. My original script used <a href="https://github.com/altdesktop/playerctl">playerctl</a>, a great little program that can control <a href="https://freedesktop.org/wiki/Specifications/mpris-spec/"><abbr class="initialism">MPRIS</abbr></a>-enabled players. It’s functionality is also exported as a Glib GObject, which we can access through the <a href="https://pygobject.readthedocs.io/en/latest/">PyGObject</a> library. The playerctl repo has a <a href="https://github.com/altdesktop/playerctl/tree/master/examples">couple examples</a> 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 <a href="https://raw.githubusercontent.com/altdesktop/playerctl/master/playerctl/playerctl-player.h">function definitions</a> of the playerctl <code>Player</code> object that I needed to use the <code>get_artist()</code> and <code>get_title()</code> methods:</p>
     69 <code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span>
     70 <span class="kn">import</span> <span class="nn">argparse</span>
     71 <span class="kn">import</span> <span class="nn">gi</span>
     72 <span class="kn">import</span> <span class="nn">lyricsgenius</span>
     73 <span class="n">gi</span><span class="o">.</span><span class="n">require_version</span><span class="p">(</span><span class="s1">'Playerctl'</span><span class="p">,</span> <span class="s1">'2.0'</span><span class="p">)</span>
     74 <span class="kn">from</span> <span class="nn">gi.repository</span> <span class="kn">import</span> <span class="n">Playerctl</span>
     75 
     76 <span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span>
     77 <span class="n">player</span> <span class="o">=</span> <span class="n">Playerctl</span><span class="o">.</span><span class="n">Player</span><span class="p">()</span>
     78 
     79 <span class="n">argparser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
     80 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-a"</span><span class="p">,</span> <span class="s2">"--artist"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song artist"</span><span class="p">)</span>
     81 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-t"</span><span class="p">,</span> <span class="s2">"--title"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song title"</span><span class="p">)</span>
     82 <span class="n">args</span> <span class="o">=</span> <span class="n">argparser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
     83 
     84 <span class="n">artist</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">artist</span> <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">artist</span> <span class="k">else</span> <span class="n">player</span><span class="o">.</span><span class="n">get_artist</span><span class="p">()</span>
     85 <span class="n">title</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span> <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span> <span class="k">else</span> <span class="n">player</span><span class="o">.</span><span class="n">get_title</span><span class="p">()</span>
     86 
     87 <span class="n">genius</span> <span class="o">=</span> <span class="n">lyricsgenius</span><span class="o">.</span><span class="n">Genius</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
     88 <span class="n">song</span> <span class="o">=</span> <span class="n">genius</span><span class="o">.</span><span class="n">search_song</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">artist</span><span class="p">)</span>
     89 
     90 <span class="k">print</span><span class="p">(</span><span class="n">song</span><span class="o">.</span><span class="n">lyrics</span><span class="p">)</span>
     91 </pre></code>
     92 <p>And now a simple test to see if it works:</p>
     93 <code><pre class="chroma">$ playerctl metadata -f <span class="s1">'{{artist}} - {{title}}'</span>
     94 Bruce Haack - Cherubic Hymn
     95 $ python test-script.py
     96 [Verse 1]
     97 Come with me into the great winter
     98 …</pre></code>
     99 <p>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 <code>search_song()</code> succeeded. Looking at its <a href="https://github.com/johnwmillr/LyricsGenius/blob/master/lyricsgenius/api.py#£232">definition</a> shows it returns <code class="bp">None</code> on failure. Lets check for that and print a simple error message:</p>
    100 <code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span>
    101 <span class="kn">import</span> <span class="nn">argparse</span>
    102 <span class="kn">import</span> <span class="nn">gi</span>
    103 <span class="kn">import</span> <span class="nn">lyricsgenius</span>
    104 <span class="n">gi</span><span class="o">.</span><span class="n">require_version</span><span class="p">(</span><span class="s1">'Playerctl'</span><span class="p">,</span> <span class="s1">'2.0'</span><span class="p">)</span>
    105 <span class="kn">from</span> <span class="nn">gi.repository</span> <span class="kn">import</span> <span class="n">Playerctl</span>
    106 
    107 <span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span>
    108 <span class="n">player</span> <span class="o">=</span> <span class="n">Playerctl</span><span class="o">.</span><span class="n">Player</span><span class="p">()</span>
    109 
    110 <span class="n">argparser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">()</span>
    111 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-a"</span><span class="p">,</span> <span class="s2">"--artist"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song artist"</span><span class="p">)</span>
    112 <span class="n">argparser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s2">"-t"</span><span class="p">,</span> <span class="s2">"--title"</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s2">"song title"</span><span class="p">)</span>
    113 <span class="n">args</span> <span class="o">=</span> <span class="n">argparser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
    114 
    115 <span class="n">artist</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">artist</span> <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">artist</span> <span class="k">else</span> <span class="n">player</span><span class="o">.</span><span class="n">get_artist</span><span class="p">()</span>
    116 <span class="n">title</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span> <span class="k">if</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span> <span class="k">else</span> <span class="n">player</span><span class="o">.</span><span class="n">get_title</span><span class="p">()</span>
    117 
    118 <span class="n">genius</span> <span class="o">=</span> <span class="n">lyricsgenius</span><span class="o">.</span><span class="n">Genius</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    119 <span class="n">song</span> <span class="o">=</span> <span class="n">genius</span><span class="o">.</span><span class="n">search_song</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">artist</span><span class="p">)</span>
    120 
    121 <span class="k">if</span> <span class="ow">not</span> <span class="n">song</span><span class="p">:</span>
    122  <span class="n">sys</span><span class="o">.</span><span class="nb">exit</span><span class="p">(</span><span class="n">f</span><span class="s2">"could not find lyrics for {artist} - {title}"</span><span class="p">)</span>
    123 
    124 <span class="k">print</span><span class="p">(</span><span class="n">f</span><span class="s2">"{artist} - {title}</span><span class="se">\n\n</span><span class="s2">{song.lyrics}"</span><span class="p">)</span>
    125 </pre></code>
    126 <p>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.</p>
    127 <p>Special thanks to John <abbr class="name">W.</abbr> Miller for maintaining the LyricsGenius package, Tony Crisci for maintaing Playerctl, and everyone else who works on high-quality open-source software. You rock!</p>
    128 	<section class="footnotes" role="doc-endnotes">
    129 		<p>References:</p>
    130 		<ol>
    131 			<li id="fn1" role="doc-endnote">
    132 				<p>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 <a href="https://www.johnwmillr.com/scraping-genius-lyrics/#scraping-song-lyrics">LyricsGenius does</a> behind the scenes. <a class="footnote-back" href="#fnref1">↩</a></p>
    133 			</li>
    134 		</ol>
    135 	</section>
    136 </article>