Example #1
0
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
Example #2
0
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)
Example #3
0
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()))
Example #4
0
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()
Example #5
0
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)