It took way longer than necessary to make this post.
Hello everyone!
In a previous post I made, I took on the task of making my first Python application which is a YouTube video downloader (You can read more about it here). From the previous post, the program I created was pretty rough, but it was able to do its basic function which is definitely some progress. In this post, I will be adding the option to select a video quality and also handle a few errors that I noticed when I was trying to break the program making a few tests.
Before I started working on the things that I set out to do, what I did first was to learn about the geometry managers in Tkinter (the pack
thingy that I mentioned in the last post) and why it was valid to put the padding values in the pack function from my program in the earlier post.
link_entry.pack(padx=10, pady=5)
What I did find out was there are actually three geometry managers in tkinter – pack, place and grid.
I decided that I was going to stick with the pack function since it basically stacks the widgets on each other from top to bottom. The grid function places the widgets in a grid setting. That sounded cool, but I didn’t really feel like using it. And finally, the place function uses x and y values to position the widgets. So, it is used to place the widgets using coordinates.
And I’ve already decided on the geometry manager that I’ll never dream of using... Ever.
Each placing function also had several properties that can be used to adjust the widgets on the window. I spent some time experimenting with the different properties while adjusting the UI of the program.
(I know it's not much, but I really did change some things)
Now, it was time to work on the things I set to accomplish.
And I decided to sort out the quality selection feature first
For starters, I would make the downloader able to download the standard video qualities that you would find on any normal YouTube video – from 144p to 1080p.
Just like the last time, the quality selection options would be created in the UI first. I decided that I was going to use radio buttons for this since only one option out of a group of them can be selected, which means that only one option can be selected. I also figured that it would be a good idea for the radio buttons to be arranged horizontally instead of vertically.
And doing that was a lot of fun… (Not)
I spent a good amount of time struggling to find out how to fix this issue until I found the thing that would eventually save me from this problem – Tkinter frames.
They are kind of like mini windows that you can put on the main window. So, what I did was to align the radio buttons for the video qualities on a frame, then join that frame to the main window. This makes things a lot easier and also doesn’t make things so confusing on where to assign a widget.
It turns out there are also label frames in tkinter. They work exactly like a normal frame would, but there’s a label on it. So, I switched to a label frame because why not.
Now that everything has been set in place, it was time to make the radio buttons to actually work.
What I did was that I labeled the radio buttons according to the video qualities that I decided on earlier (144p - 1080p). I also assigned each button to have the same value as its respective label.
-----------------------------------
# The frame for the radio buttons
-----------------------------------
qualities = ttk.LabelFrame(root, text='Qualities')
-----------------------------------
# This will store the radio buttons. Also didn't know what to name this as
-----------------------------------
side_by_side_widgets = dict()
-----------------------------------
# Created a tuple containing the video quality labels
-----------------------------------
video_qualities = ('144p', '240p', '360p', '480p', '720p', '1080p')
-----------------------------------
# Holds the value of the selected radio button
-----------------------------------
selected_quality = tk.StringVar()
-----------------------------------
# Then made a for loop to iterate through the tuple and create a radio button for the tuple items
# Also stores the radio buttons in the dictionary
-----------------------------------
for quality in video_qualities:
side_by_side_widgets[quality] = ttk.Radiobutton(
qualities,
text = quality,
value = quality,
variable = selected_quality,
)
side_by_side_widgets[quality].pack(side=tk.LEFT, expand=True, padx=5, pady=5)
In the main download function, I changed it from downloading videos in 360p only to downloading videos depending on the quality that has been selected.
def download():
url = YouTube(str(link.get()))
video = url.streams.filter(file_extension='mp4').get_by_resolution(str(selected_quality.get()))
video.download(filename=video.title, output_path='./')
showinfo(title='Success', message='Download Completed')
And it actually works.
I also figured that it would be nice if the user could know the quality of the downloaded video. So, I made it that the video gets downloaded with the video quality attached to the name of the video.
Along with that, it turns out some videos can’t be downloaded because they contain a few symbols in the video title. For instance, a video title with a slash (/) in it can’t be downloaded because the program thinks it’s trying to specify a file directory.
Also, a video can’t be downloaded if it has a colon (:) in it because Windows says it’s illegal. This makes the video become a random, empty file with a part of its name chopped off.
Fixing this wasn’t so difficult. What I did was that I imported the string module, which is an inbuilt module in Python, and made the program to iterate through the video title. If the selected character is not a symbol in a list of punctuations gotten from the string module, it becomes a part of the new video title. If the character is in the list of punctuations, nothing happens. Together with all of that, I made the program to replace the space characters with underscores just to be safe.
def download():
url = YouTube(str(link.get()))
video = url.streams.filter(file_extension='mp4').get_by_resolution(str(selected_quality.get()))
name=f'{video.title}'
new_name = []
for x in name:
if x not in list(string.punctuation):
new_name.append(x)
video.download(filename=''.join(new_name).replace(' ', '_') + ' ' + '(' + str(selected_quality.get()) + ')' + '.mp4', output_path='./')
showinfo(title='Success', message='Download Completed')
With all that out of the way, it was now time for exception handling.
After testing the program for a while and trying to explore its limits, I noticed that it displayed an error on the console when the link entry bar is empty and when a non-YouTube link is entered. It would be much preferable if the program informs the user of what they’re doing wrong instead of displaying the problem behind the scenes.
I also observed that in some cases, the video variable gets returned as a ‘NoneType’ object.
My guess for this is that the quality for the video is unavailable.
I came to this conclusion because I tried downloading the same video in a different resolution and it worked.
This made me curious, and I decided to see all the video streams available for a video.
And I was right. Well, kind of
It's a bit hard to see in the picture, but the video stream that is 'unavailable' (Marked with red) is actually available. But it doesn't have any audio. While the video quality that worked (Marked with blue) comes with both audio and video.
That means the PyTube module automatically finds and downloads video streams that come with both audio and video.
What I did next was to make the program inform the user that the selected resolution is unavailable if a situation like that occurs.
Now that issue is fixed.
For the previous issues, I used a try-except statement. First, it will try and run the code. If an error occurs from an empty entry bar, an invalid link or the video is unavailable, it will display a pop-up message to the user.
For another check, I noticed that a YouTube video still gets downloaded even when a resolution is not selected, and that’s illegal. So, I modified the program to inform the user to select a quality and does not run unless the user selects one.
In total, this is everything that has been done so far
def download():
if not str(selected_quality.get()):
showinfo(title='Error', message='Please select a video quality')
else:
try:
url = YouTube(str(link.get()))
try:
video = url.streams.filter(file_extension='mp4').get_by_resolution(str(selected_quality.get()))
name=f'{video.title}'
except AttributeError:
showerror(title='Error', message='Selected resolution is unavailable. Try a different resolution.')
new_name = []
for x in name:
if x not in list(string.punctuation):
new_name.append(x)
video.download(filename=''.join(new_name).replace(' ', '_') + ' ' + '(' + str(selected_quality.get()) + ')' + '.mp4', output_path='./')
showinfo(title='Success', message='Download Completed')
except pytube.exceptions.VideoUnavailable:
showerror(title='Error', message='Link is invalid or the video is unavailable')
except pytube.exceptions.RegexMatchError:
showerror(title='Error', message='Please enter a valid YouTube link')
At this point, I would say that my two favorite things at this point in programming are Tkinter frames and the try-except statement.
Next, I think I will add the feature of being able to select a download location and allow the user to download either videos or audios.
If there's anything that you feel that I should add, feel free to let me know and I will do my best to add them in the program.
Wow, I could tell that this project took you a lot of time to create, you sure did put in a lot of efforts into it. Some of the UI components that gave you a hard time can easily be done with CSS (Especially that part where you tried to arrange the radio buttons horizontally), is there a reason you didn't use CSS to style the UI?
Well, I didn't use CSS since it's mainly used for designing websites and this isn't a website. Plus I don't know CSS
Oh, I understand now. Thanks for your reply
Nice job creating that script
!1UP
Thanks a lot bro 😁
You have received a 1UP from @gwajnberg!
@stem-curator
And they will bring !PIZZA 🍕.
Learn more about our delegation service to earn daily rewards. Join the Cartel on Discord.
Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!
Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).
You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support.
Thank you for sharing this post on HIVE!
Your content got selected by our fellow curator priyanarc & you received a little thank you upvote from our non-profit curation initiative. Your post will be featured in one of our recurring curation compilations which is aiming to offer you a stage to widen your audience within the DIY scene of Hive.
Next time make sure to post / cross-post your creation within the DIYHub community on HIVE and you will receive a higher upvote!
Stay creative & hive on!