import sys
import os
from functools import partial
from pynput import keyboard
from PyQt6.QtCore import (
QThread, pyqtSignal, QObject, QUrl, Qt, QTimer, QSize, QSettings,
QPropertyAnimation, pyqtProperty
)
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QPushButton, QSlider, QLabel,
QVBoxLayout, QHBoxLayout, QFileDialog, QStyle, QDialog,
QDoubleSpinBox, QDialogButtonBox, QGroupBox, QAbstractSpinBox,
QStackedWidget
)
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtMultimediaWidgets import QVideoWidget
from PyQt6.QtGui import QPixmap, QFont, QPainter, QBrush, QColor, QIcon
from mutagen.mp3 import MP3
from mutagen.flac import FLAC
# פונקציה לקבלת נתיב למשאבים (עבור PyInstaller)
def resource_path(relative_path):
try:
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# וידג'ט מתג מותאם אישית עם אנימציה
class SlidingToggleSwitch(QWidget):
toggled = pyqtSignal(bool)
def __init__(self, parent=None):
super().__init__(parent);self._width=40;self._height=20;self.setFixedSize(self._width,self._height);self.setCursor(Qt.CursorShape.PointingHandCursor)
self._padding=2;self._circle_diameter=self._height-self._padding*2;self._x_off=self._padding;self._x_on=self._width-self._circle_diameter-self._padding
self._checked=False;self._circle_pos=self._x_off;self.animation=QPropertyAnimation(self,b"circle_pos");self.animation.setDuration(150)
def mousePressEvent(self,event):self._checked=not self._checked;self.toggled.emit(self._checked);self.animate();super().mousePressEvent(event)
def paintEvent(self,event):
painter=QPainter(self);painter.setRenderHint(QPainter.RenderHint.Antialiasing);bg_color=QColor("#3498DB"if self._checked else"#BDC3C7")
painter.setBrush(QBrush(bg_color));painter.setPen(Qt.PenStyle.NoPen);painter.drawRoundedRect(0,0,self._width,self._height,self._height/2,self._height/2)
painter.setBrush(QBrush(QColor("#FFFFFF")));painter.drawEllipse(int(self._circle_pos),self._padding,self._circle_diameter,self._circle_diameter)
def animate(self):end_pos=self._x_on if self._checked else self._x_off;self.animation.setStartValue(self._circle_pos);self.animation.setEndValue(end_pos);self.animation.start()
def get_circle_pos(self):return self._circle_pos
def set_circle_pos(self,pos):self._circle_pos=pos;self.update()
circle_pos=pyqtProperty(float,get_circle_pos,set_circle_pos)
def isChecked(self):return self._checked
def setChecked(self,checked):
if self._checked==checked:return
self._checked=checked;self._circle_pos=self._x_on if checked else self._x_off;self.toggled.emit(checked);self.update()
# עיצוב כהה לאפליקציה
DARK_STYLESHEET = """
QWidget {
background-color: #2b2b2b;
color: #f0f0f0;
font-family: Arial, sans-serif;
}
QMainWindow {
background-color: #2b2b2b;
}
QPushButton {
background-color: #555;
border: 1px solid #666;
padding: 8px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #666;
}
QPushButton:pressed {
background-color: #444;
}
QPushButton#ControlButton {
border: none;
font-size: 18px;
font-weight: bold;
background-color: #3c3c3c;
border-radius: 22px;
}
QPushButton#ControlButton:hover {
background-color: #505050;
}
QPushButton#ControlButton:pressed {
background-color: #2a2a2a;
}
QPushButton#ControlButton[active="true"] {
background-color: #5a9bcf;
color: white;
}
QLabel#AlbumArtLabel {
border: 2px solid #444;
border-radius: 5px;
background-color: #3c3c3c;
}
QLabel#TrackInfoLabel {
font-size: 16px;
font-weight: bold;
padding-bottom: 5px;
}
QLabel#ArtistAlbumLabel {
font-size: 12px;
color: #ccc;
padding-bottom: 10px;
}
QSlider::groove:horizontal {
border: 1px solid #444;
height: 8px;
background: #3c3c3c;
margin: 2px 0;
border-radius: 4px;
}
QSlider::sub-page:horizontal {
background: #5a9bcf;
border: 1px solid #444;
height: 8px;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: white;
border: 1px solid #aaa;
width: 16px;
margin: -5px 0;
border-radius: 8px;
}
QDialog {
background-color: #3c3c3c;
}
QGroupBox {
font-size: 14px;
font-weight: bold;
border: 1px solid #444;
border-radius: 5px;
margin-top: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top center;
padding: 0 5px;
background-color: #3c3c3c;
}
QDoubleSpinBox {
background-color: #2b2b2b;
border: 1px solid #666;
padding: 5px;
border-radius: 4px;
font-size: 14px;
}
QDialogButtonBox QPushButton {
padding: 5px 15px;
}
"""
# דיאלוג הגדרות המאפשר למשתמש לשנות את תצורת הנגן
class SettingsDialog(QDialog):
def __init__(self,parent=None):
super().__init__(parent);self.setWindowTitle("הגדרות");self.setModal(True);self.setMinimumWidth(420);self.setLayoutDirection(Qt.LayoutDirection.RightToLeft)
self.settings=QSettings("MyMusicPlayer","Settings");main_layout=QVBoxLayout(self);main_layout.setSpacing(15);main_layout.setContentsMargins(15,15,15,15)
# הגדרות השהייה בזמן הקלדה
typing_group=QGroupBox("השהייה בזמן הקלדה");typing_layout=QHBoxLayout();typing_layout.setContentsMargins(10,15,10,10);typing_layout.setSpacing(10)
self.typing_pause_switch=SlidingToggleSwitch();self.delay_spinbox=QDoubleSpinBox();self.delay_spinbox.setRange(0.5,10.0);self.delay_spinbox.setSingleStep(0.1)
self.delay_spinbox.setSuffix(" שניות");self.delay_spinbox.setAlignment(Qt.AlignmentFlag.AlignCenter);self.delay_spinbox.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.NoButtons);self.delay_spinbox.setMinimumWidth(80)
self.delay_down_button=QPushButton("-");self.delay_down_button.setFixedSize(30,30);self.delay_up_button=QPushButton("+");self.delay_up_button.setFixedSize(30,30)
typing_layout.addWidget(QLabel("הפעלה:"));typing_layout.addWidget(self.typing_pause_switch);typing_layout.addStretch();typing_layout.addWidget(QLabel("זמן:"))
typing_layout.addWidget(self.delay_spinbox);typing_layout.addWidget(self.delay_down_button);typing_layout.addWidget(self.delay_up_button)
typing_group.setLayout(typing_layout);main_layout.addWidget(typing_group)
# הגדרות חזרה לאחור בהשהייה
rewind_group=QGroupBox("חזרה לאחור בהשהייה");rewind_layout=QHBoxLayout();rewind_layout.setContentsMargins(10,15,10,10);rewind_layout.setSpacing(10)
self.rewind_on_pause_switch=SlidingToggleSwitch();self.rewind_spinbox=QDoubleSpinBox();self.rewind_spinbox.setRange(0.1,10.0);self.rewind_spinbox.setSingleStep(0.1)
self.rewind_spinbox.setSuffix(" שניות");self.rewind_spinbox.setAlignment(Qt.AlignmentFlag.AlignCenter);self.rewind_spinbox.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.NoButtons);self.rewind_spinbox.setMinimumWidth(80)
self.rewind_down_button=QPushButton("-");self.rewind_down_button.setFixedSize(30,30);self.rewind_up_button=QPushButton("+");self.rewind_up_button.setFixedSize(30,30)
rewind_layout.addWidget(QLabel("הפעלה:"));rewind_layout.addWidget(self.rewind_on_pause_switch);rewind_layout.addStretch();rewind_layout.addWidget(QLabel("זמן חזרה:"))
rewind_layout.addWidget(self.rewind_spinbox);rewind_layout.addWidget(self.rewind_down_button);rewind_layout.addWidget(self.rewind_up_button)
rewind_group.setLayout(rewind_layout);main_layout.addWidget(rewind_group)
# הגדרות מהירות ניגון
rate_group=QGroupBox("מהירות ניגון");rate_container=QWidget();rate_layout=QHBoxLayout(rate_container);rate_layout.setContentsMargins(10,15,10,10);rate_layout.setSpacing(10)
rate_container.setLayoutDirection(Qt.LayoutDirection.LeftToRight);self.decrease_rate_button=QPushButton("◀");self.decrease_rate_button.setFixedSize(30,30)
self.playback_rate_slider=QSlider(Qt.Orientation.Horizontal);self.playback_rate_slider.setRange(25,400);self.playback_rate_slider.setSingleStep(5)
self.increase_rate_button=QPushButton("▶");self.increase_rate_button.setFixedSize(30,30);self.playback_rate_label=QLabel("1.00 x");self.playback_rate_label.setFixedWidth(55)
self.playback_rate_label.setAlignment(Qt.AlignmentFlag.AlignHCenter|Qt.AlignmentFlag.AlignVCenter)
rate_layout.addWidget(self.decrease_rate_button);rate_layout.addWidget(self.playback_rate_slider);rate_layout.addWidget(self.increase_rate_button);rate_layout.addWidget(self.playback_rate_label)
group_main_layout=QVBoxLayout(rate_group);group_main_layout.addWidget(rate_container);main_layout.addWidget(rate_group)
self.connect_settings_signals()
button_box=QDialogButtonBox(QDialogButtonBox.StandardButton.Ok|QDialogButtonBox.StandardButton.Cancel);button_box.setLayoutDirection(Qt.LayoutDirection.LeftToRight)
button_box.accepted.connect(self.accept);button_box.rejected.connect(self.reject);main_layout.addStretch(1);main_layout.addWidget(button_box);self.load_settings()
def connect_settings_signals(self):
self.playback_rate_slider.valueChanged.connect(self.update_rate_label);self.decrease_rate_button.clicked.connect(self.decrease_rate);self.increase_rate_button.clicked.connect(self.increase_rate)
self.delay_down_button.clicked.connect(self.decrease_delay);self.delay_up_button.clicked.connect(self.increase_delay)
self.rewind_on_pause_switch.toggled.connect(self.rewind_spinbox.setEnabled);self.rewind_on_pause_switch.toggled.connect(self.rewind_down_button.setEnabled)
self.rewind_on_pause_switch.toggled.connect(self.rewind_up_button.setEnabled);self.rewind_down_button.clicked.connect(self.decrease_rewind);self.rewind_up_button.clicked.connect(self.increase_rewind)
def decrease_delay(self):self.delay_spinbox.stepDown()
def increase_delay(self):self.delay_spinbox.stepUp()
def decrease_rate(self):self.playback_rate_slider.setValue(self.playback_rate_slider.value()-25)
def increase_rate(self):self.playback_rate_slider.setValue(self.playback_rate_slider.value()+25)
def decrease_rewind(self):self.rewind_spinbox.stepDown()
def increase_rewind(self):self.rewind_spinbox.stepUp()
def update_rate_label(self,value):self.playback_rate_label.setText(f"{value/100.0:.2f} x")
# טעינת הגדרות שמורות
def load_settings(self):
typing_pause_enabled=self.settings.value("typingPauseEnabled",True,type=bool);delay_time=self.settings.value("delayTime",1.5,type=float)
playback_rate=self.settings.value("playbackRate",1.0,type=float);rewind_on_pause_enabled=self.settings.value("rewindOnPauseEnabled",False,type=bool)
rewind_time=self.settings.value("rewindTime",1.0,type=float);self.typing_pause_switch.setChecked(typing_pause_enabled);self.delay_spinbox.setValue(delay_time)
self.rewind_on_pause_switch.setChecked(rewind_on_pause_enabled);self.rewind_spinbox.setValue(rewind_time);self.rewind_spinbox.setEnabled(rewind_on_pause_enabled)
self.rewind_down_button.setEnabled(rewind_on_pause_enabled);self.rewind_up_button.setEnabled(rewind_on_pause_enabled)
slider_value=int(playback_rate*100);self.playback_rate_slider.setValue(slider_value);self.update_rate_label(slider_value)
# שמירת ההגדרות הנוכחיות
def save_settings(self):
self.settings.setValue("typingPauseEnabled",self.typing_pause_switch.isChecked());self.settings.setValue("delayTime",self.delay_spinbox.value())
self.settings.setValue("playbackRate",self.playback_rate_slider.value()/100.0);self.settings.setValue("rewindOnPauseEnabled",self.rewind_on_pause_switch.isChecked())
self.settings.setValue("rewindTime",self.rewind_spinbox.value())
def accept(self):self.save_settings();super().accept()
# מאזין גלובלי למקשים הפועל ברקע (ב-Thread נפרד)
class GlobalKeyListener(QObject):
keyPressed=pyqtSignal()
def __init__(self):
super().__init__()
try:self.listener=keyboard.Listener(on_press=self.on_press)
except Exception as e:print(f"Failed to create keyboard listener: {e}");self.listener=None
def on_press(self,key):self.keyPressed.emit()
def start_listening(self):
if self.listener:self.listener.start();self.listener.join()
def stop_listening(self):
if self.listener:self.listener.stop()
# החלון הראשי של נגן המוזיקה
class MusicPlayer(QMainWindow):
def __init__(self,file_to_play=None):
super().__init__()
self.normal_size=QSize(520,600);self.mini_size=QSize(460,160)
self.setWindowTitle("נגן הקלדה");self.setGeometry(100,100,self.normal_size.width(),self.normal_size.height())
# טעינת אייקון לחלון
icon_path = resource_path('נגן.ico')
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
else:
print(f"Warning: Icon file not found at {icon_path}")
self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay))
# אתחול רכיבי הנגן וההגדרות
self.settings=QSettings("MyMusicPlayer","Settings");self.player=QMediaPlayer();self.audio_output=QAudioOutput()
self.player.setAudioOutput(self.audio_output);self.video_widget=QVideoWidget();self.player.setVideoOutput(self.video_widget)
self.current_file_path=None;self.is_always_on_top=False;self.was_playing_before_typing=False;self.rewind_on_pause_enabled=False
self.rewind_time_sec=1.0;self.typing_pause_enabled=True;self.delay_time=1.5;self.playback_rate=1.0
self.typing_timer=QTimer(self);self.typing_timer.setSingleShot(True)
self.load_player_settings();self.update_typing_timer_interval();self.apply_playback_rate()
self.init_ui();self.connect_signals();self.setup_global_key_listener();self.setStyleSheet(DARK_STYLESHEET)
# טעינת קובץ אם סופק כארגומנט בשורת הפקודה
if file_to_play and os.path.exists(file_to_play):QTimer.singleShot(0,lambda:self.load_and_play_file(file_to_play))
# הגדרת מאזין המקשים הגלובלי והפעלתו ב-Thread נפרד
def setup_global_key_listener(self):
self.key_listener_thread=QThread();self.key_listener=GlobalKeyListener();self.key_listener.moveToThread(self.key_listener_thread)
self.key_listener.keyPressed.connect(self.handle_global_key_press);self.key_listener_thread.started.connect(self.key_listener.start_listening)
self.key_listener_thread.start()
# מטפל בלחיצת מקש גלובלית: משהה את הנגן ומתחיל טיימר לחידוש
def handle_global_key_press(self):
if self.typing_pause_enabled:
if self.player.playbackState()==QMediaPlayer.PlaybackState.PlayingState:
self.was_playing_before_typing=True;self.pause_with_rewind()
self.typing_timer.start()
# מבטיח שה-Thread של מאזין המקשים ייסגר כראוי עם סגירת החלון
def closeEvent(self,event):
self.key_listener.stop_listening();self.key_listener_thread.quit();self.key_listener_thread.wait();event.accept()
# טוען את הגדרות המשתמש מ-QSettings
def load_player_settings(self):
self.typing_pause_enabled=self.settings.value("typingPauseEnabled",True,type=bool);self.delay_time=self.settings.value("delayTime",1.5,type=float)
self.playback_rate=self.settings.value("playbackRate",1.0,type=float);self.rewind_on_pause_enabled=self.settings.value("rewindOnPauseEnabled",False,type=bool)
self.rewind_time_sec=self.settings.value("rewindTime",1.0,type=float)
def update_typing_timer_interval(self):self.typing_timer.setInterval(int(self.delay_time*1000))
def apply_playback_rate(self):self.player.setPlaybackRate(self.playback_rate)
# אתחול וסידור כל רכיבי ממשק המשתמש
def init_ui(self):
self.setMinimumSize(QSize(480,420))
central_widget=QWidget();self.setCentralWidget(central_widget);main_layout=QVBoxLayout(central_widget)
# סרגל עליון: פתיחת קובץ, שליטת ווליום, הצמדה והגדרות
top_bar_layout=QHBoxLayout()
self.open_file_button=QPushButton("פתח קובץ")
self.pin_button=QPushButton("📌");self.pin_button.setToolTip("הצמד למעלה (מצב מיני)")
self.pin_button.setFixedSize(45,45);self.pin_button.setObjectName("ControlButton");self.pin_button.setProperty("active",self.is_always_on_top)
font_pin=self.pin_button.font();font_pin.setPointSize(20);self.pin_button.setFont(font_pin)
self.settings_button=QPushButton("⚙️");self.settings_button.setToolTip("הגדרות");self.settings_button.setFixedSize(45,45);self.settings_button.setObjectName("ControlButton")
font_settings=self.settings_button.font();font_settings.setPointSize(22);self.settings_button.setFont(font_settings)
self.mute_button=QPushButton();self.mute_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaVolume))
self.mute_button.setFixedSize(45,45);self.mute_button.setObjectName("ControlButton");self.mute_button.setToolTip("השתק / בטל השתקה")
self.volume_slider=QSlider(Qt.Orientation.Horizontal);self.volume_slider.setRange(0,100);self.volume_slider.setValue(70)
self.audio_output.setVolume(0.7);self.volume_slider.setToolTip("עוצמת שמע")
top_bar_layout.addWidget(self.open_file_button);top_bar_layout.addStretch();top_bar_layout.addWidget(self.mute_button)
top_bar_layout.addWidget(self.volume_slider);top_bar_layout.addStretch();top_bar_layout.addWidget(self.pin_button);top_bar_layout.addWidget(self.settings_button)
# אזור תצוגת המדיה (עטיפת אלבום או וידאו)
self.media_display_stack=QStackedWidget()
self.album_art_label=QLabel("טען קובץ אודיו או וידאו");self.album_art_label.setObjectName("AlbumArtLabel")
self.album_art_label.setScaledContents(True);self.album_art_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.video_widget.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
self.media_display_stack.addWidget(self.album_art_label);self.media_display_stack.addWidget(self.video_widget)
# מידע על הרצועה
self.track_info_label=QLabel("לא נטען קובץ");self.track_info_label.setObjectName("TrackInfoLabel");self.track_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.artist_album_label=QLabel(" ");self.artist_album_label.setObjectName("ArtistAlbumLabel");self.artist_album_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
# סרגל התקדמות וזמנים
progress_layout=QHBoxLayout()
self.current_time_label=QLabel("00:00");self.progress_slider=QSlider(Qt.Orientation.Horizontal)
self.total_time_label=QLabel("00:00");self.progress_slider.setToolTip("התקדמות")
progress_layout.addWidget(self.current_time_label);progress_layout.addWidget(self.progress_slider);progress_layout.addWidget(self.total_time_label)
# כפתורי שליטה מרכזיים
controls_layout=QHBoxLayout();button_size=QSize(45,45)
self.seek_back_20_button=QPushButton("⏮️");self.seek_back_10_button=QPushButton("⏪");self.seek_back_5_button=QPushButton("◀️")
self.stop_button=QPushButton();self.play_pause_button=QPushButton();self.seek_fwd_5_button=QPushButton("▶️")
self.seek_fwd_10_button=QPushButton("⏩");self.seek_fwd_20_button=QPushButton("⏭️")
self.stop_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaStop));self.play_pause_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_MediaPlay))
self.play_pause_button.setIconSize(QSize(24,24));self.stop_button.setIconSize(QSize(20,20))
control_buttons=[(self.seek_back_20_button,"הרץ 20 שניות אחורה"),(self.seek_back_10_button,"הרץ 10 שניות אחורה"),(self.seek_back_5_button,"הרץ 5 שניות אחורה"),(self.stop_button,"עצור נגינה"),(self.play_pause_button,"נגן / השהה"),(self.seek_fwd_5_button,"הרץ 5 שניות קדימה"),(self.seek_fwd_10_button,"הרץ 10 שניות קדימה"),(self.seek_fwd_20_button,"הרץ 20 שניות קדימה")]
controls_layout.addStretch()
for button,tooltip in control_buttons:button.setFixedSize(button_size);button.setObjectName("ControlButton");button.setToolTip(tooltip);controls_layout.addWidget(button)
controls_layout.addStretch()
main_layout.addLayout(top_bar_layout);main_layout.addWidget(self.media_display_stack,1)
main_layout.addWidget(self.track_info_label);main_layout.addWidget(self.artist_album_label)
main_layout.addLayout(progress_layout);main_layout.addLayout(controls_layout)
dedication_label=QLabel("השימוש בנגן לע\"נ ר' זלמן יהודה בן ר' יצחק ז\"ל")
dedication_label.setAlignment(Qt.AlignmentFlag.AlignCenter);font=dedication_label.font();font.setPointSize(9);dedication_label.setFont(font)
dedication_label.setStyleSheet("color:#a0a0a0;padding-top:10px;padding-bottom:2px;");main_layout.addWidget(dedication_label)
# חיבור כל הסיגנלים (אירועים) לפונקציות המתאימות (סלוטים)
def connect_signals(self):
self.open_file_button.clicked.connect(self.open_file);self.play_pause_button.clicked.connect(self.play_pause)
self.stop_button.clicked.connect(self.stop_player);self.mute_button.clicked.connect(self.toggle_mute)
self.settings_button.clicked.connect(self.open_settings_dialog);self.pin_button.clicked.connect(self.toggle_always_on_top)
seek_map={self.seek_back_20_button:-20,self.seek_back_10_button:-10,self.seek_back_5_button:-5,self.seek_fwd_5_button:5,self.seek_fwd_10_button:10,self.seek_fwd_20_button:20}
for button,seconds in seek_map.items():button.clicked.connect(partial(self.seek_by_seconds,seconds))
self.player.positionChanged.connect(self.update_position);self.player.durationChanged.connect(self.update_duration)
self.player.playbackStateChanged.connect(self.update_play_pause_icon);self.player.mediaStatusChanged.connect(self.handle_media_status)
self.progress_slider.sliderMoved.connect(self.set_position);self.volume_slider.valueChanged.connect(self.set_volume)
self.typing_timer.timeout.connect(self.resume_after_typing)
def open_settings_dialog(self):
dialog=SettingsDialog(self)
if dialog.exec():self.load_player_settings();self.update_typing_timer_interval();self.apply_playback_rate()
# מעבר בין מצב רגיל למצב מיני (תמיד למעלה)
def toggle_always_on_top(self):
self.is_always_on_top=not self.is_always_on_top
self.pin_button.setProperty("active",self.is_always_on_top);self.style().polish(self.pin_button)
if self.is_always_on_top:
self.setWindowFlags(self.windowFlags()|Qt.WindowType.WindowStaysOnTopHint);self.media_display_stack.setVisible(False)
self.track_info_label.setVisible(False);self.artist_album_label.setVisible(False);self.setFixedSize(self.mini_size)
else:
self.setWindowFlags(self.windowFlags()&~Qt.WindowType.WindowStaysOnTopHint);self.media_display_stack.setVisible(True)
self.track_info_label.setVisible(True);self.artist_album_label.setVisible(True);self.setMinimumSize(0,0)
self.setMaximumSize(QSize(16777215,16777215));self.resize(self.normal_size)
self.show()
def stop_player(self):self.player.stop();self.reset_ui_to_default();self.current_file_path=None
# איפוס ממשק המשתמש למצב ההתחלתי
def reset_ui_to_default(self):
self.track_info_label.setText("לא נטען קובץ");self.artist_album_label.setText(" ");self.album_art_label.setText("טען קובץ אודיו או וידאו")
self.album_art_label.setPixmap(QPixmap());self.media_display_stack.setCurrentWidget(self.album_art_label);self.current_time_label.setText("00:00")
self.total_time_label.setText("00:00");self.progress_slider.setValue(0)
# פותח דיאלוג לבחירת קובץ מדיה
def open_file(self):
filter="קבצי מדיה (*.mp3 *.flac *.wav *.ogg *.mp4 *.mkv *.avi *.mov);;קבצי אודיו (*.mp3 *.flac *.wav *.ogg);;קבצי וידאו (*.mp4 *.mkv *.avi *.mov)"
file_path,_=QFileDialog.getOpenFileName(self,"בחר קובץ מדיה","",filter)
if file_path:self.load_and_play_file(file_path)
# טוען קובץ מדיה, מציג מטא-דאטה ומנגן אותו
def load_and_play_file(self,file_path):
self.current_file_path=file_path;ext=os.path.splitext(file_path)[1].lower()
video_ext=['.mp4','.mkv','.avi','.mov'];audio_ext=['.mp3','.flac','.wav','.ogg']
if ext in audio_ext:
meta=self.get_track_metadata(file_path);self.track_info_label.setText(meta['title']);self.artist_album_label.setText(f"{meta['artist']} - {meta['album']}")
if meta['pixmap']:self.album_art_label.setPixmap(meta['pixmap'])
else:self.album_art_label.setText("No Album Art");self.album_art_label.setPixmap(QPixmap())
self.media_display_stack.setCurrentWidget(self.album_art_label)
elif ext in video_ext:
self.track_info_label.setText(os.path.basename(file_path));self.artist_album_label.setText("קובץ וידאו")
self.media_display_stack.setCurrentWidget(self.video_widget)
else:self.reset_ui_to_default();self.track_info_label.setText("סוג קובץ לא נתמך");return
self.player.setSource(QUrl.fromLocalFile(file_path));self.apply_playback_rate();self.player.play()
# קורא מטא-דאטה (כותרת, אמן, אלבום, תמונה) מקובץ שמע
def get_track_metadata(self,file_path):
title=os.path.basename(file_path);artist="Unknown Artist";album="Unknown Album";pixmap=None
try:
if file_path.lower().endswith('.mp3'):
audio=MP3(file_path);tags=audio.tags
if'TIT2'in tags:title=tags['TIT2'].text[0]
if'TPE1'in tags:artist=tags['TPE1'].text[0]
if'TALB'in tags:album=tags['TALB'].text[0]
if'APIC:'in tags:pixmap=QPixmap();pixmap.loadFromData(tags.getall('APIC:')[0].data)
elif file_path.lower().endswith('.flac'):
audio=FLAC(file_path)
if'title'in audio:title=audio['title'][0]
if'artist'in audio:artist=audio['artist'][0]
if'album'in audio:album=audio['album'][0]
if audio.pictures:pixmap=QPixmap();pixmap.loadFromData(audio.pictures[0].data)
except Exception as e:print(f"Error reading metadata for {file_path}: {e}")
return{'title':title,'artist':artist,'album':album,'pixmap':pixmap}
def play_pause(self):
if self.player.playbackState()==QMediaPlayer.PlaybackState.PlayingState:self.pause_with_rewind()
else:
if self.current_file_path:self.player.play()
else:self.open_file()
# משהה את הנגן, ואם מוגדר - מחזיר מעט אחורה
def pause_with_rewind(self):
if self.player.playbackState()!=QMediaPlayer.PlaybackState.PlayingState:return
if self.rewind_on_pause_enabled:
current_pos=self.player.position();rewind_ms=int(self.rewind_time_sec*1000)
new_pos=max(0,current_pos-rewind_ms);self.player.setPosition(new_pos)
self.player.pause()
def seek_by_seconds(self,seconds):
if not self.current_file_path:return
current_pos=self.player.position();new_pos=current_pos+(seconds*1000);duration=self.player.duration()
new_pos=max(0,min(new_pos,duration if duration>0 else new_pos));self.player.setPosition(new_pos)
def toggle_mute(self):
is_muted=not self.audio_output.isMuted();self.audio_output.setMuted(is_muted)
icon=QStyle.StandardPixmap.SP_MediaVolumeMuted if is_muted else QStyle.StandardPixmap.SP_MediaVolume
self.mute_button.setIcon(self.style().standardIcon(icon))
# פונקציות עזר לעדכון ממשק המשתמש
def set_position(self,position):self.player.setPosition(position)
def set_volume(self,volume):self.audio_output.setVolume(volume/100.0)
def update_position(self,position):
self.progress_slider.setValue(position);self.current_time_label.setText(self.format_time(position))
def update_duration(self,duration):
self.progress_slider.setRange(0,duration);self.total_time_label.setText(self.format_time(duration))
def update_play_pause_icon(self,state):
icon=QStyle.StandardPixmap.SP_MediaPause if state==QMediaPlayer.PlaybackState.PlayingState else QStyle.StandardPixmap.SP_MediaPlay
self.play_pause_button.setIcon(self.style().standardIcon(icon))
def handle_media_status(self,status):
if status==QMediaPlayer.MediaStatus.EndOfMedia and self.current_file_path:self.player.setPosition(0);self.player.play()
def format_time(self,ms):s=round(ms/1000);m,s=divmod(s,60);return f"{m:02d}:{s:02d}"
# מחדש את הניגון לאחר שחולף זמן ההשהייה מההקלדה האחרונה
def resume_after_typing(self):
if self.was_playing_before_typing:
if self.player.playbackState()!=QMediaPlayer.PlaybackState.PlayingState:self.player.play()
self.was_playing_before_typing=False
# נקודת הכניסה הראשית של התוכנית
if __name__ == '__main__':
app = QApplication(sys.argv)
# מאפשר פתיחת קובץ דרך שורת הפקודה
file_to_open = None
if len(sys.argv) > 1: file_to_open = sys.argv[1]
player_window = MusicPlayer(file_to_play=file_to_open)
player_window.show()
sys.exit(app.exec())