Issue with 'else' sequence using Spotipy/Spotify API

72 views Asked by At

My team and I (newbies to python) have written the following code to generate spotify songs related to a specific city and related terms. If the user inputs a city that is not in our CITY_KEY_WORDS list, then it tells the user that the input will be added to a requests file, and then writes the input to a file. The code is as follows:


from random import shuffle
from typing import Any, Dict, List
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
sp = spotipy.Spotify(
    auth_manager=SpotifyClientCredentials(client_id="",
                                          client_secret="")
)
CITY_KEY_WORDS = {
    'london': ['big ben', 'fuse'],
    'paris': ['eiffel tower', 'notre dame', 'louvre'],
    'manhattan': ['new york', 'new york city', 'nyc', 'empire state', 'wall street', ],
    'rome': ['colosseum', 'roma', 'spanish steps', 'pantheon', 'sistine chapel', 'vatican'],
    'berlin': ['berghain', 'berlin wall'],
}

def main(city: str, num_songs: int) -> List[Dict[str, Any]]:
    if city in CITY_KEY_WORDS:
        """Searches Spotify for songs that are about `city`. Returns at most `num_songs` tracks."""
        results = []
        # Search for songs that have `city` in the title
        results += sp.search(city, limit=50)['tracks']['items']  # 50 is the maximum Spotify's API allows
        # Search for songs that have key words associated with `city`
        if city.lower() in CITY_KEY_WORDS.keys():
            for related_term in CITY_KEY_WORDS[city.lower()]:
                results += sp.search(related_term, limit=50)['tracks']['items']
        # Shuffle the results so that they are not ordered by key word and return at most `num_songs`
        shuffle(results)
        return results[: num_songs]
    else:
        print("Unfortunately, this city is not yet in our system. We will add it to our requests file.")
        with open('requests.txt', 'r') as text_file:
            request = text_file.read()
        request = request + city + '\n'
        with open('requests.txt', 'w+') as text_file:
            text_file.write(request)

def display_tracks(tracks: List[Dict[str, Any]]) -> None:
    """Prints the name, artist and URL of each track in `tracks`"""
    for num, track in enumerate(tracks):
        # Print the relevant details
        print(f"{num + 1}. {track['name']} - {track['artists'][0]['name']} {track['external_urls']['spotify']}")
if __name__ == '__main__':
    city = input("Virtual holiday city? ")
    number_of_songs = input("How many songs would you like? ")
    tracks = main(city, int(number_of_songs))
    display_tracks(tracks)

The code runs fine for the "if" statement (if someone enters a city we have listed). But when the else statement is run, 2 errors come up after the actions have been executed ok (it prints and writes the user's input into a file).

The errors that come up are:

Traceback (most recent call last):
  File "...", line 48, in <module>
    display_tracks(tracks)
  File "...", line 41, in display_tracks
    for num, track in enumerate(tracks):
TypeError: 'NoneType' object is not iterable

Please excuse my lack of knowledge, but please could someone help with this issue?

We would also like to create a playlist of the songs at the end, however have been facing difficulties with this.

2

There are 2 answers

1
Mahmood R On BEST ANSWER

Your main function does not have a return statement in the else clause and that causes tracks to be None. Iterating on tracks when it's None is what's causing the error. There are a few things you can do to improve the code:

  • separation of concerns: the main function is doing two different things, checking the input and fetching the tracks.
  • do .lower() once in the beginning so you don't have to repeat it.
  • following documentation conventions.
  • checking the response before using it
  • some code cleaning

see below the changes I suggested above:

def fetch_tracks(city: str, num_songs: int) -> List[Dict[str, Any]]:
    """Searches Spotify for songs that are about `city`.

    :param city: TODO: TBD
    :param num_songs:  TODO: TBD
    :return: at most `num_songs` tracks.
    """
    results = []
    for search_term in [city, *CITY_KEY_WORDS[city]]:
        response = sp.search(search_term, limit=50)
        if response and 'tracks' in response and 'items' in response['tracks']:
            results += response['tracks']['items']
    # Shuffle the results so that they are not ordered by key word and return
    # at most `num_songs`
    shuffle(results)
    return results[: num_songs]


def display_tracks(tracks: List[Dict[str, Any]]) -> None:
    """Prints the name, artist and URL of each track in `tracks`"""
    for num, track in enumerate(tracks):
        # Print the relevant details
        print(
            f"{num + 1}. {track['name']} - {track['artists'][0]['name']} "
            f"{track['external_urls']['spotify']}")


def main():
    city = input("Virtual holiday city? ")
    city = city.lower()
    # Check the input city and handle unsupported cities.
    if city not in CITY_KEY_WORDS:
        print("Unfortunately, this city is not yet in our system. "
              "We will add it to our requests file.")
        with open('requests.txt', 'a') as f:
            f.write(f"{city}\n")
        exit()

    number_of_songs = input("How many songs would you like? ")
    tracks = fetch_tracks(city, int(number_of_songs))
    display_tracks(tracks)


if __name__ == '__main__':
    main()
1
Nico On

When your if-statement is executed, you return a list of items and feed them into the display_tracks() function. But what happens when the else-statement is executed? You add the request to your text-file, but do not return anything (or a NoneType item) and feed that into display_tracks(). display_tracks then iterates of this NoneType-item, throwing your exception.

You only want to show the tracks if there actually are any tracks to display. One way to do this would be to move the call of display_tracks() into your main function, but then the same error would be thrown if no tracks are found to your search-terms. Another solution would be to first check if your tracks are not empty or to catch the TypeError-exception with something like

tracks = main(city, int(number_of_songs))
try:
    display_tracks(tracks)
except TypeError:
    pass