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.
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:
- The user can search for videos to play.
- Search results get displayed to the user.
- 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:
- Search
- Audio-Player
- Command Line Interface (CLI)
Here's a GIF of the project in action.
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
- 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.
- 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
- pafy – This package allows us to download youtube videos.
- python-vlc – This package allows us to connect to our local VLC to play videos.
- 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
- PyInquirer – This helps us make beautiful and interactive CLI programs easily.
- 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 ouros.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.
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:
part
– You declare exactly what meta-data do you want of the video. In our case, we want the videossnippet
andid
.q
– The search query, in our case this might be the name of the song.maxResults
– How many results do we want to return, five should be enough.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.
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.
- Create an empty list called
search_results
. - Loop over the response
- Create a new object with the title and URL of the video
- Append to
search_results
- 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:
- Get a YouTube URL
- Convert it to an audio link
- Give the audio link to VLC
- VLC will play the audio
- 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.
Get Audio Link
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.
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 returnFalse
which will restart the program. - If the user wants to exit, then the method
prompt_to_continue
will returnTrue
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!