1
|
|
|
import os |
2
|
|
|
import sys |
3
|
|
|
|
4
|
|
|
import natsort |
5
|
|
|
|
6
|
|
|
from PyQt5.QtCore import Qt, QFileInfo, QTime, QTimer, QUrl |
7
|
|
|
from PyQt5.QtGui import QIcon, QPixmap |
8
|
|
|
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer, QMediaPlaylist |
9
|
|
|
from PyQt5.QtWidgets import (QAction, QApplication, QDesktopWidget, QDockWidget, QFileDialog, |
10
|
|
|
QLabel, QListWidget, QListWidgetItem, QMainWindow, QSizePolicy, |
11
|
|
|
QSlider, QToolBar, QVBoxLayout, QWidget) |
12
|
|
|
|
13
|
|
|
from mosaic import about, configuration, defaults, information, library, metadata, utilities |
14
|
|
|
|
15
|
|
|
|
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.save_playlist_action = QAction('Save Playlist', self) |
176
|
|
|
self.save_playlist_action.setShortcut('CTRL+S') |
177
|
|
|
self.save_playlist_action.triggered.connect(self.save_playlist) |
178
|
|
|
|
179
|
|
|
self.exit_action = QAction('Quit', self) |
180
|
|
|
self.exit_action.setShortcut('CTRL+Q') |
181
|
|
|
self.exit_action.triggered.connect(self.closeEvent) |
182
|
|
|
|
183
|
|
|
self.file.addAction(self.open_action) |
184
|
|
|
self.file.addAction(self.open_multiple_files_action) |
185
|
|
|
self.file.addAction(self.open_playlist_action) |
186
|
|
|
self.file.addAction(self.open_directory_action) |
187
|
|
|
self.file.addSeparator() |
188
|
|
|
self.file.addAction(self.save_playlist_action) |
189
|
|
|
self.file.addSeparator() |
190
|
|
|
self.file.addAction(self.exit_action) |
191
|
|
|
|
192
|
|
|
def edit_menu(self): |
193
|
|
|
"""Add an edit menu to the menu bar. |
194
|
|
|
|
195
|
|
|
The edit menu houses the preferences item that opens a preferences dialog |
196
|
|
|
that allows the user to customize features of the music player. |
197
|
|
|
""" |
198
|
|
|
self.preferences_action = QAction('Preferences', self) |
199
|
|
|
self.preferences_action.setShortcut('CTRL+SHIFT+P') |
200
|
|
|
self.preferences_action.triggered.connect(lambda: self.preferences.exec_()) |
201
|
|
|
|
202
|
|
|
self.edit.addAction(self.preferences_action) |
203
|
|
|
|
204
|
|
|
def playback_menu(self): |
205
|
|
|
"""Add a playback menu to the menu bar. |
206
|
|
|
|
207
|
|
|
The playback menu houses |
208
|
|
|
""" |
209
|
|
|
self.play_playback_action = QAction('Play', self) |
210
|
|
|
self.play_playback_action.setShortcut('P') |
211
|
|
|
self.play_playback_action.triggered.connect(self.player.play) |
212
|
|
|
|
213
|
|
|
self.stop_playback_action = QAction('Stop', self) |
214
|
|
|
self.stop_playback_action.setShortcut('S') |
215
|
|
|
self.stop_playback_action.triggered.connect(self.player.stop) |
216
|
|
|
|
217
|
|
|
self.previous_playback_action = QAction('Previous', self) |
218
|
|
|
self.previous_playback_action.setShortcut('B') |
219
|
|
|
self.previous_playback_action.triggered.connect(self.previous) |
220
|
|
|
|
221
|
|
|
self.next_playback_action = QAction('Next', self) |
222
|
|
|
self.next_playback_action.setShortcut('N') |
223
|
|
|
self.next_playback_action.triggered.connect(self.playlist.next) |
224
|
|
|
|
225
|
|
|
self.playback.addAction(self.play_playback_action) |
226
|
|
|
self.playback.addAction(self.stop_playback_action) |
227
|
|
|
self.playback.addAction(self.previous_playback_action) |
228
|
|
|
self.playback.addAction(self.next_playback_action) |
229
|
|
|
|
230
|
|
|
def view_menu(self): |
231
|
|
|
"""Add a view menu to the menu bar. |
232
|
|
|
|
233
|
|
|
The view menu houses the Playlist, Media Library, Minimalist View, and Media |
234
|
|
|
Information menu items. The Playlist item toggles the playlist dock into and |
235
|
|
|
out of view. The Media Library items toggles the media library dock into and |
236
|
|
|
out of view. The Minimalist View item resizes the window and shows only the |
237
|
|
|
menu bar and player controls. The Media Information item opens a dialog that |
238
|
|
|
shows information relevant to the currently playing song. |
239
|
|
|
""" |
240
|
|
|
self.dock_action = self.playlist_dock.toggleViewAction() |
241
|
|
|
self.dock_action.setShortcut('CTRL+ALT+P') |
242
|
|
|
|
243
|
|
|
self.library_dock_action = self.library_dock.toggleViewAction() |
244
|
|
|
self.library_dock_action.setShortcut('CTRL+ALT+L') |
245
|
|
|
|
246
|
|
|
self.minimalist_view_action = QAction('Minimalist View', self) |
247
|
|
|
self.minimalist_view_action.setShortcut('CTRL+ALT+M') |
248
|
|
|
self.minimalist_view_action.setCheckable(True) |
249
|
|
|
self.minimalist_view_action.triggered.connect(self.minimalist_view) |
250
|
|
|
|
251
|
|
|
self.view_media_info_action = QAction('Media Information', self) |
252
|
|
|
self.view_media_info_action.setShortcut('CTRL+SHIFT+M') |
253
|
|
|
self.view_media_info_action.triggered.connect(self.media_information_dialog) |
254
|
|
|
|
255
|
|
|
self.view.addAction(self.dock_action) |
256
|
|
|
self.view.addAction(self.library_dock_action) |
257
|
|
|
self.view.addSeparator() |
258
|
|
|
self.view.addAction(self.minimalist_view_action) |
259
|
|
|
self.view.addSeparator() |
260
|
|
|
self.view.addAction(self.view_media_info_action) |
261
|
|
|
|
262
|
|
|
def help_menu(self): |
263
|
|
|
"""Add a help menu to the menu bar. |
264
|
|
|
|
265
|
|
|
The help menu houses the about dialog that shows the user information |
266
|
|
|
related to the application. |
267
|
|
|
""" |
268
|
|
|
self.about_action = QAction('About', self) |
269
|
|
|
self.about_action.setShortcut('H') |
270
|
|
|
self.about_action.triggered.connect(lambda: about.AboutDialog().exec_()) |
271
|
|
|
|
272
|
|
|
self.help_.addAction(self.about_action) |
273
|
|
|
|
274
|
|
|
def open_file(self): |
275
|
|
|
"""Open the selected file and add it to a new playlist.""" |
276
|
|
|
filename, success = QFileDialog.getOpenFileName(self, 'Open File', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly) |
277
|
|
|
|
278
|
|
View Code Duplication |
if success: |
|
|
|
|
279
|
|
|
file_info = QFileInfo(filename).fileName() |
280
|
|
|
playlist_item = QListWidgetItem(file_info) |
281
|
|
|
self.playlist.clear() |
282
|
|
|
self.playlist_view.clear() |
283
|
|
|
self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(filename))) |
284
|
|
|
self.player.setPlaylist(self.playlist) |
285
|
|
|
playlist_item.setToolTip(file_info) |
286
|
|
|
self.playlist_view.addItem(playlist_item) |
287
|
|
|
self.playlist_view.setCurrentRow(0) |
288
|
|
|
self.player.play() |
289
|
|
|
|
290
|
|
|
def open_multiple_files(self): |
291
|
|
|
"""Open the selected files and add them to a new playlist.""" |
292
|
|
|
filenames, success = QFileDialog.getOpenFileNames(self, 'Open Multiple Files', '', 'Audio (*.mp3 *.flac)', '', QFileDialog.ReadOnly) |
293
|
|
|
|
294
|
|
View Code Duplication |
if success: |
|
|
|
|
295
|
|
|
self.playlist.clear() |
296
|
|
|
self.playlist_view.clear() |
297
|
|
|
for file in natsort.natsorted(filenames, alg=natsort.ns.PATH): |
298
|
|
|
file_info = QFileInfo(file).fileName() |
299
|
|
|
playlist_item = QListWidgetItem(file_info) |
300
|
|
|
self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
301
|
|
|
self.player.setPlaylist(self.playlist) |
302
|
|
|
playlist_item.setToolTip(file_info) |
303
|
|
|
self.playlist_view.addItem(playlist_item) |
304
|
|
|
self.playlist_view.setCurrentRow(0) |
305
|
|
|
self.player.play() |
306
|
|
|
|
307
|
|
|
def open_playlist(self): |
308
|
|
|
"""Load an M3U or PLS file into a new playlist.""" |
309
|
|
|
playlist, success = QFileDialog.getOpenFileName(self, 'Open Playlist', '', 'Playlist (*.m3u *.pls)', '', QFileDialog.ReadOnly) |
310
|
|
|
|
311
|
|
View Code Duplication |
if success: |
|
|
|
|
312
|
|
|
playlist = QUrl.fromLocalFile(playlist) |
313
|
|
|
self.playlist.clear() |
314
|
|
|
self.playlist_view.clear() |
315
|
|
|
self.playlist.load(playlist) |
316
|
|
|
self.player.setPlaylist(self.playlist) |
317
|
|
|
|
318
|
|
|
for song_index in range(self.playlist.mediaCount()): |
319
|
|
|
file_info = self.playlist.media(song_index).canonicalUrl().fileName() |
320
|
|
|
playlist_item = QListWidgetItem(file_info) |
321
|
|
|
playlist_item.setToolTip(file_info) |
322
|
|
|
self.playlist_view.addItem(playlist_item) |
323
|
|
|
|
324
|
|
|
self.playlist_view.setCurrentRow(0) |
325
|
|
|
self.player.play() |
326
|
|
|
|
327
|
|
|
def save_playlist(self): |
328
|
|
|
"""Save the media in the playlist dock as a new M3U playlist.""" |
329
|
|
|
playlist, success = QFileDialog.getSaveFileName(self, 'Save Playlist', '', 'Playlist (*.m3u)', '') |
330
|
|
|
if success: |
331
|
|
|
saved_playlist = "{}.m3u" .format(playlist) |
332
|
|
|
self.playlist.save(QUrl().fromLocalFile(saved_playlist), "m3u") |
333
|
|
|
|
334
|
|
|
def load_saved_playlist(self): |
335
|
|
|
"""Load the saved playlist if user setting permits.""" |
336
|
|
|
saved_playlist = "{}/.m3u" .format(self.playlist_location) |
337
|
|
|
if os.path.exists(saved_playlist): |
338
|
|
|
playlist = QUrl().fromLocalFile(saved_playlist) |
339
|
|
|
self.playlist.load(playlist) |
340
|
|
|
self.player.setPlaylist(self.playlist) |
341
|
|
|
|
342
|
|
|
for song_index in range(self.playlist.mediaCount()): |
343
|
|
|
file_info = self.playlist.media(song_index).canonicalUrl().fileName() |
344
|
|
|
playlist_item = QListWidgetItem(file_info) |
345
|
|
|
playlist_item.setToolTip(file_info) |
346
|
|
|
self.playlist_view.addItem(playlist_item) |
347
|
|
|
|
348
|
|
|
self.playlist_view.setCurrentRow(0) |
349
|
|
|
|
350
|
|
|
def open_directory(self): |
351
|
|
|
"""Open the selected directory and add the files within to an empty playlist.""" |
352
|
|
|
directory = QFileDialog.getExistingDirectory(self, 'Open Directory', '', QFileDialog.ReadOnly) |
353
|
|
|
|
354
|
|
|
if directory: |
355
|
|
|
self.playlist.clear() |
356
|
|
|
self.playlist_view.clear() |
357
|
|
|
for dirpath, __, files in os.walk(directory): |
358
|
|
|
for filename in natsort.natsorted(files, alg=natsort.ns.PATH): |
359
|
|
|
file = os.path.join(dirpath, filename) |
360
|
|
|
if filename.endswith(('mp3', 'flac')): |
361
|
|
|
self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
362
|
|
|
playlist_item = QListWidgetItem(filename) |
363
|
|
|
playlist_item.setToolTip(filename) |
364
|
|
|
self.playlist_view.addItem(playlist_item) |
365
|
|
|
|
366
|
|
|
self.player.setPlaylist(self.playlist) |
367
|
|
|
self.playlist_view.setCurrentRow(0) |
368
|
|
|
self.player.play() |
369
|
|
|
|
370
|
|
|
def open_media_library(self, index): |
371
|
|
|
"""Open a directory or file from the media library into an empty playlist.""" |
372
|
|
|
self.playlist.clear() |
373
|
|
|
self.playlist_view.clear() |
374
|
|
|
|
375
|
|
|
if self.library_model.fileName(index).endswith(('mp3', 'flac')): |
376
|
|
|
self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(self.library_model.filePath(index)))) |
377
|
|
|
self.playlist_view.addItem(self.library_model.fileName(index)) |
378
|
|
|
|
379
|
|
|
elif self.library_model.isDir(index): |
380
|
|
|
directory = self.library_model.filePath(index) |
381
|
|
|
for dirpath, __, files in os.walk(directory): |
382
|
|
|
for filename in natsort.natsorted(files, alg=natsort.ns.PATH): |
383
|
|
|
file = os.path.join(dirpath, filename) |
384
|
|
|
if filename.endswith(('mp3', 'flac')): |
385
|
|
|
self.playlist.addMedia(QMediaContent(QUrl().fromLocalFile(file))) |
386
|
|
|
playlist_item = QListWidgetItem(filename) |
387
|
|
|
playlist_item.setToolTip(filename) |
388
|
|
|
self.playlist_view.addItem(playlist_item) |
389
|
|
|
|
390
|
|
|
self.player.setPlaylist(self.playlist) |
391
|
|
|
self.player.play() |
392
|
|
|
|
393
|
|
|
def display_meta_data(self): |
394
|
|
|
"""Display the current song's metadata in the main window. |
395
|
|
|
|
396
|
|
|
If the current song contains metadata, its cover art is extracted and shown in |
397
|
|
|
the main window while the track number, artist, album, and track title are shown |
398
|
|
|
in the window title. |
399
|
|
|
""" |
400
|
|
|
if self.player.isMetaDataAvailable(): |
401
|
|
|
file_path = self.player.currentMedia().canonicalUrl().toLocalFile() |
402
|
|
|
(album, artist, title, track_number, *__, artwork) = metadata.metadata(file_path) |
403
|
|
|
|
404
|
|
|
try: |
405
|
|
|
self.pixmap.loadFromData(artwork) |
406
|
|
|
except TypeError: |
407
|
|
|
self.pixmap = QPixmap(artwork) |
408
|
|
|
|
409
|
|
|
meta_data = '{} - {} - {} - {}' .format(track_number, artist, album, title) |
410
|
|
|
|
411
|
|
|
self.setWindowTitle(meta_data) |
412
|
|
|
self.art.setScaledContents(True) |
413
|
|
|
self.art.setPixmap(self.pixmap) |
414
|
|
|
self.layout.addWidget(self.art) |
415
|
|
|
|
416
|
|
|
def initialize_playlist(self, start): |
417
|
|
|
"""Display playlist and reset playback mode when media inserted into playlist.""" |
418
|
|
|
if start == 0: |
419
|
|
|
if self.library_dock.isVisible(): |
420
|
|
|
self.playlist_dock.setVisible(True) |
421
|
|
|
self.playlist_dock.show() |
422
|
|
|
self.playlist_dock.raise_() |
423
|
|
|
|
424
|
|
|
if self.playlist.playbackMode() != QMediaPlaylist.Sequential: |
425
|
|
|
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) |
426
|
|
|
repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png') |
427
|
|
|
self.repeat_action.setIcon(QIcon(repeat_icon)) |
428
|
|
|
|
429
|
|
|
def press_playback(self, event): |
430
|
|
|
"""Change the playback of the player on cover art mouse event. |
431
|
|
|
|
432
|
|
|
When the cover art is clicked, the player will play the media if the player is |
433
|
|
|
either paused or stopped. If the media is playing, the media is set |
434
|
|
|
to pause. |
435
|
|
|
""" |
436
|
|
|
if event.button() == 1 and configuration.Playback().cover_art_playback.isChecked(): |
437
|
|
|
if (self.player.state() == QMediaPlayer.StoppedState or |
438
|
|
|
self.player.state() == QMediaPlayer.PausedState): |
439
|
|
|
self.player.play() |
440
|
|
|
elif self.player.state() == QMediaPlayer.PlayingState: |
441
|
|
|
self.player.pause() |
442
|
|
|
|
443
|
|
|
def seek(self, seconds): |
444
|
|
|
"""Set the position of the song to the position dragged to by the user.""" |
445
|
|
|
self.player.setPosition(seconds * 1000) |
446
|
|
|
|
447
|
|
|
def song_duration(self, duration): |
448
|
|
|
"""Set the slider to the duration of the currently played media.""" |
449
|
|
|
duration /= 1000 |
450
|
|
|
self.duration = duration |
451
|
|
|
self.slider.setMaximum(duration) |
452
|
|
|
|
453
|
|
|
def song_position(self, progress): |
454
|
|
|
"""Move the horizontal slider in sync with the duration of the song. |
455
|
|
|
|
456
|
|
|
The progress is relayed to update_duration() in order |
457
|
|
|
to display the time label next to the slider. |
458
|
|
|
""" |
459
|
|
|
progress /= 1000 |
460
|
|
|
|
461
|
|
|
if not self.slider.isSliderDown(): |
462
|
|
|
self.slider.setValue(progress) |
463
|
|
|
|
464
|
|
|
self.update_duration(progress) |
465
|
|
|
|
466
|
|
|
def update_duration(self, current_duration): |
467
|
|
|
"""Calculate the time played and the length of the song. |
468
|
|
|
|
469
|
|
|
Both of these times are sent to duration_label() in order to display the |
470
|
|
|
times on the toolbar. |
471
|
|
|
""" |
472
|
|
|
duration = self.duration |
473
|
|
|
|
474
|
|
|
if current_duration or duration: |
475
|
|
|
time_played = QTime((current_duration / 3600) % 60, (current_duration / 60) % 60, |
476
|
|
|
(current_duration % 60), (current_duration * 1000) % 1000) |
477
|
|
|
song_length = QTime((duration / 3600) % 60, (duration / 60) % 60, (duration % 60), |
478
|
|
|
(duration * 1000) % 1000) |
479
|
|
|
|
480
|
|
|
if duration > 3600: |
481
|
|
|
time_format = "hh:mm:ss" |
482
|
|
|
else: |
483
|
|
|
time_format = "mm:ss" |
484
|
|
|
|
485
|
|
|
time_display = "{} / {}" .format(time_played.toString(time_format), song_length.toString(time_format)) |
486
|
|
|
|
487
|
|
|
else: |
488
|
|
|
time_display = "" |
489
|
|
|
|
490
|
|
|
self.duration_label.setText(time_display) |
491
|
|
|
|
492
|
|
|
def set_state(self, state): |
493
|
|
|
"""Change the icon in the toolbar in relation to the state of the player. |
494
|
|
|
|
495
|
|
|
The play icon changes to the pause icon when a song is playing and |
496
|
|
|
the pause icon changes back to the play icon when either paused or |
497
|
|
|
stopped. |
498
|
|
|
""" |
499
|
|
|
if self.player.state() == QMediaPlayer.PlayingState: |
500
|
|
|
pause_icon = utilities.resource_filename('mosaic.images', 'md_pause.png') |
501
|
|
|
self.play_action.setIcon(QIcon(pause_icon)) |
502
|
|
|
self.play_action.triggered.connect(self.player.pause) |
503
|
|
|
|
504
|
|
|
elif (self.player.state() == QMediaPlayer.PausedState or self.player.state() == QMediaPlayer.StoppedState): |
505
|
|
|
self.play_action.triggered.connect(self.player.play) |
506
|
|
|
play_icon = utilities.resource_filename('mosaic.images', 'md_play.png') |
507
|
|
|
self.play_action.setIcon(QIcon(play_icon)) |
508
|
|
|
|
509
|
|
|
def previous(self): |
510
|
|
|
"""Move to the previous song in the playlist. |
511
|
|
|
|
512
|
|
|
Moves to the previous song in the playlist if the current song is less |
513
|
|
|
than five seconds in. Otherwise, restarts the current song. |
514
|
|
|
""" |
515
|
|
|
if self.player.position() <= 5000: |
516
|
|
|
self.playlist.previous() |
517
|
|
|
else: |
518
|
|
|
self.player.setPosition(0) |
519
|
|
|
|
520
|
|
|
def repeat_song(self): |
521
|
|
|
"""Set the current media to repeat and change the repeat icon accordingly. |
522
|
|
|
|
523
|
|
|
There are four playback modes: repeat none, repeat all, repeat once, and shuffle. |
524
|
|
|
Clicking the repeat button cycles through each playback mode. |
525
|
|
|
""" |
526
|
|
|
if self.playlist.playbackMode() == QMediaPlaylist.Sequential: |
527
|
|
|
self.playlist.setPlaybackMode(QMediaPlaylist.Loop) |
528
|
|
|
repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_all.png') |
529
|
|
|
self.repeat_action.setIcon(QIcon(repeat_on_icon)) |
530
|
|
|
|
531
|
|
|
elif self.playlist.playbackMode() == QMediaPlaylist.Loop: |
532
|
|
|
self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop) |
533
|
|
|
repeat_on_icon = utilities.resource_filename('mosaic.images', 'md_repeat_once.png') |
534
|
|
|
self.repeat_action.setIcon(QIcon(repeat_on_icon)) |
535
|
|
|
|
536
|
|
|
elif self.playlist.playbackMode() == QMediaPlaylist.CurrentItemInLoop: |
537
|
|
|
self.playlist.setPlaybackMode(QMediaPlaylist.Random) |
538
|
|
|
repeat_icon = utilities.resource_filename('mosaic.images', 'md_shuffle.png') |
539
|
|
|
self.repeat_action.setIcon(QIcon(repeat_icon)) |
540
|
|
|
|
541
|
|
|
elif self.playlist.playbackMode() == QMediaPlaylist.Random: |
542
|
|
|
self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) |
543
|
|
|
repeat_icon = utilities.resource_filename('mosaic.images', 'md_repeat_none.png') |
544
|
|
|
self.repeat_action.setIcon(QIcon(repeat_icon)) |
545
|
|
|
|
546
|
|
|
def activate_playlist_item(self, item): |
547
|
|
|
"""Set the active media to the playlist item dobule-clicked on by the user.""" |
548
|
|
|
current_index = self.playlist_view.row(item) |
549
|
|
|
if self.playlist.currentIndex() != current_index: |
550
|
|
|
self.playlist.setCurrentIndex(current_index) |
551
|
|
|
|
552
|
|
|
if self.player.state() != QMediaPlayer.PlayingState: |
553
|
|
|
self.player.play() |
554
|
|
|
|
555
|
|
|
def change_index(self, row): |
556
|
|
|
"""Highlight the row in the playlist of the active media.""" |
557
|
|
|
self.playlist_view.setCurrentRow(row) |
558
|
|
|
|
559
|
|
|
def minimalist_view(self): |
560
|
|
|
"""Resize the window to only show the menu bar and audio controls.""" |
561
|
|
|
if self.minimalist_view_action.isChecked(): |
562
|
|
|
|
563
|
|
|
if self.playlist_dock.isVisible(): |
564
|
|
|
self.playlist_dock_state = True |
565
|
|
|
if self.library_dock.isVisible(): |
566
|
|
|
self.library_dock_state = True |
567
|
|
|
|
568
|
|
|
self.library_dock.close() |
569
|
|
|
self.playlist_dock.close() |
570
|
|
|
|
571
|
|
|
QTimer.singleShot(10, lambda: self.resize(500, 0)) |
572
|
|
|
|
573
|
|
|
else: |
574
|
|
|
self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
575
|
|
|
|
576
|
|
|
if self.library_dock_state: |
577
|
|
|
self.library_dock.setVisible(True) |
578
|
|
|
|
579
|
|
|
if self.playlist_dock_state: |
580
|
|
|
self.playlist_dock.setVisible(True) |
581
|
|
|
|
582
|
|
|
def dock_visiblity_change(self, visible): |
583
|
|
|
"""Change the size of the main window when the docks are toggled.""" |
584
|
|
|
if visible and self.playlist_dock.isVisible() and not self.library_dock.isVisible(): |
585
|
|
|
self.resize(defaults.Settings().window_size + self.playlist_dock.width() + 6, |
586
|
|
|
self.height()) |
587
|
|
|
|
588
|
|
|
elif visible and not self.playlist_dock.isVisible() and self.library_dock.isVisible(): |
589
|
|
|
self.resize(defaults.Settings().window_size + self.library_dock.width() + 6, |
590
|
|
|
self.height()) |
591
|
|
|
|
592
|
|
|
elif visible and self.playlist_dock.isVisible() and self.library_dock.isVisible(): |
593
|
|
|
self.resize(defaults.Settings().window_size + self.library_dock.width() + 6, |
594
|
|
|
self.height()) |
595
|
|
|
|
596
|
|
|
elif (not visible and not self.playlist_dock.isVisible() and not |
597
|
|
|
self.library_dock.isVisible()): |
598
|
|
|
self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
599
|
|
|
|
600
|
|
|
def media_information_dialog(self): |
601
|
|
|
"""Show a dialog of the current song's metadata.""" |
602
|
|
|
if self.player.isMetaDataAvailable(): |
603
|
|
|
file_path = self.player.currentMedia().canonicalUrl().toLocalFile() |
604
|
|
|
else: |
605
|
|
|
file_path = None |
606
|
|
|
dialog = information.InformationDialog(file_path) |
607
|
|
|
dialog.exec_() |
608
|
|
|
|
609
|
|
|
def change_window_size(self): |
610
|
|
|
"""Change the window size of the music player.""" |
611
|
|
|
self.playlist_dock.close() |
612
|
|
|
self.library_dock.close() |
613
|
|
|
self.resize(defaults.Settings().window_size, defaults.Settings().window_size + 63) |
614
|
|
|
|
615
|
|
|
def change_media_library_path(self, path): |
616
|
|
|
"""Change the media library path to the new path selected in the preferences dialog.""" |
617
|
|
|
self.library_model.setRootPath(path) |
618
|
|
|
self.library_view.setModel(self.library_model) |
619
|
|
|
self.library_view.setRootIndex(self.library_model.index(path)) |
620
|
|
|
|
621
|
|
|
def closeEvent(self, event): |
622
|
|
|
"""Override the PyQt close event in order to handle save playlist on close.""" |
623
|
|
|
playlist = "{}/.m3u" .format(self.playlist_location) |
624
|
|
|
if defaults.Settings().save_playlist_on_close: |
625
|
|
|
self.playlist.save(QUrl().fromLocalFile(playlist), "m3u") |
626
|
|
|
else: |
627
|
|
|
if os.path.exists(playlist): |
628
|
|
|
os.remove(playlist) |
629
|
|
|
QApplication.quit() |
630
|
|
|
|
631
|
|
|
|
632
|
|
|
def main(): |
633
|
|
|
"""Create an instance of the music player and use QApplication to show the GUI. |
634
|
|
|
|
635
|
|
|
QDesktopWidget() is used to move the application to the center of the user's screen. |
636
|
|
|
""" |
637
|
|
|
application = QApplication(sys.argv) |
638
|
|
|
window = MusicPlayer() |
639
|
|
|
desktop = QDesktopWidget().availableGeometry() |
640
|
|
|
width = (desktop.width() - window.width()) / 2 |
641
|
|
|
height = (desktop.height() - window.height()) / 2 |
642
|
|
|
window.show() |
643
|
|
|
window.move(width, height) |
644
|
|
|
sys.exit(application.exec_()) |
645
|
|
|
|