Skip to content

Commit f7f5d01

Browse files
authored
Create Music-player.py
1 parent adcf820 commit f7f5d01

1 file changed

Lines changed: 297 additions & 0 deletions

File tree

67-Music-player/Music-player.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
import os
2+
import threading
3+
import time
4+
import io
5+
import tkinter as tk
6+
from tkinter import filedialog, messagebox
7+
from PIL import Image, ImageTk
8+
import ttkbootstrap as tb
9+
from ttkbootstrap.constants import *
10+
11+
# ---------------- VLC / Pygame ---------------- #
12+
VLC_AVAILABLE = True
13+
try:
14+
import vlc
15+
except ImportError:
16+
VLC_AVAILABLE = False
17+
18+
if not VLC_AVAILABLE:
19+
try:
20+
import pygame
21+
pygame.mixer.init()
22+
FALLBACK = True
23+
except ImportError:
24+
FALLBACK = False
25+
else:
26+
FALLBACK = False
27+
28+
if not VLC_AVAILABLE and not FALLBACK:
29+
raise ImportError("Neither python-vlc nor pygame installed.")
30+
31+
# ---------------- TinyTag for album art ---------------- #
32+
try:
33+
from tinytag import TinyTag, TinyTagException
34+
except ImportError:
35+
TinyTag = None
36+
37+
# ---------------- GLOBALS ---------------- #
38+
playlist = []
39+
current_index = -1
40+
is_paused = False
41+
loop_mode = False
42+
album_art_cache = {}
43+
44+
if VLC_AVAILABLE:
45+
vlc_instance = vlc.Instance("--no-video", "--quiet")
46+
player = vlc_instance.media_player_new()
47+
else:
48+
player = None
49+
50+
# ---------------- FUNCTIONS ---------------- #
51+
def add_files():
52+
global playlist
53+
files = filedialog.askopenfilenames(
54+
filetypes=[("Media Files", "*.mp3 *.wav *.mp4 *.m4a *.flac")]
55+
)
56+
for f in files:
57+
playlist.append(f)
58+
listbox.insert(tk.END, os.path.basename(f))
59+
60+
def extract_album_art(path):
61+
if not TinyTag:
62+
return None
63+
try:
64+
tag = TinyTag.get(path, image=True)
65+
if tag.images and tag.images.front_cover:
66+
img_data = tag.images.front_cover.data
67+
img = Image.open(io.BytesIO(img_data)).resize((200, 200))
68+
return ImageTk.PhotoImage(img)
69+
except:
70+
return None
71+
return None
72+
73+
def play_track(index=None):
74+
global current_index, is_paused
75+
if index is not None:
76+
current_index = index
77+
if current_index < 0 or current_index >= len(playlist):
78+
return
79+
file = playlist[current_index]
80+
81+
now_playing_label.config(text=f"Now Playing: {os.path.basename(file)}")
82+
83+
# Album art
84+
if file in album_art_cache:
85+
art_img = album_art_cache[file]
86+
else:
87+
art_img = extract_album_art(file)
88+
album_art_cache[file] = art_img
89+
if art_img:
90+
album_art_label.config(image=art_img)
91+
album_art_label.image = art_img
92+
else:
93+
album_art_label.config(image='')
94+
95+
# Play
96+
if VLC_AVAILABLE:
97+
media = vlc_instance.media_new(file)
98+
player.set_media(media)
99+
player.play()
100+
time.sleep(0.1)
101+
duration_sec = max(player.get_length()/1000, 1)
102+
duration_label.config(text=f"Duration: {format_time(0)} / {format_time(duration_sec)}")
103+
threading.Thread(target=update_progress, daemon=True).start()
104+
threading.Thread(target=monitor_track_end, daemon=True).start()
105+
elif FALLBACK:
106+
try:
107+
pygame.mixer.music.load(file)
108+
pygame.mixer.music.play()
109+
duration_label.config(text="Duration: Unknown")
110+
threading.Thread(target=monitor_track_end, daemon=True).start()
111+
except:
112+
messagebox.showerror("Error", f"Cannot play file: {file}")
113+
114+
is_paused = False
115+
116+
def pause_track():
117+
global is_paused
118+
if VLC_AVAILABLE:
119+
if player.is_playing():
120+
player.pause()
121+
is_paused = True
122+
elif is_paused:
123+
player.play()
124+
is_paused = False
125+
elif FALLBACK:
126+
if pygame.mixer.music.get_busy():
127+
if not is_paused:
128+
pygame.mixer.music.pause()
129+
is_paused = True
130+
else:
131+
pygame.mixer.music.unpause()
132+
is_paused = False
133+
134+
def stop_track():
135+
global is_paused
136+
if VLC_AVAILABLE:
137+
player.stop()
138+
elif FALLBACK:
139+
pygame.mixer.music.stop()
140+
is_paused = False
141+
progress_var.set(0)
142+
now_playing_label.config(text="Now Playing: ")
143+
album_art_label.config(image='')
144+
duration_label.config(text="Duration: 00:00 / 00:00")
145+
146+
def next_track():
147+
global current_index
148+
if current_index + 1 < len(playlist):
149+
current_index += 1
150+
play_track(current_index)
151+
elif loop_mode and playlist:
152+
current_index = 0
153+
play_track(current_index)
154+
155+
def prev_track():
156+
global current_index
157+
if current_index - 1 >= 0:
158+
current_index -= 1
159+
play_track(current_index)
160+
elif loop_mode and playlist:
161+
current_index = len(playlist) - 1
162+
play_track(current_index)
163+
164+
def set_volume(val):
165+
vol = int(float(val))
166+
if VLC_AVAILABLE:
167+
player.audio_set_volume(vol)
168+
elif FALLBACK:
169+
pygame.mixer.music.set_volume(vol / 100)
170+
171+
def update_progress():
172+
while True:
173+
if VLC_AVAILABLE and player.is_playing():
174+
length = max(player.get_length()/1000, 1)
175+
pos = player.get_time()/1000
176+
progress_bar.config(to=length)
177+
progress_var.set(pos)
178+
duration_label.config(text=f"Duration: {format_time(pos)} / {format_time(length)}")
179+
elif FALLBACK and pygame.mixer.music.get_busy():
180+
pos = pygame.mixer.music.get_pos()/1000
181+
progress_var.set(pos)
182+
else:
183+
break
184+
time.sleep(0.5)
185+
186+
def seek(event):
187+
if VLC_AVAILABLE and player.get_media():
188+
player.set_time(int(progress_var.get()*1000))
189+
elif FALLBACK:
190+
pygame.mixer.music.play(start=progress_var.get())
191+
192+
def format_time(seconds):
193+
seconds = int(seconds)
194+
m = seconds // 60
195+
s = seconds % 60
196+
return f"{m:02d}:{s:02d}"
197+
198+
def monitor_track_end():
199+
"""Automatically play next track when current track ends"""
200+
global current_index
201+
while True:
202+
if VLC_AVAILABLE and player.get_media():
203+
if not player.is_playing() and not is_paused:
204+
time.sleep(0.2)
205+
next_track()
206+
break
207+
elif FALLBACK:
208+
if not pygame.mixer.music.get_busy() and not is_paused:
209+
time.sleep(0.2)
210+
next_track()
211+
break
212+
else:
213+
break
214+
time.sleep(0.5)
215+
216+
# Drag & Drop playlist
217+
drag_data = None
218+
def on_drag_start(event):
219+
global drag_data
220+
drag_data = {"widget": event.widget, "index": event.widget.nearest(event.y)}
221+
222+
def on_drag_release(event):
223+
global drag_data
224+
if not drag_data:
225+
return
226+
widget = drag_data.get("widget")
227+
start_index = drag_data.get("index")
228+
if widget is None or start_index is None:
229+
drag_data = None
230+
return
231+
end_index = widget.nearest(event.y)
232+
if start_index != end_index:
233+
playlist[start_index], playlist[end_index] = playlist[end_index], playlist[start_index]
234+
listbox.delete(0, tk.END)
235+
for f in playlist:
236+
listbox.insert(tk.END, os.path.basename(f))
237+
drag_data = None
238+
239+
# ---------------- GUI ---------------- #
240+
app = tb.Window(themename="darkly", title="VLC-Style Music Player", size=(900, 550))
241+
242+
# Playlist
243+
left_frame = tb.Frame(app)
244+
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
245+
listbox = tk.Listbox(left_frame)
246+
listbox.pack(fill=tk.BOTH, expand=True)
247+
listbox.bind("<Double-1>", lambda e: (
248+
play_track(listbox.curselection()[0]) if listbox.curselection() else None
249+
))
250+
listbox.bind("<Button-1>", on_drag_start)
251+
listbox.bind("<ButtonRelease-1>", on_drag_release)
252+
tb.Button(left_frame, text="Add Files", bootstyle="primary", command=add_files).pack(pady=5)
253+
254+
# Controls + Album Art
255+
right_frame = tb.Frame(app)
256+
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)
257+
258+
album_art_label = tk.Label(right_frame)
259+
album_art_label.pack(pady=10)
260+
261+
now_playing_label = tb.Label(right_frame, text="Now Playing: ", font=("Segoe UI", 10, "bold"))
262+
now_playing_label.pack()
263+
264+
duration_label = tb.Label(right_frame, text="Duration: 00:00 / 00:00", font=("Segoe UI", 9))
265+
duration_label.pack()
266+
267+
progress_var = tk.DoubleVar()
268+
progress_bar = tb.Scale(right_frame, variable=progress_var, from_=0, to=100, orient=tk.HORIZONTAL)
269+
progress_bar.pack(fill=tk.X, padx=5)
270+
progress_bar.bind("<ButtonRelease-1>", seek)
271+
272+
control_frame = tb.Frame(right_frame)
273+
control_frame.pack(pady=10)
274+
tb.Button(control_frame, text="⏮ Prev", bootstyle="secondary", command=prev_track).pack(side=tk.LEFT, padx=5)
275+
tb.Button(control_frame, text="▶ Play", bootstyle="success", command=lambda: (
276+
play_track(listbox.curselection()[0] if listbox.curselection() else None)
277+
)).pack(side=tk.LEFT, padx=5)
278+
tb.Button(control_frame, text="⏸ Pause", bootstyle="warning", command=pause_track).pack(side=tk.LEFT, padx=5)
279+
tb.Button(control_frame, text="⏹ Stop", bootstyle="danger", command=stop_track).pack(side=tk.LEFT, padx=5)
280+
tb.Button(control_frame, text="⏭ Next", bootstyle="secondary", command=next_track).pack(side=tk.LEFT, padx=5)
281+
282+
loop_var = tk.IntVar()
283+
tb.Checkbutton(right_frame, text="Loop Playlist", variable=loop_var, bootstyle="info",
284+
command=lambda: set_loop()).pack(pady=5)
285+
286+
volume_frame = tb.Frame(right_frame)
287+
volume_frame.pack(pady=5)
288+
tb.Label(volume_frame, text="Volume").pack(side=tk.LEFT, padx=5)
289+
volume_slider = tb.Scale(volume_frame, from_=0, to=100, orient=tk.HORIZONTAL, command=set_volume)
290+
volume_slider.set(50)
291+
volume_slider.pack(side=tk.LEFT)
292+
293+
def set_loop():
294+
global loop_mode
295+
loop_mode = bool(loop_var.get())
296+
297+
app.mainloop()

0 commit comments

Comments
 (0)