sgregoratto.me

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

commit c1fa56f677a11273956b3d14a94bccd9d9b68fe8
parent 9df645b9662947f451d9e84e52de49d40db8eb39
Author: Stephen Gregoratto <dev@sgregoratto.me>
Date:   Thu, 14 Nov 2019 19:55:31 +1100

add simple-python-script article

Also modify the templates/css to look more like the standard-ebooks
stylesheet.

Diffstat:
Mcss/style.css | 106++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mposts | 6+++---
Asimple-lyrics-script-python.xml | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplates/post.xml | 1+
4 files changed, 205 insertions(+), 45 deletions(-)

diff --git a/css/style.css b/css/style.css @@ -1,29 +1,46 @@ body { margin: auto; - max-width: 38rem; - padding: 2rem; - width: auto; - font-family: sans-serif; - font-size: 1rem; - border: none; - vertical-align: baseline; - text-align: justify; - text-justify: inter-word; + max-width: 40em; hyphens: auto; } -header { - font-weight: bold; -} -p { - text-indent: 1rem; -} h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { font-size: inherit; } -blockquote + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, header + p, -hr + p, ol + p, ul + p, table + p, pre + p, code + p, p:first-child { +blockquote + p, h2 + p, h3 + p, h4 + p, h5 + p, h6 + p, header + p, hr + p, +ol + p, ul + p, table + p, code + p, pre + p, p:first-child { text-indent: 0; } +table { + width: 100%; + border-collapse: collapse; +} +tr:nth-child(even) { + background-color: #f2f2f2; +} +p { + margin: 0; + text-indent: 1em; +} +sup, sub { + height: 0; + line-height: 1; + vertical-align: baseline; + position: relative; +} +sup { + bottom: 1ex; +} +pre { + background-color: #f2f2f2; + border: 0.1rem solid #ccc; + border-radius: 0.5em; + overflow: auto; + word-wrap: normal; + font-family: monospace; + white-space: pre; + font-size: 9pt; + padding: 1em; +} hr { border: none; border-top: 1px solid; @@ -31,36 +48,41 @@ hr { margin: 1.5em auto; width: 25%; } -table { - width: 100%; - border-collapse: collapse; +q::before, q::after { + content: ''; } -figure { +h1, h2, h3, h4, h5, h6 { + font-variant: small-caps; + hyphens: none; + page-break-after: avoid; + page-break-inside: avoid; text-align: center; } -code { - font-family: monospace; - font-size: 9pt; +cite { + font-style: normal; } -pre { - background-color: #f2f2f2; - border: 0.1rem solid #ccc; - border-radius: 0.5em; - overflow: auto; - word-wrap: normal; - font-family: monospace; - white-space: pre; - font-size: 9pt; - padding: 1em; -} -blockquote { - padding: 0 0 0rem 2rem; - margin: 0rem; - border-left: 0.3rem #aaa solid; - color: #666; +abbr { + border: none; + white-space: nowrap; } -tr:nth-child(even) { - background-color: #f2f2f2; +blockquote cite { + display: block; + font-style: italic; + text-align: right; +} +blockquote cite i { + font-style: normal; +} +b, strong { + font-variant: small-caps; + font-weight: normal; +} +header { + page-break-inside: avoid; + text-align: center; +} +article > header + *, section > header + * { + margin-top: 1em; } @media print{ body{ diff --git a/posts b/posts @@ -1,4 +1,4 @@ # vim: ft=make -POSTS := \ - gpg-sync-all-pub-keys.xml - +POSTS = \ + gpg-sync-all-pub-keys.xml \ + simple-lyrics-script-python.xml diff --git a/simple-lyrics-script-python.xml b/simple-lyrics-script-python.xml @@ -0,0 +1,137 @@ +<?xml version="1.0" encoding="utf-8"?> +<article data-sblg-article="1"> + <header> + <h1>Making a Simple Lyrics Fetcher in Python</h1> + <time datetime="2019-11-13T20:18:39Z">November 13, 2019</time> + </header> + <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> + <code><pre>#!/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'</pre></code> +<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> +<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> +<code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span> +<span class="kn">import</span> <span class="nn">lyricsgenius</span> + +<span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span> +<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">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> +<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> +<p>This should spit out the following:</p> +<pre> +Searching for "Big Brother" by David Bowie … Done. +[Verse 1] +Don't talk of dust and roses +… +</pre> +<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> +<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> +<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> +<code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span> +<span class="kn">import</span> <span class="nn">lyricsgenius</span> +<span class="kn">import</span> <span class="nn">argparse</span> + +<span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span> + +<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> +<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> +<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> +<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> + +<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> +<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="n">title</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">title</span> + +<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> +<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> + <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> +<code><pre class="chroma">$ python test-script.py -a <span class="s2">"King Crimson"</span> -t <span class="s2">"One More Red Nightmare"</span> +[Verse 1] +Pan American nightmare +…</pre></code> +<p>Here comes the “hardest” part, detecting the currently playing song. My original script used <a href="https://github.com/altdesktop/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> +<code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span> +<span class="kn">import</span> <span class="nn">argparse</span> +<span class="kn">import</span> <span class="nn">gi</span> +<span class="kn">import</span> <span class="nn">lyricsgenius</span> +<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> +<span class="kn">from</span> <span class="nn">gi.repository</span> <span class="kn">import</span> <span class="n">Playerctl</span> + +<span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span> +<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> + +<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> +<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> +<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> +<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> + +<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> +<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> + +<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> +<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> + +<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> +<p>And now a simple test to see if it works:</p> +<code><pre class="chroma">$ playerctl metadata -f <span class="s1">'{{artist}} - {{title}}'</span> +Bruce Haack - Cherubic Hymn +$ python test-script.py +[Verse 1] +Come with me into the great winter +…</pre></code> +<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> +<code><pre class="chroma"><span class="ch">#!/usr/bin/env python</span> +<span class="kn">import</span> <span class="nn">argparse</span> +<span class="kn">import</span> <span class="nn">gi</span> +<span class="kn">import</span> <span class="nn">lyricsgenius</span> +<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> +<span class="kn">from</span> <span class="nn">gi.repository</span> <span class="kn">import</span> <span class="n">Playerctl</span> + +<span class="n">token</span> <span class="o">=</span> <span class="s2">"your-token-here"</span> +<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> + +<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> +<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> +<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> +<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> + +<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> +<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> + +<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> +<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> + +<span class="k">if</span> <span class="ow">not</span> <span class="n">song</span><span class="p">:</span> + <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> + +<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> +</pre></code> +<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> +<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> +<hr/> + <section class="footnotes" role="doc-endnotes"> + <p>References:</p> + <ol> + <li id="fn1" role="doc-endnote"> + <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> + </li> + </ol> + </section> +</article> diff --git a/templates/post.xml b/templates/post.xml @@ -11,6 +11,7 @@ <body> <article data-sblg-article="1" data-sblg-permlink="0" /> <nav> + <hr/> <a href="/">Home</a> <a href="/blog.html">Blog</a> <a href="/atom.xml">Feed</a>