Creating the player
Now that we have everything we need to authenticate and consume the Spotify Rest API, we are going to create a small terminal client where we can search for an artist, browse his/her albums, and select a track to play in the Spotify client. Note that to use the client, we will have to issue an access token from a premium account and the authentication flow we need to use here is the AUTHENTICATION_CODE.
We will also need to require from the user of our application the user-modify-playback-state scope, which will allow us to control playback. With that said, let's get right into it!
First, we need to create a new directory to keep all the client's related files in it, so go ahead and create a directory named musicterminal/client.
Our client will only have three views. In the first view, we are going to get the user input and search for an artist. When the artist search is complete, we are going to switch to the second view, where a list of albums for the selected artist will be presented. In this view, the user will be able to select an album on the list using the keyboard's Up and Down arrow keys and select an album by hitting the Enter key.
Lastly, when an album is selected, we are going to switch to the third and final view on our application, where the user will see a list of tracks for the selected album. Like the previous view, the user will also be able to select a track using the keyboard's Up and Down arrow key; hitting Enter will send a request to the Spotify API to play the selected track on the user's available devices.
One approach is to use curses.panel. Panels are a kind of window and they are very flexible, allowing us to stack, hide and show, and switch panels, go back to the top of the stack of panels, and so on, which is perfect for our purposes.
So, let's create a file inside the musicterminal/client directory called panel.py with the following contents:
import curses
import curses.panel
from uuid import uuid1
class Panel:
def __init__(self, title, dimensions):
height, width, y, x = dimensions
self._win = curses.newwin(height, width, y, x)
self._win.box()
self._panel = curses.panel.new_panel(self._win)
self.title = title
self._id = uuid1()
self._set_title()
self.hide()
All we do here is import the modules and functions we need and create a class called Panel. We are also importing the uuid module so we can create a GUID for every new panel.
The Panel's initializer gets two arguments: title, which is the title of the window, and dimensions. The dimensions argument is a tuple and follows the curses convention. It is composed of height, width, and the positions y and x, where the panel should start to be drawn.
We unpack the values of the dimensions tuple so it is easier to work with and then we use the newwin function to create a new window; it will have the same dimensions that we passed in the class initializer. Next, we call the box function to draw lines on the four sides of the terminal.
Now that we have the window created, it is time to create the panel for the window that we just created, calling curses.panel.new_panel and passing the window. We also set the window title and create a GUID.
Lastly, we set the state of the panel to hidden. Continuing working on this class, let's add a new method called hide:
def hide(self):
self._panel.hide()
This method is quite simple; the only thing that it does is call the hide method in our panel.
The other method that we call in the initializer is _set_title; let's create it now:
def _set_title(self):
formatted_title = f' {self._title} '
self._win.addstr(0, 2, formatted_title, curses.A_REVERSE)
In _set_title, we format the title by adding some extra padding on both sides of the title string, and then we call the addstr method of the window to print the title in row zero, column two, and we use the constant A_REVERSE, which will invert the colors of the string, like this:
We have a method to hide the panel; now, we need a method to show the panel. Let's add the show method:
def show(self):
self._win.clear()
self._win.box()
self._set_title()
curses.curs_set(0)
self._panel.show()
The show method first clears the window and draws the borders around it with the box method. Then, we set the title again. The cursers.curs_set(0) call will disable the cursor; we do that here because we don't want the cursor visible when we are selecting the items in the list. Finally, we call the show method in the panel.
It would also be nice to have a way to know whether the current panel is visible or not. So, let's add a method called is_visible:
def is_visible(self):
return not self._panel.hidden()
Here, we can use the hidden method on the panel, which returns true if the panel is hidden and false if the panel is visible.
The last touch in this class is to add the possibility of comparing panels. We can achieve this by overriding some special methods; in this case, we want to override the __eq__ method, which will be invoked every time we use the == operator. Remember that we created an id for every panel? We can use that id now to test the equality:
def __eq__(self, other):
return self._id == other._id
Perfect! Now that we have the Panel base class, we are ready to create a special implementation of the panel that will contain menus to select items.