| Total Complexity | 96 |
| Total Lines | 601 |
| Duplicated Lines | 12.48 % |
| Changes | 16 | ||
| Bugs | 0 | Features | 3 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like MusicPlayer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | import os |
||
| 16 | class MusicPlayer(QMainWindow): |
||
| 17 | """MusicPlayer houses all of elements that directly interact with the main window.""" |
||
| 18 | |||
| 19 | def __init__(self, parent=None): |
||
| 20 | """Initialize the QMainWindow widget. |
||
| 21 | |||
| 22 | The window title, window icon, and window size are initialized here as well |
||
| 23 | as the following widgets: QMediaPlayer, QMediaPlaylist, QMediaContent, QMenuBar, |
||
| 24 | QToolBar, QLabel, QPixmap, QSlider, QDockWidget, QListWidget, QWidget, and |
||
| 25 | QVBoxLayout. The connect signals for relavant widgets are also initialized. |
||
| 26 | """ |
||
| 27 | super(MusicPlayer, self).__init__(parent) |
||
| 28 | self.setWindowTitle('Mosaic') |
||
| 29 | |||
| 30 | window_icon = utilities.resource_filename('mosaic.images', 'icon.png') |
||
| 31 | self.setWindowIcon(QIcon(window_icon)) |
||
| 32 | self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
||
| 33 | |||
| 34 | # Initiates Qt objects to be used by MusicPlayer |
||
| 35 | self.player = QMediaPlayer() |
||
| 36 | self.playlist = QMediaPlaylist() |
||
| 37 | self.playlist_location = defaults.Settings().playlist_path |
||
| 38 | self.content = QMediaContent() |
||
| 39 | self.menu = self.menuBar() |
||
| 40 | self.toolbar = QToolBar() |
||
| 41 | self.art = QLabel() |
||
| 42 | self.pixmap = QPixmap() |
||
| 43 | self.slider = QSlider(Qt.Horizontal) |
||
| 44 | self.duration_label = QLabel() |
||
| 45 | self.playlist_dock = QDockWidget('Playlist', self) |
||
| 46 | self.library_dock = QDockWidget('Media Library', self) |
||
| 47 | self.playlist_view = QListWidget() |
||
| 48 | self.library_view = library.MediaLibraryView() |
||
| 49 | self.library_model = library.MediaLibraryModel() |
||
| 50 | self.preferences = configuration.PreferencesDialog() |
||
| 51 | self.widget = QWidget() |
||
| 52 | self.layout = QVBoxLayout(self.widget) |
||
| 53 | self.duration = 0 |
||
| 54 | self.playlist_dock_state = None |
||
| 55 | self.library_dock_state = None |
||
| 56 | |||
| 57 | # Sets QWidget() as the central widget of the main window |
||
| 58 | self.setCentralWidget(self.widget) |
||
| 59 | self.layout.setContentsMargins(0, 0, 0, 0) |
||
| 60 | self.art.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) |
||
| 61 | |||
| 62 | # Initiates the playlist dock widget and the library dock widget |
||
| 63 | self.addDockWidget(defaults.Settings().dock_position, self.playlist_dock) |
||
| 64 | self.playlist_dock.setWidget(self.playlist_view) |
||
| 65 | self.playlist_dock.setVisible(defaults.Settings().playlist_on_start) |
||
| 66 | self.playlist_dock.setFeatures(QDockWidget.DockWidgetClosable) |
||
| 67 | |||
| 68 | self.addDockWidget(defaults.Settings().dock_position, self.library_dock) |
||
| 69 | self.library_dock.setWidget(self.library_view) |
||
| 70 | self.library_dock.setVisible(defaults.Settings().media_library_on_start) |
||
| 71 | self.library_dock.setFeatures(QDockWidget.DockWidgetClosable) |
||
| 72 | self.tabifyDockWidget(self.playlist_dock, self.library_dock) |
||
| 73 | |||
| 74 | # Sets the range of the playback slider and sets the playback mode as looping |
||
| 75 | self.slider.setRange(0, self.player.duration() / 1000) |
||
| 76 | self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) |
||
| 77 | |||
| 78 | # OSX system menu bar causes conflicts with PyQt5 menu bar |
||
| 79 | if sys.platform == 'darwin': |
||
| 80 | self.menu.setNativeMenuBar(False) |
||
| 81 | |||
| 82 | # Initiates Settings in the defaults module to give access to settings.toml |
||
| 83 | defaults.Settings() |
||
| 84 | |||
| 85 | # Signals that connect to other methods when they're called |
||
| 86 | self.player.metaDataChanged.connect(self.display_meta_data) |
||
| 87 | self.slider.sliderMoved.connect(self.seek) |
||
| 88 | self.player.durationChanged.connect(self.song_duration) |
||
| 89 | self.player.positionChanged.connect(self.song_position) |
||
| 90 | self.player.stateChanged.connect(self.set_state) |
||
| 91 | self.playlist_view.itemActivated.connect(self.activate_playlist_item) |
||
| 92 | self.library_view.activated.connect(self.open_media_library) |
||
| 93 | self.playlist.currentIndexChanged.connect(self.change_index) |
||
| 94 | self.playlist.mediaInserted.connect(self.initialize_playlist) |
||
| 95 | self.playlist_dock.visibilityChanged.connect(self.dock_visiblity_change) |
||
| 96 | self.library_dock.visibilityChanged.connect(self.dock_visiblity_change) |
||
| 97 | self.preferences.dialog_media_library.media_library_line.textChanged.connect(self.change_media_library_path) |
||
| 98 | self.preferences.dialog_view_options.dropdown_box.currentIndexChanged.connect(self.change_window_size) |
||
| 99 | self.art.mousePressEvent = self.press_playback |
||
| 100 | |||
| 101 | # Creating the menu controls, media controls, and window size of the music player |
||
| 102 | self.menu_controls() |
||
| 103 | self.media_controls() |
||
| 104 | self.load_saved_playlist() |
||
| 105 | |||
| 106 | def menu_controls(self): |
||
| 107 | """Initiate the menu bar and add it to the QMainWindow widget.""" |
||
| 108 | self.file = self.menu.addMenu('File') |
||
| 109 | self.edit = self.menu.addMenu('Edit') |
||
| 110 | self.playback = self.menu.addMenu('Playback') |
||
| 111 | self.view = self.menu.addMenu('View') |
||
| 112 | self.help_ = self.menu.addMenu('Help') |
||
| 113 | |||
| 114 | self.file_menu() |
||
| 115 | self.edit_menu() |
||
| 116 | self.playback_menu() |
||
| 117 | self.view_menu() |
||
| 118 | self.help_menu() |
||
| 119 | |||
| 120 | def media_controls(self): |
||
| 121 | """Create the bottom toolbar and controls used for media playback.""" |
||
| 122 | self.addToolBar(Qt.BottomToolBarArea, self.toolbar) |
||
| 123 | self.toolbar.setMovable(False) |
||
| 124 | |||
| 125 | play_icon = utilities.resource_filename('mosaic.images', 'md_play.png') |
||
| 126 | self.play_action = QAction(QIcon(play_icon), 'Play', self) |
||
| 127 | self.play_action.triggered.connect(self.player.play) |
||
| 128 | |||
| 129 | stop_icon = utilities.resource_filename('mosaic.images', 'md_stop.png') |
||
| 130 | self.stop_action = QAction(QIcon(stop_icon), 'Stop', self) |
||
| 131 | self.stop_action.triggered.connect(self.player.stop) |
||
| 132 | |||
| 133 | previous_icon = utilities.resource_filename('mosaic.images', 'md_previous.png') |
||
| 134 | self.previous_action = QAction(QIcon(previous_icon), 'Previous', self) |
||
| 135 | self.previous_action.triggered.connect(self.previous) |
||
| 136 | |||
| 137 | next_icon = utilities.resource_filename('mosaic.images', 'md_next.png') |
||
| 138 | self.next_action = QAction(QIcon(next_icon), 'Next', self) |
||
| 139 | self.next_action.triggered.connect(self.playlist.next) |
||
| 140 | |||
| 141 | repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png') |
||
| 142 | self.repeat_action = QAction(QIcon(repeat_icon), 'Repeat', self) |
||
| 143 | self.repeat_action.triggered.connect(self.repeat_song) |
||
| 144 | |||
| 145 | self.toolbar.addAction(self.play_action) |
||
| 146 | self.toolbar.addAction(self.stop_action) |
||
| 147 | self.toolbar.addAction(self.previous_action) |
||
| 148 | self.toolbar.addAction(self.next_action) |
||
| 149 | self.toolbar.addAction(self.repeat_action) |
||
| 150 | self.toolbar.addWidget(self.slider) |
||
| 151 | self.toolbar.addWidget(self.duration_label) |
||
| 152 | |||
| 153 | def file_menu(self): |
||
| 154 | """Add a file menu to the menu bar. |
||
| 155 | |||
| 156 | The file menu houses the Open File, Open Multiple Files, Open Playlist, |
||
| 157 | Open Directory, and Exit Application menu items. |
||
| 158 | """ |
||
| 159 | self.open_action = QAction('Open File', self) |
||
| 160 | self.open_action.setShortcut('O') |
||
| 161 | self.open_action.triggered.connect(self.open_file) |
||
| 162 | |||
| 163 | self.open_multiple_files_action = QAction('Open Multiple Files', self) |
||
| 164 | self.open_multiple_files_action.setShortcut('M') |
||
| 165 | self.open_multiple_files_action.triggered.connect(self.open_multiple_files) |
||
| 166 | |||
| 167 | self.open_playlist_action = QAction('Open Playlist', self) |
||
| 168 | self.open_playlist_action.setShortcut('CTRL+P') |
||
| 169 | self.open_playlist_action.triggered.connect(self.open_playlist) |
||
| 170 | |||
| 171 | self.open_directory_action = QAction('Open Directory', self) |
||
| 172 | self.open_directory_action.setShortcut('D') |
||
| 173 | self.open_directory_action.triggered.connect(self.open_directory) |
||
| 174 | |||
| 175 | self.exit_action = QAction('Quit', self) |
||
| 176 | self.exit_action.setShortcut('CTRL+Q') |
||
| 177 | self.exit_action.triggered.connect(self.closeEvent) |
||
| 178 | |||
| 179 | self.file.addAction(self.open_action) |
||
| 180 | self.file.addAction(self.open_multiple_files_action) |
||
| 181 | self.file.addAction(self.open_playlist_action) |
||
| 182 | self.file.addAction(self.open_directory_action) |
||
| 183 | self.file.addSeparator() |
||
| 184 | self.file.addAction(self.exit_action) |
||
| 185 | |||
| 186 | def edit_menu(self): |
||
| 187 | """Add an edit menu to the menu bar. |
||
| 188 | |||
| 189 | The edit menu houses the preferences item that opens a preferences dialog |
||
| 190 | that allows the user to customize features of the music player. |
||
| 191 | """ |
||
| 192 | self.preferences_action = QAction('Preferences', self) |
||
| 193 | self.preferences_action.setShortcut('CTRL+SHIFT+P') |
||
| 194 | self.preferences_action.triggered.connect(lambda: self.preferences.exec_()) |
||
| 195 | |||
| 196 | self.edit.addAction(self.preferences_action) |
||
| 197 | |||
| 198 | def playback_menu(self): |
||
| 199 | """Add a playback menu to the menu bar. |
||
| 200 | |||
| 201 | The playback menu houses |
||
| 202 | """ |
||
| 203 | self.play_playback_action = QAction('Play', self) |
||
| 204 | self.play_playback_action.setShortcut('P') |
||
| 205 | self.play_playback_action.triggered.connect(self.player.play) |
||
| 206 | |||
| 207 | self.stop_playback_action = QAction('Stop', self) |
||
| 208 | self.stop_playback_action.setShortcut('S') |
||
| 209 | self.stop_playback_action.triggered.connect(self.player.stop) |
||
| 210 | |||
| 211 | self.previous_playback_action = QAction('Previous', self) |
||
| 212 | self.previous_playback_action.setShortcut('B') |
||
| 213 | self.previous_playback_action.triggered.connect(self.previous) |
||
| 214 | |||
| 215 | self.next_playback_action = QAction('Next', self) |
||
| 216 | self.next_playback_action.setShortcut('N') |
||
| 217 | self.next_playback_action.triggered.connect(self.playlist.next) |
||
| 218 | |||
| 219 | self.playback.addAction(self.play_playback_action) |
||
| 220 | self.playback.addAction(self.stop_playback_action) |
||
| 221 | self.playback.addAction(self.previous_playback_action) |
||
| 222 | self.playback.addAction(self.next_playback_action) |
||
| 223 | |||
| 224 | def view_menu(self): |
||
| 225 | """Add a view menu to the menu bar. |
||
| 226 | |||
| 227 | The view menu houses the Playlist, Media Library, Minimalist View, and Media |
||
| 228 | Information menu items. The Playlist item toggles the playlist dock into and |
||
| 229 | out of view. The Media Library items toggles the media library dock into and |
||
| 230 | out of view. The Minimalist View item resizes the window and shows only the |
||
| 231 | menu bar and player controls. The Media Information item opens a dialog that |
||
| 232 | shows information relevant to the currently playing song. |
||
| 233 | """ |
||
| 234 | self.dock_action = self.playlist_dock.toggleViewAction() |
||
| 235 | self.dock_action.setShortcut('CTRL+ALT+P') |
||
| 236 | |||
| 237 | self.library_dock_action = self.library_dock.toggleViewAction() |
||
| 238 | self.library_dock_action.setShortcut('CTRL+ALT+L') |
||
| 239 | |||
| 240 | self.minimalist_view_action = QAction('Minimalist View', self) |
||
| 241 | self.minimalist_view_action.setShortcut('CTRL+ALT+M') |
||
| 242 | self.minimalist_view_action.setCheckable(True) |
||
| 243 | self.minimalist_view_action.triggered.connect(self.minimalist_view) |
||
| 244 | |||
| 245 | self.view_media_info_action = QAction('Media Information', self) |
||
| 246 | self.view_media_info_action.setShortcut('CTRL+SHIFT+M') |
||
| 247 | self.view_media_info_action.triggered.connect(self.media_information_dialog) |
||
| 248 | |||
| 249 | self.view.addAction(self.dock_action) |
||
| 250 | self.view.addAction(self.library_dock_action) |
||
| 251 | self.view.addSeparator() |
||
| 252 | self.view.addAction(self.minimalist_view_action) |
||
| 253 | self.view.addSeparator() |
||
| 254 | self.view.addAction(self.view_media_info_action) |
||
| 255 | |||
| 256 | def help_menu(self): |
||
| 257 | """Add a help menu to the menu bar. |
||
| 258 | |||
| 259 | The help menu houses the about dialog that shows the user information |
||
| 260 | related to the application. |
||
| 261 | """ |
||
| 262 | self.about_action = QAction('About', self) |
||
| 263 | self.about_action.setShortcut('H') |
||
| 264 | self.about_action.triggered.connect(lambda: about.AboutDialog().exec_()) |
||
| 265 | |||
| 266 | self.help_.addAction(self.about_action) |
||
| 267 | |||
| 268 | def open_file(self): |
||
| 269 | """Open the selected file and add it to a new playlist.""" |
||
| 270 | View Code Duplication | filename, success = QFileDialog.getOpenFileName(self, 'Open File', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly) |
|
|
|
|||
| 271 | |||
| 272 | if success: |
||
| 273 | file_info = QFileInfo(filename).fileName() |
||
| 274 | playlist_item = QListWidgetItem(file_info) |
||
| 275 | self.playlist.clear() |
||
| 276 | self.playlist_view.clear() |
||
| 277 | self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(filename))) |
||
| 278 | self.player.setPlaylist(self.playlist) |
||
| 279 | playlist_item.setToolTip(file_info) |
||
| 280 | self.playlist_view.addItem(playlist_item) |
||
| 281 | self.playlist_view.setCurrentRow(0) |
||
| 282 | self.player.play() |
||
| 283 | |||
| 284 | def open_multiple_files(self): |
||
| 285 | """Open the selected files and add them to a new playlist.""" |
||
| 286 | filenames, success = QFileDialog.getOpenFileNames(self, 'Open Multiple Files', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly) |
||
| 287 | |||
| 288 | View Code Duplication | if success: |
|
| 289 | self.playlist.clear() |
||
| 290 | self.playlist_view.clear() |
||
| 291 | for file in natsort.natsorted(filenames, alg=natsort.ns.PATH): |
||
| 292 | file_info = QFileInfo(file).fileName() |
||
| 293 | playlist_item = QListWidgetItem(file_info) |
||
| 294 | self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
||
| 295 | self.player.setPlaylist(self.playlist) |
||
| 296 | playlist_item.setToolTip(file_info) |
||
| 297 | self.playlist_view.addItem(playlist_item) |
||
| 298 | self.playlist_view.setCurrentRow(0) |
||
| 299 | self.player.play() |
||
| 300 | |||
| 301 | def open_playlist(self): |
||
| 302 | """Load an M3U or PLS file into a new playlist.""" |
||
| 303 | playlist, success = QFileDialog.getOpenFileName(self, 'Open Playlist', '', 'Playlist (*.m3u *.pls)', '', QFileDialog.ReadOnly) |
||
| 304 | |||
| 305 | if success: |
||
| 306 | playlist = QUrl.fromLocalFile(playlist) |
||
| 307 | View Code Duplication | self.playlist.clear() |
|
| 308 | self.playlist_view.clear() |
||
| 309 | self.playlist.load(playlist) |
||
| 310 | self.player.setPlaylist(self.playlist) |
||
| 311 | |||
| 312 | for song_index in range(self.playlist.mediaCount()): |
||
| 313 | file_info = self.playlist.media(song_index).canonicalUrl().fileName() |
||
| 314 | playlist_item = QListWidgetItem(file_info) |
||
| 315 | playlist_item.setToolTip(file_info) |
||
| 316 | self.playlist_view.addItem(playlist_item) |
||
| 317 | |||
| 318 | self.playlist_view.setCurrentRow(0) |
||
| 319 | self.player.play() |
||
| 320 | |||
| 321 | def load_saved_playlist(self): |
||
| 322 | """Load the saved playlist if user setting permits.""" |
||
| 323 | saved_playlist = "{}/.m3u" .format(self.playlist_location) |
||
| 324 | if os.path.exists(saved_playlist): |
||
| 325 | playlist = QUrl().fromLocalFile(saved_playlist) |
||
| 326 | self.playlist.load(playlist) |
||
| 327 | self.player.setPlaylist(self.playlist) |
||
| 328 | |||
| 329 | for song_index in range(self.playlist.mediaCount()): |
||
| 330 | file_info = self.playlist.media(song_index).canonicalUrl().fileName() |
||
| 331 | playlist_item = QListWidgetItem(file_info) |
||
| 332 | playlist_item.setToolTip(file_info) |
||
| 333 | self.playlist_view.addItem(playlist_item) |
||
| 334 | |||
| 335 | self.playlist_view.setCurrentRow(0) |
||
| 336 | |||
| 337 | def open_directory(self): |
||
| 338 | """Open the selected directory and add the files within to an empty playlist.""" |
||
| 339 | directory = QFileDialog.getExistingDirectory(self, 'Open Directory', '', QFileDialog.ReadOnly) |
||
| 340 | |||
| 341 | if directory: |
||
| 342 | self.playlist.clear() |
||
| 343 | self.playlist_view.clear() |
||
| 344 | View Code Duplication | for dirpath, __, files in os.walk(directory): |
|
| 345 | for filename in natsort.natsorted(files, alg=natsort.ns.PATH): |
||
| 346 | file = os.path.join(dirpath, filename) |
||
| 347 | if filename.endswith(('mp3', 'flac')): |
||
| 348 | self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
||
| 349 | playlist_item = QListWidgetItem(filename) |
||
| 350 | playlist_item.setToolTip(filename) |
||
| 351 | self.playlist_view.addItem(playlist_item) |
||
| 352 | |||
| 353 | self.player.setPlaylist(self.playlist) |
||
| 354 | self.playlist_view.setCurrentRow(0) |
||
| 355 | self.player.play() |
||
| 356 | |||
| 357 | def open_media_library(self, index): |
||
| 358 | """Open a directory or file from the media library into an empty playlist.""" |
||
| 359 | self.playlist.clear() |
||
| 360 | self.playlist_view.clear() |
||
| 361 | |||
| 362 | if self.library_model.fileName(index).endswith(('mp3', 'flac')): |
||
| 363 | self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(self.library_model.filePath(index)))) |
||
| 364 | self.playlist_view.addItem(self.library_model.fileName(index)) |
||
| 365 | |||
| 366 | elif self.library_model.isDir(index): |
||
| 367 | directory = self.library_model.filePath(index) |
||
| 368 | for dirpath, __, files in os.walk(directory): |
||
| 369 | for filename in natsort.natsorted(files, alg=natsort.ns.PATH): |
||
| 370 | file = os.path.join(dirpath, filename) |
||
| 371 | if filename.endswith(('mp3', 'flac')): |
||
| 372 | self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
||
| 373 | playlist_item = QListWidgetItem(filename) |
||
| 374 | playlist_item.setToolTip(filename) |
||
| 375 | self.playlist_view.addItem(playlist_item) |
||
| 376 | |||
| 377 | self.player.setPlaylist(self.playlist) |
||
| 378 | self.player.play() |
||
| 379 | |||
| 380 | def display_meta_data(self): |
||
| 381 | """Display the current song's metadata in the main window. |
||
| 382 | |||
| 383 | If the current song contains metadata, its cover art is extracted and shown in |
||
| 384 | the main window while the track number, artist, album, and track title are shown |
||
| 385 | in the window title. |
||
| 386 | """ |
||
| 387 | if self.player.isMetaDataAvailable(): |
||
| 388 | file_path = self.player.currentMedia().canonicalUrl().toLocalFile() |
||
| 389 | (album, artist, title, track_number, *__, artwork) = metadata.metadata(file_path) |
||
| 390 | |||
| 391 | try: |
||
| 392 | self.pixmap.loadFromData(artwork) |
||
| 393 | except TypeError: |
||
| 394 | self.pixmap = QPixmap(artwork) |
||
| 395 | |||
| 396 | meta_data = '{} - {} - {} - {}' .format(track_number, artist, album, title) |
||
| 397 | |||
| 398 | self.setWindowTitle(meta_data) |
||
| 399 | self.art.setScaledContents(True) |
||
| 400 | self.art.setPixmap(self.pixmap) |
||
| 401 | self.layout.addWidget(self.art) |
||
| 402 | |||
| 403 | def initialize_playlist(self, start): |
||
| 404 | """Display playlist and reset playback mode when media inserted into playlist.""" |
||
| 405 | if start == 0: |
||
| 406 | if self.library_dock.isVisible(): |
||
| 407 | self.playlist_dock.setVisible(True) |
||
| 408 | self.playlist_dock.show() |
||
| 409 | self.playlist_dock.raise_() |
||
| 410 | |||
| 411 | if self.playlist.playbackMode() != QMediaPlaylist.Sequential: |
||
| 412 | self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) |
||
| 413 | repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png') |
||
| 414 | self.repeat_action.setIcon(QIcon(repeat_icon)) |
||
| 415 | |||
| 416 | def press_playback(self, event): |
||
| 417 | """Change the playback of the player on cover art mouse event. |
||
| 418 | |||
| 419 | When the cover art is clicked, the player will play the media if the player is |
||
| 420 | either paused or stopped. If the media is playing, the media is set |
||
| 421 | to pause. |
||
| 422 | """ |
||
| 423 | if event.button() == 1 and configuration.Playback().cover_art_playback.isChecked(): |
||
| 424 | if (self.player.state() == QMediaPlayer.StoppedState or |
||
| 425 | self.player.state() == QMediaPlayer.PausedState): |
||
| 426 | self.player.play() |
||
| 427 | elif self.player.state() == QMediaPlayer.PlayingState: |
||
| 428 | self.player.pause() |
||
| 429 | |||
| 430 | def seek(self, seconds): |
||
| 431 | """Set the position of the song to the position dragged to by the user.""" |
||
| 432 | self.player.setPosition(seconds * 1000) |
||
| 433 | |||
| 434 | def song_duration(self, duration): |
||
| 435 | """Set the slider to the duration of the currently played media.""" |
||
| 436 | duration /= 1000 |
||
| 437 | self.duration = duration |
||
| 438 | self.slider.setMaximum(duration) |
||
| 439 | |||
| 440 | def song_position(self, progress): |
||
| 441 | """Move the horizontal slider in sync with the duration of the song. |
||
| 442 | |||
| 443 | The progress is relayed to update_duration() in order |
||
| 444 | to display the time label next to the slider. |
||
| 445 | """ |
||
| 446 | progress /= 1000 |
||
| 447 | |||
| 448 | if not self.slider.isSliderDown(): |
||
| 449 | self.slider.setValue(progress) |
||
| 450 | |||
| 451 | self.update_duration(progress) |
||
| 452 | |||
| 453 | def update_duration(self, current_duration): |
||
| 454 | """Calculate the time played and the length of the song. |
||
| 455 | |||
| 456 | Both of these times are sent to duration_label() in order to display the |
||
| 457 | times on the toolbar. |
||
| 458 | """ |
||
| 459 | duration = self.duration |
||
| 460 | |||
| 461 | if current_duration or duration: |
||
| 462 | time_played = QTime((current_duration / 3600) % 60, (current_duration / 60) % 60, |
||
| 463 | (current_duration % 60), (current_duration * 1000) % 1000) |
||
| 464 | song_length = QTime((duration / 3600) % 60, (duration / 60) % 60, (duration % 60), |
||
| 465 | (duration * 1000) % 1000) |
||
| 466 | |||
| 467 | if duration > 3600: |
||
| 468 | time_format = "hh:mm:ss" |
||
| 469 | else: |
||
| 470 | time_format = "mm:ss" |
||
| 471 | |||
| 472 | time_display = "{} / {}" .format(time_played.toString(time_format), song_length.toString(time_format)) |
||
| 473 | |||
| 474 | else: |
||
| 475 | time_display = "" |
||
| 476 | |||
| 477 | self.duration_label.setText(time_display) |
||
| 478 | |||
| 479 | def set_state(self, state): |
||
| 480 | """Change the icon in the toolbar in relation to the state of the player. |
||
| 481 | |||
| 482 | The play icon changes to the pause icon when a song is playing and |
||
| 483 | the pause icon changes back to the play icon when either paused or |
||
| 484 | stopped. |
||
| 485 | """ |
||
| 486 | if self.player.state() == QMediaPlayer.PlayingState: |
||
| 487 | pause_icon = utilities.resource_filename('mosaic.images', 'md_pause.png') |
||
| 488 | self.play_action.setIcon(QIcon(pause_icon)) |
||
| 489 | self.play_action.triggered.connect(self.player.pause) |
||
| 490 | |||
| 491 | elif (self.player.state() == QMediaPlayer.PausedState or self.player.state() == QMediaPlayer.StoppedState): |
||
| 492 | self.play_action.triggered.connect(self.player.play) |
||
| 493 | play_icon = utilities.resource_filename('mosaic.images', 'md_play.png') |
||
| 494 | self.play_action.setIcon(QIcon(play_icon)) |
||
| 495 | |||
| 496 | def previous(self): |
||
| 497 | """Move to the previous song in the playlist. |
||
| 498 | |||
| 499 | Moves to the previous song in the playlist if the current song is less |
||
| 500 | than five seconds in. Otherwise, restarts the current song. |
||
| 501 | """ |
||
| 502 | if self.player.position() <= 5000: |
||
| 503 | self.playlist.previous() |
||
| 504 | else: |
||
| 505 | self.player.setPosition(0) |
||
| 506 | |||
| 507 | def repeat_song(self): |
||
| 508 | """Set the current media to repeat and change the repeat icon accordingly. |
||
| 509 | |||
| 510 | There are four playback modes: repeat none, repeat all, repeat once, and shuffle. |
||
| 511 | Clicking the repeat button cycles through each playback mode. |
||
| 512 | """ |
||
| 513 | if self.playlist.playbackMode() == QMediaPlaylist.Sequential: |
||
| 514 | self.playlist.setPlaybackMode(QMediaPlaylist.Loop) |
||
| 515 | repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_all.png') |
||
| 516 | self.repeat_action.setIcon(QIcon(repeat_on_icon)) |
||
| 517 | |||
| 518 | elif self.playlist.playbackMode() == QMediaPlaylist.Loop: |
||
| 519 | self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop) |
||
| 520 | repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_once.png') |
||
| 521 | self.repeat_action.setIcon(QIcon(repeat_on_icon)) |
||
| 522 | |||
| 523 | elif self.playlist.playbackMode() == QMediaPlaylist.CurrentItemInLoop: |
||
| 524 | self.playlist.setPlaybackMode(QMediaPlaylist.Random) |
||
| 525 | repeat_icon = utilities.resource_filename('mosaic.images', 'md_shuffle.png') |
||
| 526 | self.repeat_action.setIcon(QIcon(repeat_icon)) |
||
| 527 | |||
| 528 | elif self.playlist.playbackMode() == QMediaPlaylist.Random: |
||
| 529 | self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) |
||
| 530 | repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png') |
||
| 531 | self.repeat_action.setIcon(QIcon(repeat_icon)) |
||
| 532 | |||
| 533 | def activate_playlist_item(self, item): |
||
| 534 | """Set the active media to the playlist item dobule-clicked on by the user.""" |
||
| 535 | current_index = self.playlist_view.row(item) |
||
| 536 | if self.playlist.currentIndex() != current_index: |
||
| 537 | self.playlist.setCurrentIndex(current_index) |
||
| 538 | |||
| 539 | if self.player.state() != QMediaPlayer.PlayingState: |
||
| 540 | self.player.play() |
||
| 541 | |||
| 542 | def change_index(self, row): |
||
| 543 | """Highlight the row in the playlist of the active media.""" |
||
| 544 | self.playlist_view.setCurrentRow(row) |
||
| 545 | |||
| 546 | def minimalist_view(self): |
||
| 547 | """Resize the window to only show the menu bar and audio controls.""" |
||
| 548 | if self.minimalist_view_action.isChecked(): |
||
| 549 | |||
| 550 | if self.playlist_dock.isVisible(): |
||
| 551 | self.playlist_dock_state = True |
||
| 552 | if self.library_dock.isVisible(): |
||
| 553 | self.library_dock_state = True |
||
| 554 | |||
| 555 | self.library_dock.close() |
||
| 556 | self.playlist_dock.close() |
||
| 557 | |||
| 558 | QTimer.singleShot(10, lambda: self.resize(500, 0)) |
||
| 559 | |||
| 560 | else: |
||
| 561 | self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
||
| 562 | |||
| 563 | if self.library_dock_state: |
||
| 564 | self.library_dock.setVisible(True) |
||
| 565 | |||
| 566 | if self.playlist_dock_state: |
||
| 567 | self.playlist_dock.setVisible(True) |
||
| 568 | |||
| 569 | def dock_visiblity_change(self, visible): |
||
| 570 | """Change the size of the main window when the docks are toggled.""" |
||
| 571 | if visible and self.playlist_dock.isVisible() and not self.library_dock.isVisible(): |
||
| 572 | self.resize(defaults.Settings().window_size + self.playlist_dock.width() + 6, |
||
| 573 | self.height()) |
||
| 574 | |||
| 575 | elif visible and not self.playlist_dock.isVisible() and self.library_dock.isVisible(): |
||
| 576 | self.resize(defaults.Settings().window_size + self.library_dock.width() + 6, |
||
| 577 | self.height()) |
||
| 578 | |||
| 579 | elif visible and self.playlist_dock.isVisible() and self.library_dock.isVisible(): |
||
| 580 | self.resize(defaults.Settings().window_size + self.library_dock.width() + 6, |
||
| 581 | self.height()) |
||
| 582 | |||
| 583 | elif (not visible and not self.playlist_dock.isVisible() and not |
||
| 584 | self.library_dock.isVisible()): |
||
| 585 | self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
||
| 586 | |||
| 587 | def media_information_dialog(self): |
||
| 588 | """Show a dialog of the current song's metadata.""" |
||
| 589 | if self.player.isMetaDataAvailable(): |
||
| 590 | file_path = self.player.currentMedia().canonicalUrl().toLocalFile() |
||
| 591 | else: |
||
| 592 | file_path = None |
||
| 593 | dialog = information.InformationDialog(file_path) |
||
| 594 | dialog.exec_() |
||
| 595 | |||
| 596 | def change_window_size(self): |
||
| 597 | """Change the window size of the music player.""" |
||
| 598 | self.playlist_dock.close() |
||
| 599 | self.library_dock.close() |
||
| 600 | self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
||
| 601 | |||
| 602 | def change_media_library_path(self, path): |
||
| 603 | """Change the media library path to the new path selected in the preferences dialog.""" |
||
| 604 | self.library_model.setRootPath(path) |
||
| 605 | self.library_view.setModel(self.library_model) |
||
| 606 | self.library_view.setRootIndex(self.library_model.index(path)) |
||
| 607 | |||
| 608 | def closeEvent(self, event): |
||
| 609 | """Override the PyQt close event in order to handle save playlist on close.""" |
||
| 610 | playlist = "{}/.m3u" .format(self.playlist_location) |
||
| 611 | if defaults.Settings().save_playlist_on_close: |
||
| 612 | self.playlist.save(QUrl().fromLocalFile(playlist), "m3u") |
||
| 613 | else: |
||
| 614 | if os.path.exists(playlist): |
||
| 615 | os.remove(playlist) |
||
| 616 | QApplication.quit() |
||
| 617 | |||
| 632 |