8
\$\begingroup\$

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()

Picture of the GUI

\$\endgroup\$
18
  • 1
    \$\begingroup\$ Maybe I'll write a full review later - some quick observations: def func... - No! Just no! And don't replace docstrings with comments. \$\endgroup\$ Dec 20, 2022 at 12:50
  • 2
    \$\begingroup\$ The keyword 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\$ Dec 20, 2022 at 20:54
  • 2
    \$\begingroup\$ @WillemvanHouten funcSearch is like changing your name to HumanWillem and your dog's name to DogFido. Just search, Willem and Fido are sufficient. \$\endgroup\$
    – ggorlen
    Dec 21, 2022 at 16:51
  • 1
    \$\begingroup\$ There is a lot to be said on this code. Please can you send me the requirements.txt file to import bs4, youtube_search, vlc, pafy, yt_dlp for the code to run. Once running I would love to add my comments and suggestions. \$\endgroup\$ Dec 22, 2022 at 14:43
  • 1
    \$\begingroup\$ Posting a picture of your GUI would have helped for people that don't use all your libraries. \$\endgroup\$ Dec 23, 2022 at 1:15

4 Answers 4

4
\$\begingroup\$

You could make a more object-oriented approach

  1. 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 like play(),download(), and add_to_playlist(). And then store all the Song instances in a List or a dictionary.
  2. 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 like add_song(), remove_song(), shuffle() and play(). Then you can use a Playlist instance to manage the playlist in your app
  3. 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 like play(), pause(), stop() and set_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.

\$\endgroup\$
1
  • \$\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\$ Dec 24, 2022 at 19:23
3
\$\begingroup\$

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)
\$\endgroup\$
2
  • \$\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\$ 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\$ Dec 28, 2022 at 12:56
2
\$\begingroup\$

You got me inspired that I took some time to refactor your program. There are some significant changes:

  • Use of classes (YouTubePlayer and TkGuiPlayer)
  • Renaming all variable names to Python standard naming convention
  • Import tkinter specific methods and constants (do not do from tkinter import *)
  • ran = song_index = dur = timescale_var = 0 is incorrect, you could do ran, 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 do URLlist = []; playlist = []
  • Business logic and display are all mixed up, making the program hard to read
  • Avoid using place in tkinter; my preference is to use the grid method and put frames inside a main frame. Buttons and other widgets that belong together I usually pack inside a dedicated Frame.
  • Always use the construction: if __name__ == '__main__': main(), where main is a function to start the program.

In any case, the refactored program youtube_player.py with a slightly revised UI.

enter image description here

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
\$\endgroup\$
4
  • \$\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\$ 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\$ 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\$ 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\$ Dec 28, 2022 at 10:26
1
\$\begingroup\$

enter image description here

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
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I got it running with Python 3.11, will have a look at it and probably refactor using classes \$\endgroup\$ Dec 24, 2022 at 9:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.