7
\$\begingroup\$

This music player app hasn't really completed yet but it runs the basic functions of a music player, which include play/pause, volume adjustment, next/prev, shuffle and repeat functionalities. However, the code is running under a single giant class that handles the entire program. It is extremely messy and further implementing the features will just lead to more mess. I mean, workable, but messy code. For this mess, I'm thinking of maybe separate the workings of some methods into another file, that or I create classes. I would really appreciate if anybody could check it for me. I'm open to listen to any suggestion or improvements that could help improve the structure of my code. Thank you in advance.

import os
import tkinter as tk
from tkinter import Button, filedialog as fd
from tkinter import ttk
from random import randint
from PIL import Image, ImageTk
from pygame import mixer
import pygame


# Constant variable to check whether or not current track has ended
END_OF_MUSIC = pygame.USEREVENT
mixer.music.set_endevent(END_OF_MUSIC)


class MusicPlayer(tk.Tk):
    def __init__(self) -> None:
        tk.Tk.__init__(self)
        pygame.init()
        mixer.init()

        self.music_end = END_OF_MUSIC

        self.icons = {}

        self.is_music_paused = False
        self.music_playing = False
        self.is_on_repeat = False
        self.is_mute = False
        self.is_load_file = False
        self.file_extension = ()

        self.track_index = 0
        self.track_info = None

        self.song_list = []
        self.song_file_paths = []
        self.file_info = {}
        self.file_indices = 0

        # Value of current volume
        self.sound_volume = 0

        # Value of previous volume
        self.previous_volume = 0

        self.song_directory = None
        self.track_count = 0

        self.is_shuffle = False

        self.is_track_repeat = False
        self.is_playlist_repeat = False

        self.configure_window()
        self.create_menubar()
        self.create_widgets()

    def configure_window(self):
        # Add a titlebar
        self.title("My Music Player")

        # Set background color
        self.configure(background="#212125")

        # Set the height and width of the window
        self.window_width = 1280
        self.window_height = 800

        # Set window geometry and center the window
        self.screen_width = self.winfo_screenwidth()
        self.screen_height = self.winfo_screenheight()
        self.x_coordinate = int((self.screen_width / 2) - (self.window_width / 2))
        self.y_coordinate = int((self.screen_height / 2.5) - (self.window_height / 2.5))
        self.geometry(
            f"{self.window_width}x{self.window_height}+{self.x_coordinate}+{self.y_coordinate}"
        )

        # Make window not resizable
        self.resizable(0, 0)

    def create_menubar(self):
        # Create a main menu
        self.menubar = tk.Menu(self, tearoff=False)

        # Create file menu under main menu
        self.filemenu = tk.Menu(self.menubar, tearoff=False)

        # Add a separator above the menu
        self.filemenu.add_separator()

        # Add submenus under main menu
        self.filemenu.add_command(
            label="Open...", font="Segoe 10", command=self.browse_file
        )
        self.filemenu.add_command(
            label="Open Folder...", font="Segoe 10", command=self.browse_directory
        )

        # Add a separator above the "Exit" command
        self.filemenu.add_separator()

        self.filemenu.add_command(label="Exit...", font="Segoe 10", command=exit)

        # Add file menu to main menu
        self.menubar.add_cascade(label="File", font="Segoe 10", menu=self.filemenu)

        # Configure main menu to root window
        self.config(menu=self.menubar)

    def create_widgets(self):
        self.create_bottom_frame()
        self.create_buttons()
        self.create_volume_slider()

    def create_bottom_frame(self):
        self.bottom_frame = tk.Frame(bg="silver")
        self.bottom_frame.grid(row=1, column=0, sticky="ew")
        self.bottom_frame.grid_columnconfigure(6, minsize=50)
        self.bottom_frame.grid_rowconfigure(0, weight=1)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

    def browse_directory(self):
        self.file_extension = ('mp3', 'wav', 'ogg',)
        self.song_directory = fd.askdirectory(initialdir=os.getcwd(), title="Select Directory With Songs")

        if self.song_directory:
            self.create_playlist_panel()
            self.shuffle_button.config(state=tk.NORMAL)
            self.previous_button.config(state=tk.NORMAL)
            self.next_button.config(state=tk.NORMAL)
            self.play_button.config(state=tk.NORMAL)
            self.repeat_button.config(state=tk.NORMAL)

        self.load_songs()

    def load_songs(self):
        for file_index, song_filename in enumerate(os.listdir(self.song_directory)):
            song_path = os.path.join(self.song_directory, song_filename)
            if song_filename.endswith(self.file_extension):
                self.track_count += 1
                self.song_list.append(song_filename)
                self.playlist_box.insert(tk.END, f"{self.track_count}. {song_filename}")

                self.file_info[file_index] = [song_filename, song_path]

            self.file_indices += 1

    def browse_file(self):
        filetypes = [("Audio Files", ".mp3 .ogg .wav")]

        self.music_file = fd.askopenfilename(
            initialdir=os.getcwd(), title="Select A Song", filetypes=filetypes
        )

        if self.music_file:
            self.play_button.config(state=tk.NORMAL)
            self.repeat_button.config(state=tk.NORMAL)

        mixer.music.load(self.music_file)

    def load_icons(self):
        self.icons = {
            "shuffle_icon": ImageTk.PhotoImage(Image.open("./icons/shuffle.png").resize((30, 30))),
            "previous_icon": ImageTk.PhotoImage(Image.open("./icons/previous.png").resize((30, 30))),
            "next_icon": ImageTk.PhotoImage(Image.open("./icons/next.png").resize((30, 30))),
            "play_pause_icon": (ImageTk.PhotoImage(Image.open("./icons/play.png").resize((30, 30))),
                                ImageTk.PhotoImage(Image.open("./icons/pause.png").resize((30, 30)))),
            "repeat_icon": (ImageTk.PhotoImage(Image.open("./icons/repeat.png").resize((30, 30))),
                            ImageTk.PhotoImage(Image.open("./icons/repeat_one.png").resize((30, 30)))),
            "volume_icon": (ImageTk.PhotoImage(Image.open("./icons/mute.png").resize((30, 30))),
                            ImageTk.PhotoImage(Image.open("./icons/low_volume.png").resize((30, 30))),
                            ImageTk.PhotoImage(Image.open("./icons/medium_volume.png").resize((30, 30))),
                            ImageTk.PhotoImage(Image.open("./icons/max_volume.png").resize((30, 30))))}

    def create_buttons(self):
        self.load_icons()

        self.shuffle_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["shuffle_icon"], bg="silver", state=tk.DISABLED, highlightthickness=0, command=self.toggle_shuffle)
        self.shuffle_button.grid(row=0, column=0, padx=65, pady=15)

        self.previous_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["previous_icon"], bg="silver", state=tk.DISABLED, highlightthickness=0, command=self.play_previous_track)
        self.previous_button.grid(row=0, column=1, padx=65, pady=15)

        self.play_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["play_pause_icon"][0], bg="silver", state=tk.DISABLED, highlightthickness=0, command=self.toggle_play_pause)
        self.play_button.grid(row=0, column=2, padx=65, pady=15)

        self.next_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["next_icon"], bg="silver", state=tk.DISABLED, highlightthickness=0, command=self.play_next_song)
        self.next_button.grid(row=0, column=4, padx=65, pady=15)

        self.repeat_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["repeat_icon"][0], bg="silver", state=tk.DISABLED, highlightthickness=0, command=self.toggle_repeat)
        self.repeat_button.grid(row=0, column=5, padx=65, pady=15)

        self.audio_mute_button = Button(self.bottom_frame, relief=tk.FLAT, width=55, height=55, image=self.icons["volume_icon"][0], bg="silver", highlightthickness=0, command=self.toggle_mute_unmute)
        self.audio_mute_button.grid(row=0, column=8, pady=15)

    def toggle_shuffle(self) -> None:
        if self.is_shuffle:
            self.shuffle_button.config(relief=tk.FLAT, highlightthickness=0)
            self.is_shuffle = False
        else:
            self.shuffle_button.config(relief=tk.SUNKEN)
            self.is_shuffle = True

    def create_playlist_panel(self) -> None:
        # Create a frame for the playlist listbox
        self.playlist_frame = tk.Frame(self)
        self.playlist_frame.grid(row=0, column=0, sticky="w", padx=15)

        # Create a listbox for the playlist panel
        self.playlist_box = tk.Listbox(self.playlist_frame, bg="#111", fg="White", width=60, height=40, highlightthickness=0, relief=tk.FLAT)
        self.playlist_box.grid(row=0, column=0)

        # Create a vertical scrollbar for the playlist panel
        self.vertical_scrollbar = tk.Scrollbar(self.playlist_frame, orient=tk.VERTICAL)
        self.vertical_scrollbar.grid(row=0, column=1, sticky="ns")

        # Create a horizontal scrollbar for the playlist panel
        self.horizontal_scrollbar = tk.Scrollbar(self.playlist_frame, orient=tk.HORIZONTAL)
        self.horizontal_scrollbar.grid(row=1, column=0, sticky="we")

        # Attach the vertical and horizontal scrollbars to the listbox
        self.playlist_box.config(yscrollcommand=self.vertical_scrollbar.set)
        self.playlist_box.config(xscrollcommand=self.horizontal_scrollbar.set)

        # Set the listbox's scrollbar property to "yview" for vertical and "xview" for horizontal
        self.vertical_scrollbar.config(command=self.playlist_box.yview)
        self.horizontal_scrollbar.config(command=self.playlist_box.xview)

    def start_playlist_music(self):
        self.track_info = self.file_info[self.playlist_box.curselection()[0]]
        self.play_button.config(image=self.icons["play_pause_icon"][1])
        mixer.music.load(self.track_info[1])
        mixer.music.play()

    def play_previous_track(self):
        try:
            self.track_index = self.playlist_box.curselection()[0] - 1
            # Clear active bar in the playlist panel
            self.playlist_box.selection_clear(0, tk.END)
            # Set the active bar to the next song in the playlist
            self.playlist_box.selection_set(self.track_index)
            # Activate the new song bar
            self.playlist_box.activate(self.track_index)
            self.start_playlist_music()
        except IndexError:
            # Set the active bar to the last track in the playlist
            self.playlist_box.selection_set(tk.END)
            # Activate new selection bar for the last track in the playlist
            self.playlist_box.activate(tk.END)
            self.start_playlist_music()

    def play_next_song(self) -> None:
        if self.is_shuffle:
            self.track_index = randint(0, self.playlist_box.size() - 1)
            self.playlist_box.selection_clear(0, tk.END)
            self.playlist_box.selection_set(self.track_index)
            self.playlist_box.activate(self.track_index)
            self.start_playlist_music()
        else:
            try:
                self.track_index = self.playlist_box.curselection()[0] + 1
                # Clear active bar in the playlist panel
                self.playlist_box.selection_clear(0, tk.END)
                # Set the active bar to the next song in the playlist
                self.playlist_box.selection_set(self.track_index)
                # Activate the new song bar
                self.playlist_box.activate(self.track_index)
                self.start_playlist_music()
            except IndexError:
                self.playlist_box.selection_set(0)
                self.playlist_box.activate(0)
                self.start_playlist_music()
    
    def check_event(self):
        self.music_end = pygame.USEREVENT
        pygame.mixer.music.set_endevent(self.music_end)

        for event in pygame.event.get():
            if event.type == self.music_end:
                self.play_next_song()
        
        self.after(500, self.check_event)

    def play_music(self) -> None:
        if self.song_directory is not None:
            self.check_event()
            self.start_playlist_music()
        else:
            mixer.music.play()

        self.play_button.config(image=self.icons["play_pause_icon"][1])
        self.music_playing = True

    def pause_music(self) -> None:
        mixer.music.pause()
        self.play_button.config(image=self.icons["play_pause_icon"][0])
        self.is_music_paused = True

    def unpause_music(self) -> None:
        mixer.music.unpause()
        self.play_button.config(image=self.icons["play_pause_icon"][1])
        self.is_music_paused = False

    def toggle_play_pause(self) -> None:
        if not self.music_playing:
            self.play_music()
        else:
            if self.is_music_paused:
                self.unpause_music()
            elif not self.is_music_paused:
                self.pause_music()

    def toggle_repeat(self) -> None:
        if self.is_playlist_repeat:
            self.repeat_button.config(relief=tk.SUNKEN, image=self.icons["repeat_icon"][1])
            self.is_track_repeat = True
            self.is_playlist_repeat = False
        elif self.is_track_repeat:
            self.repeat_button.config(relief=tk.FLAT, image=self.icons["repeat_icon"][0])
            self.is_playlist_repeat = False
            self.is_track_repeat = False
        else:
            self.repeat_button.config(relief=tk.SUNKEN)
            self.is_playlist_repeat = True

    def set_volume(self, volume: str) -> None:
        self.sound_volume = float(volume)

        if self.sound_volume == 0.0:
            self.audio_mute_button.config(image=self.icons["volume_icon"][0])
        elif 0.25 >= self.sound_volume <= 0.5:
            self.audio_mute_button.config(image=self.icons["volume_icon"][1])
        elif 0.5 >= self.sound_volume <= 0.75:
            self.audio_mute_button.config(image=self.icons["volume_icon"][2])
        else:
            self.audio_mute_button.config(image=self.icons["volume_icon"][3])

        mixer.music.set_volume(self.sound_volume)

    def create_volume_slider(self):
        self.volume_bar = ttk.Scale(
            self.bottom_frame,
            from_=0, to=1,
            orient=tk.HORIZONTAL,
            length=150,
            command=self.set_volume)

        # Set the initial volume bar to 0.5
        self.volume_bar.set(0.5)
        # Grid the volume bar
        self.volume_bar.grid(row=0, column=9, padx=15)

    def toggle_mute_unmute(self):
        if not self.is_mute:
            # Store the value of volume before muting
            self.previous_volume = self.sound_volume

            mixer.music.set_volume(0.0)
            self.volume_bar.set(0.0)
            self.audio_mute_button.config(image=self.icons["volume_icon"][0])
            self.is_mute = True
        else:
            mixer.music.set_volume(self.previous_volume)
            # Set volume bar back to previous volume
            self.volume_bar.set(self.previous_volume)
            self.is_mute = False


if __name__ == "__main__":
    root = MusicPlayer()
    root.mainloop()

Again, the program works fine, just that the problem I'm having is this one giant class handling all the functionalities of the app.

\$\endgroup\$

1 Answer 1

-1
\$\begingroup\$

First I want to compliment you on your very understandable code.

I have the same question for a tkinter app of my own.

I found this post on stack Overflow to be useful.

I had 2 classes contained in my original app. Here's what I did.

  1. I copied one of them, class VerticalScrolledFrame(tkinter.Frame) into a file named code\codeReviewExamples\vscroll_frame.py
  2. I added a __init__.py file to the directory to enable modules.
  3. In the original BIG file I:
    1. commented out class VerticalScrolledFrame
    2. added import vscroll_frame
    3. changed the definition of self.frame
      • was: self.frame = VerticalScrolledFrame(root)
      • now: self.frame = vscroll_frame.VerticalScrolledFrame(root)
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I feel this could be better posed as a comment, rather than an answer. If you have any specific code review points, please add them! \$\endgroup\$
    – Linny
    Apr 5, 2022 at 18:30
  • 1
    \$\begingroup\$ Other than the code is very readable do you have any other insightful observations about the original code that you could make? Please read How do I write a good answer? \$\endgroup\$
    – pacmaninbw
    Apr 6, 2022 at 12:13
  • \$\begingroup\$ Hi @pacmaninbw , No I don't have any other comments at this time. \$\endgroup\$ Aug 15, 2022 at 19:51

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.