class Control: """A class that handles the logic behind the program by manipulating the GUI classes and calling their methods in response to received signals.""" MAGIC = b"\x01\xff" def __init__(self, screens: list) -> None: self.player = QMediaPlayer() self.player.setAudioRole(QAudio.MusicRole) self.playlist = QMediaPlaylist() self.player.setPlaylist(self.playlist) self.mainWindow = MainWindow(self, screens) self.mainArea = self.mainWindow.centralWidget().upperBox.mainArea self.songList = self.mainWindow.centralWidget().upperBox.songList self.mediaControlArea = self.mainWindow.centralWidget( ).mediaControlArea self.mainWindow.show() self.library = None self.currentSong = None self.playing = False self.random = False self.repeat = 0 self.volume = 50 self.volumeChange(self.volume) self.mediaControlArea.volumeControl.setValue(self.volume) self.mainTimer = QTimer() self.mainTimer.setInterval(100) self.mainTimer.timeout.connect(self.updateSongProgress) self.mainTimer.start() self.libraryUpdateTimer = QTimer() self.libraryUpdateTimer.setInterval(15_000) self.libraryUpdateTimer.timeout.connect(self.updateLibrary) self.libraryUpdateTimer.start() self._connection = None self.connections() self.displayedType = None self.displayedName = None self.types = None self.songListWidth = None values = self.load() if values: self.mainWindow.setGeometry(*values) self.volumeChange(self.volume) self.mediaControlArea.volumeControl.setValue(self.volume) self.setUpTimer = QTimer() self.setUpTimer.setInterval(20) self.setUpTimer.setSingleShot(True) self.setUpTimer.timeout.connect(self.setAreas) self.setUpTimer.start() def connections(self): self.player.currentMediaChanged.connect(self.updateCurrentSong) self.player.durationChanged.connect(self.updateSongProgressRange) self.player.stateChanged.connect(self.playerStatusChanged) self.mediaControlArea.previousButton.click.connect( self.playlist.previous) self.mediaControlArea.repeatButton.click.connect( self.repeatButtonClick) self.mediaControlArea.stopButton.click.connect(self.stopButtonClick) self.mediaControlArea.playButton.click.connect(self.playButtonClick) self.mediaControlArea.randomButton.click.connect( self.randomButtonClick) self.mediaControlArea.nextButton.click.connect(self.playlist.next) self.mediaControlArea.muteButton.click.connect(self.mute) self.mediaControlArea.songProgress.sliderMoved.connect( self.songProgressMove) self.mediaControlArea.volumeControl.sliderMoved.connect( self.volumeChange) def setAreas(self) -> None: """Called after the GUI is created to provide user with a feedback that the program is running in case a larger amount of songs will be added when the Library class is initialized.""" # TODO add a tooltip that will notify the user larger amount of songs is being loaded # (freezes the program as the execution moves to the Library class.) self.library = library.Library() self.types = { "artist": self.library.getSongsForArtist, "album": self.library.getSongsForAlbum, "playlist": self.library.getSongsForPlaylist } self.mainArea.setAreas(self.library) self.setUpTimer.deleteLater() self.setUpTimer = None self.getSongs(self.displayedType, self.displayedName) if self.songListWidth is not None: songListGeometry = self.songList.geometry() self.songList.preferredWidth = songListGeometry.width( ) - self.songListWidth self.mainWindow.centralWidget().upperBox.line.resizeWidgets( songListGeometry.width() - self.songListWidth) def updateLibrary(self) -> None: self.library.update() self.mainArea.updateView(self.library) def updateCurrentSong(self) -> None: """Update all areas that may display information about the currently playing song - SongList, Now Playing tab, BottomBox""" media = self.player.currentMedia() self.currentSong = media.request().url().toLocalFile().replace( "/", "\\") if self.currentSong in self.library.library: self.songList.updateActiveSong(self.currentSong) self.mainArea.updateActiveSong(self.playlist.currentIndex()) songEntry = self.library.library[self.currentSong] self.mediaControlArea.updateSongInfo( f"{songEntry[ARTIST]} - {songEntry[NAME]}") def updateSongProgressRange(self) -> None: """Updates the range of the slider that represents the song position.""" self.mediaControlArea.updateSongProgressRange(self.player.duration()) def playerStatusChanged(self) -> None: """Used to properly update the player look after the current playlist has finished.""" index = self.playlist.currentIndex() if index == -1: self.stopButtonClick() def getSongs(self, isType: str, name: str) -> None: """Retrieves the songs for a given artist, album or playlist based on type and passes the resulting list to the SongList class.""" if isType is None: isType = self.displayedType if name is None: name = self.displayedName orderBy = self.songList.buttonOrderBy.text() reverse = True if self.songList.buttonOrderReverse.text() == chr( 0x25bc) else False listForType = self.types[isType](name, orderBy, reverse) playlist = None if isType == "playlist": playlist = name if len(listForType) == 0: artists = self.library.artists if len(artists): listForType = self.library.getSongsForArtist(artists[0]) self.songList.updateSongList(listForType, self.library.library, self.currentSong, playlist, isType) self.displayedType = isType self.displayedName = name def playSongList(self, song: str = None) -> None: """Called when user double-clicks on an artist/album/playlist widget or a song in right-hand side panel.""" self.playlist.clear() index = 0 loopIndex = 0 for songPath in self.songList.garbageProtector: if song == songPath: index = loopIndex self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(songPath))) loopIndex += 1 if self.playlist.isEmpty(): return self.player.play() if index > 0: self.playlist.setCurrentIndex(index) self.playing = True self.mediaControlArea.playButton.updatePictures( bottom.pausePixmap, bottom.pauseHoverPixmap, False) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playSongWidget(self, songPath: str, afterCurrent: bool = False) -> None: if afterCurrent: index = self.playlist.currentIndex() + 1 self.playlist.insertMedia( index, QMediaContent(QUrl.fromLocalFile(songPath))) else: self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(songPath))) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) self.playing = True def removeFromNowPlaying(self, widget) -> None: if self.playlist.mediaCount() > 1: for row in range(self.mainArea.nowPlayingLayout.rowCount()): for column in range( self.mainArea.nowPlayingLayout.columnCount()): if self.mainArea.nowPlayingLayout.itemAtPosition( row, column).widget() is widget: self.playlist.removeMedia(row - 1) break else: continue break else: self.stopButtonClick() self.playlist.clear() self.mainArea.setNowPlayingArea(self.library) if self.playing: self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playMediaWidget(self, isType: str, target: str, startOver: bool, afterCurrent: bool) -> None: """Called from MediaWidget - plays all songs for MediaWidget's type and name.""" if startOver: self.playlist.clear() if afterCurrent: index = self.playlist.currentIndex() + 1 for songPath in self.types[isType](target): self.playlist.insertMedia( index, QMediaContent(QUrl.fromLocalFile(songPath))) index += 1 else: for songPath in self.types[isType](target): self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(songPath))) if startOver: self.player.play() self.playing = True self.mediaControlArea.playButton.updatePictures( bottom.pausePixmap, bottom.pauseHoverPixmap, False) self.mainArea.setNowPlayingArea(self.library) self.mainArea.updateActiveSong(self.playlist.currentIndex()) def playFromNowPlaying(self, song: str) -> None: """Called when user double-clicks on a song in the Now Playing tab.""" for n in range(self.playlist.mediaCount()): media = self.playlist.media(n) if song == media.request().url().toLocalFile().replace("/", "\\"): self.playlist.setCurrentIndex(n) if not self.playing: self.player.play() self.playing = True return def createPlaylist(self, playlistName: str) -> None: self.library.createPlaylist(playlistName) self.mainArea.setMainAreaPlaylists(self.library) def addToExistingPlaylist(self, playlist: str, songOrWidget: str, isType: str) -> None: if isType in self.types: for song in self.types[isType](songOrWidget): self.library.addToPlaylist(playlist, song) else: self.library.addToPlaylist(playlist, songOrWidget) self.library.update() def removeFromPlaylist(self, playlist: str, song: str) -> None: self.library.deleteFromPlaylist(playlist, song) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() self.getSongs("playlist", playlist) def renamePlaylist(self, playlistName: str, newPlaylistName: str) -> None: self.library.renamePlaylist(playlistName, newPlaylistName) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() def deletePlaylist(self, playlistName: str) -> None: self.library.deletePlaylist(playlistName) self.mainArea.setMainAreaPlaylists(self.library) self.library.update() def addWatchedFolder(self, folder: str) -> None: """Adds a folder to the Library class. all mp3 files within the folder and its sub-folders will be added to the library and accessible to the player.""" self.library.addFolder(folder.replace("/", "\\")) self.mainArea.updateView(self.library) def removeWatchedFolder(self, folder: str) -> None: """Removes folder from the library, updates view and stops playback if the current song was in the now-removed folder.""" self.library.deleteFolder(folder) self.mainArea.updateView(self.library) if self.currentSong not in self.library.library: self.songList.updateSongList([], [], "", "") self.player.stop() self.playlist.clear() self.mediaControlArea.updateSongInfo("") self.songList.nowPlayingSong = None self.mainArea.nowPlayingSong = None self.playing = False self.mediaControlArea.updatePlayButton(self.playing, False) def playButtonClick(self, passMove: bool = True) -> None: if not self.playing: if self.playlist.isEmpty(): self.playSongList() return self.playing = True self.player.play() self.mainArea.updateActiveSong(self.playlist.currentIndex()) self.songList.updateActiveSong(self.currentSong) else: self.playing = False self.player.pause() self.mediaControlArea.updatePlayButton(self.playing, passMove) def repeatButtonClick(self) -> None: if self.repeat == 0: self.repeat = 1 self.playlist.setPlaybackMode(QMediaPlaylist.Loop) elif self.repeat == 1: self.repeat = 2 self.playlist.setPlaybackMode(QMediaPlaylist.CurrentItemInLoop) elif self.repeat == 2: self.repeat = 0 self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) self.mediaControlArea.updateRepeatButton(self.repeat) def randomButtonClick(self) -> None: if not self.random: self.random = True self.playlist.setPlaybackMode(QMediaPlaylist.Random) else: self.random = False self.playlist.setPlaybackMode(QMediaPlaylist.Sequential) self.mediaControlArea.updateRandomButton(self.random) def stopButtonClick(self) -> None: self.playing = False self.player.stop() if self.songList.nowPlayingSong is not None: self.songList.nowPlayingSong.clear() if self.mainArea.nowPlayingSong is not None: self.mainArea.nowPlayingSong.clear() self.mediaControlArea.updatePlayButton(self.playing, False) def mute(self) -> None: if not self.player.isMuted(): self.player.setMuted(True) self.mediaControlArea.showMute() else: self.player.setMuted(False) self.volumeChange(self.volume) def volumeChange(self, volume: int) -> None: logVolume = QAudio.convertVolume(volume / 100, QAudio.LogarithmicVolumeScale, QAudio.LinearVolumeScale) * 100 self.player.setVolume(logVolume) self.volume = volume self.mediaControlArea.updateVolumeBar(volume) def songProgressMove(self, position: int) -> None: self.player.setPosition(position) def updateSongProgress(self) -> None: position = self.player.position() if 0 <= position < 2_000_000_000: if self.player.state() > 0: self.mediaControlArea.updateSongProgress(position) if self.playing: self.songList.activeSongPixmap() self.mainArea.activeSongPixmap() else: self.mediaControlArea.updateSongProgress(0) def disconnect(self): self.player.currentMediaChanged.disconnect() self.player.durationChanged.disconnect() self.player.stateChanged.disconnect() self.mediaControlArea.previousButton.click.disconnect() self.mediaControlArea.repeatButton.click.disconnect() self.mediaControlArea.stopButton.click.disconnect() self.mediaControlArea.playButton.click.disconnect() self.mediaControlArea.randomButton.click.disconnect() self.mediaControlArea.nextButton.click.disconnect() self.mediaControlArea.muteButton.click.disconnect() self.mediaControlArea.songProgress.sliderMoved.disconnect() self.mediaControlArea.volumeControl.sliderMoved.disconnect() def close(self) -> None: self.disconnect() self.player.stop() self.mainTimer.stop() self.save() def save(self) -> None: """Called on exit, saves current view, geometry and volume.""" with gzip.open(r"musicplayer\mpdata", "wb") as fh: fh.write(self.MAGIC) toBeWritten = struct.pack(f"<h{len(self.displayedType.encode())}s", len(self.displayedType.encode()), self.displayedType.encode()) fh.write(toBeWritten) fh.write(self.MAGIC) toBeWritten = struct.pack(f"<h{len(self.displayedName.encode())}s", len(self.displayedName.encode()), self.displayedName.encode()) fh.write(toBeWritten) fh.write(self.MAGIC) geo = self.mainWindow.geometry() toBeWritten = struct.pack("<4h", geo.x(), geo.y(), geo.width(), geo.height()) fh.write(toBeWritten) toBeWritten = struct.pack("<h", self.volume) fh.write(toBeWritten) toBeWritten = struct.pack("<h", self.songList.width()) fh.write(toBeWritten) def load(self) -> [bool, tuple]: """Called on startup, loads view, geometry and volume saved on previous run.""" try: with gzip.open(r"musicplayer\mpdata", "rb") as fh: if not fh.read(2) == self.MAGIC: return False length = fh.read(2) length = struct.unpack("<h", length)[0] displayedType = fh.read(length) displayedType = struct.unpack(f"<{length}s", displayedType)[0].decode("utf8") if displayedType in ["artist", "album", "playlist"]: self.displayedType = displayedType if not fh.read(2) == self.MAGIC: return False length = fh.read(2) length = struct.unpack("<h", length)[0] displayedName = fh.read(length) displayedName = struct.unpack(f"<{length}s", displayedName)[0].decode("utf8") if not fh.read(2) == self.MAGIC: return False self.displayedName = displayedName variables = [] for n in range(6): var = fh.read(2) var = struct.unpack("<h", var)[0] variables.append(var) x, y, width, height, volume, songListWidth = variables self.volume = volume self.songListWidth = songListWidth return x, y, width, height except Exception: return False
class MusicPlayer(QWidget): def __init__(self, parent=None): QWidget.__init__(self, parent) self.options = self.parent().options self.ffmpeg = self.options['paths']['ffmpeg_bin'] self.thumbnails = self.options['paths']['thumbnails'] self.thumb_width = self.options['thumbnail']['width'] self.thumb_height = self.options['thumbnail']['height'] self.jumping = False self.blocked = False self.durations = {} self.playback_value = 0 self.text = ["None", "Repeat", "Random"] self.playlist_list = [] self.values = [ QMediaPlaylist.Loop, QMediaPlaylist.CurrentItemInLoop, QMediaPlaylist.Random ] # Thumbnail widget self.image_label = QLabel() # Control widgets self.playButton = QToolButton(clicked=self.play) self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.stopButton = QToolButton(clicked=self.stop) self.stopButton.setIcon(self.style().standardIcon(QStyle.SP_MediaStop)) self.stopButton.setEnabled(False) self.playbackButton = QToolButton(clicked=self.playback_mode) self.playbackButton.setText(self.text[0]) self.nextButton = QToolButton(clicked=self.next_song) self.nextButton.setIcon(self.style().standardIcon( QStyle.SP_MediaSkipForward)) self.previousButton = QToolButton(clicked=self.previous_song) self.previousButton.setIcon(self.style().standardIcon( QStyle.SP_MediaSkipBackward)) self.muteButton = QToolButton(clicked=self.mute_clicked) self.muteButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume)) self.volumeSlider = QSlider(Qt.Horizontal, sliderMoved=self.change_volume) self.volumeSlider.setRange(0, 100) self.volumeSlider.setPageStep(1) self.volumeSlider.setValue(50) # Player and playlist setup self.player = QMediaPlayer() self.player.setVolume(50) self.player.stateChanged.connect(self.waveform) self.playlist = QMediaPlaylist() self.playlist.setPlaybackMode(self.values[0]) self.playlist.setCurrentIndex(1) self.player.setPlaylist(self.playlist) self.playlistModel = PlaylistModel() self.playlistModel.setPlaylist(self.playlist) self.playlistView = QListView() self.playlistView.setModel(self.playlistModel) self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) self.playlistView.activated.connect(self.jump) self.playlistView.setContextMenuPolicy(Qt.CustomContextMenu) self.playlistView.customContextMenuRequested.connect( self.list_view_menu) self.playlist.currentIndexChanged.connect( lambda position: self.change_thumbnail(position)) song_search = QLineEdit() song_search.textChanged.connect(self.search) song_search.setClearButtonEnabled(True) # Playlist self.playlist_name = QComboBox() self.playlist_name.currentTextChanged.connect(self.switch_playlist) self.refresh_lists() self.up_button = QToolButton(clicked=self.move_up) self.up_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowUp)) self.down_button = QToolButton(clicked=self.move_down) self.down_button.setIcon(self.style().standardIcon( QStyle.SP_ArrowDown)) # Sound wave widget self.wave_graphic = WaveGraphic(self) #self.wave_graphic.hide() # Testing slider again self.slider = QSlider(Qt.Horizontal) self.slider.setRange(0, self.player.duration() / 1000) self.slider.sliderMoved.connect(self.seek) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) # Shortcuts QShortcut(QKeySequence(Qt.CTRL + Qt.Key_F), self, song_search.setFocus) # Layouts setup playlist_layout = QHBoxLayout() playlist_layout.addWidget(self.playlist_name) playlist_layout.addWidget(self.up_button) playlist_layout.addWidget(self.down_button) control_layout = QHBoxLayout() control_layout.setContentsMargins(0, 0, 0, 0) control_layout.addWidget(self.stopButton) control_layout.addWidget(self.previousButton) control_layout.addWidget(self.playButton) control_layout.addWidget(self.nextButton) control_layout.addWidget(self.muteButton) control_layout.addWidget(self.volumeSlider) control_layout.addWidget(self.playbackButton) display_layout = QVBoxLayout() display_layout.addWidget(song_search) display_layout.addWidget(self.playlistView) display_layout.addLayout(playlist_layout) music_layout = QVBoxLayout() music_layout.addWidget(self.image_label) music_layout.addWidget(self.slider) music_layout.addLayout(control_layout) main_layout = QHBoxLayout() main_layout.addLayout(music_layout) main_layout.addLayout(display_layout) main_2_layout = QVBoxLayout() main_2_layout.addLayout(main_layout) main_2_layout.addWidget(self.wave_graphic) self.setLayout(main_2_layout) def waveform(self, status): if status == QMediaPlayer.PlayingState: self.wave_graphic.start() elif status == QMediaPlayer.PausedState: self.wave_graphic.pause() else: self.wave_graphic.stop() def list_view_menu(self, point): menu = QMenu("Menu", self) recommend_action = QAction('&Recommend Songs', self) menu.addAction(recommend_action) # TODO: [FEATURE] add rename song recommend_action.triggered.connect( lambda: self.parent().call_download_manager(self.get_links())) rename_action = QAction('&Rename', self) menu.addAction(rename_action) # TODO: [FEATURE] add rename song rename_action.triggered.connect(lambda: print("rename")) # Show the context menu. menu.exec_(self.playlistView.mapToGlobal(point)) def get_links(self): title = self.playlistView.selectedIndexes()[0].data() link = getYoutubeURLFromSearch(title) if len(link) > 1: return youtube_recommendations(link) def move_up(self): index = self.playlistView.currentIndex().row() if index - 1 >= 0: item, above = self.playlist.media(index), self.playlist.media( index - 1) self.playlist.removeMedia(index) self.playlist.removeMedia(index - 1) self.playlist.insertMedia(index - 1, item) self.playlist.insertMedia(index, above) self.blocked = True self.playlistView.setCurrentIndex( self.playlistModel.index(index - 1, 0)) self.blocked = False self.stop() def move_down(self): index = self.playlistView.currentIndex().row() if index + 1 <= self.playlistModel.rowCount(): item, below = self.playlist.media(index), self.playlist.media( index + 1) self.playlist.removeMedia(index + 1) self.playlist.removeMedia(index) self.playlist.insertMedia(index, below) self.playlist.insertMedia(index + 1, item) self.blocked = True self.playlistView.setCurrentIndex( self.playlistModel.index(index + 1, 0)) self.blocked = False self.stop() def search(self, part_of_song): for index in range(self.playlistModel.rowCount()): item = self.playlistModel.data(self.playlistModel.index( index, 0)).lower() self.playlistView.setRowHidden(index, part_of_song.lower() not in item) def change_thumbnail(self, position=0): self.playlistView.setCurrentIndex(self.playlistModel.index( position, 0)) song = self.playlistView.selectedIndexes()[0].data() if self.wave_graphic.is_song_cached(song): self.wave_graphic.load_waves(song) else: wc_ = WaveConverter(song, self.wave_graphic.set_wav) wc_.convert() if self.playlistView.currentIndex().data() is None or self.blocked: return max_ratio = 0 img = None for item in listdir(self.thumbnails): if item.endswith('.jpg'): ratio = similar( item, self.playlistView.currentIndex().data().replace( '.mp3', '.jpg')) if ratio > max_ratio: max_ratio = ratio img = item if img: p = QPixmap(self.thumbnails + img) self.image_label.setPixmap( p.scaled(self.thumb_width, self.thumb_height, Qt.KeepAspectRatio)) def switch_playlist(self, current_text): self.playlist.clear() if current_text == "No Playlist": self.refresh() else: if read_playlist(current_text): songs = read_playlist(current_text).split('\n') for song in songs: self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(song))) def refresh(self): # Change it so it will go to same song. if self.playlist_name.currentText() != "No Playlist": return paths = fetch_options()['paths']['music_path'].split(';') current_songs = [ self.playlistModel.data(self.playlistModel.index(row, 0)) for row in range(self.playlistModel.rowCount()) ] for path in paths: if not path: continue for item in listdir(path): if isfile(join(path, item)) and item.endswith(".mp3") and ( item not in current_songs): self.playlist.addMedia( QMediaContent(QUrl.fromLocalFile(join(path, item)))) def refresh_lists(self): path = fetch_options()['paths']['playlist'] self.playlist_name.clear() self.playlist_list = ["No Playlist"] self.playlist_name.addItem("No Playlist") for item in listdir(path): if isfile(join(path, item)) and item.endswith(".lst"): self.playlist_list.append(item.split('.')[0]) self.playlist_name.addItem(item.split('.')[0]) def playback_mode(self): # Normal -> Loop -> Random def up_value(value, max_value=3): if value + 1 == max_value: return 0 return value + 1 self.playback_value = up_value(self.playback_value) self.playlist.setPlaybackMode(self.values[self.playback_value]) self.playbackButton.setText(self.text[self.playback_value]) def jump(self, index): if index.isValid() and not self.blocked: self.playlist.setCurrentIndex(index.row()) self.jumping = True self.play() def play(self): if self.blocked: return if self.player.state() != QMediaPlayer.PlayingState or self.jumping: self.player.play() self.playButton.setIcon(self.style().standardIcon( QStyle.SP_MediaPause)) self.jumping = False else: self.player.pause() self.playButton.setIcon(self.style().standardIcon( QStyle.SP_MediaPlay)) self.stopButton.setEnabled(True) def change_volume(self, value): self.player.setVolume(value) def stop(self): if self.player.state() != QMediaPlayer.StoppedState: self.player.stop() self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) self.stopButton.setEnabled(False) def next_song(self): self.playlist.next() self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) def previous_song(self): self.playlist.previous() self.playlistView.setCurrentIndex( self.playlistModel.index(self.playlist.currentIndex(), 0)) def mute_clicked(self): self.player.setMuted(not self.player.isMuted()) self.muteButton.setIcon(self.style().standardIcon( QStyle.SP_MediaVolume if not self.player.isMuted() else QStyle. SP_MediaVolumeMuted)) def durationChanged(self, duration): duration /= 1000 self.slider.setMaximum(duration) def positionChanged(self, progress): progress /= 1000 if not self.slider.isSliderDown(): self.slider.setValue(progress) def seek(self, seconds): self.player.setPosition(seconds * 1000)
class Ui_MainWindow(object): # for stylizing progress bar progress_styleSheet = """ QSlider::groove:horizontal { background: dark; height: 40px; } QSlider::sub-page:horizontal { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #07406a, stop: 1 #696969); background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, stop: 0 #696969, stop: 1 #07406a); height: 40px; } QSlider::add-page:horizontal { background: #696969; height: 40px; } QSlider::handle:horizontal { background: #aaa; border: 0px; width: 5px; margin-top: 0px; margin-bottom: 0px; border-radius: 0px; } """ def setupUi(self, MainWindow): MainWindow.setObjectName("Music Player") MainWindow.resize(640, 480) MainWindow.setAcceptDrops(True) self.playlist = QMediaPlaylist() self.player = QMediaPlayer() self.player.setPlaylist(self.playlist) self.player.setVolume(20) self.player.setPlaybackRate(jdata['playback']) self.playlist.setPlaybackMode(jdata['curr_playstyle']) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout.setObjectName("gridLayout") self.gridLayout_2 = QtWidgets.QGridLayout() self.gridLayout_2.setObjectName("gridLayout_2") self.progress = QtWidgets.QSlider(self.centralwidget) self.progress.setOrientation(QtCore.Qt.Horizontal) self.progress.setStyleSheet(self.progress_styleSheet) self.progress.setObjectName("progress") self.progress.setSingleStep(1) self.progress.setPageStep(1) self.gridLayout_2.addWidget(self.progress, 1, 0, 1, 3) self.stop_but = QtWidgets.QPushButton(self.centralwidget) self.stop_but.setObjectName("stop_but") self.gridLayout_2.addWidget(self.stop_but, 0, 2, 1, 1) self.backward_b = QtWidgets.QPushButton(self.centralwidget) self.backward_b.setObjectName("backward") self.gridLayout_2.addWidget(self.backward_b, 0, 0, 1, 1) self.cont_pau = QtWidgets.QPushButton(self.centralwidget) self.cont_pau.setObjectName("cont_pau") self.gridLayout_2.addWidget(self.cont_pau, 0, 1, 1, 1) self.playstyle = QtWidgets.QPushButton(self.centralwidget) self.playstyle.setObjectName("playstyle") self.playstyle.setText("Loop") self.gridLayout_2.addWidget(self.playstyle, 0, 4, 1, 1) self.forward_b = QtWidgets.QPushButton(self.centralwidget) self.forward_b.setObjectName("forward") self.gridLayout_2.addWidget(self.forward_b, 0, 3, 1, 1) self.time_remain = QtWidgets.QLabel(self.centralwidget) self.time_remain.setObjectName("time_remain") self.gridLayout_2.addWidget(self.time_remain, 1, 3, 1, 1) self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") self.volume = QtWidgets.QLabel(self.centralwidget) self.volume.setAlignment(QtCore.Qt.AlignCenter) self.volume.setObjectName("volume") self.verticalLayout_2.addWidget(self.volume) self.volume_ctrl = QtWidgets.QSlider(self.centralwidget) self.volume_ctrl.setOrientation(QtCore.Qt.Horizontal) self.volume_ctrl.setObjectName("volume_ctrl") self.volume_ctrl.setValue(jdata["volume"]) self.volume_ctrl.setRange(-1, 101) self.volume_ctrl.setSingleStep(1) self.volume_ctrl.setPageStep(1) self.verticalLayout_2.addWidget(self.volume_ctrl) self.gridLayout_2.addLayout(self.verticalLayout_2, 1, 4, 1, 1) self.gridLayout.addLayout(self.gridLayout_2, 1, 0, 1, 1) self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.playing = QtWidgets.QLabel(self.centralwidget) font = QtGui.QFont() font.setFamily("Arial") font.setPointSize(12) self.playing.setFont(font) self.playing.setAlignment(QtCore.Qt.AlignCenter) self.playing.setObjectName("playing") self.playing.setScaledContents(True) self.playing.setWordWrap(True) self.verticalLayout.addWidget(self.playing) self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1) self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3.setObjectName("verticalLayout_3") self.song_list = ListWidget(self.centralwidget) self.song_list.setMinimumSize(QtCore.QSize(183, 0)) self.song_list.setAcceptDrops(True) self.song_list.setDragDropMode( QtWidgets.QAbstractItemView.InternalMove) self.song_list.setObjectName("song_list") self.song_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.song_list.setWordWrap(True) self.verticalLayout_3.addWidget(self.song_list) self.gridLayout.addLayout(self.verticalLayout_3, 0, 1, 2, 1) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 632, 25)) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(self.menubar) self.menuFile.setObjectName("menuFile") self.menuAbout = QtWidgets.QMenu(self.menubar) self.menuAbout.setObjectName("menuAbout") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.actionOpen = QtWidgets.QAction(MainWindow) self.actionOpen.setObjectName("actionOpen") self.actionLisence_Information = QtWidgets.QAction(MainWindow) self.actionLisence_Information.setObjectName( "actionLisence_Information") self.menuFile.addAction(self.actionOpen) self.menuAbout.addAction(self.actionLisence_Information) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuAbout.menuAction()) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) self.gridLayout.addLayout(self.gridLayout_2, 1, 0, 1, 1) self.info = QMessageBox() self.info.setWindowTitle("License Information") self.info.setText("MIT License\n\nCopyright (c) 2021 Marganotvke") self.err = QMessageBox() self.err.setWindowTitle("Error Loading File") self.err.setText("Error Loading File!\n\nPlease check file type!") self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) self.actionOpen.triggered.connect(lambda: self.open_trig()) self.actionLisence_Information.triggered.connect( lambda: self.info.exec_()) self.cont_pau.clicked.connect(lambda: self.cur_playing()) self.volume_ctrl.sliderMoved.connect(lambda: self.change_vol()) self.stop_but.clicked.connect(lambda: self.stop_playing()) self.forward_b.clicked.connect(lambda: self.forward()) self.backward_b.clicked.connect(lambda: self.backward()) self.playstyle.clicked.connect(lambda: self.playlist_style()) self.song_list.dropped.connect(lambda e: self.start_play(e)) self.song_list.itemDoubleClicked.connect( lambda: self.jump_start(self.song_list.currentRow())) self.song_list.model().rowsAboutToBeMoved.connect( lambda e, f, g, h, i: self.re_arr(f, i)) self.song_list.dele.connect(lambda e: self.delete_q(e)) self.player.metaDataAvailableChanged.connect(lambda: self.set_meta()) self.player.stateChanged.connect(lambda e: self.set_state(e)) self.player.positionChanged.connect(lambda e: self.set_pos(e)) self.progress.sliderMoved.connect(lambda e: self.skip_to(e)) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "Music Player")) self.stop_but.setText(_translate("MainWindow", "Stop")) self.backward_b.setText(_translate("MainWindow", "Backward")) self.cont_pau.setText(_translate("MainWindow", "Load Song")) self.playstyle.setText( _translate("MainWindow", f"{playstyles[jdata['curr_playstyle']]}")) self.forward_b.setText(_translate("MainWindow", "Forward")) self.time_remain.setText(_translate("MainWindow", "--/--")) self.volume.setText( _translate("MainWindow", f"Volume: {jdata['volume']}%")) self.playing.setText( _translate("MainWindow", "Currently playing: None")) self.menuFile.setTitle(_translate("MainWindow", "File")) self.menuAbout.setTitle(_translate("MainWindow", "About")) self.actionOpen.setText(_translate("MainWindow", "Open")) self.actionLisence_Information.setText( _translate("MainWindow", "License Information")) self.actionOpen.setShortcut(_translate("MainWindow", "Ctrl+O")) def re_arr(self, e=None, f=None): f -= 1 if self.song_list.count() == f else 0 media = self.playlist.media(e) self.playlist.removeMedia(e) self.playlist.insertMedia(f, media) def jump_start(self, index=None): if self.playlist.currentIndex() != index: self.playlist.setCurrentIndex(index) self.player.stop() self.start_play() def start_play(self, url=None): if url: print("Emitted from drop event:", url) threading.Thread(target=self.open_file(url)).start() self.player.play() def stop_playing(self): self.player.stop() def cur_playing(self): if self.playlist.isEmpty(): self.open_trig() else: if self.player.state() == 1: self.player.pause() self.cont_pau.setText("Play") else: self.player.play() self.cont_pau.setText("Pause") def forward(self): self.playlist.next() def backward(self): if round(self.player.position() / 1000) >= 5: self.player.setPosition(0) if not self.player.state(): self.start_play() else: self.playlist.previous() def open_trig(self): filename = QFileDialog.getOpenFileName( None, 'Open File', filter= "MPEG-2 (*.mp3);;WAVE Audio (*.wav,*.aif,*.aiff);;MPEG-1/DD/ACC (*.aac);;MIDI (*.mid,*.midi);;Windows Media Audio (*.wma);;Xiph.Org OGG Vorbis (*.ogg);;NeXT SouND (*.snd);;FLAC (*.flac);;All files(*)" ) threading.Thread(target=self.open_file(filename[0])).start() def open_file(self, file_url): url = QUrl.fromLocalFile(file_url) avai = ('.mp3', '.wav', 'ogg', '.aac', '.aif', '.aiff', '.mid', '.midi', '.wma', '.snd', '.flac') if url.url() != '' and not url.fileName().lower().endswith(avai): self.err.exec_() elif url.fileName().lower().endswith(avai): song_name = url.fileName().split(".")[0] self.song_list.addItem(song_name) if self.playlist.isEmpty(): self.playlist.addMedia(QMediaContent(url)) self.cont_pau.setText("Pause") self.start_play() else: self.playlist.addMedia(QMediaContent(url)) def delete_q(self, i): self.playlist.removeMedia(i) self.song_list.takeItem(i) def playlist_style(self): i = self.playlist.playbackMode() i += 1 i *= (i < 5) self.playlist.setPlaybackMode(i) self.playstyle.setText(f"{playstyles[self.playlist.playbackMode()]}") jdata["curr_playstyle"] = self.playlist.playbackMode() print(playstyles[self.playlist.playbackMode()]) def change_vol(self): vol = self.volume_ctrl.value() self.player.setVolume(vol) self.volume.setText(f"Volume: {vol}%") jdata["volume"] = vol def set_pos(self, e): if self.player.isMetaDataAvailable(): t = self.player.duration() dt = datetime.datetime.fromtimestamp( round(self.player.duration() / 1000)).strftime('%M:%S') dt_ct = datetime.datetime.fromtimestamp(round( e / 1000)).strftime('%M:%S') self.thread = threading.Thread( target=self.progress.setValue(round((e / t) * 100))) self.thread.start() self.time_remain.setText(f"{dt_ct}/{dt}") def set_meta(self): if self.player.isMetaDataAvailable(): self.duraiton = self.player.duration() self.progress.setRange(0, 100) self.playing.setText( f"Currently playing: {self.player.currentMedia().canonicalUrl().fileName().split('.')[0]}" ) else: if self.playlist.isEmpty(): self.cont_pau.setText("Load Song") self.time_remain.setText("--/--") def set_state(self, e): if not e: self.playing.setText("Currently playing: None") if self.playlist.isEmpty(): self.cont_pau.setText("Load Song") else: self.cont_pau.setText("Play") else: if self.player.isMetaDataAvailable(): self.playing.setText( f"Currently playing: {self.player.currentMedia().canonicalUrl().fileName().split('.')[0]}" ) def skip_to(self, e): self.player.setPosition(round((e / 100) * self.player.duration()))
class Player(QMediaPlayer): def __init__(self, parent=None): super(Player, self).__init__(parent) self.parent = parent self.player = QMediaPlayer() self.queueList = QMediaPlaylist() self.player.setPlaylist(self.queueList) self.queueData = [] self.position = 0 self.volume = 100 self.player.mediaStatusChanged.connect(self.qmp_mediaStatusChanged) self.player.positionChanged.connect(self.qmp_positionChanged) self.player.durationChanged.connect(self.durationChanged) self.queueList.currentIndexChanged.connect(self.playlistPosChanged) def add(self, data): """Add track to the queue""" queueData = { 'idEpisode': data['idEpisode'], 'pc_title': data['pc_title'], 'title': data['title'], 'url': data['url'], 'date': data['date_format'], 'description': data['description'], 'localfile': data['localfile'] } url = data['url'] if data['localfile']: url = 'file://' + data['localfile'] self.queueData.append(queueData) self.queueList.addMedia(QMediaContent(QUrl(url))) self.parent.playBtn.setEnabled(True) self.parent.stopBtn.setEnabled(True) self.parent.back10Btn.setEnabled(True) self.parent.for10Btn.setEnabled(True) self.parent.timeSlider.setEnabled(True) self.parent.timeSlider.setEnabled(True) def playPause(self): icon = QIcon.fromTheme("media-playback-pause") if self.player.state() == QMediaPlayer.StoppedState: if self.player.mediaStatus() == QMediaPlayer.NoMedia: if self.queueList.mediaCount() != 0: self.player.play() elif self.player.mediaStatus() == QMediaPlayer.LoadedMedia: self.queueList.setCurrentIndex(self.position) self.player.play() elif self.player.mediaStatus() == QMediaPlayer.BufferedMedia: self.player.play() elif self.player.state() == QMediaPlayer.PlayingState: icon = QIcon.fromTheme("media-playback-start") self.player.pause() elif self.player.state() == QMediaPlayer.PausedState: self.player.play() self.parent.playBtn.setIcon(icon) def startPlay(self): data = self.queueData[0] self.queueList.setCurrentIndex(0) self.parent.curPCLabel.setText(data['pc_title']) self.parent.curTrackName.setText(data['title']) self.parent.curTrackName.setToolTip(data['title']) self.player.play() icon = QIcon.fromTheme("media-playback-pause") self.parent.playBtn.setIcon(icon) def stop(self): self.player.stop() icon = QIcon.fromTheme("media-playback-start") self.parent.playBtn.setIcon(icon) def setPosition(self, pos): self.player.setPosition(pos) def durationChanged(self, duration): total_time = '0:00:00' duration = self.player.duration() total_time = ms_to_time(duration) self.parent.timeSlider.setMaximum(duration) self.currentTrackDuration = duration self.parent.totalTimeLabel.setText(total_time) def qmp_mediaStatusChanged(self, status): icon = QIcon.fromTheme("media-playback-pause") if self.player.state() == QMediaPlayer.StoppedState: icon = QIcon.fromTheme("media-playback-start") elif self.player.state() == QMediaPlayer.PausedState: icon = QIcon.fromTheme("media-playback-start") self.parent.playBtn.setIcon(icon) def qmp_positionChanged(self, position, senderType=False): self.currentTime = position current_time = '0:00:00' if position != -1: current_time = ms_to_time(position) self.parent.timeLabel.setText(current_time) self.parent.timeSlider.blockSignals(True) self.parent.timeSlider.setValue(position) self.parent.timeSlider.blockSignals(False) def playlistPosChanged(self): if self.queueList.mediaCount() > 0: pos = self.queueList.currentIndex() data = self.queueData[pos] self.parent.curPCLabel.setText(data['pc_title']) self.parent.curTrackName.setText(data['title']) self.parent.curTrackName.setToolTip(data['title']) self.parent.infoEpisodeLabel.setText('') windowTitle = '{0} - {1}'.format(data['pc_title'], data['title']) self.parent.setWindowTitle(windowTitle) if self.queueList.mediaCount() > 1: if pos < self.queueList.mediaCount() - 1: self.parent.queueNextBtn.setEnabled(True) else: self.parent.queueNextBtn.setEnabled(False) if pos > 0: self.parent.queuePrevBtn.setEnabled(True) else: self.parent.queuePrevBtn.setEnabled(False) if pos < self.queueList.mediaCount(): prevPos = 0 if self.position < pos: prevPos = pos - 1 else: prevPos = pos + 1 prevItem = self.parent.queueList.item(prevPos) prevWidget = self.parent.queueList.itemWidget(prevItem) if prevItem: prevWidget.statusIcon.setPixmap(QPixmap()) self.position = pos item = self.parent.queueList.item(pos) widget = self.parent.queueList.itemWidget(item) if widget: icon = QIcon.fromTheme("media-playback-start") widget.statusIcon.setPixmap(icon.pixmap(16, 16)) def setVolume(self, volume): self.player.setVolume(volume) def rev10Secs(self): position = self.player.position() new_pos = position - 10000 self.player.setPosition(new_pos) def for10Secs(self): position = self.player.position() new_pos = position + 10000 self.player.setPosition(new_pos) def delete(self, position): """ Delete the track and her data from position""" self.queueData.pop(position) self.queueList.removeMedia(position) if self.queueList.mediaCount() > 0: if position == self.position: self.playlistPosChanged() else: self.parent.setWindowTitle('Minimal Podcasts Player') self.parent.infoEpisodeLabel.setText('') self.parent.curPCLabel.setText(_translate("MainWindow", "Podcast")) self.parent.curTrackName.setText( _translate("MainWindow", "Episode Title")) self.parent.queuePrevBtn.setEnabled(False) self.parent.queueNextBtn.setEnabled(False) self.parent.playBtn.setEnabled(False) self.parent.stopBtn.setEnabled(False) self.parent.back10Btn.setEnabled(False) self.parent.for10Btn.setEnabled(False) self.parent.timeSlider.setEnabled(False) def changeUrl(self, idEpisode, url): pos = next((i for i, item in enumerate(self.queueData) if item['idEpisode'] == idEpisode), None) if (pos): self.queueData[pos]['url'] = url self.queueList.removeMedia(pos) self.queueList.insertMedia(pos, QMediaContent(QUrl(url))) def changePos(self, pos): prevPos = self.queueList.currentIndex() if prevPos > -1: prevItem = self.parent.queueList.item(prevPos) prevWidget = self.parent.queueList.itemWidget(prevItem) if prevItem: prevWidget.statusIcon.setPixmap(QPixmap()) self.queueList.setCurrentIndex(pos) self.playlistPosChanged() if self.player.state( ) == QMediaPlayer.StoppedState or self.player.state( ) == QMediaPlayer.PausedState: self.player.play()
class Win(QWidget): """ 重写 QWidget 类,自定义创建一个播放器窗体 """ def __init__(self): """ 构造方法:类的初始化 """ # 初始化父类 super().__init__() # 定义类属性 self.moveMode = False # 是否开启窗口移动模式 self.windowPoint = QPoint(0, 0) # 记录窗口移动时位置 self.startPoint = QPoint(0, 0) # 记录窗口移动前位置 # 设定窗体基本属性(标题、尺寸、图标等) self.setWindowTitle("简易MP3音乐播放器") # 窗口标题 self.setObjectName("app") # 对象名称 self.resize(260, 348) # 窗口尺寸 self.setFixedSize(self.width(), self.height()) # 禁止拉伸窗口大小 self.setWindowFlags(Qt.FramelessWindowHint) # 窗口无边框 self.setAutoFillBackground(True) # 允许背景填充 palette = QPalette() palette.setBrush(QPalette.Background, QBrush(QPixmap('./img/bg.jpg'))) self.setPalette(palette) # 填充背景图片 # 定义窗体组件(定义按钮、标签、列表框、滑块条等)及其基本属性 self.create_widgets() # 创建播放器和播放列表 self.media = QMediaPlayer() self.list = QMediaPlaylist() self.media.setPlaylist(self.list) # 设置窗体组件的样式和布局(颜色、图片、背景、位置、字体等) self.set_style() # 绑定事件(信号槽) self.bind_slots() # 其他初始化操作 self.list.setPlaybackMode(QMediaPlaylist.Loop) # 循环播放 self.media.setVolume(100) # 设置默认音量 self.slider_volume.setRange(0, 100) self.slider_volume.setValue(100) self.slider_pos.setRange(0, 0) # 设置默认进度 self.slider_pos.setValue(0) self.label_title.setText("简易MP3音乐播放器") # 设置默认标题 # 加载已保存的播放列表 self.load_playlist() def create_widgets(self): """ 定义窗体组件(定义按钮、标签、列表框、滑块条等)及其基本属性 :return: """ self.frame_player = QFrame(self) # 播放器布局框 self.frame_player.setObjectName("frame_player") self.frame_player.setGeometry(0, 0, 260, 120) self.frame_list = QFrame(self) # 播放列表布局框 self.frame_list.setObjectName("frame_list") self.frame_list.setGeometry(0, 120, 260, 228) self.btn_play = QPushButton(self.frame_player) # 播放按钮 self.btn_play.setObjectName("btn_play") self.btn_play.setGeometry(8, 78, 32, 32) self.btn_pause = QPushButton(self.frame_player) # 暂停按钮 self.btn_pause.setObjectName("btn_pause") self.btn_pause.setGeometry(8, 78, 32, 32) self.btn_pause.setVisible(False) self.btn_stop = QPushButton(self.frame_player) # 停止按钮 self.btn_stop.setObjectName("btn_stop") self.btn_stop.setGeometry(40, 78, 32, 32) self.btn_previous = QPushButton(self.frame_player) # 上一曲按钮 self.btn_previous.setObjectName("btn_previous") self.btn_previous.setGeometry(72, 78, 32, 32) self.btn_next = QPushButton(self.frame_player) # 下一曲按钮 self.btn_next.setObjectName("btn_next") self.btn_next.setGeometry(104, 78, 32, 32) self.btn_mute = QPushButton(self.frame_player) # 静音按钮 self.btn_mute.setObjectName("btn_mute") self.btn_mute.setGeometry(144, 90, 20, 20) self.label_title = QLabel(self.frame_player) # 歌曲标题标签 self.label_title.setObjectName("label_title") self.label_title.setGeometry(8, 38, 244, 32) self.label_time = QLabel("00:00", self.frame_player) # 时间标签 self.label_time.setObjectName("label_time") self.label_time.setGeometry(220, 74, 30, 20) self.slider_volume = QSlider(self.frame_player) # 音量滑块条 self.slider_volume.setObjectName("slider_volume") self.slider_volume.setGeometry(172, 90, 80, 20) self.slider_volume.setOrientation(Qt.Horizontal) self.slider_pos = QSlider(self.frame_player) # 播放进度条 self.slider_pos.setObjectName("slider_pos") self.slider_pos.setGeometry(0, 114, 260, 6) self.slider_pos.setOrientation(Qt.Horizontal) self.btn_add = QPushButton(self.frame_list) # 添加文件按钮 self.btn_add.setObjectName("btn_add") self.btn_add.setGeometry(8, 196, 24, 24) self.btn_remove = QPushButton(self.frame_list) # 移除文件按钮 self.btn_remove.setObjectName("btn_remove") self.btn_remove.setGeometry(40, 196, 24, 24) self.play_list = QListWidget(self.frame_list) # 播放列表 self.play_list.setObjectName("play_list") self.play_list.setGeometry(8, 8, 244, 180) self.label_version = QLabel(self.frame_list) # 软件版本说明标签 self.label_version.setObjectName("label_version") self.label_version.setGeometry(72, 196, 172, 24) self.label_version.setText("Version: 1.0 | By: yowfung") self.btn_close = QPushButton(self) # 关闭窗口按钮 self.btn_close.setObjectName("btn_close") self.btn_close.setGeometry(236, 0, 24, 24) def set_style(self): """ 设置窗体组件的样式和布局(颜色、图片、背景、位置、字体等) :return: """ # 通过各种选择器的方式,并结合qss样式表对组件进行样式美化和布局 self.setStyleSheet( # 窗体全局样式 "QWidget{font-family:'微软雅黑'}" # 播放器框和播放列表框的样式 "QFrame#frame_player{background-color: rgba(0,0,0,0)}" "QFrame#frame_list{background-color:rgba(200, 240, 255, 0.3)}" # 歌曲标题标签、时间标签、版本说明标签的样式 "QLabel#label_title{font-size:18px; color: white}" "QLabel#label_time{color: #bbb; font-size:11px; }" "QLabel#label_version{color:#444;font-size:12px}" # 音量滑块条、进度条样式 "QSlider#slider_volume::groove:horizontal{height:4px;background:rgb(167,176,255)}" "QSlider#slider_volume::handle:horizontal{width:4px;background:rgb(180, 220, 255);margin:-2px 0px;}" "QSlider#slider_pos::groove:horizontal{height:2px;background:rgb(50,80,120)}" "QSlider#slider_pos::handle:horizontal{width:6px;background:rgb(240, 50, 60);margin:-2px 0;border-radius:1px}" # 按钮样式 "QPushButton{border: none;}" "QPushButton:pressed{margin: 1px}" "QPushButton#btn_play:pressed, QPushButton#btn_pause:pressed, QPushButton#btn_stop:pressed{margin:2px}" "QPushButton#btn_play{image: url('./img/icons/play.png')}" "QPushButton#btn_pause{image: url('./img/icons/pause.png')}" "QPushButton#btn_stop{image: url('./img/icons/stop.png')}" "QPushButton#btn_previous{image: url('./img/icons/previous.png')}" "QPushButton#btn_next{image: url('./img/icons/next.png')}" "QPushButton#btn_mute{image: url('./img/icons/volume-normal.png')}" "QPushButton#btn_add{image: url('./img/icons/add.png')}" "QPushButton#btn_remove{image: url('./img/icons/remove.png')}" "QPushButton#btn_close{image: url('./img/icons/close.png')}" # 播放列表框样式 "QListWidget{border:1px solid #ccc; font-size:12px;color:#444;background-color:rgba(255,255,255,0.6)}" "QListWidget::Item:hover{background-color:rgba(120,120,120,0.15);color:black}" "QListWidget::Item:selected{background-color:rgba(120,120,150,0.3);color:#208}" # 垂直滚动条样式 "QScrollBar:vertical{background-color:#eee;width:6px}" "QScrollBar::handle:vertical{background-color:#ccc;min-height:16px}" "QScrollBar::handle::vertical:hover {background-color:#bbb;}" "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {height: 0}" # 水平滚动条样式 "QScrollBar:horizontal{background-color: #eee;height: 6px;}" "QScrollBar::handle:horizontal {background-color:#ccc;min-width:16px;}" "QScrollBar::handle::horizontal:hover {background-color:#bbb;}" "QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {width:0;}" ) def bind_slots(self): """ 绑定信号槽 :return: """ self.btn_close.clicked.connect(self.close) # 关闭按钮:被单击 self.btn_play.clicked.connect(self.con_btn_clicked_event) # 播放按钮:被单击 self.btn_pause.clicked.connect(self.con_btn_clicked_event) # 暂停按钮:被单击 self.btn_stop.clicked.connect(self.con_btn_clicked_event) # 停止按钮:被单击 self.btn_next.clicked.connect(self.con_btn_clicked_event) # 下一曲按钮:被单击 self.btn_previous.clicked.connect( self.con_btn_clicked_event) # 上一曲按钮:被单击 self.btn_mute.clicked.connect(self.con_btn_clicked_event) # 静音按钮:被单击 self.btn_add.clicked.connect(self.con_btn_clicked_event) # 添加文件按钮:被单击 self.btn_remove.clicked.connect( self.con_btn_clicked_event) # 移除项目按钮:被单击 self.slider_volume.valueChanged.connect( self.vol_changed_event) # 音量滑块条:值被改变 self.slider_pos.sliderReleased.connect( self.progress_changed_event) # 进度滑块条:值被改变 self.play_list.doubleClicked.connect( self.list_double_clicked_event) # 播放列表:被双击 self.media.positionChanged.connect( self.pos_changed_event) # 媒体播放器:播放位置被改变 self.media.durationChanged.connect( self.duration_changed_event) # 媒体播放器:时间区间被改变 self.media.stateChanged.connect( self.state_changed_event) # 媒体播放器:播放状态被改变 self.list.currentIndexChanged.connect( self.cur_changed_event) # 媒体播放列表:当前播放项被改变 def closeEvent(self, e): """ 重写 closeEvent:关闭程序前停止播放并保存记录 :param e: :return: """ self.save_playlist() self.media.stop() def mousePressEvent(self, e): """ 重写 mousePressEvent:鼠标在顶部按下时进入“移动窗口模式” :param e: :return: """ if e.button() == Qt.LeftButton \ and 2 <= e.pos().x() <= self.width() - 2 \ and 2 <= e.pos().y() <= 28: self.moveMode = True self.startPoint = e.globalPos() self.windowPoint = self.geometry().topLeft() def mouseMoveEvent(self, e): """ 重写 mouseMoveEvent:在“移动窗口模式”中进行窗口位置移动 :param e: :return: """ if e.buttons() & Qt.LeftButton and self.moveMode: re_point = e.globalPos() - self.startPoint self.move(self.windowPoint + re_point) def mouseReleaseEvent(self, e): """ 重写 mouseReleaseEvent:鼠标释放时离开“移动窗口模式” :param e: :return: """ self.moveMode = False def con_btn_clicked_event(self): """ 播放/暂停/上一曲/下一曲/停止/静音/添加文件/移除文件等控制按钮鼠标被单击事件 :return: """ # 取出事件对象(信号发送者) sender = self.sender() # 播放 if sender.objectName() == "btn_play": self.media.play() # 暂停 if sender.objectName() == "btn_pause": self.media.pause() # 停止 if sender.objectName() == "btn_stop": self.media.stop() # 上一曲 if sender.objectName() == "btn_previous": self.previous() # 下一曲 if sender.objectName() == "btn_next": self.next() # 静音 if sender.objectName() == "btn_mute": # 如果音量为0,或者原先未静音,则设置成静音 status = self.slider_volume.value() is 0 or not self.media.isMuted( ) self.media.setMuted(status) img = "./img/icons/volume-mute.png" if status else "./img/icons/volume-normal.png" self.btn_mute.setStyleSheet("QPushButton#btn_mute{image:url('" + img + "')}") # 静音按钮的图标样式 # 添加文件到列表 if sender.objectName() == "btn_add": self.add_to_list() # 从列表移除 if sender.objectName() == "btn_remove": self.remove_from_list() def list_double_clicked_event(self): """ 播放列表框中的项目被双击事件 :return: """ # 播放选中的歌曲 index = self.play_list.currentRow() if 0 <= index < self.play_list.count(): self.list.setCurrentIndex(index) self.media.play() def vol_changed_event(self, val): """ 音量滑块条值被改变事件:调节音量 :return: """ self.media.setVolume(val) self.media.setMuted(val is 0) # 音量为0时设置为静音 img = "./img/icons/volume-mute.png" if val is 0 else "./img/icons/volume-normal.png" self.btn_mute.setStyleSheet("QPushButton#btn_mute{image:url('" + img + "')}") # 静音按钮图标样式 def state_changed_event(self, state): """ 播放状态被改变事件:变化播放按钮和暂停按钮的可视状态 :param state: :return: """ self.btn_play.setVisible( True if state != 1 else False) # 只要不是“正在播放”,就显示播放按钮 self.btn_pause.setVisible( True if state == 1 else False) # 只要“正在播放”,就显示暂停按钮 def progress_changed_event(self): """ 播放进度条值被改变事件:定位播放 :return: """ # 将播放位置设置为进度条调节的位置 self.media.setPosition(self.slider_pos.value()) def pos_changed_event(self, position): """ 播放位置被改变事件:更新播放时间和进度条位置 :return: """ time = self.calc_time(position) # 计算播放时间 self.label_time.setText(time) # 显示播放时间 self.slider_pos.setValue(position) # 设置播放进度 def duration_changed_event(self, duration): """ 时间区间被改变事件:更新进度条区间 :param duration: :return: """ self.slider_pos.setRange(0, duration) def cur_changed_event(self, index): """ 当前播放索引被改变事件:更新播放列表索引 :param index: :return: """ self.play_list.setCurrentRow(index) # 在列表中选中已显示正在播放的项 self.label_title.setText(self.play_list.currentItem().text()) # 显示歌曲标题 self.label_time.setText("00:00") # 设置为默认时间 self.save_playlist() # 保存播放列表记录 def next(self): """ 下一曲 :return: """ # 判断播放列表是否为空 if self.list.mediaCount() < 0: return index = self.list.currentIndex() index = 0 if index + 1 >= self.list.mediaCount( ) else index + 1 # 如果当前是最后一首,则下一首就为第一首 self.list.setCurrentIndex(index) self.media.play() def previous(self): """ 上一曲 :return: """ # 判断播放列表是否为空 if self.list.mediaCount() < 0: return index = self.list.currentIndex() index = self.list.mediaCount( ) - 1 if index - 1 < 0 else index - 1 # 如果当前为第一首,则上一首就为最后一首 self.list.setCurrentIndex(index) self.media.play() def add_to_list(self): """ 添加项目到播放列表中 """ # 弹出多文件选择对话框,默认目录为程序当前目录,仅支持MP3格式 files = QFileDialog.getOpenFileNames(self, "打开MP3文件", QDir.currentPath(), "MP3文件(*.mp3)") for filename in files[0]: # 判断是否为MP3文件 if os.path.exists(filename) and str.lower(filename[-4:]) == ".mp3": tmp = filename.replace('\\', '/') # 路径转义 name = tmp.split('/')[-1][:-4] # 提取出简单的歌曲文件名 index = self.list.mediaCount() - 1 song = QMediaContent(QUrl.fromLocalFile(filename)) if self.list.insertMedia(index, song): self.play_list.insertItem(index, name) # 保存当前播放列表 self.save_playlist() def remove_from_list(self): """ 从播放列表中移除选中的项目 :return: bool 是否移除成功 """ index = self.play_list.currentRow() # 记录将要删除的索引 tmp_index = -1 # 记录删除后继续播放的索引 tmp_position = 0 # 记录删除后继续播放的位置 exec_play = self.media.state() == 1 # 删除后是否要执行播放操作 if index < 0: # 索引无效,不作处理 return elif index == self.list.currentIndex(): # 如果要删除的歌曲为正在播放的那首歌曲,则先停止播放,删除后播放下一曲 tmp_index = index if index + 1 < self.list.mediaCount() else 0 self.media.stop() elif index <= self.list.currentIndex(): # 如果要删除的歌曲是当前播放歌曲之前的歌曲,则改变索引继续播放 tmp_index = self.list.currentIndex() - 1 tmp_position = self.media.position() self.media.stop() # 删除操作 if self.list.removeMedia(index): self.play_list.takeItem(index) # 设置新的播放 self.label_title.setText("简易MP3音乐播放器") if 0 <= tmp_index <= self.list.mediaCount() - 1: self.list.setCurrentIndex(tmp_index) if exec_play: self.media.setPosition(tmp_position) self.media.play() self.label_title.setText(self.play_list.item(tmp_index).text()) # 保存当前播放列表 self.save_playlist() def load_playlist(self): """ 加载播放列表 :return: """ if not os.path.exists("./default.playlist"): return try: with open("./default.playlist", 'rb') as f: data = eval(f.read()) self.list.clear() self.play_list.clear() for item in data["list"]: # 判断是否为MP3文件 if os.path.exists(item["path"]) and str.lower( item["path"][-4:]) == ".mp3": index = self.list.mediaCount() song = QMediaContent(QUrl.fromLocalFile(item["path"])) if self.list.insertMedia(index, song): self.play_list.insertItem(index, item["name"]) if -1 <= data["currentItem"] < self.list.mediaCount(): self.list.setCurrentIndex(data["currentItem"]) self.play_list.setCurrentRow(data["currentItem"]) self.label_title.setText( self.play_list.currentItem().text()) self.slider_pos.setRange(0, data["duration"]) self.slider_pos.setValue(data["currentPosition"]) self.media.setPosition(data["currentPosition"]) self.media.setVolume(data["volume"]) self.slider_volume.setValue(data["volume"]) self.media.setMuted(data["muted"]) img = "./img/icons/volume-mute.png" if data[ "muted"] else "./img/icons/volume-normal.png" self.btn_mute.setStyleSheet( "QPushButton#btn_mute{image:url('" + img + "')}") # 静音按钮图标样式 except: pass def save_playlist(self): """ 保存播放列表 :return: """ data = {"list": [], "current": -1} for i in range(self.list.mediaCount()): path = self.list.media(i).canonicalUrl().path() name = self.list.media(i).canonicalUrl().fileName()[:-4] data["list"].append({"path": path[1:], "name": name}) data["currentItem"] = self.list.currentIndex() data["currentPosition"] = self.media.position() data["duration"] = self.media.duration() data["volume"] = self.media.volume() data["muted"] = self.media.isMuted() with open("./default.playlist", 'wb') as f: f.write(str(data).encode('utf-8')) @staticmethod def calc_time(millisecond): """ 计算时间(毫秒数格式转“分:秒”格式) :param millisecond: int 毫秒数 :return: string min:sec """ second = millisecond / 1000 minute = int(second / 60) second = int(second % 60) # 如果是个位数的话,就在前面加个“0” minute = "0" + str(minute) if minute < 10 else str(minute) second = "0" + str(second) if second < 10 else str(second) return str(minute) + ":" + str(second)