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.