I don’t like using services like Spotify, Apple Music or Amazon Prime, as they need DRM. YouTube Music, works fine but — but you need a Google Account!

Previously I manually downloaded playlists from YouTube using yt-dlp and maintained a tagged collection of my favourite songs. I would discover new artists using LastFM and listen using my browser. Fast forward to August this year — I started attending university: sharks ahead >–< spotty internet! Even LTE is of no help in this part of the city. Getting the site to load was a task in of itself. It was getting annoying.

I wanted to fetch my LastFM music recommendations using yt-dlp, without needing to open a web-browser. I made a bash script called /jukebox: it lets you play your LastFM music recommendations on GNU/Linux. It fits into the UNIX ecosystem. It does one thing, and does it properly.

Get started

git clone

After cloning, cd into the jukebox folder and make sure you have mpd setup. Install the prerequisites as prescribed in the /readme.

# setting perms to make this program executeable
chmod +x jukebox
jukebox -h
  -a: <artist name>
  -g: <genre name>
  -s: <song name>
  -u: <lastfm: username>
  -m: <lastfm: mix | recommended | library>
  -p: <lastfm: playlist url>
  -l: vosk: search songs with your voice
  -y: use ytmusic tags for songs
  -c: top charts in your region (setup your api_key)


Time for a little webscraping and reverse-engineering!

I went to the LastFM site. After clicking the play-mix button a little YouTube iframe-window pops-up and it automatically starts playing the song one after-the-other. You get to see how it’s scrobbling with-in the browser.

I assumed this was using some api as these were regular YouTube videos, manually tagged by fellow LastFM users (like how you can edit wiki pages)


After inspecting the network-requests the site was making, LastFM had a simple json endpoint—that returns a playlist of songs that the browser needs to play.

Let’s try writing a script for it!

First we’ll use curl(1) to fetch the response and understand how it works!

# $username and $mode is your LastFM username & playback mode of choice
response=$(curl -s "${username}/${mode}")

The response 👀

We see are in search of the title, artist, and the music-video of the song. The full length response is about: 3000 lines

  "playlist": [
		"_name": "Crash My Car",
		"url": "/music/Coin/_/Crash+My+Car",
		"artists": [
				"_name": "Coin",
				"url": "/music/Coin",
				"name": "Coin"
		"name": "Crash My Car",
		"playlinks": [
				"affiliate": "youtube",
				"id": "kv1wZPsvtuo",
				"url": "",
				"source": "global_suggestion"

Note: I have shortened this json response and so we can focus on the relevant parts for this demo

I used jq to parse the json response to get the video $id, $artist and $title of the song and store them in variables. Since there are about 3000 lines I made a function called: loop() which takes each entry, sets the variables and calls the dl() method to start the download.

loop() {
    echo "queueing"
    length=$(jq -r '.playlist | length' <<< "$response")
    for ((i=0; i<length; i++))
        id="$(jq -r ".playlist[$i].playlinks[0].id" <<< "$response")"
        a="$(jq -r ".playlist[$i].artists[0].name" <<< "$response")"
        t="$(jq -r ".playlist[$i].name" <<< "$response")"
        q="$a - $t"
    echo "finished queueing"

Downloading & metadata

dl() uses yt-dlp to download the video and extract the audio file. Here is where things got a bit tricky — some songs would not be tagged properly, some refuse to play in my region. So I had to query YouTube Music and use it as a backend to fetch the correct video $id instead of relying on LastFM.

dl() {
    if [ "$ytm_backend" = true ]; then 
        if yt-dlp ${ytargs} --playlist-end 1 --default-search "" "$q" --parse-metadata "title:%(title)s" --parse-metadata "artist:%(artist)s" --add-metadata -o '%(artist)s - %(title)s.%(ext)s'; then 
            echo "ERROR:${id}"
        if yt-dlp ${ytargs} --postprocessor-args "ffmpeg:-metadata title=${t@Q} -metadata artist=${a@Q}" -o "$a - $t.%(ext)s" "$id"; then
            echo "ERROR:${id}"

Queuing and playback

name=$(ls -u | head -1)

Initially I was using a hacky-way of queuing songs. It was trying to check if there were any updates to the local filesystem and uses it to queue songs. It works — but isn’t the right approach. It adds a lot of CPU overhead as it’s running in a loop unattended.

queue() {
    mpc -q update; sleep 0.1
    if [ "$priority" = true ]; then
        name=$(ls -u | head -1); q=$name
        if [ "$(mpc playlist | wc -l)" -eq 0 ]; then
            mpc -q insert "dl/${name}"
            mpc -q play
            mpc -q insert "dl/${name}"
            mpc -q play && mpc -q next
        mpc -q add "dl/$q.opus"
    echo "queueing: $q"

Now I use $q which is just $artist - $title concatenated together. A tad bit efficient. Priority mode is when you need to play a single song check the -s flag.


This is by-far the most fun part! I’ve designed jukebox to be simple and does only what it’s supposed to. Any additional functionality that isn’t a core-functionality I have written extensions, or other bash scripts that can be found in the same repository. Let me walk you through.

The -l flag let’s you search for songs using your microphone, it’s basically the -s mode but uses vosk to transcribe your voice to text!

# variables
set -e

# speech to text
mpc -q pause
google_speech "Listening: for $time seconds"

ffmpeg -y -f alsa -i default -acodec pcm_s16le -ac 1 -ar 44100 -t $time -f wav ~/.cache/audio.wav -loglevel warning
vosk-transcriber -m $VOSK_MODEL_PATH -i ~/.cache/audio.wav -o ~/.cache/audio_text.txt --log-level warning
read audio_input < ~/.cache/audio_text.txt
[ -z "$audio_input" ] && mpc -q play && exit 1

# pipe into jukebox
google_speech "Searching: $audio_input"; mpc -q play
~/.local/repos/jukebox/jukebox -s "$audio_input"


The -c flag fetches the top charts in your region and uses it as the playlist instead of recommendations. Note: this isn’t accurate compared to (Spotify/Apple music)—because they have a far better sample size (userbase).

# requires an api_key

response=$(curl -s "${country}&api_key=${api_key}&format=json")
length=$(jq -r '.tracks.track | length' <<< "$response")
for ((i=0; i<length; i++))
    a="$(jq -r ".tracks.track[$i]" <<< "$response")"
    t="$(jq -r ".tracks.track[$i].name" <<< "$response")"
    ~/.local/bin/jukebox -s "$t by $a"

glossary: an extensive list of tools I use to listen to my music

  • mpd to play and manage your songs
  • mpc controlling mpd via a cli
  • ncmpcpp interacting with mpd
  • jukebox download and queue LastFM recommendations
  • mpdscribble scrobbling to LastFM
  • mpd-mpris using hardware buttons to control-playback
  • waybar display the last playing song on my status-bar
  • snapcast to stream music to multiple speakers/devices
  • tailscale to access MPD ports and serve songs over a private VPN (I use it to ssh into my VPS) and stream songs using snapcast
  • psync bash script to copy your current mpd-playlist to your phone—for offline playback and scrobbling on the go!
  • pScrobbler scrobble songs on my phone
  • Pod Classic android music player with an iPod interface


If I need to switch devices, or catch the bus in the morning I made this script called psync—to copy my current mpd-playlist to the phone: for offline playback and scrobbling on the go! I can pickup where I left off and continue to listen on my phone. Checkout pScrobbler (like mpdscribble but for Android)

# cleanup
cd ~/.local/share/.Music/
rm -rf ~/.cache/songs; mkdir ~/.cache/songs
rm ~/.local/share/mpd/playlists/songs.m3u; mpc -q save songs
adb shell mkdir -p /sdcard/Music

# copy to phone
rsync -avL --files-from="$HOME/.local/share/mpd/playlists/songs.m3u" ./ ~/.cache/songs
adb push ~/.cache/songs/ "/sdcard/Music"
adb push ~/.local/share/mpd/playlists/songs.m3u "/sdcard/Music/songs" && mpc -q clear


Since mpd is available on all *nix systems, I can setup a remote mpd session and stream songs instead of having to carry my collection around in a hard-drive

[mpd ⟶ snapcast_server (on nimbu)] <~tailscale~> [snapcast_client (on vince): nimbu]

• nimbu: my remote machine (vps)
• vince: my local machine (android)


I can also leave it unattended in a for-loop, let it cache some songs for the weekend. But this assumes—you’re not having spotty-internet when you stream from your remote machine back to your devices.


Instead of streaming from a remote server, use snapcast to stream over your local network, and let all your friends play music in-sync.



If you’re okay with DRM (Spotify) or other such platforms. You can continue to use Spotify but, setup scrobbling to LastFM, then run jukebox with the -m "library" flag to download an archive of all your recently played songs!

/listening widget on your website

I deploy this nifty-widget that lets me showcase what songs I am playing at the moment. Also checkout blackpirate’s widget, which I showcase here: /listening



This post is a part of a talk I gave at uni as a part of </HSP> — our college’s FOSS community.

me on stage: ranting about non-free music services

sem hall 2

sem hall 2 but from the back

HSP team &lt;3

Free Software is about community, feel free to dig through the source code and open some issues on my GitHub. Happy Hacking!