(λ (x) (create x) '(knowledge))

One Thousand & One Albums

And offline first consumption · February 4th, 2024

There is this delightful project that first existed as a book, kept up to date every few handful of years, and now as a little web services complete with a simple json API. That project is 1001 Albums to listen to before you die and its modern iteration is a simple (but excellently built!) web generator that hands out one album each day until you've listened to everything on the list, and then it swaps over to a list of community generated suggestions if you really love the idea. Now as you can imagine that little project will take a while to get through with so much music, a 2 3/4 years precisely. And that is a long time to remember to do things.

Hell I can barely remember to charge things some days, how will I ever remember to keep track of a new album every single day? I could probably just pull the list manually, download everything, and listen to it straight! That'd get it done faster, but I doubt I'd remember anything. Also the temptation to skip things I don't immediately like would be too high. No, it's much better to just keep with the theme, listen to things as they trickle in and enjoy the moment. All of this is fine in theory, you sign up, hop over to your project page, click a little link for your new album that takes you to a YouTube music, Spotify, Apple music, or something else. Listen to it in the android app, enjoy the moment. It's a nice little system, but I really don't want to use my phone for this sort of thing.

I've been waxing poetic about offline first design with my friends lately. I just really don't like what having a smart phone does to my attention span, and my ability to operate autonomously. Maybe some of this has been brought on by the recent birth of my daughter, but the fact of the matter is that I pick up a smartphone to listen to a randomly selected album, and then end up 30 minutes later having checked my email, my work email, work tickets, work projects, personal projects, Alpine merge requested, IRC, etc, etc, etc. The rabbit hole goes so freaking deep it isn't even funny. Some of it is a lack of impulse control on my part, some of it I firmly believe is the design of the modern smartphone itself.

See, I have less problems when the system I use are purposefully designed. I trim down my computers to be driven by the CLI primarily, there's nothing distracting about text, and I have to have intention to do something as there's a mental barrier to accomplishing a task. Say for example I want to check my email, mutt gives me a nice distraction free environment. And if I want to move on from mutt to say YouTube, I need to completely context switch and open a separate application. But if I'm checking my email using an app, then YouTube is one or two finger pokes away from sweet sweet distraction! And usually speaking these systems drag you in, I usually don't want to check my work email or support tickets. But if I'm on my phone and sometime pops up, I will click into it, and the spiral begins.

Yes I mitigate a lot of this by removing notifications, but some of it can't be prevented. I need notifications from Slack and email and monitoring systems. What is the point of these communication systems if they do not alert when things go wrong? No, there really is no good solution there that involves turning off notifications so that I can use my android phone the way I want. Instead I've been slowly tinkering with the idea of digital minimalism through physical maximal-ism. Cheeky right?

What I mean by this is that I'm actively reducing my smart phone usage, finding ways to offload what I do to systems that I can control/configure in a way that is respectful of my time and how I want to use it. So this little music generator, is a fun quick and easy example of that ideology that I was able to squeeze into a few minutes here and there, with great results! By using a dedicated mp3 player, and a little bit of Fennel, I was able to throw together a little script that fetches the latest album from the 1001 generator and downloads it from YouTube for offline listening. Then once I've reviewed the music I can remove it, review the album on my netbook, and grab the next iteration. Instead of getting sucked into a rabbit hole just checking the album cycle, I built a process that revolves around a primarily offline device, and that little bit of friction makes a difference to me currently.

Anyways that's enough waxing poetic about why, we all know the what is the cool part. Under the hood this little fennel script just pings the API for your given project, pulls the current album name and artist, then invokes yt-dlp to pull down MP3s. It'll dump those into whatever directory you want, and fortunately the little AGPTEK mp3 player I'm using has a removable micro SD card, so I can just point the script directly at /mnt and run it after I mount the SD card.

(local json (require :cjson))
(local https (require :ssl.https))
(local ltn12 (require :ltn12))
(local mime (require :mime))
(local lfs (require :lfs))
(local inspect (require :inspect))

(var conf {"media" "/mnt/"
           "project" ""})

;;Check if a directory exists. Could probably replace this with a lfs call..
(fn util/dir_exists? [dir]
      [(stat err code) (os.rename dir dir)]
    (when (= stat nil)
      (when (= err 13)
        (lua "return true"))
      (lua "return false"))

;;Fetch a url and return the results as a string
(fn get [link]
      [resp {}
       (r c h s) (https.request {"url" link
                                 "sink" (ltn12.sink.table resp)
                                 "method" "GET"
                                 "headers" {"content-type" "application/json"}
    (table.concat resp)))

;;Invoke yt-dlp on the given url, embed thumbnail and metadata, output to specified directory
(fn ytdlp [link dir]
      [cmd "yt-dlp -f 'ba' -x --embed-thumbnail --audio-format mp3 --add-metadata -o "
       yt "https://music.youtube.com/playlist?list="]
    (os.execute (.. cmd "'" dir "/%(title)s.%(ext)s' " yt link))))

;;The magic happens here, skim the 1001 api and then decode the json to a lua table
    [req (get (.. "https://1001albumsgenerator.com/api/v1/projects/" conf.project))
     data (json.decode req)]

;;Do some simple error handling, just in case we hit a rate limit or something.
  (if (= (. data "error") true)
        (print (. data "errorCode"))
        (print (. data "message"))

;;Extract the artist and album name
      [artist (. data "currentAlbum" "artist")
       album (. data "currentAlbum" "name")]

;;If either are nil abort, something is wrong with the API return
    (if (or (= artist nil) (= album nil))
          (print "Invalid data fetched from API! Dumping payload and aborting.")
          (print (inspect data))))

;;But if we get names for these things, check to see if they exist at the path already (maybe it's a pre-existing artist or album?)
    (if (not (util/dir_exists? (.. conf.media artist)))
          (print (.. "New Artist! [" artist "]"))
          (lfs.mkdir (.. conf.media artist))))

;;If the album specifically doesn't exist already, invoke yt-dlp!
    (if (not (util/dir_exists? (.. conf.media artist "/" album)))
          (print (.. "New Album! [" album "]"))
          (lfs.mkdir (.. conf.media artist "/" album))
          (ytdlp (. data "currentAlbum" "youtubeMusicId") (.. conf.media artist "/" album)))
        (print "No new album, skipping.."))))

It's pretty cool to think that ~70 lines of fennel and I have a nice functional offline method to participate in this little musical roulette. It's things like this that make me consistently reach for it when I just want to get things done. The convergence between lua's simplicity and ecosystem, and the lisp syntax itself is just amazing in my mind. Nim feels a lot like this too.

Anyways, if for some reason after all of this you want to see my ratings for some specific reason, they can be found here. But it's probably far more interesting if you sign up and following along yourself. How knows, you might find something interesting too!


(defparameter *Will_Sinatra* '((Age . 31) (Occupation . DevOps Engineer) (FOSS-Dev . true) (Locale . Maine) (Languages . ("Lisp" "Fennel" "Lua" "Go" "Nim")) (Certs . ("LFCS"))))

"Very little indeed is needed to live a happy life." - Aurelius