feat: remember last played track, playback position, and volume across restarts

This commit is contained in:
Maksim Totmin 2026-06-01 01:22:16 +07:00
parent 949783e76c
commit 06f6529cbe
2 changed files with 90 additions and 20 deletions

View File

@ -94,40 +94,54 @@ class ConfigLoader:
if not self.config.has_section('General'): if not self.config.has_section('General'):
self.config.add_section('General') self.config.add_section('General')
self.config.set('General', 'output_dir', os.path.join(os.path.expanduser("~"), "Music", "iPod")) if not self.config.has_option('General', 'output_dir'):
self.config.set('General', 'format', 'm4a') self.config.set('General', 'output_dir', os.path.join(os.path.expanduser("~"), "Music", "iPod"))
self.config.set('General', 'quality', '256') if not self.config.has_option('General', 'format'):
self.config.set('General', 'format', 'm4a')
if not self.config.has_option('General', 'quality'):
self.config.set('General', 'quality', '256')
# Video section # Video section
if not self.config.has_section('Video'): if not self.config.has_section('Video'):
self.config.add_section('Video') self.config.add_section('Video')
self.config.set('Video', 'enable_video', 'false') if not self.config.has_option('Video', 'enable_video'):
self.config.set('Video', 'resolution', '640x480') self.config.set('Video', 'enable_video', 'false')
if not self.config.has_option('Video', 'resolution'):
self.config.set('Video', 'resolution', '640x480')
# Metadata section # Metadata section
if not self.config.has_section('Metadata'): if not self.config.has_section('Metadata'):
self.config.add_section('Metadata') self.config.add_section('Metadata')
self.config.set('Metadata', 'embed_artwork', 'true') if not self.config.has_option('Metadata', 'embed_artwork'):
self.config.set('Metadata', 'use_musicbrainz', 'false') self.config.set('Metadata', 'embed_artwork', 'true')
if not self.config.has_option('Metadata', 'use_musicbrainz'):
self.config.set('Metadata', 'use_musicbrainz', 'false')
# Device section # Device section
if not self.config.has_section('Device'): if not self.config.has_section('Device'):
self.config.add_section('Device') self.config.add_section('Device')
self.config.set('Device', 'auto_detect', 'true') if not self.config.has_option('Device', 'auto_detect'):
self.config.set('Device', 'mount_point', '') self.config.set('Device', 'auto_detect', 'true')
if not self.config.has_option('Device', 'mount_point'):
self.config.set('Device', 'mount_point', '')
# Advanced section # Advanced section
if not self.config.has_section('Advanced'): if not self.config.has_section('Advanced'):
self.config.add_section('Advanced') self.config.add_section('Advanced')
self.config.set('Advanced', 'temp_dir', 'temp') if not self.config.has_option('Advanced', 'temp_dir'):
self.config.set('Advanced', 'concurrent_downloads', '2') self.config.set('Advanced', 'temp_dir', 'temp')
self.config.set('Advanced', 'debug', 'false') if not self.config.has_option('Advanced', 'concurrent_downloads'):
self.config.set('Advanced', 'clean_temp', 'true') self.config.set('Advanced', 'concurrent_downloads', '2')
self.config.set('Advanced', 'show_download', 'false') if not self.config.has_option('Advanced', 'debug'):
self.config.set('Advanced', 'debug', 'false')
if not self.config.has_option('Advanced', 'clean_temp'):
self.config.set('Advanced', 'clean_temp', 'true')
if not self.config.has_option('Advanced', 'show_download'):
self.config.set('Advanced', 'show_download', 'false')
# Hotkeys section # Hotkeys section
if not self.config.has_section('Hotkeys'): if not self.config.has_section('Hotkeys'):
@ -153,7 +167,18 @@ class ConfigLoader:
'delete_selected': 'Delete', 'delete_selected': 'Delete',
} }
for key, value in hotkey_defaults.items(): for key, value in hotkey_defaults.items():
self.config.set('Hotkeys', key, value) if not self.config.has_option('Hotkeys', key):
self.config.set('Hotkeys', key, value)
# Playback section
if not self.config.has_section('Playback'):
self.config.add_section('Playback')
if not self.config.has_option('Playback', 'last_track_path'):
self.config.set('Playback', 'last_track_path', '')
if not self.config.has_option('Playback', 'last_position_ms'):
self.config.set('Playback', 'last_position_ms', '0')
if not self.config.has_option('Playback', 'last_volume'):
self.config.set('Playback', 'last_volume', '80')
def get(self, section: str, option: str, fallback: Any = None) -> Any: def get(self, section: str, option: str, fallback: Any = None) -> Any:
""" """

View File

@ -294,12 +294,17 @@ class MainWindow(QMainWindow):
self.audio_output: Optional[QAudioOutput] = None self.audio_output: Optional[QAudioOutput] = None
self.current_playback_index: int = -1 self.current_playback_index: int = -1
self._is_playing = False self._is_playing = False
self._saved_volume: int = 80
self._saved_track_path: str = ""
self._saved_position_ms: int = 0
self._pending_seek_ms: int = 0
self._setup_ui() self._setup_ui()
self._setup_player()
self._load_settings() self._load_settings()
self._setup_player()
self._setup_hotkeys() self._setup_hotkeys()
self._scan_library() self._scan_library()
self._resume_playback_if_saved()
self._on_refresh_devices_clicked() self._on_refresh_devices_clicked()
def _cleanup_worker(self): def _cleanup_worker(self):
@ -345,6 +350,10 @@ class MainWindow(QMainWindow):
self.show_download_tab = show_download self.show_download_tab = show_download
self.tab_widget.setTabVisible(self.download_tab_index, show_download) self.tab_widget.setTabVisible(self.download_tab_index, show_download)
self._saved_volume = config.get_int("Playback", "last_volume", fallback=80)
self._saved_track_path = config.get("Playback", "last_track_path", fallback="")
self._saved_position_ms = config.get_int("Playback", "last_position_ms", fallback=0)
def _save_settings(self): def _save_settings(self):
config = self.config_loader config = self.config_loader
config.set("General", "output_dir", self.output_dir_input.text()) config.set("General", "output_dir", self.output_dir_input.text())
@ -357,6 +366,23 @@ class MainWindow(QMainWindow):
config.set("Advanced", "clean_temp", str(self.clean_temp_check.isChecked()).lower()) config.set("Advanced", "clean_temp", str(self.clean_temp_check.isChecked()).lower())
config.set("Device", "auto_detect", str(self.auto_detect_check.isChecked()).lower()) config.set("Device", "auto_detect", str(self.auto_detect_check.isChecked()).lower())
config.set("Advanced", "show_download", str(self.show_download_check.isChecked()).lower()) config.set("Advanced", "show_download", str(self.show_download_check.isChecked()).lower())
config.set("Playback", "last_volume", str(self.volume_slider.value()))
if self.current_playback_index >= 0 and self.player.source().isValid():
track_path = self.player.source().toLocalFile()
if track_path and os.path.exists(track_path):
config.set("Playback", "last_track_path", track_path)
position = self.player.position()
duration = self.player.duration()
if duration > 0 and position > duration - 2000:
position = 0
config.set("Playback", "last_position_ms", str(position))
else:
config.set("Playback", "last_track_path", "")
config.set("Playback", "last_position_ms", "0")
else:
config.set("Playback", "last_track_path", "")
config.set("Playback", "last_position_ms", "0")
config.save() config.save()
def _setup_hotkeys(self): def _setup_hotkeys(self):
@ -799,14 +825,14 @@ class MainWindow(QMainWindow):
"""Initialize QMediaPlayer and QAudioOutput for playback.""" """Initialize QMediaPlayer and QAudioOutput for playback."""
self.player = QMediaPlayer() self.player = QMediaPlayer()
self.audio_output = QAudioOutput() self.audio_output = QAudioOutput()
self.audio_output.setVolume(0.8) self.audio_output.setVolume(self._saved_volume / 100.0)
self.player.setAudioOutput(self.audio_output) self.player.setAudioOutput(self.audio_output)
self.player.positionChanged.connect(self._on_media_position_changed) self.player.positionChanged.connect(self._on_media_position_changed)
self.player.durationChanged.connect(self._on_media_duration_changed) self.player.durationChanged.connect(self._on_media_duration_changed)
self.player.mediaStatusChanged.connect(self._on_media_status_changed) self.player.mediaStatusChanged.connect(self._on_media_status_changed)
def _play_track_from_index(self, index: int): def _play_track_from_index(self, index: int, start_position_ms: int = 0):
"""Start playing a track from the library table by row index.""" """Start playing a track from the library table by row index."""
if index < 0 or index >= self.library_table.rowCount(): if index < 0 or index >= self.library_table.rowCount():
return return
@ -834,7 +860,6 @@ class MainWindow(QMainWindow):
self.cover_label.setPixmap(pixmap) self.cover_label.setPixmap(pixmap)
self.cover_label.setToolTip(track_data.get("album", "")) self.cover_label.setToolTip(track_data.get("album", ""))
else: else:
# Fallback: try looking up by artist/album in cover cache
artist = track_data.get("artist", "") artist = track_data.get("artist", "")
album = track_data.get("album", "") album = track_data.get("album", "")
cached = cover_cache.get(artist, album) if artist else None cached = cover_cache.get(artist, album) if artist else None
@ -846,10 +871,14 @@ class MainWindow(QMainWindow):
self.cover_label.clear() self.cover_label.clear()
self.cover_label.setToolTip("") self.cover_label.setToolTip("")
# Highlight the current row
self.library_table.clearSelection() self.library_table.clearSelection()
self.library_table.selectRow(index) self.library_table.selectRow(index)
if start_position_ms > 0:
self._pending_seek_ms = start_position_ms
else:
self._pending_seek_ms = 0
def _on_play_pause_clicked(self): def _on_play_pause_clicked(self):
if self.player is None: if self.player is None:
return return
@ -946,6 +975,10 @@ class MainWindow(QMainWindow):
elif status == QMediaPlayer.MediaStatus.NoMedia: elif status == QMediaPlayer.MediaStatus.NoMedia:
self.play_btn.setText("\u25B6") self.play_btn.setText("\u25B6")
self._is_playing = False self._is_playing = False
elif status == QMediaPlayer.MediaStatus.LoadedMedia:
if self._pending_seek_ms > 0:
self.player.setPosition(self._pending_seek_ms)
self._pending_seek_ms = 0
def _on_table_double_clicked(self, row: int): def _on_table_double_clicked(self, row: int):
self._play_track_from_index(row) self._play_track_from_index(row)
@ -1037,6 +1070,18 @@ class MainWindow(QMainWindow):
self.library_scanned_sources = sources self.library_scanned_sources = sources
self._refresh_library_ui() self._refresh_library_ui()
def _resume_playback_if_saved(self):
if not self._saved_track_path or not os.path.exists(self._saved_track_path):
return
for row in range(self.library_table.rowCount()):
data_item = self.library_table.item(row, 0)
if data_item is None:
continue
_is_ready, track_data = data_item.data(Qt.ItemDataRole.UserRole)
if track_data and track_data.get("path", "") == self._saved_track_path:
self._play_track_from_index(row, start_position_ms=self._saved_position_ms)
return
def _extract_ready_track(self, fpath: str, fname: str, handler) -> Dict[str, Any]: def _extract_ready_track(self, fpath: str, fname: str, handler) -> Dict[str, Any]:
size = os.path.getsize(fpath) size = os.path.getsize(fpath)
cover_path = None cover_path = None