diff --git a/src/main.py b/src/main.py index d64ef63..be9d7f0 100644 --- a/src/main.py +++ b/src/main.py @@ -15,9 +15,10 @@ from PyQt6.QtWidgets import ( QPushButton, QLabel, QLineEdit, QProgressBar, QFileDialog, QComboBox, QCheckBox, QTabWidget, QListWidget, QListWidgetItem, QMessageBox, QSpinBox, QGroupBox, QHeaderView, - QTableWidget, QTableWidgetItem + QTableWidget, QTableWidgetItem, QSlider ) -from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl +from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput from youtube_downloader import YouTubeDownloader, TrackInfo from audio_converter import AudioConverter @@ -221,7 +222,13 @@ class MainWindow(QMainWindow): self.ipod_devices: List[Dict] = [] self.current_mount_point: Optional[str] = None + self.player: Optional[QMediaPlayer] = None + self.audio_output: Optional[QAudioOutput] = None + self.current_playback_index: int = -1 + self._is_playing = False + self._setup_ui() + self._setup_player() self._scan_library() self._on_refresh_devices_clicked() @@ -310,19 +317,80 @@ class MainWindow(QMainWindow): def _setup_library_tab(self, tab): layout = QVBoxLayout(tab) - output_layout = QHBoxLayout() - output_label = QLabel("Output Directory:") - self.output_dir_input = QLineEdit() - self.output_dir_input.setText(os.path.join(os.path.expanduser("~"), "Music", "iPod")) - browse_btn = QPushButton("Browse...") - browse_btn.clicked.connect(self._on_browse_output_clicked) - refresh_btn = QPushButton("Refresh") + # === Playback controls bar === + playback_group = QGroupBox() + playback_layout = QVBoxLayout(playback_group) + playback_layout.setContentsMargins(8, 6, 8, 6) + + # Top row: transport buttons + progress + time + volume + controls_row = QHBoxLayout() + controls_row.setSpacing(8) + + self.prev_btn = QPushButton("\u23EE") + self.prev_btn.setFixedWidth(36) + self.prev_btn.clicked.connect(self._on_prev_clicked) + + self.play_btn = QPushButton("\u25B6") + self.play_btn.setFixedWidth(44) + self.play_btn.clicked.connect(self._on_play_pause_clicked) + + self.next_btn = QPushButton("\u23ED") + self.next_btn.setFixedWidth(36) + self.next_btn.clicked.connect(self._on_next_clicked) + + controls_row.addWidget(self.prev_btn) + controls_row.addWidget(self.play_btn) + controls_row.addWidget(self.next_btn) + + self.time_label_start = QLabel("0:00") + self.time_label_start.setFixedWidth(40) + self.time_label_start.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + + self.position_slider = QSlider(Qt.Orientation.Horizontal) + self.position_slider.setMinimum(0) + self.position_slider.setMaximum(1000) + self.position_slider.setValue(0) + self.position_slider.sliderPressed.connect(self._on_slider_pressed) + self.position_slider.sliderReleased.connect(self._on_slider_released) + + self.time_label_end = QLabel("0:00") + self.time_label_end.setFixedWidth(40) + self.time_label_end.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + + controls_row.addWidget(self.time_label_start) + controls_row.addWidget(self.position_slider, stretch=1) + controls_row.addWidget(self.time_label_end) + + vol_icon = QLabel("\U0001F50A") + self.volume_slider = QSlider(Qt.Orientation.Horizontal) + self.volume_slider.setFixedWidth(80) + self.volume_slider.setRange(0, 100) + self.volume_slider.setValue(80) + self.volume_slider.valueChanged.connect(self._on_volume_changed) + + controls_row.addWidget(vol_icon) + controls_row.addWidget(self.volume_slider) + playback_layout.addLayout(controls_row) + + # Bottom row: now playing info + self.now_playing_label = QLabel("Select a track to play") + self.now_playing_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.now_playing_label.setStyleSheet("color: palette(mid); font-size: 12px;") + now_playing_layout = QHBoxLayout() + now_playing_layout.addStretch() + now_playing_layout.addWidget(self.now_playing_label) + now_playing_layout.addStretch() + playback_layout.addLayout(now_playing_layout) + + layout.addWidget(playback_group) + + # Refresh button + refresh_row = QHBoxLayout() + refresh_btn = QPushButton("\u21BB Refresh Library") refresh_btn.clicked.connect(self._on_refresh_library_clicked) - output_layout.addWidget(output_label) - output_layout.addWidget(self.output_dir_input) - output_layout.addWidget(browse_btn) - output_layout.addWidget(refresh_btn) - layout.addLayout(output_layout) + refresh_row.addStretch() + refresh_row.addWidget(refresh_btn) + layout.addLayout(refresh_row) self.ready_label = QLabel("Ready to Transfer:") layout.addWidget(self.ready_label) @@ -337,6 +405,7 @@ class MainWindow(QMainWindow): self.library_ready_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.library_ready_table.setAlternatingRowColors(True) self.library_ready_table.setSortingEnabled(True) + self.library_ready_table.cellDoubleClicked.connect(self._on_table_double_clicked) header = self.library_ready_table.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) @@ -369,7 +438,7 @@ class MainWindow(QMainWindow): self.library_convert_btn.clicked.connect(self._on_library_convert_clicked) layout.addWidget(self.library_convert_btn) - self.library_transfer_btn = QPushButton("Transfer Selected to iPod ▸") + self.library_transfer_btn = QPushButton("Transfer Selected to iPod \u25B8") self.library_transfer_btn.clicked.connect(self._on_library_transfer_clicked) layout.addWidget(self.library_transfer_btn) @@ -433,6 +502,21 @@ class MainWindow(QMainWindow): def _setup_settings_tab(self, tab): layout = QVBoxLayout(tab) + library_group = QGroupBox("Library") + library_layout = QVBoxLayout(library_group) + + output_layout = QHBoxLayout() + output_label = QLabel("Output Directory:") + self.output_dir_input = QLineEdit() + self.output_dir_input.setText(os.path.join(os.path.expanduser("~"), "Music", "iPod")) + browse_btn = QPushButton("Browse...") + browse_btn.clicked.connect(self._on_browse_output_clicked) + output_layout.addWidget(output_label) + output_layout.addWidget(self.output_dir_input) + output_layout.addWidget(browse_btn) + library_layout.addLayout(output_layout) + layout.addWidget(library_group) + video_group = QGroupBox("Video Support") video_layout = QVBoxLayout(video_group) @@ -488,6 +572,118 @@ class MainWindow(QMainWindow): about_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(about_label) + def _setup_player(self): + """Initialize QMediaPlayer and QAudioOutput for playback.""" + self.player = QMediaPlayer() + self.audio_output = QAudioOutput() + self.audio_output.setVolume(0.8) + self.player.setAudioOutput(self.audio_output) + + self.player.positionChanged.connect(self._on_media_position_changed) + self.player.durationChanged.connect(self._on_media_duration_changed) + self.player.mediaStatusChanged.connect(self._on_media_status_changed) + + def _play_track_from_index(self, index: int): + """Start playing a track from the library table by row index.""" + if index < 0 or index >= self.library_ready_table.rowCount(): + return + data_item = self.library_ready_table.item(index, 0) + if data_item is None: + return + track_data = data_item.data(Qt.ItemDataRole.UserRole) + if not track_data or not os.path.exists(track_data.get("path", "")): + return + + self.current_playback_index = index + self.player.setSource(QUrl.fromLocalFile(track_data["path"])) + self.player.play() + self._is_playing = True + self.play_btn.setText("\u23F8") + + self.now_playing_label.setText(f"{track_data['artist']} \u2014 {track_data['title']}") + self.now_playing_label.setStyleSheet( + "color: palette(text); font-weight: bold;" + ) + + # Highlight the current row + self.library_ready_table.clearSelection() + self.library_ready_table.selectRow(index) + + def _on_play_pause_clicked(self): + if self.player is None: + return + if self.player.playbackState() == QMediaPlayer.PlaybackState.PlayingState: + self.player.pause() + self._is_playing = False + self.play_btn.setText("\u25B6") + else: + if self.player.playbackState() == QMediaPlayer.PlaybackState.StoppedState: + # Nothing playing yet — play first selected row or row 0 + selected_rows = {item.row() for item in self.library_ready_table.selectedItems()} + start = min(selected_rows) if selected_rows else 0 + self._play_track_from_index(start) + else: + self.player.play() + self._is_playing = True + self.play_btn.setText("\u23F8") + + def _on_prev_clicked(self): + if self.player is None: + return + if self.player.position() > 3000: + self.player.setPosition(0) + return + idx = max(0, self.current_playback_index - 1) + self._play_track_from_index(idx) + + def _on_next_clicked(self): + if self.player is None: + return + idx = min(self.library_ready_table.rowCount() - 1, self.current_playback_index + 1) + self._play_track_from_index(idx) + + def _on_slider_pressed(self): + if self.player: + self.player.setPosition( + int(self.position_slider.value() / 1000.0 * self.player.duration()) + ) + + def _on_slider_released(self): + pass + + def _on_volume_changed(self, value): + if self.audio_output: + self.audio_output.setVolume(value / 100.0) + + def _on_media_position_changed(self, position: int): + if self.position_slider is None or self.player is None: + return + duration = self.player.duration() + if duration > 0 and not self.position_slider.isSliderDown(): + self.position_slider.setValue(int(position / duration * 1000)) + self.time_label_start.setText(self._format_time_ms(position)) + + def _on_media_duration_changed(self, duration: int): + self.time_label_end.setText(self._format_time_ms(duration)) + if self.position_slider and not self.position_slider.isSliderDown(): + self.position_slider.setValue(0) + + def _on_media_status_changed(self, status): + if status == QMediaPlayer.MediaStatus.EndOfMedia: + self._on_next_clicked() + elif status == QMediaPlayer.MediaStatus.NoMedia: + self.play_btn.setText("\u25B6") + self._is_playing = False + + def _on_table_double_clicked(self, row: int): + self._play_track_from_index(row) + + @staticmethod + def _format_time_ms(ms: int) -> str: + total_seconds = ms // 1000 + m, s = divmod(total_seconds, 60) + return f"{m}:{s:02d}" + def _format_duration(self, seconds: int) -> str: if seconds <= 0: return "??:??"