For a long time, I had a small osascript to fetch into about the song I’m currently listening. It was fine, most of the time. Sometimes it just stuck and started to eat my memory. The other big problem with this solution was, I’m using not only MacOS, but I have windows and Linux too and all of them has tmux.

One night I was a bit upset that I can’t see what am I listening right now without switching workspace and check in the Spotify window. I started to dig in a bit deeper. A few years back there was no API for Spotify to access information like what am I listening right now and what is the progress.

I don’t know when did they introduce this shiny API, but I’m happy about it. So I decided to write a small python script that does nothing else just fetches the artist, title and progression.

I already have my python package for these simple tasks, so it was obvious to extend that instead of creating a standalone script.

Client Authentication

First of all, we have to authenticate somehow, the easiest way I found was to use OAuth with ClientId and ClientSecret. On their Developer Portal we can create a new application. We have to get verification if we would develop a commercial application, but it is definitely not a commercial one, so we simply don’t care about verification.

    cred_manager = SpotifyOAuth(
                client_id=config['credentials']['client_id'],
                client_secret=config['credentials']['client_secret'],
                redirect_uri='http://localhost/spoty',
                cache_path=f'{home}/.cache/spoty',
                scope="user-read-playback-state")

ClientID and ClientSecret is obvious, they are coming from Spotify’s Developer Console. The scope is very tight because we want this app to read only the playback state, anything else would be a security problem if the keys are somehow leaked. The cache is a file, where it stores the session, so place it somewhere safe on a multi-user computer, and in general place it somewhere safe. The last piece is the redirect_uri, it does not really matter because we will not listening for that, but the browser will redirect us there, so localhost is perfect it will be a 404 Not Found page, but it’s fine (or server does not exist).

Next step is to actually log in. By default the Spotify client refreshes the token if it’s expired, but easier to just check if it’s valid or not and if it’s not valid print out a short error message that can be displayed in tmux without problem.

        token = cred_manager.get_cached_token()
        if token is None or is_token_expired(token):
            print('! Expired: call Auth')
            return

        spoty = spotipy.Spotify(client_credentials_manager=cred_manager)

Executing this will shows us a URL to open. Open it, it will redirect us to localhost, copy that URL and paste it into the app. And we are done. We successfully authenticated ourself.

Playback State

First of all, we have to check if we are listening anything or not. It can be None (I think it happens if no active Spotify client is running) or it’s a hash. If the is_playing value in the hash is False, well it means it’s paused or stopped.

    state = spoty.current_playback()
    if state is None or not state['is_playing']:
        print('Not playing')
        return

And now the artist and title. The tricky part is, there are songs with no artist info, or there are more then one artist. It can even be like seven artists if it’s a big collaboration product like Falconshield has a few with a lot of artists. As I know Title is always there. For the artist, to be fair I care most about the “main” artist and I can look up the rest if I’m interested, but I don’t have a 4000 character long statusbar for my tmux to display all of them. So the workaround is a simple flag.

    if first_artist:
        artist = state['item']['artists'][0]['name']
    else:
        artist = ", ".join([a['name'] for a in state['item']['artists']])
    title = state['item']['name']
    duration = state['item']['duration_ms']
    progress = state['progress_ms']
    percentage = round(progress / duration * 100)

The cookies will be done in one minute

Rest of the code is basically some extra features for tmux.

from eferland.helpers.config_manager import ConfigManager
from spotipy.oauth2 import SpotifyOAuth, is_token_expired
import click
import os
import spotipy

@click.command()
@click.option('--auth/--no-auth', default=False)
@click.option('--tmux/--no-tmux', default=False)
@click.option('--first-artist/--all-artists', default=False)
@click.option('--artist-max-length', default=0)
@click.option('--title-max-length', default=0)
@click.option('--total-max-length', default=0)
def cli(auth, tmux, first_artist,
        artist_max_length, title_max_length, total_max_length):
    home = os.environ.get('HOME')
    config = ConfigManager('spoty')
    display = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]

    cred_manager = SpotifyOAuth(
                client_id=config['credentials']['client_id'],
                client_secret=config['credentials']['client_secret'],
                redirect_uri='http://localhost/spoty',
                cache_path=f'{home}/.cache/spoty',
                scope="user-read-playback-state")

    if not auth:
        token = cred_manager.get_cached_token()
        if token is None or is_token_expired(token):
            print('! Expired: call Auth')
            return

    spoty = spotipy.Spotify(client_credentials_manager=cred_manager)

    state = spoty.current_playback()
    if state is None or not state['is_playing']:
        print('Not playing')
        return

    if first_artist:
        artist = state['item']['artists'][0]['name']
    else:
        artist = ", ".join([a['name'] for a in state['item']['artists']])
    title = state['item']['name']
    duration = state['item']['duration_ms']
    progress = state['progress_ms']
    percentage = round(progress / duration * 100)

    if artist_max_length > 0:
        title = title[:artist_max_length]

    if title_max_length > 0:
        title = title[:title_max_length]

    def wrap(s):
        return f'▕#[fg=colour89]{s}#[fg=colour16]▏'
    
    char = display[round(percentage / 100 * (len(display) - 1))]
    output = ""
    if tmux:
        output = f'{output}♫ #@#'

    output = f'{output}{artist} - {title}'
    if total_max_length > 0:
        output = output[:total_max_length]

    print(output.replace('#@#', wrap(char)))

After this, we can simply call this script as part of status-right in tmux configuration as #(spoty --tmux --first-artist --total-max-length=30).

And how it looks like in tmux?

… the cookies are done.