1
\$\begingroup\$

I made a Tkinter MP3 Player with Pygame and am looking for tips to improve. Explanation offered in comments in code, but it generally works by getting all the files in a folder. (Have censored filepaths for privacy.)

import os
import glob
import shutil
import pygame
import tkinter as tk
from pytube import YouTube
import tkinter.messagebox
from tkinter import filedialog as fd
from tkinter import simpledialog
from mutagen.mp3 import MP3
from klaxon import klaxon
import re
import applescript
from youtube_search import YoutubeSearch

def sorted_alphanumeric(data): #for sorting out the music in the tkinter listbox
    convert = lambda text: int(text) if text.isdigit() else text.lower()
    alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ] 
    return sorted(data, key=alphanum_key)
def listdir_nohidden(path): #for getting all files in a folder without 
    return sorted_alphanumeric(glob.glob(os.path.join(path, '*')))
def move_file(file): #for moving a music file to the folder
    try:
        name_and_ext = os.path.basename(file)
        target = name_and_ext
        shutil.copyfile(file, target)
    except TypeError:
        pass
def mp4_mp3(filepath): #for converting the downloaded mp4 file from pytube into mp3 format
    klaxon(title = 'MP3 Player', subtitle = 'Downloading YouTube...', message = 'This will take some time.')
    from moviepy.editor import VideoFileClip
    try:
        clip = VideoFileClip(filepath)
        clip.write_videofile(filepath.replace('.mp4', '.mp3'), logger = None)
        clip.close()
    except:
        pass
    os.remove(filepath)
    name, ext = os.path.splitext(filepath)
    os.remove(name + 'TEMP_MPY_wvf_snd.mp3')
def download(link): #for downloading the youtube video
    yt = YouTube(link)
    video = yt.streams.first()
    filepath = video.download('/Users/****/Desktop/****/****/****/****/Files')
    mp4_mp3(filepath)
    update_music()
def get_results(search): #for getting the youtube video names
    results = YoutubeSearch(search, max_results = 10).to_dict()
    res = []
    for x in range(10):
        video = results[x]
        info = []
        info.append(video['title'])
        info.append('youtube.com' + video['url_suffix'])
        res.append(info)
    return res
def open_window(): #for displaying the youtube video names for downloading
    ans = simpledialog.askstring(title = 'YouTube Video Downloader', prompt = 'What would you like to seach for on YouTube?')
    if ans is not None:
        window2 = tk.Tk()
        window2.title(f'YouTube Search Results For {ans}')
        res = get_results(ans)
        tk.Button(window2, text = res[0][0], command = lambda : download(res[0][1])).grid(row = 0, column = 0, padx = 5, pady = 5)
        tk.Button(window2, text = res[1][0], command = lambda : download(res[1][1])).grid(row = 0, column = 1, padx = 5, pady = 5)
        tk.Button(window2, text = res[2][0], command = lambda : download(res[2][1])).grid(row = 1, column = 0, padx = 5, pady = 5)
        tk.Button(window2, text = res[3][0], command = lambda : download(res[3][1])).grid(row = 1, column = 1, padx = 5, pady = 5)
        tk.Button(window2, text = res[4][0], command = lambda : download(res[4][1])).grid(row = 2, column = 0, padx = 5, pady = 5)
        tk.Button(window2, text = res[5][0], command = lambda : download(res[5][1])).grid(row = 2, column = 1, padx = 5, pady = 5)
        tk.Button(window2, text = res[6][0], command = lambda : download(res[6][1])).grid(row = 3, column = 0, padx = 5, pady = 5)
        tk.Button(window2, text = res[7][0], command = lambda : download(res[7][1])).grid(row = 3, column = 1, padx = 5, pady = 5)
        tk.Button(window2, text = res[8][0], command = lambda : download(res[8][1])).grid(row = 4, column = 0, padx = 5, pady = 5)
        tk.Button(window2, text = res[9][0], command = lambda : download(res[9][1])).grid(row = 4, column = 1, padx = 5, pady = 5)
def convert_mils(mils): #for converting the time from pygame.mixer.music.get_pos() into correct format
    seconds = int((mils/1000) % 60)
    minutes = int((mils/(1000 * 60)) % 60)
    return f'{minutes:02d}:{seconds:02d}'
def convert_secs(seconds): #for converting the seconds from mutagen.mp3.MP3(song).info.length into correct format
    m = int(seconds % 3600 // 60)
    s = int(seconds % 3600 % 60)
    return f'{m:02d}:{s:02d}'
def add_music(): #adding music as the user wishes
    try:
        res = applescript.AppleScript('display dialog "How would you like to add music?" buttons {"Cancel", "YouTube Search", "File Upload"} default button "File Upload"').run()
        for item in res:
            new_res = res[item]
        if new_res == 'File Upload':
            play_pause()
            file = get_music()
            if file != 'improper format':
                move_file(file)
                window.after(100, update_music)
        else:
            open_window()
            window.after(100, update_music)    
    except applescript.ScriptError:
        pass
def delete_music():
    selected_file = play_list.get(tk.ACTIVE)
    confirm = tkinter.messagebox.askquestion('MP3 Player', 'Are you sure you want to delete ' + selected_file + '?')
    if confirm == 'yes':
        if selected_file == title.get():
            pygame.mixer.music.stop()
            title.set('')
        os.remove(selected_file + '.mp3')
    window.after(100, update_music)
def rename_music(): #renameing the song
    selected_song = play_list.get(tk.ACTIVE)
    ans = simpledialog.askstring(title = 'Music Renaming', prompt = f'What would you like to rename {selected_song} to?')
    if ans is not None:
        os.rename(selected_song + '.mp3', ans + '.mp3')
        title.set(ans)
        update_music()
def edit_music(): #displaying options for editing
    selected_file = play_list.get(tk.ACTIVE)
    try:
        response = applescript.AppleScript('set res to display dialog "Actions For Song ' + selected_file + '" buttons {"Cancel", "Rename", "Delete"} default button "Delete"').run()
        for item in response:
            res = response[item]
        if res == "Delete":
            delete_music()
        else:
            rename_music()
    except:
        pass
def update_music(): #for updating the files in tkinter Listbox
    play_list.delete(0, tk.END)
    song_list = listdir_nohidden('/Users/****/Desktop/****/****/****/****/Files')
    for x in range(len(song_list)):
        orig = song_list[x]
        name = os.path.basename(orig)
        song_list[x] = name
    pos = 0
    for item in song_list:
        play_list.insert(pos, item.replace('.mp3', ''))
        pos += 1
    if play_list.size() > 10:
        scroll.pack(side = 'right', fill = 'y')
    else:
        scroll.pack_forget()
def unconvert(mins_and_secs): #for changing the '{mins:02d}:{secs:02d}' format into seconds
    mins, secs = mins_and_secs.split(':')
    if mins[0] == '0':
        mins = mins[1:]
    if secs[0] == '0':
        secs = secs[1:]
    more_secs = int(mins) * 60
    total_secs = more_secs + int(secs)
    return total_secs
def update_time(): #for updating music time tracker
    global already_run

    if pygame.mixer.music.get_pos() != -1:
        current_time = pygame.mixer.music.get_pos()
        song = MP3(title.get() + '.mp3')
        seconds_total = int(song.info.length)
        mins_and_secs = convert_mils(current_time)
        current_sec = unconvert(mins_and_secs)
        remaining_secs = seconds_total - current_sec
        remaining_secs = convert_secs(remaining_secs)
        music_time.set(f'{mins_and_secs}\t-{remaining_secs}')
    else:
        if already_run is True:
            music_time.set('')
            next_song()
    already_run = True
    window.after(99, update_time)
def get_music(): #getting a file to add to songs
    f = fd.askopenfile(filetypes = (('MP3 Files', '*.mp3'), ('All Files', '*.*')))
    if f is not None:
        file = f.name
        root, ext = os.path.splitext(file)
        if ext in ['.mp3']:
            return file
        else:
            tkinter.messagebox.showinfo('MP3 Player', 'Must be MP3 file format!')
            return 'improper format'
def play(): #playing the music
    if already_run is not True:
        update_time()
    pygame.mixer.music.stop()
    pygame.mixer.music.load(play_list.get(tk.ACTIVE) + '.mp3')
    title.set(play_list.get(tk.ACTIVE))
    pygame.mixer.music.play()
    status.set('Pause')
def pause(): #pausing music
    pygame.mixer.music.pause()
def unpause(): #resuming music
    pygame.mixer.music.unpause()
def change_volume(volume): #chaning volume with tk.Scale
    pygame.mixer.music.set_volume(int(volume)/100)
def on_closing(): #what to do when window is closed
    pygame.mixer.music.stop()
    pygame.mixer.quit()
    window.destroy()
def next_selection(): #to select the next song
    song_name = title.get()
    index = play_list.get(0, "end").index(song_name)
    selection_indices = play_list.curselection()
    play_list.selection_clear(selection_indices)
    play_list.activate(index)
    play_list.selection_set(index)
    selection_indices = play_list.curselection()
    next_selection = 0
    if len(selection_indices) > 0:
        last_selection = int(selection_indices[-1])
        play_list.selection_clear(selection_indices)
        if last_selection < play_list.size() - 1:
            next_selection = last_selection + 1
    play_list.activate(next_selection)
    play_list.selection_set(next_selection)
def next_song(): #to select next song and play it
    next_selection()
    window.after(99, play)
def pause_play(): #to change the button from pause to resume
    stat = status.get()
    if stat == 'Pause':
        pause()
        status.set('Resume')
    else:
        unpause()
        status.set('Pause')
def main(): #main function
    
    global window, title, play_list, music_time, scroll, already_run, status

    already_run = False
    
    window = tk.Tk()
    window.title('MP3 Player')
    window.configure(bg = 'blue')
    window.resizable(False, False)
    window.protocol('WM_DELETE_WINDOW', on_closing)

    controls = tk.Frame()
    controls.grid(column = 1, row = 1, sticky = 'nsew', padx = 10, pady = 10)
    controls.configure(bg = 'blue')

    buttons = tk.Frame()
    buttons.grid(column = 0, row = 0, sticky = 'nsew', padx = 10, pady = 10)
    buttons.configure(bg = 'blue')
    
    songs = tk.Frame()
    songs.grid(column = 0, row = 1, sticky = 'nsew', padx = 10, pady = 10)
    songs.configure(bg = 'blue')

    song = tk.Frame()
    song.grid(column = 1, row = 0, sticky = 'nsew', padx = 10, pady = 10)
    song.configure(bg = 'blue')
    
    song_list = listdir_nohidden('/Users/****/Desktop/****/****/****/****/Files')
    for x in range(len(song_list)):
        orig = song_list[x]
        name = os.path.basename(orig)
        song_list[x] = name
        
    play_list = tk.Listbox(songs, bg='yellow', selectmode=tk.SINGLE)
    scroll = tk.Scrollbar(songs)
    play_list.config(yscrollcommand = scroll.set)
    scroll.config(command = play_list.yview)
    
    pos = 0
    
    for item in song_list:
        play_list.insert(pos, item.replace('.mp3', ''))
        pos += 1

    pygame.init()
    pygame.mixer.init()
    
    title = tk.StringVar()
    music_time = tk.StringVar()
    status = tk.StringVar()
    status.set('Pause')
    
    pause_button = tk.Button(controls, textvariable = status, command = lambda : pause_play())
    play_button = tk.Button(controls, text = 'Play', command = lambda : play())
    add_music_button = tk.Button(buttons, text = 'Add Music', command = lambda : add_music())
    delete_music_button = tk.Button(buttons, text = 'Edit Music', command = lambda : edit_music())
    song_title = tk.Label(song, textvariable = title, fg = 'yellow', bg = 'blue')
    song_time = tk.Label(song, textvariable = music_time, fg = 'yellow', bg = 'blue')
    volume_scale = tk.Scale(controls, from_ = 0, to = 100, tickinterval = 0.00001, showvalue = 0, label = '     Volume', command = change_volume, orient = tk.HORIZONTAL)
    volume_scale.set(50)
    
    add_music_button.pack(side='top', fill = 'x')
    volume_scale.grid(column = 0, row = 4, sticky = 'nsew', padx = 4, pady = 4)
    delete_music_button.pack(side='bottom', fill = 'x')
    play_list.pack(side = 'left', fill = 'both')
    song_title.pack(side = 'top', fill = 'x')
    song_time.pack(side = 'bottom', fill = 'x')
    play_button.grid(column = 0, row = 1, sticky = 'nsew', padx = 4, pady = 4)
    pause_button.grid(column = 0, row = 2, sticky = 'nsew', padx = 4, pady = 4)
    
    os.chdir('/Users/****/Desktop/****/****/****/****/Files')
    
    window.after(10, update_music())
    window.mainloop()

if __name__ == '__main__':
    main()
    
\$\endgroup\$
1
  • \$\begingroup\$ There seems to be some code missing. play_pause is undefined. \$\endgroup\$
    – Reinderien
    Jul 17, 2021 at 15:41

1 Answer 1

1
\$\begingroup\$

In this section:

convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]

convert can be moved to a regular function. In fact you can rephrase your alphanumeric sort key as an iterator:

split_alpha = re.compile(r'([0-9]+)').split


def alphanumeric_parts(key: str) -> Iterable[Union[int, str]]:
    for c in split_alpha(key):
        if c.isdigit():
            yield int(c)
        else:
            yield c.lower()


def alphanumeric_key(key: str) -> Tuple[Union[int, str], ...]:
    return tuple(alphanumeric_parts(key))


def sorted_alphanumeric(data: Iterable[str]) -> List[str]:  # for sorting out the music in the tkinter listbox
    return sorted(data, key=alphanumeric_key)

This avoids lambdas and comprehensions in favour of generator functions, pre-compiles and pre-binds your regular expression, and replaces your inner lists with inner tuples which are a better representation of immutable data. When looking at the disassembled bytecode of your original code, there would have been three inner subroutines defined - one for each of your lambdas and one for the comprehension; the shown alternative uses two. Also note the PEP8-standard spacing between function definitions and PEP484 type hints.

Otherwise:

  • Consider replacing os.path, several os.*, and shutil calls with pathlib calls
  • Move your moviepy import to the top of the file
  • clip having a close(), that explicit call should be removed and replaced with a context manager, as the source shows is possible due to the implementation of __exit__
  • Never try/except/pass; if you're going to swallow a specific exception, show it
  • Don't hard-code the path for download(); allow the user to choose, or default to somewhere like ~/Downloads
  • Your calls to tk.Button should be converted into a loop
  • The use of AppleScript for display dialog is unjustified. A more portable solution is right at your fingertips - use tkinter instead.
  • Your call to play_pause is incorrect and you probably meant pause_play
  • Your rampant use of globals window, title, play_list, music_time, scroll, already_run, status needs to be cleaned up - easy wins include classes, partials or closures
  • You have a bunch of UI code mixed right in with business logic, including in main. Try to separate these out.
\$\endgroup\$
1
  • \$\begingroup\$ Importing moviepy at the beginning makes Python quit unexpectedly. \$\endgroup\$
    – cold10
    Jul 17, 2021 at 20:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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