Python Forum
Basic Music Player with tkinter
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Basic Music Player with tkinter
#1
#! /usr/bin/env python3

import tkinter as tk
import pygame as pg
from os import listdir, chdir
from functools import partial
import re

class MusicPlayer:
    def __init__(self, parent):
        self.parent = parent
        self.parent.columnconfigure(0, weight=1, uniform='cols')
        self.parent.columnconfigure(1, weight=1, uniform='cols')
        self.parent.rowconfigure(0, weight=1)

        self.mytext = None

        pg.init()
        pg.mixer.init()

        track_label_frame = tk.LabelFrame(self.parent, text='Track')
        track_label_frame.grid(column=0, row=0, sticky='new')
        for i in range(2):
            track_label_frame.grid_columnconfigure(i, weight=3, uniform='labels')

        self.label = tk.Label(track_label_frame, text=None, bg='lightblue')
        self.label['relief'] = 'groove'
        self.label.grid(column=1, row=0, sticky='new')

        self.status = tk.Label(track_label_frame, text=None, bg='lightblue')
        self.status['relief'] = 'groove'
        self.status.grid(column=0, row=0, sticky='new')


        btn_label_frame = tk.LabelFrame(self.parent, text='Controls')
        btn_label_frame.grid(column=0, row=1, sticky='new')
        for i in range(4):
            btn_label_frame.grid_columnconfigure(i, weight=3, uniform='btns')

        self.btn_play = tk.Button(btn_label_frame, text='Play')
        self.btn_play['command'] = partial(self.play, state=0)
        self.btn_play.grid(column=0, row=0, sticky='new', padx=3, pady=2)

        btn_stop = tk.Button(btn_label_frame, text='Stop')
        btn_stop['command'] = partial(self.play, state=None)
        btn_stop.grid(column=1, row=0, sticky='new', padx=3, pady=2)

        btn_next = tk.Button(btn_label_frame, text='Next')
        btn_next['command'] = partial(self.nextsong)
        btn_next.grid(column=2, row=0, sticky='new', padx=3, pady=2)

        track_list_frame = tk.LabelFrame(self.parent, text='Track List')
        track_list_frame.grid(column=1, row=0, rowspan=2, sticky='news')
        track_list_frame.grid_columnconfigure(0, weight=3)
        track_list_frame.grid_rowconfigure(0, weight=3)

        scrollbar = tk.Scrollbar(track_list_frame, orien='vertical')
        self.playlist = tk.Listbox(track_list_frame, yscrollcommand=scrollbar.set, \
        selectbackground='lightblue', selectforeground='navy', selectmode='single', width=50)
        scrollbar.grid(column=1, row=0, sticky='news')
        scrollbar.config(command=self.playlist.yview)
        self.playlist.grid(column=0, row=0, sticky='news')

        chdir('My Music')
        songs = listdir()
        for song in songs:
            self.playlist.insert(tk.END, song)


    def play(self, state):
        if state == 0:
            display_label = self.playlist.get(tk.ACTIVE)[:-4]
            patt = r'\w+\s\w+'
            label = ' '.join(re.findall(patt, display_label))
            self.label['text'] = label
            self.status['text'] = 'Now Playing'

            pg.mixer.music.load(self.playlist.get(tk.ACTIVE))
            pg.mixer.music.play()
            self.btn_play['text'] = 'Pause'
            self.btn_play['command'] = partial(self.play, state=1)
            if not self.playlist.curselection():
                self.playlist.select_set(0)
            else:
                self.playlist.select_set(self.playlist.curselection()[0])

        elif state == 1:
            self.status['text'] = 'Paused'
            pg.mixer.music.pause()
            self.btn_play['text'] = 'Play'
            self.btn_play['command'] = partial(self.play, state=2)
        elif state == 2:
            self.status['text'] = 'Now Playing'
            pg.mixer.music.unpause()
            self.btn_play['text'] = 'Pause'
            self.btn_play['command'] = partial(self.play, state=1)
        else:
            self.status['text'] = 'Stopped'
            pg.mixer.music.stop()
            self.btn_play['command'] = partial(self.play, state=0)
            self.btn_play['text'] = 'Play'


    def nextsong(self):
        pg.mixer.music.stop()
        select_index = self.playlist.curselection()
        next_select = 0

        if len(select_index) > 0:
            last_select = int(select_index[-1])

            self.playlist.selection_clear(select_index)

            if last_select < self.playlist.size() - 1:
                next_select = last_select + 1
        self.playlist.activate(next_select)
        self.playlist.selection_set(next_select)
        self.play(state=0)


def main():
    root = tk.Tk()
    root.title('Tkinter Music Player')
    MusicPlayer(root)
    root.mainloop()

main()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#2
Version 2 has a few tweaks. Still to do - make a play all option and clean up the code.
#! /usr/bin/env python3
import pygame as pg
import tkinter as tk
from tkinter import filedialog, messagebox
from os import chdir, listdir, sys
from functools import partial


PAUSE = pg.USEREVENT+1
TRACK_END = pg.USEREVENT+1

class MusicFolder:
    def __init__(self):
        self.folder = None

    def get_files(self, tracklist, *args, **kwargs):
        self.folder = kwargs['folder']
        tracklist.delete(0, tk.END)
        chdir(self.folder)
        tracks = []
        formats = ['mp3', 'wav', 'ogg']
        playlist = listdir()
        for track in playlist:
            if track[-3:] in formats:
                tracklist.insert(tk.END, track)
        tracklist.select_set(0)

class Controls:
    def __init__(self):
        pass

    def play(self, *args, **kwargs):
        file = f'{kwargs["folder"]}/{kwargs["active"]}'
        pg.mixer.music.set_endevent(TRACK_END)
        try :
            pg.mixer.music.load(file)
            pg.mixer.music.play()
        except Exception:
            messagebox.showerror(title='No folder selected', \
            message='You must select a music folder to load the play list. The program will exit now.')
            sys.exit()

    def pause(self):
        pg.mixer.music.pause()

    def unpause(self):
        pg.mixer.music.unpause()

    def next(self, *args, **kwargs):
        pass

    def prev(self, *args, **kwargs):
        pass

    def stop(self, *args, **kwargs):
        pg.mixer.music.stop()
        pg.mixer.music.set_endevent()
        file = f'{kwargs["folder"]}/{kwargs["active"]}'
        pg.mixer.music.set_endevent(TRACK_END)
        pg.mixer.music.load(file)


class Player:
    def __init__(self, parent):
        self.parent = parent
        self.parent.update()
        self.width = self.parent.winfo_width()
        self.height = self.parent.winfo_height()
        self.parent.grid_columnconfigure(0, weight=1)
        self.parent.grid_rowconfigure(0, weight=1)
        bgcolor = 'light blue'
        fgcolor = 'navy'
        self.folder = None
        pg.init()
        pg.mixer.init()


        pg.mixer.music.set_endevent(TRACK_END)

        self.control = Controls()
        self.music = MusicFolder()

        # Container
        container = tk.Frame(self.parent)
        container.grid(column=0, row=0, sticky='news')

        # Contains the header image/canvas
        headerframe = tk.Frame(container)
        headerframe.grid(column=0, row=0, sticky='new', pady=2, padx=2)

        # Contains 3 columns of info - status/track/choose folder button
        frame1 = tk.Frame(container)
        frame1.grid(column=0, row=1, sticky='new', pady=2)

        # Status Label
        self.status_label = tk.Label(frame1, padx=8, bg=bgcolor, fg=fgcolor, \
        width=17, text='No status', anchor='w')
        self.status_label['bd'] = 1
        self.status_label['relief'] = 'ridge'
        self.status_label.grid(column=0, row=0, sticky='news', padx=2)

        # Track playing label
        self.track_label = tk.Label(frame1, padx=8, bg=bgcolor, fg=fgcolor, \
        width=70, text='No track is playing', anchor='w')
        self.track_label['bd'] = 1
        self.track_label['relief'] = 'ridge'
        self.track_label.grid(column=1, row=0, sticky='news', padx=4)

        # Button for populating our listbox with tracks
        self.button = tk.Button(frame1, text='Choose Music Folder', \
        fg='navy', bg='lightsteelblue')
        self.button['command'] = partial(self.get_music)
        self.button.grid(column=2, row=0, sticky='new', padx=2)
        self.button.bind('<Enter>', partial(self.on_enter, self.button))
        self.button.bind('<Leave>', partial(self.on_exit, self.button))

        # Contains 3 columns - spacer/listbox/scrollbar
        frame2 = tk.Frame(container)
        frame2.grid(column=0, row=2, sticky='new', pady=2)
        frame2.grid_columnconfigure(0, weight=3)

        # Just a spacer label. May use to show album image?
        spacer_label = tk.Label(frame2, bg='silver', bd=1, relief='ridge')
        spacer_label['height'] = 15
        spacer_label['width'] = 30
        spacer_label.grid(column=0, row=0, sticky='news', padx=2)

        # Frame for listbox to give appearence of text not against side
        padframe = tk.Frame(frame2, bd=1, relief='ridge', bg='aliceblue', padx=8, \
        pady=5)
        padframe['highlightcolor'] = '#999999'
        padframe['highlightbackground'] = '#999999'
        padframe['highlightthickness'] = 1
        padframe.grid(column=2, row=0, sticky='news', padx=2)
        padframe.grid_rowconfigure(0, weight=3)
        padframe.grid_columnconfigure(0, weight=3)

        # Listbox and scrollbar
        self.scrollbar = tk.Scrollbar(frame2, orient='vertical')
        self.playlist = tk.Listbox(padframe, width=70, bd=0, bg='aliceblue')
        self.playlist['yscrollcommand'] = self.scrollbar.set
        self.playlist['selectmode'] = 'single'
        self.playlist['selectbackground'] = 'lightsteelblue'
        self.playlist['selectforeground'] = 'navy'
        self.playlist['highlightcolor'] = 'white'
        self.playlist['highlightbackground'] = 'white'
        self.playlist['highlightthickness'] = 0
        self.playlist['bd'] = 0
        self.playlist.grid(column=0, row=0, sticky='news')
        self.scrollbar.grid(column=3, row=0, sticky='ns', padx=2)

        # Contains the control buttons - play/stop/next/prev
        frame3 = tk.Frame(container)
        frame3.grid(column=0, row=3, sticky='new', pady=2)
        for i in range(4):
            frame3.grid_columnconfigure(i, weight=3, uniform='control_btns')

        # The buttons - play/stop/next/prev
        # play button will double as a pause button
        self.play_btn = tk.Button(frame3, text='Play', fg='navy', bg='lightsteelblue')
        self.play_btn.grid(column=0, row=0, sticky='new', padx=2)
        self.play_btn['command'] = partial(self.play, state='play')
        self.play_btn.bind('<Enter>', partial(self.on_enter, self.play_btn))
        self.play_btn.bind('<Leave>', partial(self.on_exit, self.play_btn))


        self.stop_btn = tk.Button(frame3, text='Stop', fg='navy', bg='lightsteelblue')
        self.stop_btn.grid(column=1, row=0, sticky='new', padx=2)
        self.stop_btn['command'] = partial(self.play, state='stop')
        self.stop_btn.bind('<Enter>', partial(self.on_enter, self.stop_btn))
        self.stop_btn.bind('<Leave>', partial(self.on_exit, self.stop_btn))


        self.next_btn = tk.Button(frame3, text='Next', fg='navy', bg='lightsteelblue')
        self.next_btn.grid(column=2, row=0, sticky='new', padx=2)
        self.next_btn['command'] = partial(self.next)
        self.next_btn.bind('<Enter>', partial(self.on_enter, self.next_btn))
        self.next_btn.bind('<Leave>', partial(self.on_exit, self.next_btn))

        self.back_btn = tk.Button(frame3, text='Prev', fg='navy', bg='lightsteelblue')
        self.back_btn.grid(column=3, row=0, sticky='new', padx=2)
        self.back_btn['command'] = partial(self.prev)
        self.back_btn.bind('<Enter>', partial(self.on_enter, self.back_btn))
        self.back_btn.bind('<Leave>', partial(self.on_exit, self.back_btn))


    #     self.update()
    #
    # def update(self):
    #     for event in pg.event.get():
    #         if event.type == TRACK_END:
    #             self.play_btn['text'] = 'Play'
    #             self.status_label['text'] = 'No status'
    #             self.track_label['text'] = 'No track is playing'
    #         pass
    #
    #     self.parent.after(100, self.update)

    def get_music(self):
        self.folder = filedialog.askdirectory()
        self.music.get_files(self.playlist, folder=self.folder)

    # Define some button animations
    def on_enter(self, btn, event):
        btn['bg'] = 'powderblue'
        btn['fg'] = 'navy'
        btn['cursor'] = 'hand2'

    def on_exit(self, btn, event):
        btn['bg'] = 'lightsteelblue'
        btn['fg'] = 'navy'

    def next(self):
        pg.mixer.music.stop()
        index = self.playlist.curselection()
        if index:
            next_index = 0
            if len(index) > 0:
                last_index = int(index[-1])
                self.playlist.selection_clear(index)

                if last_index < self.playlist.size()-1:
                    next_index = last_index + 1
            self.playlist.activate(next_index)
            self.playlist.selection_set(next_index)
            self.status_label['text'] = 'Now Playing here'
            self.play(state='play')
        else:
            pass

    def prev(self):
        try:
            pg.mixer.music.stop()
            index = self.playlist.curselection()
            last_index = int(index[-1])
            if last_index == 0:
                last_index = self.playlist.size()
            self.playlist.selection_clear(index)
            last_index = last_index - 1
            self.playlist.activate(last_index)
            self.playlist.selection_set(last_index)
            self.play(state='play')
        except Exception:
            pass

    def play(self, *args, **kwargs):
        if self.playlist.get(tk.ACTIVE):
            state = kwargs['state']
            self.track_label['text'] = self.playlist.get(tk.ACTIVE)[:-4]

            if state == 'play':
                self.play_btn['text'] = 'Pause'
                self.play_btn['command'] = partial(self.play, state='pause')
                self.status_label['text'] = 'Now Playing'
                self.control.play(active=self.playlist.get(tk.ACTIVE), folder=self.folder)


            elif state == 'pause':
                self.play_btn['text'] = 'Resume'
                self.play_btn['command'] = partial(self.play, state='unpause')
                self.status_label['text'] = 'Paused'
                self.control.pause()

            elif state == 'unpause':
                self.play_btn['text'] = 'Pause'
                self.play_btn['command'] = partial(self.play, state='pause')
                self.status_label['text'] = 'Now Playing'
                self.control.unpause()

            else:
                try:
                    index = self.playlist.curselection()
                    self.play_btn['text'] = 'Play'
                    self.play_btn['command'] = partial(self.play, state='play')
                    self.status_label['text'] = 'No status'
                    self.track_label['text'] = 'No track is playing'
                    self.playlist.selection_clear(index)
                    self.playlist.select_set(0)
                    self.playlist.activate(0)
                    self.control.stop(folder=self.folder, active=self.playlist.get(tk.ACTIVE))
                except Exception:
                    pass
        else:
            messagebox.showerror(title='No folder selected.', message='Please choose a folder with music files.')
            pass




def main():
    root = tk.Tk()
    root.title('Tkinter Music Player')
    root.geometry('805x315+250+250')
    root.resizable(0, 0)
    root['padx'] = 10
    root['pady'] = 5
    Player(root)
    root.mainloop()

if __name__ == '__main__':
    main()

# pg.mixer.music.load(f'{folder}/{playlist[0]}')
# pg.mixer.music.play()
# playlist.pop(0)
#
# running = True
# while running:
#     TRACK_END = pg.USEREVENT+1
#     pg.mixer.music.set_endevent(TRACK_END)
#
#     for event in pg.event.get():
#         if event.type == TRACK_END:
#             if len(playlist) > 0:
#                 pg.mixer.music.load(f'{folder}/{playlist[0]}')
#                 playlist.pop(0)
#                 pg.mixer.music.play()
#             else:
#                 running = False
#                 break
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#3
I haven't looked at all the code, but why is Controls a class? There's no state it encapsulates that the methods share. Couldn't those methods just be functions inside a module instead?
Reply
#4
yes. I'm just playing around with classes. It's even easier getting the code to work if it's just a function or methods in the same class.
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply
#5
Just try and use classes appropriately - they aren't always the right way to group related functionality, as in this case. I'm not sure what you mean by "easier to get the code to work" by using a class there. The functions are completely independent of one another, so if they were in a module, you'd just have to import them and use them.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  PyQt5 Music Player menator01 22 14,317 Nov-03-2021, 05:59 PM
Last Post: Axel_Erfurt
  Tkinter basic email client menator01 4 3,647 Jul-23-2020, 08:16 AM
Last Post: ndc85430

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020