Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .qwen/settings.json

This file was deleted.

2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ librosa
sounddevice
scipy
numpy
pyobjc-core; sys_platform == 'darwin'
pyobjc-framework-MediaPlayer; sys_platform == 'darwin'
bump2version
appdirs
black
Expand Down
80 changes: 78 additions & 2 deletions src/audio/audio_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,11 +471,87 @@ def _extract_album_art(self, audio_file):
return None

def get_album_art(self):
"""Returns embedded album art data of the loaded track."""
if self.metadata and "album_art" in self.metadata:
"""Returns album art data of the loaded track - either embedded or from external file."""
import os

# First, try to get embedded album art from metadata
if (
self.metadata
and "album_art" in self.metadata
and self.metadata["album_art"]
):
return self.metadata["album_art"]

# If no embedded art, search for local folder images
if self.file_path:
folder_path = os.path.dirname(self.file_path)
album_art_file = self._search_local_album_art(folder_path)

if album_art_file:
try:
# Read the image file and return its binary data
with open(album_art_file, "rb") as f:
return f.read()
except Exception as e:
print(f"Error reading album art file {album_art_file}: {e}")

return None

def _search_local_album_art(self, folder_path):
"""Search for local album art files in the specified folder."""
import os

if not os.path.isdir(folder_path):
return None

# Define the search order according to the specification
search_files = [
"folder.jpg",
"folder.png",
"cover.jpg",
"cover.png",
"album.jpg",
"album.png",
]

# First, check for standard filenames in order of preference
for filename in search_files:
file_path = os.path.join(folder_path, filename)
if os.path.exists(file_path):
return file_path

# If standard names aren't found, search for album title matches
# Get the album name from metadata if possible
metadata = self.get_metadata()
album_title = metadata.get("album", "Unknown")

# Normalize album title for matching
normalized_title = self._normalize_filename(album_title)

# Look for files starting with the album title
for filename in os.listdir(folder_path):
if os.path.isfile(os.path.join(folder_path, filename)):
name, ext = os.path.splitext(filename)
if ext.lower() in [".jpg", ".jpeg", ".png", ".bmp", ".gif"]:
if self._normalize_filename(name).startswith(normalized_title):
return os.path.join(folder_path, filename)

return None

def _normalize_filename(self, filename):
"""Normalize a filename for comparison by converting to lowercase and handling special characters."""
import re

# Convert to lowercase
normalized = filename.lower()
# Replace common separators with spaces
normalized = re.sub(r"[-_\s]+", " ", normalized)
# Remove common characters that don't affect matching
normalized = re.sub(r"[^\w\s]", "", normalized)
# Split and rejoin to normalize multiple spaces to single spaces
normalized = " ".join(normalized.split())
return normalized

def get_metadata(self):
"""Returns metadata of the loaded track."""
return self.metadata
Expand Down
82 changes: 79 additions & 3 deletions src/ui/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ def __init__(self):
# Initialize UI state
self.ui_state = UIState()

# Initialize macOS media integration if on macOS
self.mac_media_integration = None
try:
from ..utils.mac_media_integration import create_mac_media_integration

print("Attempting to create macOS media integration...")
self.mac_media_integration = create_mac_media_integration(self)
if self.mac_media_integration:
print("macOS media integration successfully loaded")
else:
print(
"macOS media integration not loaded (likely not on macOS or PyObjC unavailable)"
)
except ImportError as e:
print(
f"Could not import mac_media_integration: {e}"
) # pyobjc not available

# Set up keyboard shortcuts for media controls
self.setup_media_shortcuts()

Expand Down Expand Up @@ -1004,7 +1022,10 @@ def update_playback_position(self, position, duration):
# This callback receives position updates from the audio engine
# We store this info so the UI timer can use it consistently
# The actual UI update is still handled by the timer to avoid race conditions
pass # The audio engine's current_position is already updated, just let the timer handle the UI update

# Update macOS media integration if available
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_playback_state()

def update_ui_from_engine(self):
"""Update UI based on audio engine state."""
Expand Down Expand Up @@ -1104,6 +1125,14 @@ def play_track_at_index(self, index):
if self.album_art_window.isVisible():
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration with new track info
if (
hasattr(self, "mac_media_integration")
and self.mac_media_integration
):
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()

self.update()
return True
return False
Expand All @@ -1120,6 +1149,11 @@ def play_next_track(self):
if self.album_art_window.isVisible():
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()

def play_previous_track(self):
"""Play the previous track in the playlist."""
if self.playlist and self.current_track_index > 0:
Expand All @@ -1129,6 +1163,11 @@ def play_previous_track(self):
if self.album_art_window.isVisible():
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()

def play_selected_track(self, index):
"""Play the track at the given index when clicked in the playlist."""
self.play_track_at_index(index)
Expand All @@ -1137,6 +1176,11 @@ def play_selected_track(self, index):
if self.album_art_window.isVisible():
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()

def _handle_stop_action(self):
"""Handle the stop action from media key or stop button."""
self.audio_engine.stop()
Expand All @@ -1145,6 +1189,10 @@ def _handle_stop_action(self):
if hasattr(self, "playlist_window") and self.playlist_window:
self.playlist_window.set_current_track_index(self.current_track_index)

# Update macOS media integration with new playback state
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_playback_state()

def check_track_completion(self):
"""Check if the current track has finished playing and advance if needed."""
# Get current playback state from audio engine
Expand Down Expand Up @@ -1499,6 +1547,14 @@ def mousePressEvent(self, event):
and self.album_art_window.isVisible()
):
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration with new track info
if (
hasattr(self, "mac_media_integration")
and self.mac_media_integration
):
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()
else:
self.ui_state.current_track_title = "Error loading track"

Expand Down Expand Up @@ -1978,6 +2034,10 @@ def toggle_play_pause(self):
selected_track_index = 0
self.play_track_at_index(selected_track_index)

# Update macOS media integration with new playback state
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.update_playback_state()

def load_and_play_file(self, file_path):
"""
Load and play a file passed from command line or file opening event.
Expand Down Expand Up @@ -2035,6 +2095,14 @@ def load_and_play_file(self, file_path):
):
self.album_art_window.refresh_album_art(self.audio_engine)

# Update macOS media integration with new track info
if (
hasattr(self, "mac_media_integration")
and self.mac_media_integration
):
self.mac_media_integration.update_now_playing_info()
self.mac_media_integration.update_playback_state()

return True
else:
self.ui_state.current_track_title = "Error loading track"
Expand Down Expand Up @@ -2329,6 +2397,10 @@ def _on_close(self, event):
if hasattr(self, "visualization_timer"):
self.visualization_timer.stop()

# Clean up macOS media integration
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.cleanup()

event.accept()

def _initiate_shutdown(self):
Expand Down Expand Up @@ -2367,10 +2439,14 @@ def _perform_coordinated_shutdown(self):
if hasattr(self, "audio_engine"):
self.audio_engine.stop()

# 4. Save current window visibility states to preferences
# 4. Clean up macOS media integration
if hasattr(self, "mac_media_integration") and self.mac_media_integration:
self.mac_media_integration.cleanup()

# 5. Save current window visibility states to preferences
self._save_window_visibility_states()

# 5. Save main window position
# 6. Save main window position
self.preferences.set_main_window_position(self.x(), self.y())

def _save_window_visibility_states(self):
Expand Down
8 changes: 8 additions & 0 deletions src/ui/playlist_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -3581,6 +3581,14 @@ def _handle_transport_control_action(self, control_name):

# Update playlist window to show currently playing track
self.set_current_track_index(0)

# Update macOS media integration with new track info
if (
hasattr(self.main_window, "mac_media_integration")
and self.main_window.mac_media_integration
):
self.main_window.mac_media_integration.update_now_playing_info()
self.main_window.mac_media_integration.update_playback_state()
else:
self.main_window.current_track_title = "Error loading track"
# Update the visual state after action
Expand Down
Loading
Loading