How to create a YouTube audio player using Python?

A couple of weeks ago I joined a Discord server, where a bunch of programmers hang out and do projects together.

One of these projects was a Youtube GUI player.

I started working on the project and realized it's a lot harder than I thought it would be.

I scrapped the idea of creating a GUI because I'm too lazy to design a good UI, and simply decided it would be a command-line program.

I eventually finished it and decided it deserves a tutorial of its own.

In this article, we will go over how to create a simple YouTube audio player that allows you to search for videos using the YouTube API and play it using python-vlc.

PS. The full program is found on GitHub below.

GitHub - TamerlanG/Youtube-Console-MP3-Player: Simple YouTube audio-player with minimal functionality
Simple YouTube audio-player with minimal functionality - GitHub - TamerlanG/Youtube-Console-MP3-Player: Simple YouTube audio-player with minimal functionality

Prerequisites

  • I expect you to know the basics of python such as conditionals, functions, loops, etc...
  • Python 3+
  • VLC Media Player (we use this to play audio/video)
  • Youtube Data v3 API Key

PS. You can get the API key from Google Developers or if you want something more visual you can check out this excellent tutorial on how to get the API key here.  

What does the final project look like?

Well for this project we have three main specifications:

  1. The user can search for videos to play.
  2. Search results get displayed to the user.
  3. Finally, the user can pick a search result and the audio clip will automatically play.

If we simplify the requirements, it boils down to three things:

  1. Search
  2. Audio-Player
  3. Command Line Interface (CLI)

Here's a GIF of the project in action.

Our Final Project

Setting up the Project

Let's quickly go over setting up the project.

1.  Create a directory for the project and cd into it.

mkdir youtube-player
cd youtube-player

2.  Create a virtual environment for the project

python -m venv venv

3.  Create a requirements.txt file for our dependencies

touch requirements.txt

4.  Finally, source into our virtual environment

source ./venv/bin/activate

Installing Dependencies

To save time, we will install all needed dependencies at once.

Search Dependencies

  1. python-dotenv – It's dangerous to put our API key directly in our code, that's why we will be using environment variables to store our API key.
  2. google-api-python-client  – This is the official Google API python client. Instead of making raw HTTP requests to the API, the client will simplify this process with methods.

Install these two libraries:

pip install python-dotenv google-api-python-client 

Player Dependencies

  1. pafy – This package allows us to download youtube videos.
  2. python-vlc – This package allows us to connect to our local VLC to play videos.
  3. youtube-dl – Our package pafy depends upon youtube-dl, but we have to install it separately.

Install these three libraries:

pip install pafy python-vlc youtube-dl

CLI Dependencies

  1. PyInquirer – This helps us make beautiful and interactive CLI programs easily.
  2. pyfiglet – This allows us to make cool looking headers for our CLI programs.

Install these two libraries:

pip install PyInquirer pyfiglet

Now that we installed all dependencies, let's save them in our requirements.txt for future use.

pip freeze > requirements.txt

Implementing the Search Functionality

The search will be pretty simple.

We simply create a function that accepts a string and returns search results.

The search results will be a list of objects in this format:

{
  "name": "Video Title",
  "value": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
   

But before we get into the nitty-gritty details of searching, we have to import our API key.

To keep our API safe from the outside world, we will make it an environment variable.

1.  Create a .env file in the project root.

touch .env

2.  Add the API key into the .env file

# .env 

YOUTUBE_API_KEY=YOUR_API_KEY

Now that we have that done, let's implement the actual search.

Create a search.py file in the project root and add this code:

import os
from dotenv import load_dotenv
from googleapiclient.discovery import build

load_dotenv()
YOUTUBE_API_KEY = os.environ.get('YOUTUBE_API_KEY')

youtube = build('youtube', 'v3',
                developerKey=YOUTUBE_API_KEY)

Let's break this down:

  • load_dotenv – This loads the variables from our .env file into our os.environ, thus allowing us to get the API key in the next line.
  • build – This creates a resource object for us to communicate with our desired API. In our case, we want the YouTube service and specifically version 3 of it. In the third parameter, we input our API key.

Google offers many other services, you can check them out below.

google-api-python-client/index.md at main · googleapis/google-api-python-client
🐍 The official Python client library for Google’s discovery based APIs. - google-api-python-client/index.md at main · googleapis/google-api-python-client

Let's create our main search function.

Add this code to our search.py

# search.py
# -- previous code

def search(query):
    request = youtube.search().list(
        part='id,snippet',
        q=query,
        maxResults=5,
        type='video'
    )

    response = request.execute()

Let's break this down:

We use the search().list() method to search the youtube database.

Parameters:

  1. part – You declare exactly what meta-data do you want of the video. In our case, we want the videos snippet and id.
  2. q – The search query, in our case this might be the name of the song.
  3. maxResults – How many results do we want to return, five should be enough.
  4. type – What type of content do you want to be returned, it may be playlists, channels or videos. In our case, we want videos only.

All this information is available from the official documentation. You can check it out below.

API Reference | YouTube Data API | Google Developers

Finally, we execute the request and get back a JSON response.

The JSON response looks something like this:

{
    // Meta Data
    "items": [
    	 {
            "id": {
            	"videoId": "dQw4w9WgXcQ"
            },
            "snippet": {
            	"title": "best song ever!!"
            }
        },
    
    ]
}

We only need the title and video ID.

We can do that with a simple for loop, let's add that to our function:

# search.py
# -- previous code

def search(query):
    request = youtube.search().list(
        part='id,snippet',
        q=query,
        maxResults=5,
        type='video'
    )

    response = request.execute()

    search_results = []

    for video in response['items']:
        title = video["snippet"]["title"] 
        video_id = video["id"]["videoId"]
        item = {
            'name': title,
            'value': f'https://www.youtube.com/watch?v={video_id}',
        }

        search_results.append(item)

    return search_results

If it's too confusing, this is the pseudocode version of what the code above does.

  1. Create an empty list called search_results.
  2. Loop over the response
  3. Create a new object with the title and URL of the video
  4. Append to search_results
  5. Return search_results

That's it, the search functionality is complete.

Implementing the Audio Player

So what will our audio player do?

Play audio? isn't that obvious?

But how does it do that?

No worries, it's not some black magic.

Essentially it comes down to these steps:

  1. Get a YouTube URL
  2. Convert it to an audio link
  3. Give the audio link to VLC
  4. VLC will play the audio
  5. When the audio is done, VLC will shut down.

Let's implement these steps in a single function called play which will take a youtube URL and play the video in audio format.

First of all, create a file called player.py in the project root.

Add this code:

import pafy
import vlc

def play_song(url):
    is_opening = False
    is_playing = False

    video = pafy.new(url)
    best = video.getbestaudio()
    play_url = best.url

    instance = vlc.Instance()
    player = instance.media_player_new()
    media = instance.media_new(play_url)
    media.get_mrl()
    player.set_media(media)
    player.play()

    good_states = [
    	"State.Playing", 
    	"State.NothingSpecial", 
    	"State.Opening"
    ]
    
    while str(player.get_state()) in good_states:
        if str(player.get_state()) == "State.Opening" and is_opening is False:
            print("Status: Loading")
            is_opening = True

        if str(player.get_state()) == "State.Playing" and is_playing is False:
            print("Status: Playing")
            is_playing = True

    print("Status: Finish")
    player.stop()

Okay, don't panic. Let's break down this code.

video = pafy.new(url)
best = video.getbestaudio()
play_url = best.urlon

Pafy loads the URL, and then we select the best audio stream of the video.

You can think of this as selecting the quality of a video, it may be 360p, 480p, or 1080p.

For audio, we simply select the best bitrate.

Finally, we get the link of the stream using best.url

PS. If you want this to be a video player, you can replace getbestaudio with getbest .

Play the Audio Stream

instance = vlc.Instance()
player = instance.media_player_new()
media = instance.media_new(play_url)
media.get_mrl()
player.set_media(media)
player.play()

This might be the most confusing part of the article but bear with me.

We first create an instance of VLC, using vlc.Instance()

Using this instance we then create two other instances.

  • MediaPlayer instance using instance.media_player_new()
  • Media instance using media_new(play_url)

We then get the media resource locater using the media.get_mrl() method. It's essentially a way to get the actual resource link of our audio file.

You can read more about it here.

Media resource locator - Wikipedia

Finally, we load the media player with our media instance and play it.  

Maintaining State

is_opening = False
is_playing = False

good_states = [
    "State.Playing", 
    "State.NothingSpecial", 
    "State.Opening"
]
    
while str(player.get_state()) in good_states:
 if str(player.get_state()) == "State.Opening" and is_opening is False:
    print("Status: Loading")
    is_opening = True

 if str(player.get_state()) == "State.Playing" and is_playing is False:
    print("Status: Playing")
    is_playing = True

print("Status: Finish")
player.stop()

VLC will take some time to load, and we don't want the application to stop, so that's why we check the state of the player.

If it's in a positive state meaning that it's either opening or playing the audio clip, we keep the application running.

We also print the states but only once, not to spam our terminal.

Once the audio clip stops or some error happens we exit the good_states loop, and the player stops.

Putting it all together in a CLI

First of all, create a file named main.py

We will add lots of code to it, but it's fairly simple.

Later on, I will explain what each function does.

Add this code to it:

from __future__ import print_function, unicode_literals

from PyInquirer import prompt

from search import search
from player import play_song
from pyfiglet import Figlet

EXIT_TOGGLE = False


def main():
    questions = [
        {
            'type': 'input',
            'name': 'query',
            'message': 'Search:',
        }
    ]

    query = prompt(questions)

    search_results = search(query.get("query"))

    choice = list_search_results(search_results)

    play_song(choice['search'])

    return prompt_to_continue()


def list_search_results(search_list):
    questions = [
        {
            'type': 'list',
            'name': 'search',
            'message': 'Search Results:',
            'choices': search_list,
        },
    ]

    answer = prompt(questions)

    return answer


def prompt_to_continue():
    questions = [
        {
            'type': 'confirm',
            'message': 'Do you want to continue?',
            'name': 'continue',
            'default': True,
        },
    ]

    answer = prompt(questions)

    return not answer['continue']


if __name__ == '__main__':
    # Big Header
    f = Figlet(font='slant')
    print(f.renderText('MP3 Player'))

    # Main Life Cycle Loop
    while True:

        if EXIT_TOGGLE:
            break

        EXIT_TOGGLE = main()

    print("Thanks for using!!")

I know this is a pretty big gist compared to others but don't worry it's simple.

Let's break down each function.

Main Magic Method

EXIT_TOGGLE = False

if __name__ == '__main__':
    # Big Header
    f = Figlet(font='slant')
    print(f.renderText('MP3 Player'))

    # Main Life Cycle Loop
    while True:

        if EXIT_TOGGLE:
            break

        EXIT_TOGGLE = main()

    print("Thanks for using!!")

This is the heart of the program, it contains the main lifecycle.

This allows us to run the program in a loop until the user decides to quit, hence the EXIT_TOGGLE will turn to True and the program will exit.

It also prints out our header thanks to pyfiglet.

Main Function

def main():
    questions = [
        {
            'type': 'input',
            'name': 'query',
            'message': 'Search:',
        }
    ]

    query = prompt(questions)

    search_results = search(query.get("query"))

    choice = list_search_results(search_results)

    play_song(choice['search'])

    return prompt_to_continue()

This has the main business logic of the program.

We first ask the user an input question on what he wants to search.

This is easily done using PyInquirer.

We simply have to create a list of questions, and the prompt function will do its magic.

Once we get the users query, we pass it to our search.py and get our search_results.

We then have to give the user the option to choose the song he wants to play, once again this is done using PyInquirer in the method list_search_choice.

The function returns the choice of the user, and we pass it on to our player to play.

Once the song finishes we ask the user if he wants to search again.

List Search Results

def list_search_results(search_list):
    questions = [
        {
            'type': 'list',
            'name': 'search',
            'message': 'Search Results:',
            'choices': search_list,
        },
    ]

    answer = prompt(questions)

    return answer

This time we create a list type question, that takes in our search_results.

The reason why our search results were in this format:

{
  "name": "Video Title",
  "value": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}
   

Is that PyInquirer will show the name property (our title) to the user, but the value that will be returned from the prompt will be the value property (our URL).

Prompt To Continue

def prompt_to_continue():
    questions = [
        {
            'type': 'confirm',
            'message': 'Do you want to continue?',
            'name': 'continue',
            'default': True,
        },
    ]

    answer = prompt(questions)

    return not answer['continue']

This will prompt the user a confirm type question whether he wants to continue or not.

We simply return the opposite of what he picked.

Why do we do that, well because:

  • If the user wants to continue, then the method prompt_to_continue will return False which will restart the program.
  • If the user wants to exit, then the method prompt_to_continue will return True which will break the life cycle loop and exit our program.
if EXIT_TOGGLE:
	break

EXIT_TOGGLE = main()

Conclusion

I certainly do hope that you don't run into any errors while following this tutorial.

But if you do, feel free to message me on Twitter and we might find a solution together.

But, if it worked, and you are still up for a challenge.

You can add these functionalities:

  • Implement options for the user to play/pause audio.
  • Autoplay to the next related song, or the next song in queue.

Anyways, I hope you learned something today, and if you enjoyed this article, share it with your friends and colleagues!

Thanks for reading!