Implementation of a PyQt5 Music Player with Pygame and Audio Metadata Processing
# Core Library Imports
import sys
import os
import pygame
import time
import random
import threading
# GUI and Multimedia Libraries
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QListWidget, QListWidgetItem
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtGui import QPixmap
import qtawesome
# Audio and Image Processing
from mutagen import File
from PIL import Image, ImageDraw, ImageFilter
import requests
Audio Playback and List Management
Handlnig double-click events on playlist items:
def handle_double_click(self, playlist_widget):
"""Process double-click events on playlist items to play selected audio."""
global current_track_index
selected_item = QListWidgetItem(playlist_widget.currentItem())
track_name = selected_item.text()
current_track_index = playlist_widget.currentRow()
# Extract and display filename
full_path = audio_file_paths[current_track_index]
directory, filename_with_ext = os.path.split(full_path)
filename, extension = os.path.splitext(filename_with_ext)
self.track_display_label.setText(filename)
self.initiate_playback(current_track_index)
def initiate_playback(self, track_index):
"""Load and play the selected audio file."""
global playback_paused
self.extract_album_art(track_index)
self.play_pause_button.setIcon(qtawesome.icon('fa.pause', color='#3FC89C', font=18))
playback_paused = False
try:
pygame.mixer.stop()
except:
pass
pygame.mixer.init()
self.progress_timer = QTimer()
self.progress_timer.start(500)
audio_path = audio_file_paths[track_index]
pygame.mixer.music.load(audio_path)
pygame.mixer.music.play()
Volume Control Implementation
class VolumeManager:
"""Manages audio volume adjustments."""
def __init__(self):
self.current_volume = 0.5
def increase_volume(self):
"""Increase volume by 10%."""
self.current_volume += 0.1
if self.current_volume > 1.0:
self.current_volume = 1.0
pygame.mixer.music.set_volume(self.current_volume)
def decrease_volume(self):
"""Decrease volume by 10%."""
self.current_volume -= 0.1
if self.current_volume < 0.0:
self.current_volume = 0.0
pygame.mixer.music.set_volume(self.current_volume)
Playback State Control
def toggle_playback(self):
"""Toggle between play and pause states."""
global playback_paused
if playback_paused:
try:
pygame.mixer.music.unpause()
except:
pass
self.play_pause_button.setIcon(qtawesome.icon('fa.pause', color='#3FC89C', font=18))
playback_paused = False
else:
try:
pygame.mixer.music.pause()
except:
pass
self.play_pause_button.setIcon(qtawesome.icon('fa.play', color='#F76677', font=18))
playback_paused = True
Directory Browsing for Audio Files
def browse_audio_directory(self):
"""Open directory dialog and load supported audio files."""
global audio_file_paths, current_track_index, base_directory
current_track_index = 0
directory_path = QFileDialog.getExistingDirectory(None, "Select Music Folder", "")
if not directory_path:
return
self.playlist_widget.clear()
base_directory = directory_path + '/'
# Collect audio files
supported_extensions = ('.mp3', '.flac', '.wma')
audio_files = []
for filename in os.listdir(directory_path):
if filename.lower().endswith(supported_extensions):
audio_files.append(filename)
audio_file_paths = [base_directory + file for file in audio_files]
# Populate playlist
for idx, audio_file in enumerate(audio_files):
self.playlist_widget.addItem(audio_file)
self.playlist_widget.item(idx).setForeground(QtCore.Qt.white)
Custom Window Draggnig
def mousePressEvent(self, event):
"""Handle mouse press for window dragging."""
self.drag_active = True
self.drag_start_position = event.globalPos() - self.pos()
event.accept()
def mouseMoveEvent(self, event):
"""Handle window movement during drag."""
if self.drag_active:
self.move(event.globalPos() - self.drag_start_position)
event.accept()
def mouseReleaseEvent(self, event):
"""Clean up after drag operation."""
self.drag_active = False
Audio Metadata Image Extraction
def extract_album_art(self, track_index):
"""Extract and display album art from audio files."""
try:
audio_metadata = File(audio_file_paths[track_index])
album_art_data = audio_metadata.tags['APIC:'].data
# Save raw image data
with open('temp_cover.png', 'wb') as image_file:
image_file.write(album_art_data)
# Process and display image
source_path = './temp_cover.png'
processed_path = './processed_cover.png'
self.create_circular_image(source_path, processed_path)
cover_pixmap = QtGui.QPixmap(processed_path)
scaled_cover = cover_pixmap.scaled(300, 300, QtCore.Qt.KeepAspectRatio)
self.cover_display_label.setPixmap(scaled_cover)
except KeyError:
# Use default image if no album art found
if os.path.exists("default_cover.png"):
default_pixmap = QtGui.QPixmap('./default_cover.png')
scaled_default = default_pixmap.scaled(300, 300, QtCore.Qt.KeepAspectRatio)
self.cover_display_label.setPixmap(scaled_default)
Image Processing Utilities
def create_max_square_crop(image):
"""Crop image to largest possible square."""
return center_crop(image, min(image.size), min(image.size))
def center_crop(image, target_width, target_height):
"""Crop image from center to specified dimensions."""
image_width, image_height = image.size
return image.crop(((image_width - target_width) // 2,
(image_height - target_height) // 2,
(image_width + target_width) // 2,
(image_height + target_height) // 2))
def create_circular_mask(image, blur_radius, offset=0):
"""Apply circular mask with transparency."""
offset = blur_radius * 2 + offset
mask = Image.new("L", image.size, 0)
draw = ImageDraw.Draw(mask)
draw.ellipse((offset, offset,
image.size[0] - offset,
image.size[1] - offset), fill=255)
blurred_mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))
result_image = image.copy()
result_image.putalpha(blurred_mask)
return result_image
def process_cover_image(source_path, output_path):
"""Process cover image into circular thumbnail."""
original_image = Image.open(source_path)
target_size = 600
# Create square crop
square_crop = create_max_square_crop(original_image)
resized_image = square_crop.resize((target_size, target_size), Image.LANCZOS)
# Apply circular mask
circular_image = create_circular_mask(resized_image, 0)
circular_image.save(output_path)
os.remove(source_path)
Main Application Structure
class MusicPlayerApp(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initialize_ui()
self.setup_ui_elements()
def initialize_ui(self):
"""Set up main window properties."""
self.setFixedSize(960, 700)
self.setWindowOpacity(0.9)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
# Initialize playback state
self.playback_mode = 'sequential'
self.current_volume = 0.5
self.is_paused = False
self.audio_file_paths = []
self.current_track_index = 0
def setup_ui_elements(self):
"""Create and configure UI components."""
# Main layout containers
self.main_widget = QtWidgets.QWidget()
self.main_layout = QtWidgets.QGridLayout()
self.main_widget.setLayout(self.main_layout)
# Playlist display
self.playlist_widget = QListWidget(self)
self.playlist_widget.doubleClicked.connect(
lambda: self.handle_double_click(self.playlist_widget))
# Control buttons
self.play_pause_button = QtWidgets.QPushButton(
qtawesome.icon('fa.pause', color='#3FC89C', font=18), "")
self.play_pause_button.clicked.connect(self.toggle_playback)
# Volume controls
self.volume_up_button = QtWidgets.QPushButton(
qtawesome.icon('fa.volume-up', color='#3FC89C', font=18), "")
self.volume_up_button.clicked.connect(self.increase_volume)
self.volume_down_button = QtWidgets.QPushButton(
qtawesome.icon('fa.volume-down', color='#3FC89C', font=18), "")
self.volume_down_button.clicked.connect(self.decrease_volume)
# Track information display
self.track_display_label = QtWidgets.QLabel("No track selected")
self.cover_display_label = QtWidgets.QLabel()