Follow Up: A GUI Youtube Player (2)
I made a GUI that can make a YouTube query and play the audio. It includes some basic functionalities like volume control, a time-scale, putting songs in waiting lists, shuffle, etc.
I spent some time looking into this website, of how good, readable code is made, and managed to change a whole bunch of things, making better names for the variables, sorting stuff out. So, I'll just post the whole thing here.
Main work of the app is in the functions called funcSearch
, Every_second
and funcStream
. Any thoughts would be much appreciated.
To make my question more concrete:
I have heard that global vars are problematic, but since I know nearly nothing about object oriented programming, they are needed. If somebody could maybe illustrate me, or indicate, how I could take a object oriented approach, this would be super interesting. Even vague indications, for instance: should I make the songs into objects? Or even the individual search results? And then do I store them in a list? Also what I wondered: is it conventional that my functions are relatively long, or is it more common to split them up into smaller parts? My approach has been to make as few functions as possible, to make them do all that can be done within the scope of the function.
PS: I already found that my code imports BeautifulSoup without using it; its a relic from an older version of the code.
#! /usr/bin/python3
from tkinter import *
from bs4 import BeautifulSoup
from youtube_search import YoutubeSearch
import re,random, vlc, pafy, datetime, time,yt_dlp
#global vars
ran = song_index = dur = timescale_var = 0
auto = 1
play_index=-1
volume_var= 100
soundquality = 5
soundqualities = "best"
URLlist = playlist = []
#window, player
win = Tk()
win.geometry('610x100')
win.title("Youtube Player")
menubar = Menu(win)
instance = vlc.Instance()
player = instance.media_player_new()
#making the search
def funcSearch(event):
global song_index, URLlist
search_querie = str(SearchBox.get())
song_index = 0
results = YoutubeSearch(search_querie, max_results=40).to_dict()
title=[]
URLlist=[]
resultcount = -1
#the following loop is to optionally select only short songs <5min
for v in results:
duration = v['duration']
if duration != 0:
if duration.count(':') > 1 and dur == 1:
continue
if duration.count(':') == 1:
m, s = duration.split(':')
duration = int(m) * 60 + int(s)
if duration > 300 and dur == 1:
continue
URLlist.append("https://www.youtube.com" + v['url_suffix'])
resultcount+= 1
title.append(re.sub(r"[^a-zA-Z0-9 .,:;+-=!?/()öäßü]", "", v['title']))
btnPlay.focus()
btnL.config(command = (lambda: funcMOVE(title, -1, resultcount)))
btnR.config(command = (lambda:funcMOVE(title, 1, resultcount)))
btnPlay.config(command = (lambda: funcStream(1,1)))
btnAddsong.config(command = (lambda: funcAddsong()))
btnDL.place(x=505, y=2)
btnDL.config(command =(lambda: funcDL(URLlist[song_index],title[song_index])))
title_label.config(text = title[song_index])
#moving through the songlist
def funcMOVE(title, move, resultcount):
win.focus()
global song_index
song_index += move
if song_index < 0:
song_index =resultcount
if song_index > resultcount:
song_index = 0
title_label.config(text = title[song_index])
#this function keeps track of time and moves the timescale
def every_second():
length = player.get_length()
place = player.get_time()
if player.is_playing() == 0 and abs(place-length) < 10000 and len(playlist) > 0 and auto == 1:
funcStream(0, 1)
if player.is_playing() == 1:
time_info =str(datetime.timedelta(seconds = round(place/1000))) + " / " + str(datetime.timedelta(seconds = round(length/1000)))
time_label.config(text=time_info)
timescale.set(place)
win.after(1000,lambda:every_second())
#starting the song
def funcStream(question, direction):
win.focus()
#first, the play_index is updated
global play_index
play_index += direction
if play_index > (len(playlist)-1):
play_index = 0
if question == 1:
playlist.insert(play_index,URLlist[song_index])
else:
if ran == 1 and len(playlist) > 1:
counttemp = play_index
while counttemp == play_index:
play_index = random.randrange(len(playlist)-1)
#then, the song gets initialised
audio = pafy.new(playlist[play_index])
if soundqualities == "best":
stream = audio.getbestaudio()
else:
qualities = len(audio.audiostreams)
if soundquality > qualities:
stream = audio.audiostreams[qualities]
else:
stream = audio.audiostreams[soundquality]
print(stream.quality, int(stream.get_filesize()/10000)/100, "mb")
playurl = stream.url
media=instance.media_new(playurl)
media.get_mrl()
player.set_media(media)
player.set_time(0)
timescale.set(0)
btnPP.place(x=340, y=2)
btnAddsong.place(x=170, y=62)
btnBACK.place(x=295, y=2)
btnBACK2.place(x=240, y=2)
btnFWD.place(x=395, y=2)
btnFWD2.place(x=445, y=2)
timescale.place(x=370, y=68)
player.play()
btnPP.config(text="||")
while player.is_playing() == 0:
time.sleep(1)
timescale.config(to = player.get_length())
win.title(audio.title)
return()
#this is to select the next song in the list
def funcAddsong():
win.focus()
playlist.append(URLlist[song_index])
#next or previous song
def songskip(direction):
win.focus()
skip = play_index + direction
if direction == -1 and player.get_time() > 10000:
player.set_time(0)
elif skip >= 0 and skip < len(playlist):
funcStream(0, direction)
#this function is for downloading the song
def funcDL(song_url, song_title):
outtmpl = song_title + '.%(ext)s'
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': outtmpl,
'postprocessors': [
{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3',
'preferredquality': '192',
},
{'key': 'FFmpegMetadata'},
],
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info("youtube.com" + song_url, download=True)
#moving through the scale for time
def settime(timescale_var):
place = player.get_time()
if abs(int(timescale_var) - place) > 4000:
player.set_time(int(timescale_var))
#this function is for moving back and forth in time of the song
def funcSKIP(amount):
win.focus()
time_sum = player.get_time() + amount
time_all = player.get_length()
if time_sum < 0:
time_sum = 0
if time_sum>time_all:
time_sum = time_all
timescale.set(time_sum)
#to pause by keypress (space)
def funcPPTRANSFER():
if str(win.focus_get()) != str(".!entry"):
funcPP()
#to pause by keypress or click
def funcPP():
win.focus()
pause = player.is_playing()
player.set_pause(pause)
if pause == 1:
btnPP.config(text="|>")
else:
btnPP.config(text="||")
#import all songs from querie
def funcImportall():
playlist.extend(URLlist)
#controlling the volume
def funcVolume(volume_var):
player.audio_set_volume(int(volume_var))
#clear playlist
def funcClear():
global playlist
playlist = []
#setting sound quality
def funcQual(amount):
global soundquality, soundqualities
if amount == "best":
soundqualities = "best"
else:
soundqualities = ""
if amount == 0:
soundquality = 0
elif amount == 1 or amount ==-1:
soundquality += amount
#toggle autoplay
def funcAuto():
global auto
auto = not(auto)
#toggle limit duration of song
def funcDur():
global dur
dur = not(dur)
#toggling shuffle
def funcRAN():
global ran
ran = not(ran)
btnPP = Button(win, text = "||", command =(lambda: funcPP()))
btnBACK = Button(win, text = "<", command =(lambda: funcSKIP(-10000)))
btnBACK2 = Button(win, text = "<<", command =(lambda: songskip(-1)))
btnFWD = Button(win, text = ">", command =(lambda: funcSKIP(10000)))
btnFWD2 = Button(win, text = ">>", command =(lambda: songskip(1)))
btnDL = Button(win, text = "↓")
btnL = Button(win, text = "<-")
btnR = Button(win, text = "->")
btnPlay = Button(win, text = "OK")
btnAddsong = Button(win, text = "+")
timescale = Scale(win, from_=0, to=1000, orient=HORIZONTAL,length=200, variable = timescale_var, showvalue=0, command = settime)
volume_scale = Scale(win, from_=200, to=0, orient=VERTICAL,length=80, variable = volume_var, showvalue=0, command = funcVolume)
volume_scale.place(x=580, y=2)
volume_scale.set(100)
title_label = Label(win, text = "")
title_label.place(x=5, y=36)
time_label = Label(win, text = "")
time_label.place(x=220, y=66)
SearchBox = Entry(win, width=20)
SearchBox.place(x=5, y=5)
SearchBox.bind('<Return>', funcSearch)
btnL.place(x=5, y=62)
btnR.place(x=60, y=62)
btnPlay.place(x=115, y=62)
win.bind_all("<Button-1>", lambda event: event.widget.focus_set())
filemenu = Menu(win, tearoff=0)
filemenu.add_command(label="toggle shuffle", command=funcRAN)
filemenu.add_command(label="toggle limit duration", command=funcDur)
filemenu.add_command(label="toggle autoplay", command=funcAuto)
editmenu = Menu(menubar, tearoff=0)
editmenu.add_command(label="all results to playlist", command=funcImportall)
editmenu.add_command(label="clear playlist", command=funcClear)
qualmenu = Menu(menubar, tearoff=0)
qualmenu.add_command(label="quality up", command=(lambda: funcQual(1)))
qualmenu.add_command(label="quality down", command=(lambda: funcQual(-1)))
qualmenu.add_command(label="best quality", command=(lambda: funcQual("best")))
qualmenu.add_command(label="worst quality", command=(lambda: funcQual(0)))
menubar.add_cascade(label="Quality", menu=qualmenu)
menubar.add_cascade(label="Options", menu=filemenu)
menubar.add_cascade(label="Playlists", menu=editmenu)
win.config(menu=menubar)
win.bind('<space>',lambda event:funcPPTRANSFER())
win.after(2000, lambda:every_second())
SearchBox.focus()
win.mainloop()
4 Answers 4
You could make a more object-oriented approach
- You could create a class called
Song
which represents a song in your playlist. It's attributes would be e.g.title
,url
,duration
,quality
, and own methods likeplay()
,download()
, andadd_to_playlist()
. And then store all theSong
instances in a List or a dictionary. - You could create a class called
Playlist
which represents the entire playlist. It's attriubutes would be e.g.songs
,current_song
,shuffle
and own methods likeadd_song()
,remove_song()
,shuffle()
andplay()
. Then you can use aPlaylist
instance to manage the playlist in your app - You could create a class called
Player
which represents the media player in your app. It's attributes would be e.g.volume
,quality
,timescale
, and own methods likeplay()
,pause()
,stop()
andset_volume()
. This class would be used to control the media player in your app.
Removing the func...
in front of every function would also be a good idea because the keyword def
already implies that it is a function.
To answer your other questions:
It is generally considered good practice to keep functions short and focused, rather than trying to do too much in a single function. Splitting up your functions into smaller, more specialized functions can make your code easier to read, understand, and maintain.
It is not necessarily a problem to use global variables, but they can make it more difficult to understand how your code works, and they can lead to unexpected behavior if not used carefully. In general, it is generally better to avoid using global variables whenever possible, and to pass variables between functions as arguments instead.
-
\$\begingroup\$ Thanks. As to shortening the functions, I am still in progress, but bit by bit I made the functions very clear, making simple functions with a clear input and output and therefor clear function; like converting youtube URL to stream Url, or moving the index forwards or so. As to the objects, I didnt start yet, but I am thinking a lot about it, and think I could manage. \$\endgroup\$Willem van Houten– Willem van Houten2022年12月24日 19:23:26 +00:00Commented Dec 24, 2022 at 19:23
I have heard that global vars are problematic
That is true for various reasons, a few of them are listed here:
- Naming conflicts - The global namespace is available in the whole module, so you might overwrite them.
- Keeping track of all the global and therefore reserved names are rather difficult.
If somebody could maybe illustrate me, or indicate, how I could take a object oriented approach, this would be super interesting.
You can check another answer from me on StackOverflow for an introduction. However, be aware that there are different schools of thoughts here. Some say they never write classes and that works best for them and others like me use classes for reuse-ability and structure, because we are used to it.
There are also different approaches on how to do this. Subclassing like in the linked answer has some downsides, like you need to take care, as much as with global variables, not to overwrite some internals of the class. You might want to write a wrapper class instead, but this also could lead to redundant code and difficulties with garbage collection. So it is more a topic to figure out for yourself, what works best for you.
is it conventional that my functions are relatively long, or is it more common to split them up into smaller parts?
Pabashani Herath has written some interesting articles about clean code and in the aspect of functions and methods she writes:
Functions that have 200 or 300 lines are kind of messy functions. They should not be longer than 20 lines and mostly less than 10 lines. Functions should be making as few arguments as possible, ideally none. Small functions are easy to understand and their intention is clear.
In my opinion, small functions are good. But when you split them up into pieces, it should be in a reasonable way. In the best cases you can split it into smaller functions you could use outside of your "mother function". Bare in mind that you might come back to your code and you want to be able to understand it, even if you did not worked on it for years.
I spent some time looking into this website, of how good, readable code is made
You have missed the best source of all the PEP 8 - Style Guide and the famous PEP 20 - The Zen of Python. There you will find some hints about names, format and the does and dont`s. Like use camelcase for class names, uppercase variable names indicate constants and so on.
Besides the format of your code, there are definitely things you can improve.
- Don't tuple your functions, tkinter has to flatten every single one of these:
command = (lambda: funcMOVE(title, -1, resultcount)
- Don't use lambda if you actually don't need it, like here:
lambda: funcAddsong()
- You can replace some of the lambda expression with just using constants:
lambda: funcSKIP(-10000)
place
is the most powerful geometry manager in tkinter, but it is hard to get right. Instead you should use place for the edge cases where the otherspack
andgrid
are not sufficient enough for the job. I also have written something about the basics of tkinters geometry management- Instead of dumping everything into the global namespace you could use SimpleNamespace as an entrypoint to classes
- You could use these namespaces and attach widgets in a loop
- Consider using ttk widgets for styling, mapping, customizing and more color options
-
\$\begingroup\$ Hello, I just want to write that I integrated a couple of things from this. I didn't use SimpleNamespace as such, but inspired by that I made a dictionary of my toggle-variables (I guess a dictionayry is a bit similar to a simplenamespace), and used the dictionary to dynamically toggle stuff; to add another toggle factor I just need to expand the dictionary so its super simple and made the whole app like 100 lines shorter. I also flattened most of the functions now. As to grid, yeah, I have to learn that.... \$\endgroup\$Willem van Houten– Willem van Houten2022年12月28日 12:45:36 +00:00Commented Dec 28, 2022 at 12:45
-
\$\begingroup\$ @WillemvanHouten I'm glad you had found something useful in it, took me awhile to write this answer and a 100 lines less code, plus learning something new seems quite nice to me. :) \$\endgroup\$Thingamabobs– Thingamabobs2022年12月28日 12:56:56 +00:00Commented Dec 28, 2022 at 12:56
You got me inspired that I took some time to refactor your program. There are some significant changes:
- Use of classes (
YouTubePlayer
andTkGuiPlayer
) - Renaming all variable names to Python standard naming convention
- Import
tkinter
specific methods and constants (do not dofrom tkinter import *
) ran = song_index = dur = timescale_var = 0
is incorrect, you could doran, song_index, dur, timescale_var = 0, 0, 0, 0
URLlist = playlist = []
makes the two list the same by different names and results in buggy behavior of the program. You must doURLlist = []; playlist = []
- Business logic and display are all mixed up, making the program hard to read
- Avoid using
place
intkinter
; my preference is to use thegrid
method and putframes
inside a mainframe
.Buttons
and other widgets that belong together I usuallypack
inside a dedicatedFrame
. - Always use the construction:
if __name__ == '__main__': main()
, wheremain
is a function to start the program.
In any case, the refactored program youtube_player.py
with a slightly revised UI.
import re
import random
import datetime
from tkinter import (
Tk, Menu, Frame, Label, Button, Scale, Entry, DISABLED, StringVar
)
from youtube_search import YoutubeSearch
import vlc
import yt_dlp
MAX_SEARCH_RESULTS = 40
MAX_SONG_LENGTH = 300
YOUTUBE_BASE_URL = 'https://www.youtube.com'
NON_CHARS = r'[^a-zA-Z0-9 .,:;+-=!?/()öäßü]'
class YouTubePlayer:
''' Class with methods to search songs on YouTube, get the
audio urls and play songs with a vlc player
'''
def __init__(self, short_song: bool=False):
self.short_song_ = short_song
self.instance = vlc.Instance()
self.player = self.instance.media_player_new()
def search(self, search_query):
results = YoutubeSearch(
search_query, max_results=MAX_SEARCH_RESULTS
).to_dict()
song_list = []
for result in results:
if self.short_song_:
try:
min, sec = result['duration'].split(':')
if int(min) + int(sec) > MAX_SONG_LENGTH:
continue
except ValueError:
continue
song_list.append({
'url': ''.join([YOUTUBE_BASE_URL, result['url_suffix']]),
'title': re.sub(NON_CHARS, '', result['title'])
})
return song_list
def get_audio_urls(self, url):
audio_urls = []
with yt_dlp.YoutubeDL({}) as ydl:
info = ydl.extract_info(url, download=False)
title = info['title']
# get the audio urls different quality resolutions
for format in info['formats']:
if format['resolution'] == 'audio only':
audio_urls.append(format['url'])
return audio_urls, title
def get_player(self, url):
media = self.instance.media_new(url)
media.get_mrl()
self.player.set_media(media)
def play(self):
self.player.play()
def pause(self, pause):
self.player.set_pause(pause)
@property
def short_song(self) -> bool:
return self.short_song_
@short_song.setter
def short_song(self, val: bool) -> None:
self.short_song_ = val
@property
def length(self):
return self.player.get_length()
@property
def time(self):
return self.player.get_time()
@time.setter
def time(self, val):
self.player.set_time(val)
@property
def volume(self):
self.player.audio_get_volume()
@volume.setter
def volume(self, val):
self.player.audio_set_volume(int(val))
def download(self, url, title):
output_template = f'{title}.%(ext)s'
ydl_options = {
'format': 'bestaudio/best',
'outtmpl': output_template,
'postprocessors': [
{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3',
'preferredquality': '192',
},
{'key': 'FFmpegMetadata'},
],
}
with yt_dlp.YoutubeDL(ydl_options) as ydl:
_ = ydl.extract_info(YOUTUBE_BASE_URL + url, download=True)
class TkGuiPlayer:
geometry_window = '610x160'
window_title = 'Youtube Player'
query_width = 30
pl_width = 50
poll_time = 1000
length_progress_bar = 210
progress_bar_resolution = 0.001
skip_time = 10000
length_volume_bar = 80
max_volume_bar = 100
init_volume = 30
def __init__(self):
self.ytp = YouTubePlayer()
self.window = Tk()
self.main_frame = Frame(self.window)
self.main_frame.grid(row=1, column=1, padx=5, sticky='new')
self.window.geometry(self.geometry_window)
self.window.protocol('WM_DELETE_WINDOW', self.quit)
self.window.title(self.window_title)
self.query_song_title = StringVar()
self.query_song_title.set('')
self.pl_current_title = StringVar()
self.pl_current_title.set('')
self.pl_next_title = StringVar()
self.pl_next_title.set('')
self.quality_text = StringVar()
self.quality_text.set(str(1))
self.shuffle_text = StringVar()
self.short_text = StringVar()
self.auto_text = StringVar()
self.pl_song_time_text = StringVar()
self.pl_song_time_text.set(' / '.join([str(datetime.timedelta(0)),
str(datetime.timedelta(0))]))
self.playlist = []
self.pl_index = 0
self.pl_not_played_indexes = []
self.querylist = []
self.query_index = 0
self.current_song = None
self.prev_song = None
self.quality_level = 1
self.shuffle = False
self.shuffle_text.set('Y' if self.shuffle else 'N')
self.short_song = False
self.short_text.set('Y' if self.short_song else 'N')
self.autoplay = True
self.auto_text.set('Y' if self.autoplay else 'N')
self.pause = False
self.set_menubar()
self.set_query_frame()
self.set_pl_frame()
self.pl_show_title()
self.set_status_frame()
self.poll_song_status()
self.window.mainloop()
def set_menubar(self):
menubar = Menu(self.window)
self.set_quality_menu()
menubar.add_cascade(label='Quality', menu=self.quality_menu)
self.set_file_menu()
menubar.add_cascade(label='Option', menu=self.file_menu)
self.set_playlist_menu()
menubar.add_cascade(label='Playlist', menu=self.playlist_menu)
self.window.config(menu=menubar)
def set_quality_menu(self):
self.quality_menu = Menu(self.window, tearoff=0)
self.quality_menu.add_command(
label='best quality', command=lambda: self.set_quality('max'))
self.quality_menu.add_command(
label='quality up', command=lambda: self.set_quality(1))
self.quality_menu.add_command(
label='quality down', command=lambda: self.set_quality(-1))
self.quality_menu.add_command(
label='least quality', command=lambda: self.set_quality(0))
def set_file_menu(self):
self.file_menu = Menu(self.window, tearoff=0)
self.file_menu.add_command(label='toggle shuffle', command=self.toggle_shuffle)
self.file_menu.add_command(label='toggle short song', command=self.toggle_short_song)
self.file_menu.add_command(label='toggle autoplay', command=self.toggle_autoplay)
def set_playlist_menu(self):
self.playlist_menu = Menu(self.window, tearoff=0)
self.playlist_menu.add_command(
label='all results to playlist', command=self.import_all_to_playlist)
self.playlist_menu.add_command(
label='clear playlist', command=self.clear_playlist)
def import_all_to_playlist(self):
self.playlist += self.querylist
def clear_playlist(self):
self.playlist = []
self.pl_show_title()
def toggle_autoplay(self):
self.autoplay = not self.autoplay
self.auto_text.set('Y' if self.autoplay else 'N')
def toggle_short_song(self):
self.ytp.short_song = not self.ytp.short_song
self.short_text.set('Y' if self.ytp.short_song else 'N')
def toggle_shuffle(self):
self.shuffle = not self.shuffle
self.shuffle_text.set('Y' if self.shuffle else 'N')
def set_quality(self, val):
match val:
case 'max':
self.quality_level = 999
case 1:
self.quality_level += 1
case -1:
self.quality_level -= 1
if self.quality_level < 0:
self.quality_level = 0
case 0:
self.quality_level = 0
case _:
self.quality_level = 1
self.quality_text.set(str(self.quality_level))
def set_query_frame(self):
query_frame = Frame(self.main_frame)
query_frame.grid(row=1, column=1, sticky='new')
title_label = Label(query_frame, text='Search your song ...', width=self.query_width)
title_label.grid(row=1, column=1, sticky='nw')
self.query_text_entry = Entry(query_frame, width=self.query_width)
self.query_text_entry.grid(row=2, column=1, sticky='nw')
self.song_text_entry = Entry(
query_frame, textvariable=self.query_song_title, state=DISABLED, width=self.query_width)
self.song_text_entry.grid(row=3, column=1, stick='nw')
Label(query_frame, text=' ').grid(row=4, column=1)
button_frame = Frame(query_frame)
button_frame.grid(row=5, column=1, sticky='nw')
Button(button_frame, text='<-', command=self.query_prev).pack(side='left')
Button(button_frame, text='->', command=self.query_next).pack(side='left')
Button(button_frame, text='|>', command=self.query_play).pack(side='left')
Button(button_frame, text='add', command=self.query_add_song).pack(side='left')
self.query_text_entry.bind('<Return>', self.query_songs)
def query_songs(self, _):
self.querylist = self.ytp.search(str(self.query_text_entry.get()))
if self.querylist:
self.query_index = 0
self.query_song_title.set(self.querylist[self.query_index]['title'])
def query_prev(self):
if self.querylist:
self.query_index = (
self.query_index - 1 if self.query_index > 0
else len(self.querylist) - 1
)
self.query_song_title.set(self.querylist[self.query_index]['title'])
def query_next(self):
if self.querylist:
self.query_index = (
self.query_index + 1 if self.query_index < len(self.querylist) - 1
else 0
)
self.query_song_title.set(self.querylist[self.query_index]['title'])
def query_play(self):
if self.querylist:
self.play_song(self.querylist[self.query_index])
def query_add_song(self):
if self.querylist:
self.playlist.append(self.querylist[self.query_index])
self.pl_not_played_indexes = [i for i in range(len(self.playlist))]
self.pl_index = 0
self.pl_show_title()
def set_pl_frame(self):
pl_frame = Frame(self.main_frame)
pl_frame.grid(row=1, column=2, sticky='new')
title_label = Label(pl_frame, text='Playlist ...', width=self.query_width)
title_label.grid(row=1, column=1, columnspan=2, sticky='nw')
self.pl_current_entry = Entry(pl_frame, textvariable=self.pl_current_title, state=DISABLED, width=self.pl_width)
self.pl_current_entry.grid(row=2, column=1, sticky='nw')
self.pl_next_entry = Entry(pl_frame, textvariable=self.pl_next_title, state=DISABLED, width=self.pl_width)
self.pl_next_entry.grid(row=3, column=1, sticky='nw')
progress_frame = Frame(pl_frame)
progress_frame.grid(row=4, column=1, sticky='nw')
Label(progress_frame, textvariable=self.pl_song_time_text, anchor='w').pack(side='left')
self.progress_bar = Scale(progress_frame, from_=0, to_=int(1 / self.progress_bar_resolution),
length=self.length_progress_bar, orient='horizontal', showvalue=0)
self.progress_bar.pack(side='left')
self.progress_bar.bind('<ButtonRelease-1>', self.set_song_time)
button_frame = Frame(pl_frame)
button_frame.grid(row=5, column=1, sticky='nw')
Button(button_frame, text='<<', command=self.pl_play_prev).pack(side='left')
Button(button_frame, text='<', command=self.pl_song_back).pack(side='left')
self.pl_play_pause_button = Button(button_frame, text='|>', command=self.pl_play_or_pause)
self.pl_play_pause_button.pack(side='left')
Button(button_frame, text='>', command=self.pl_song_forward).pack(side='left')
Button(button_frame, text='>>', command=self.pl_play_next).pack(side='left')
Label(button_frame, text=' ').pack(side='left')
Button(button_frame, text='<-', command=self.pl_prev).pack(side='left')
Button(button_frame, text='->', command=self.pl_next).pack(side='left')
Button(button_frame, text='Remove', command=self.pl_remove_song).pack(side='left')
self.volume_bar = Scale(pl_frame, from_=self.max_volume_bar, to_=0,
length=self.length_volume_bar, orient='vertical', showvalue=1)
self.volume_bar.grid(row=2, column=2, rowspan=4, sticky='nw', padx=30)
self.volume_bar.bind('<ButtonRelease-1>', self.set_volume)
self.volume_bar.set(self.init_volume)
def pl_show_title(self):
if self.playlist:
self.pl_next_title.set(self.playlist[self.pl_index]['title'])
else:
self.pl_next_title.set('')
def pl_play_prev(self):
if self.prev_song:
self.play_song(self.prev_song)
def pl_song_back(self):
if self.ytp.length > 0:
self.ytp.time = self.ytp.time - self.skip_time if self.ytp.time > self.skip_time else 0.0
def pl_play_or_pause(self):
self.pause = not self.pause
self.ytp.pause(self.pause)
if self.pause:
self.pl_play_pause_button.config(text='|>')
else:
self.pl_play_pause_button.config(text='||')
def pl_song_forward(self):
if (length := self.ytp.length) > 0:
self.ytp.time = (
self.ytp.time + self.skip_time
if self.ytp.time + self.skip_time < length else length
)
def pl_play_next(self):
if self.playlist:
self.prev_song = self.current_song
self.play_song(self.playlist[self.pl_index])
self.pl_not_played_indexes.remove(
self.pl_index) if self.pl_index in self.pl_not_played_indexes else None
self.pl_next()
def pl_prev(self):
if self.playlist:
if not self.shuffle:
self.pl_index = divmod(self.pl_index - 1, len(self.playlist))[1]
self.pl_show_title()
else:
self.pl_next()
def pl_next(self):
if self.playlist:
if not self.shuffle:
self.pl_index = divmod(self.pl_index + 1, len(self.playlist))[1]
else:
if not self.pl_not_played_indexes:
self.pl_not_played_indexes = [i for i in range(len(self.playlist))]
self.pl_index = random.choice(self.pl_not_played_indexes)
self.pl_show_title()
def pl_remove_song(self):
if self.playlist:
del self.playlist[self.pl_index]
self.pl_not_played_indexes = [i for i in range(len(self.playlist))]
self.pl_index = self.pl_index - 1 if self.playlist else 0
self.pl_next()
def set_song_time(self, _):
self.ytp.time = int(self.progress_bar.get() * self.progress_bar_resolution * self.ytp.length )
def set_volume(self, _):
self.ytp.volume = self.volume_bar.get()
def select_stream(self, song_url):
if song_url:
audio_urls, _ = self.ytp.get_audio_urls(song_url)
if self.quality_level > len(audio_urls) - 1:
self.stream = audio_urls[-1]
self.quality_level = len(audio_urls) - 1
else:
self.stream = audio_urls[self.quality_level]
self.quality_text.set(str(self.quality_level))
def play_song(self, song_dict):
self.select_stream(song_dict['url'])
self.ytp.get_player(self.stream)
self.ytp.play()
self.pl_play_pause_button.config(text='||')
self.pl_current_title.set(song_dict['title'])
self.current_song = song_dict
self.pause = False
def poll_song_status(self):
time = self.ytp.time * 0.001
length = self.ytp.length * 0.001
self.pl_song_time_text.set(' / '.join([
str(datetime.timedelta(seconds=int(time))),
str(datetime.timedelta(seconds=int(length)))]))
self.progress_bar.set(time / length / self.progress_bar_resolution
if length > 0 else 0.0)
# fetch the song again if completed; if autoplay then play next song
if length > 0 and abs(time - length) < 1:
if self.autoplay:
self.pl_play_next()
else:
self.ytp.get_player(self.stream)
self.ytp.play()
self.pause = False
self.pl_play_or_pause()
self.window.after(self.poll_time, self.poll_song_status)
def set_status_frame(self):
main_status_frame = Frame(self.main_frame)
main_status_frame.grid(row=2, column=1, columnspan=2, sticky='nw')
Label(main_status_frame, text=' ').grid(row=1, column=1)
status_frame = Frame(main_status_frame)
status_frame.grid(row=2, column=1, sticky='nw')
Label(status_frame, text='Q:', anchor='w', width=2).pack(side='left')
Label(status_frame, textvariable=self.quality_text, anchor='w', width=3).pack(side='left')
Label(status_frame, text='Shuffle:', anchor='w', width=6).pack(side='left')
Label(status_frame, textvariable=self.shuffle_text, anchor='w', width=3).pack(side='left')
Label(status_frame, text='Short:', anchor='w', width=6).pack(side='left')
Label(status_frame, textvariable=self.short_text, anchor='w', width=3).pack(side='left')
Label(status_frame, text='Auto:', anchor='w', width=6).pack(side='left')
Label(status_frame, textvariable=self.auto_text, anchor='w', width=3).pack(side='left')
def quit(self):
self.window.after(500, self.window.destroy)
def main():
TkGuiPlayer()
if __name__ == '__main__':
main()
Program tested for Python 3.11 with requirements.txt
Brotli==1.0.9
certifi==2022年12月7日
charset-normalizer==2.1.1
idna==3.4
mutagen==1.46.0
pycryptodomex==3.16.0
python-vlc==3.0.18121
requests==2.28.1
urllib3==1.26.13
websockets==10.4
youtube-search==2.1.2
yt-dlp==2022年11月11日
-
\$\begingroup\$ Got it running. Match doesnt work on old python, but I rewrote that one bit of code, and got it working. Your system for removing songs from the playlist is great, I had been struggling with that. I have a bit of a problem now because in the meanwhile, I have expanded the code a lot, among other things I added the option to toggle video (its quite easy to extract the video urls, and to embed a vlc window). Since I know nothing about OOP I dont know if I could handle integrating it into your code. But Ill look at that. \$\endgroup\$Willem van Houten– Willem van Houten2022年12月27日 19:34:39 +00:00Commented Dec 27, 2022 at 19:34
-
\$\begingroup\$ Thank you for the feedback, I would suggest if you make these quite complicated GUI applications you should do this with OOP, I.e. classes and spending some time to learn this, will benefit you in the end. \$\endgroup\$Bruno Vermeulen– Bruno Vermeulen2022年12月27日 23:33:16 +00:00Commented Dec 27, 2022 at 23:33
-
\$\begingroup\$ I am now looking into your code, and trying to use it to learn OOP. I'm slowly understanding how decorators work. Very nifty. Generally, I'm really happy that you posted this because it gives me a sort of direct insight in how the things I do would look in OOP style. I also didn't know the concept of business logic, but separating that seems a great idea. I'm now partly taking things from your code and integrating in mine, partly trying to learn OOP, which seems manageable, so I could maybe add to your code some of my ideas. \$\endgroup\$Willem van Houten– Willem van Houten2022年12月27日 23:50:47 +00:00Commented Dec 27, 2022 at 23:50
-
\$\begingroup\$ After some debugging and small improvements I have put a version on Github: github.com/bvermeulen/youtube_player. Now looking at refactoring to get rid of the indexes. Mogelijk in het nieuwe jaar. \$\endgroup\$Bruno Vermeulen– Bruno Vermeulen2022年12月28日 10:26:54 +00:00Commented Dec 28, 2022 at 10:26
I found one major improvement to my code, which is to make it independant of pafy. Pafy is a badly functioning library, and in fact, it is only there to make using yt_dlp easier.
Also, I add a requirements.txt file to make install easier.
#! /usr/bin/python3
from tkinter import *
from youtube_search import YoutubeSearch
import re,random, vlc,datetime, time,yt_dlp
#global vars
ran = song_index = dur = timescale_var = 0
auto = 1
play_index=-1
volume_var= 100
soundquality = 5
soundqualities = "best"
URLlist = playlist = []
#window, player
win = Tk()
win.geometry('610x100')
win.title("Youtube Player")
menubar = Menu(win)
instance = vlc.Instance()
player = instance.media_player_new()
#making the search
def Search(event):
global song_index, URLlist
search_querie = str(SearchBox.get())
song_index = 0
results = YoutubeSearch(search_querie, max_results=40).to_dict()
title=[]
URLlist=[]
resultcount = -1
#the following loop is to optionally select only short songs <5min
for v in results:
duration = v['duration']
if duration != 0:
if duration.count(':') > 1 and dur == 1:
continue
if duration.count(':') == 1:
m, s = duration.split(':')
duration = int(m) * 60 + int(s)
if duration > 300 and dur == 1:
continue
URLlist.append("https://www.youtube.com" + v['url_suffix'])
resultcount+= 1
title.append(re.sub(r"[^a-zA-Z0-9 .,:;+-=!?/()öäßü]", "", v['title']))
btnPlay.focus()
btnL.config(command = (lambda: TitleShift(title, -1, resultcount)))
btnR.config(command = (lambda:TitleShift(title, 1, resultcount)))
btnPlay.config(command = (lambda: NewSong("insert",1)))
btnAddsong.config(command = (lambda: Addsong()))
btnDL.place(x=505, y=2)
btnDL.config(command =(lambda: Download(URLlist[song_index],title[song_index])))
title_label.config(text = title[song_index])
#moving through the songlist
def TitleShift(title,move, resultcount):
win.focus()
global song_index
song_index += move
if song_index < 0:
song_index =resultcount
if song_index > resultcount:
song_index = 0
title_label.config(text = title[song_index])
#this function keeps track of time and moves the timescale
def UpdateTime():
length = player.get_length()
place = player.get_time()
if player.is_playing() == 0 and abs(place-length) < 10000 and len(playlist) > 0 and auto == 1:
NewSong("next", 1)
place = 0
player.set_time(0)
timescale.set(0)
if player.is_playing() == 1:
time_info =str(datetime.timedelta(seconds = round(place/1000))) + " / " + str(datetime.timedelta(seconds = round(length/1000)))
time_label.config(text=time_info)
timescale.set(place)
win.after(1000,lambda:UpdateTime())
def GenerateStreamUrl(URL):
audio = []
ydl_opts = {}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info = ydl.extract_info(URL, download=False)
formats = info['formats']
songtitle = info['title']
for i,format in enumerate(formats):
url = format['url']
other = format['resolution']
if other == "audio only":
audio.append(url)
return(audio, songtitle)
#starting the song
def NewSong(question, direction):
win.focus()
#first, the play_index is updated
global play_index
play_index += direction
if play_index > (len(playlist)-1):
play_index = 0
if question == "insert":
playlist.insert(play_index,URLlist[song_index])
else:
if ran == 1 and len(playlist) > 1:
counttemp = play_index
while counttemp == play_index:
play_index = random.randrange(len(playlist)-1)
audio, songtitle = GenerateStreamUrl(playlist[play_index])
if soundqualities == "best":
stream = audio[len(audio)-1]
else:
qualities = len(audio)
if soundquality > qualities:
stream = audio[qualities]
else:
stream = audio[soundquality]
#print(stream.quality, int(stream.get_filesize()/10000)/100, "mb")
playurl = stream
media=instance.media_new(playurl)
media.get_mrl()
player.set_media(media)
player.set_time(0)
timescale.set(0)
btnPP.place(x=340, y=2)
btnAddsong.place(x=170, y=62)
btnBACK.place(x=295, y=2)
btnBACK2.place(x=240, y=2)
btnFWD.place(x=395, y=2)
btnFWD2.place(x=445, y=2)
timescale.place(x=370, y=68)
player.play()
btnPP.config(text="||")
while player.is_playing() == 0:
time.sleep(1)
timescale.config(to = player.get_length())
win.title(songtitle)
return(0)
#this is to select the next song in the list
def Addsong():
win.focus()
playlist.append(URLlist[song_index])
#next or previous song
def SkipSong(direction):
win.focus()
skip = play_index + direction
if direction == -1 and player.get_time() > 10000:
player.set_time(0)
elif skip >= 0 and skip < len(playlist):
NewSong("next", direction)
#this function is for downloading the song
def Download(song_url, song_title):
outtmpl = song_title + '.%(ext)s'
ydl_opts = {
'format': 'bestaudio/best',
'outtmpl': outtmpl,
'postprocessors': [
{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3',
'preferredquality': '192',
},
{'key': 'FFmpegMetadata'},
],
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
info_dict = ydl.extract_info("youtube.com" + song_url, download=True)
#moving through the scale for time
def SetTime(timescale_var):
place = player.get_time()
if abs(int(timescale_var) - place) > 4000:
player.set_time(int(timescale_var))
#this function is for moving back and forth in time of the song
def SkipTime(amount):
win.focus()
time_sum = player.get_time() + amount
time_all = player.get_length()
if time_sum < 0:
time_sum = 0
if time_sum>time_all:
time_sum = time_all
timescale.set(time_sum)
#to pause by keypress (space)
def TogglePause1():
if str(win.focus_get()) != str(".!entry"):
funcPP()
#to pause by keypress or click
def TogglePause2():
win.focus()
pause = player.is_playing()
player.set_pause(pause)
if pause == 1:
btnPP.config(text="|>")
else:
btnPP.config(text="||")
#import all songs from querie
def ImportAll():
playlist.extend(URLlist)
#controlling the volume
def ChangeVolume(volume_var):
player.audio_set_volume(int(volume_var))
#clear playlist
def ClearPlaylist():
global playlist
playlist = []
#setting sound quality
def ChangeQuality(amount):
global soundquality, soundqualities
if amount == "best":
soundqualities = "best"
else:
soundqualities = ""
if amount == 0:
soundquality = 0
elif amount == 1 or amount ==-1:
soundquality += amount
#toggle autoplay
def ToggleAutoplay():
global auto
auto = not(auto)
#toggle limit duration of song
def ToggleDurationLimit():
global dur
dur = not(dur)
#toggling shuffle
def ToggleShuffle():
global ran
ran = not(ran)
btnPP = Button(win, text = "||", command =(lambda: TogglePause2()))
btnBACK = Button(win, text = "<", command =(lambda: SkipTime(-10000)))
btnFWD = Button(win, text = ">", command =(lambda: SkipTime(10000)))
btnBACK2 = Button(win, text = "<<", command =(lambda: SkipSong(-1)))
btnFWD2 = Button(win, text = ">>", command =(lambda: SkipSong(1)))
btnDL = Button(win, text = "↓")
btnL = Button(win, text = "<-")
btnR = Button(win, text = "->")
btnPlay = Button(win, text = "OK")
btnAddsong = Button(win, text = "+")
timescale = Scale(win, from_=0, to=1000, orient=HORIZONTAL,length=200, variable = timescale_var, showvalue=0, command = SetTime)
volume_scale = Scale(win, from_=200, to=0, orient=VERTICAL,length=80, variable = volume_var, showvalue=0, command = ChangeVolume)
volume_scale.place(x=580, y=2)
volume_scale.set(100)
title_label = Label(win, text = "")
title_label.place(x=5, y=36)
time_label = Label(win, text = "")
time_label.place(x=220, y=66)
SearchBox = Entry(win, width=20)
SearchBox.place(x=5, y=5)
SearchBox.bind('<Return>', Search)
btnL.place(x=5, y=62)
btnR.place(x=60, y=62)
btnPlay.place(x=115, y=62)
win.bind_all("<Button-1>", lambda event: event.widget.focus_set())
filemenu = Menu(win, tearoff=0)
filemenu.add_command(label="toggle shuffle", command=ToggleShuffle)
filemenu.add_command(label="toggle limit duration", command=ToggleDurationLimit)
filemenu.add_command(label="toggle autoplay", command=ToggleAutoplay)
editmenu = Menu(menubar, tearoff=0)
editmenu.add_command(label="all results to playlist", command=ImportAll)
editmenu.add_command(label="clear playlist", command=ClearPlaylist)
qualmenu = Menu(menubar, tearoff=0)
qualmenu.add_command(label="quality up", command=(lambda: ChangeQuality(1)))
qualmenu.add_command(label="quality down", command=(lambda: ChangeQuality(-1)))
qualmenu.add_command(label="best quality", command=(lambda: ChangeQuality("best")))
qualmenu.add_command(label="worst quality", command=(lambda: ChangeQuality(0)))
menubar.add_cascade(label="Quality", menu=qualmenu)
menubar.add_cascade(label="Options", menu=filemenu)
menubar.add_cascade(label="Playlists", menu=editmenu)
win.config(menu=menubar)
win.bind('<space>',lambda event:TogglePause1())
win.after(2000, lambda:UpdateTime())
SearchBox.focus()
win.mainloop()
Requirements:
python_vlc==3.0.7110
youtube_search==2.1.2
yt_dlp==2022年11月11日
-
1\$\begingroup\$ I got it running with Python 3.11, will have a look at it and probably refactor using classes \$\endgroup\$Bruno Vermeulen– Bruno Vermeulen2022年12月24日 09:54:02 +00:00Commented Dec 24, 2022 at 9:54
def func...
- No! Just no! And don't replace docstrings with comments. \$\endgroup\$def
implies that a function definition is following. So you don't need to include the fact that the defined object is a function in its name. It's redundant. And no, none of the functions in your code classify as methods. \$\endgroup\$funcSearch
is like changing your name to HumanWillem and your dog's name to DogFido. Justsearch
, Willem and Fido are sufficient. \$\endgroup\$requirements.txt
file to importbs4
,youtube_search
,vlc
,pafy
,yt_dlp
for the code to run. Once running I would love to add my comments and suggestions. \$\endgroup\$