שיתוף | נגן הקלדה עם מספר פיצ'רים נחמדים...
-
ייתכן ויש כבר וגם ככה זה לא דבר כזה נצרך אבל לצורך מסויים יצרתי את זה ביחד עם AI לא השקעתי בזה הרבה... בכל אופן זה התוצאה...
הנגן מיועד לאנשים שמעוניינים להקליד שיעורים וכדו' וקשה להם להמשיך לעקוב אחרי ההמשך תוך כדי הקלדה...
קישור למג'יקוד (הקובץ חתום) עד שמישהו ( @kasnik אולי?) יעלה את זה לדרייב...עריכה: הועלה לדרייב... (קבוע בע"ה) קרדיט - @kasnik
קוד המקור... (אל תכעסו אל הארכיטקטורה...)
התקנות תלויות למי שמשתמש בקוד מקור
pip install PyQt6 pynput mutagen
או הקוד עצמו בספויילר
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())
הפיצ'רים כדלהלן:
-
בעת הקלדה על המקלדת הנגן מפסיק את פעולת הזרמת השמע וממשיך כאשר מפסיקים להקליד
-
ניתן לבטל את הפיצ'ר או להגדיר את זמן ההפסקה בכפתור ההגדרות
-
ניתן להגדיר שבחזרה להזרמת המדיה הנגן יחזור להשמיע מספר שניות אחורה (עד 10 שניות)
-
ניתן להגביר או להנמיך את מהירות הזרמת השמע בחלונית ההגדרות
-
ניתן לנעוץ את הנגן כך שיופיע תמיד מעל כל החלונות וכך תוך כדי הקלדה לדלג קדימה/אחורה בייתר קלות...
-
בעת נעיצת הנגן הוא מצטמצם לגודל מינימאלי שלא יתפוס יותר מידי שטח מסך...
-
ניתן להפעיל ע"י 'פתח באמצעות' (הסבר בהמשך השרשור...)
-
תומך בקבצי אודיו/וידיאו (סיומות נתמכות: MP3, FLAC, WAV, OGG, MP4, MKV, AVI, MOV)
צילומי מסך:
החלון הראשי
מצב נעוץ
חלונית ההגדרות -
-
שדרוגים נוספים:
-
ניתן להגדיר שבחזרה להזרמת המדיה הנגן יחזור להשמיע מספר שניות אחורה (עד 10 שניות)
-
התווספה תמיכה בקבצי אודיו/וידיאו (סיומות נתמכות: MP3, FLAC, WAV, OGG, MP4, MKV, AVI, MOV)
-
ניתן להפעיל ע"י 'פתח באמצעות' (סודר באג בסדר ההרצה שגרם לכך שכאשר נפתח בע"י הפתח באמצעות נפתח הנגן אבל לא מזרים את המדיה...)
הסבר: צריך ללחוץ קליק ימני ואז פתח באמצעות ואז בחר אפליקציה אחרת ואז למטה בחר אפליקציה מהמחשב שלך לדפדף להיכן שנמצא הקובץ EXE ושלהפעיל דרכו...
לע"ע הוסרה הגירסא שתומכת רק באודיו אם יש דורש תעדכנו ואעלה שוב...
-
-
ייתכן ויש כבר וגם ככה זה לא דבר כזה נצרך אבל לצורך מסויים יצרתי את זה ביחד עם AI לא השקעתי בזה הרבה... בכל אופן זה התוצאה...
הנגן מיועד לאנשים שמעוניינים להקליד שיעורים וכדו' וקשה להם להמשיך לעקוב אחרי ההמשך תוך כדי הקלדה...
קישור למג'יקוד (הקובץ חתום) עד שמישהו ( @kasnik אולי?) יעלה את זה לדרייב...עריכה: הועלה לדרייב... (קבוע בע"ה) קרדיט - @kasnik
קוד המקור... (אל תכעסו אל הארכיטקטורה...)
התקנות תלויות למי שמשתמש בקוד מקור
pip install PyQt6 pynput mutagen
או הקוד עצמו בספויילר
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())
הפיצ'רים כדלהלן:
-
בעת הקלדה על המקלדת הנגן מפסיק את פעולת הזרמת השמע וממשיך כאשר מפסיקים להקליד
-
ניתן לבטל את הפיצ'ר או להגדיר את זמן ההפסקה בכפתור ההגדרות
-
ניתן להגדיר שבחזרה להזרמת המדיה הנגן יחזור להשמיע מספר שניות אחורה (עד 10 שניות)
-
ניתן להגביר או להנמיך את מהירות הזרמת השמע בחלונית ההגדרות
-
ניתן לנעוץ את הנגן כך שיופיע תמיד מעל כל החלונות וכך תוך כדי הקלדה לדלג קדימה/אחורה בייתר קלות...
-
בעת נעיצת הנגן הוא מצטמצם לגודל מינימאלי שלא יתפוס יותר מידי שטח מסך...
-
ניתן להפעיל ע"י 'פתח באמצעות' (הסבר בהמשך השרשור...)
-
תומך בקבצי אודיו/וידיאו (סיומות נתמכות: MP3, FLAC, WAV, OGG, MP4, MKV, AVI, MOV)
צילומי מסך:
החלון הראשי
מצב נעוץ
חלונית ההגדרות -
-
@2580 כתב בשיתוף | נגן הקלדה עם מספר פיצ'רים נחמדים...:
ייתכן ויש כבר
בכל אופן אני חושב שזה יותר יפה ונוח כמו כן תומך גם בוידיאו למי שמתמלל מהסרטות...