Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
PyQt5 Music Player
#16
Added a method for adding and removing stations. Alternating colors on track list. Only works when the app is running. After closing and restarting the default will be the only stations.

#! /usr/bin/env python3.9
import sys
from mutagen.mp3 import MP3
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QMediaPlaylist, QMediaMetaData
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QListWidget, QWidget, QHBoxLayout, \
                            QVBoxLayout, QGridLayout, QLabel, QPushButton, QFrame, QSlider, \
                            QStyle, QRadioButton, QFileDialog, QLineEdit, QMessageBox
from PyQt5.QtGui import QIcon, QPixmap, QGradient, QPalette, QBrush


class Window(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Set the window title
        self.setWindowTitle('PyQt5 Audio Player')

        # Set window background color
        p = QPalette()
        gradient = QGradient(QGradient.RichMetal)
        p.setBrush(QPalette.Window, QBrush(gradient))
        self.setPalette(p)


        # Set some variables
        self.player = QMediaPlayer()
        self.playlist = QMediaPlaylist()
        self.musiclist = QListWidget()
        self.entry = QLineEdit()
        self.entry.setPlaceholderText('Add a station. Example - https://url/to/internet/station/stream')
        self.entry.setFocusPolicy(Qt.StrongFocus)
        self.player.setPlaylist(self.playlist)

        # Changes cursor to hand when over button or link
        self.hand = Qt.PointingHandCursor

        # Set the spacing on the musiclist
        self.musiclist.setSpacing(2)

        # Create the needed containers
        container = QGridLayout()
        container.setSpacing(0)

        info_container = QGridLayout()
        info_frame = QFrame()
        info_frame.setContentsMargins(0, 3, 2, 3)
        info_frame.setMinimumHeight(300)
        info_frame.setMaximumWidth(361)
        info_frame.setFrameShape(QFrame.Box)
        info_frame.setFrameShadow(QFrame.Sunken)
        info_frame.setLayout(info_container)

        radio_frame = QFrame()
        radio_frame.setContentsMargins(0,0,0,0)
        radio_frame.setFrameShape(QFrame.Box)
        radio_frame.setFrameShadow(QFrame.Sunken)
        radio_frame.setMinimumHeight(60)
        radio_frame.setMaximumHeight(60)
        radio_container = QGridLayout()
        radio_frame.setLayout(radio_container)

        # Frame for the slider
        slider_box = QGridLayout()
        slider_frame = QFrame()
        slider_frame.setContentsMargins(0, 3, 2, 3)
        slider_frame.setFrameShape(QFrame.Box)
        slider_frame.setFrameShadow(QFrame.Sunken)
        slider_frame.setLayout(slider_box)

        # Control Box to hold volume, play, next, prev, stop
        control_box = QGridLayout()
        control_frame = QFrame()
        control_frame.setFrameShape(QFrame.Box)
        control_frame.setFrameShadow(QFrame.Sunken)
        control_frame.setContentsMargins(4,4,1,4)
        control_frame.setLayout(control_box)

        # Station edit frame
        self.station_frame = QFrame()
        self.station_frame.setFrameShape(QFrame.Box)
        self.station_frame.setFrameShadow(QFrame.Sunken)

        station_box = QGridLayout()
        self.station_frame.setLayout(station_box)
        self.station_frame.setMaximumHeight(60)
        self.station_frame.hide()


        # Create some common styles
        self.musiclist.setFrameShadow(QFrame.Sunken)
        self.musiclist.setAlternatingRowColors(True)
        self.musiclist.setStyleSheet('''
                                     margin-top: 3px; margin-bottom: 3px; margin-left: 2px;
                                     alternate-background-color: lightblue; background-color:white;
                                     padding: 12px;
                                     ''')

        style = '''
                    background-color: snow; padding-left: 8px; padding-right: 20px;
                    border: 1px solid lightgray; font-weight: bold; font-size: 10pt;
                '''
        style2 = '''
                 background-color: snow; padding-left: 8px; padding-right: 20px;
                 border: 1px solid lightgray; font-weight: 400; font-size: 10pt; color: navy;
                 '''

        # Common button style
        self.btn_style = '''
                        QPushButton{background-color: skyblue; color: navy;
                                    font-size: 10pt; font-weight: 500; padding: 6px;
                                    margin-left: 6px;}
                        QPushButton:hover{background-color: lightskyblue; color: blue;
                                            font-weight: 600; padding: 6px;}
                        QPushButton:pressed{background-color: dodgerblue; color: lightblue;
                                            font-weight: 400; padding: 4px;}
                    '''

        # Create the needed labels. Labels without self will not change data.
        #Labels with self will change data
        status_label = QLabel('Status:')
        status_label.setMinimumHeight(30)
        status_label.setMaximumHeight(30)
        status_label.setMinimumWidth(80)
        status_label.setMaximumWidth(80)
        status_label.setStyleSheet(style)

        self.status_label = QLabel('Now Stopped')
        self.status_label.setMinimumHeight(30)
        self.status_label.setMaximumHeight(30)
        self.status_label.setMinimumWidth(200)
        self.status_label.setMaximumWidth(200)
        self.status_label.setStyleSheet(style2)

        track_label = QLabel('Track:')
        track_label.setMinimumHeight(30)
        track_label.setMaximumHeight(30)
        track_label.setMinimumWidth(80)
        track_label.setMaximumWidth(80)
        track_label.setStyleSheet(style)

        self.track_label = QLabel()
        self.track_label.setMinimumHeight(30)
        self.track_label.setMaximumHeight(30)
        self.track_label.setMinimumWidth(420)
        self.track_label.setStyleSheet(style2)

        artist = QLabel('Artist:')
        artist.setStyleSheet(style)
        artist.setMaximumHeight(30)
        artist.setMinimumHeight(30)

        title = QLabel('Title:')
        title.setStyleSheet(style)
        title.setMaximumHeight(30)
        title.setMinimumHeight(30)

        album = QLabel('Album')
        album.setStyleSheet(style)
        album.setMaximumHeight(30)
        album.setMinimumHeight(30)

        released = QLabel('Released:')
        released.setStyleSheet(style)
        released.setMaximumHeight(30)
        released.setMinimumHeight(30)

        genre = QLabel('Genre:')
        genre.setStyleSheet(style)
        genre.setMaximumHeight(30)
        genre.setMinimumHeight(30)

        self.artist = QLabel()
        self.artist.setStyleSheet(style2)
        self.artist.setMinimumWidth(235)
        self.artist.setMaximumHeight(30)
        self.artist.setMinimumHeight(30)

        self.title = QLabel()
        self.title.setStyleSheet(style2)
        self.title.setMinimumWidth(235)
        self.title.setMaximumHeight(30)
        self.title.setMinimumHeight(30)

        self.album = QLabel()
        self.album.setStyleSheet(style2)
        self.album.setMinimumWidth(235)
        self.album.setMaximumHeight(30)
        self.album.setMinimumHeight(30)

        self.released = QLabel()
        self.released.setStyleSheet(style2)
        self.released.setMinimumWidth(235)
        self.released.setMaximumHeight(30)
        self.released.setMinimumHeight(30)

        self.genre = QLabel()
        self.genre.setStyleSheet(style2)
        self.genre.setMinimumWidth(235)
        self.genre.setMaximumHeight(30)
        self.genre.setMinimumHeight(30)

        self.cover_art = QLabel()
        self.cover_art.setMinimumSize(300, 300)
        self.cover_art.setFrameShape(QFrame.Box)
        self.cover_art.setFrameShadow(QFrame.Sunken)

        # Just a couple of spacers to keep everything push up when expanding
        self.spacer = QLabel()
        self.spacer2 = QLabel()
        self.duration_labelheader = QLabel('Duration:')
        self.duration_labelheader.setMinimumHeight(40)
        self.duration_labelheader.setMaximumHeight(40)
        self.duration_labelheader.setStyleSheet('font-size: 11pt; font-weight:500;')
        self.duration_timer = QLabel('00:00:00/00:00:00')
        self.duration_timer.setStyleSheet('font-size: 10pt; font-weight: 400; color: gray;')


        # Create the buttons
        self.get_btn = QPushButton('Get Audio')
        self.get_btn.setStyleSheet(self.btn_style)
        self.get_btn.setCursor(self.hand)
        self.get_btn.released.connect(self._files)

        clear_btn = QPushButton('Clear Playlist')
        clear_btn.setStyleSheet(self.btn_style)
        clear_btn.setCursor(self.hand)
        clear_btn.released.connect(self._clear)

        self.volume_slider = QSlider(Qt.Horizontal)
        self.volume_slider.setRange(0, 100)
        self.volume_slider.setTickInterval(10)
        self.volume_slider.setValue(70)
        self.volume_slider.setTickPosition(QSlider.TicksAbove)

        self.volume_label = QLabel(f'Volume: {self.volume_slider.value()}')
        self.volume_label.setMinimumWidth(200)
        self.volume_label.setStyleSheet('font-size: 11pt; padding-left: 25px;')

        self.play_btn = QPushButton()
        self.play_btn.setIcon(self.play_btn.style().standardIcon(QStyle.SP_MediaPlay))
        self.play_btn.setCursor(self.hand)
        self.play_btn.released.connect(self.player.play)

        stop_btn = QPushButton()
        stop_btn.setIcon(stop_btn.style().standardIcon(QStyle.SP_MediaStop))
        stop_btn.setCursor(self.hand)
        stop_btn.released.connect(self.player.stop)

        prev_btn = QPushButton()
        prev_btn.setIcon(prev_btn.style().standardIcon(QStyle.SP_MediaSkipBackward))
        prev_btn.setCursor(self.hand)
        prev_btn.released.connect(self._prev)

        next_btn = QPushButton()
        next_btn.setIcon(next_btn.style().standardIcon(QStyle.SP_MediaSkipForward))
        next_btn.setCursor(self.hand)
        next_btn.released.connect(self._next)

        # Radio buttons to choose internet radio or local files
        self.radio_btn = QRadioButton('Internet Radio')
        self._local_file_btn = QRadioButton('Local Files')
        self._local_file_btn.setChecked(True)

        # Buttons for adding and removing stations
        self.add_btn = QPushButton('Add Station')
        self.add_btn.setStyleSheet(self.btn_style)
        self.add_btn.setCursor(self.hand)
        self.add_btn.released.connect(self.add_station)

        self.del_btn = QPushButton('Remove Station')
        self.del_btn.setStyleSheet(self.btn_style)
        self.del_btn.setCursor(self.hand)
        self.del_btn.released.connect(self.remove_station)

        # Create the slider for track length
        self.track_slider = QSlider(Qt.Horizontal)
        self.track_slider.setRange(0, 100)
        # self.track_slider.setTickInterval(10)
        # self.track_slider.setTickPosition(QSlider.TicksAbove)

        # Add the control buttons to the control box
        control_box.addWidget(self.volume_slider, 0,0, 1, 1)
        control_box.addWidget(self.volume_label, 0, 1, 1, 1)
        control_box.addWidget(self.play_btn, 0, 2, 1, 1)
        control_box.addWidget(stop_btn,0, 3, 1, 1)
        control_box.addWidget(prev_btn,0, 4, 1, 1)
        control_box.addWidget(next_btn, 0, 5, 1, 1)

        # Add station edit controls to station grid box
        station_box.addWidget(self.add_btn, 0, 0, 1, 1)
        station_box.addWidget(self.del_btn, 0, 1, 1, 1)
        station_box.addWidget(self.entry, 0, 2, 1, 3)

        # Add the radio buttons to the radio container
        radio_container.addWidget(self.radio_btn, 0, 0, 1, 1)
        radio_container.addWidget(self._local_file_btn, 0, 1, 1, 1)

        # Add the slider to the slider box
        slider_box.addWidget(self.duration_labelheader, 0, 0, 1, 1)
        slider_box.addWidget(self.duration_timer, 0, 1, 1, 1)
        slider_box.addWidget(self.track_slider,1, 0,1, 3)

        # Add widgets to info_frame/container
        info_container.addWidget(artist, 0, 0, 1, 1)
        info_container.addWidget(title, 1, 0, 1, 1)
        info_container.addWidget(album, 2, 0, 1, 1)
        info_container.addWidget(released, 3, 0, 1, 1)
        info_container.addWidget(genre, 4, 0, 1, 1)
        info_container.addWidget(self.artist, 0, 1, 1, 1)
        info_container.addWidget(self.title, 1, 1, 1, 1)
        info_container.addWidget(self.album, 2, 1, 1, 1)
        info_container.addWidget(self.released, 3, 1, 1, 1)
        info_container.addWidget(self.genre, 4, 1, 1, 1)
        info_container.addWidget(self.cover_art, 5, 0, 1, 3)
        info_container.addWidget(self.spacer, 6, 0, 1, 3)
        info_container.addWidget(self.spacer2, 7, 0, 1, 3)
        info_container.addWidget(radio_frame, 8, 0, 1, 3)

        # Add widgets and layout to container layout
        container.addWidget(status_label, 1, 0, 1, 1)
        container.addWidget(self.status_label, 1, 1, 1, 1)
        container.addWidget(track_label, 1, 2, 1, 1)
        container.addWidget(self.track_label, 1, 3, 1, 1)
        container.addWidget(self.get_btn, 1, 4, 1, 1)
        container.addWidget(clear_btn, 1, 5, 1, 1)
        container.addWidget(info_frame, 2, 0, 1, 4)
        container.addWidget(self.musiclist,2, 3, 1, 3)
        container.addWidget(self.station_frame, 3, 0, 1, 6)
        container.addWidget(slider_frame, 4, 0, 1, 3)
        container.addWidget(control_frame, 4, 3, 1, 3)

        widget = QWidget()
        widget.setLayout(container)
        self.setCentralWidget(widget)

        # Set channels for updates and changes happening in the gui
        self.volume_slider.valueChanged.connect(self._volume, self.volume_slider.value())
        self.radio_btn.toggled.connect(self._music)
        self.player.metaDataChanged.connect(self._update)
        self.player.stateChanged.connect(self._state)
        self.player.positionChanged.connect(self._slider_pos)
        self.player.durationChanged.connect(self._duration)
        self.track_slider.valueChanged.connect(self._timer)
        self.musiclist.itemDoubleClicked.connect(self._doubleclick)

    # Method for adding stations
    def add_station(self):
        station = self.entry.text()
        if station and 'http://' in station or 'https://' in station:
            self.musiclist.addItem(station)
            self.playlist.addMedia(QMediaContent(QUrl(station)))
        else:
            message = QMessageBox.warning(self,'Warning!','Please use the correct \
format to add your station. \nExample: https://some/url/to/station/stream')
        self.entry.clear()

    # Method for removing stations
    def remove_station(self):
        if self.musiclist.count() > 0:
            row = self.musiclist.currentRow()
            item = self.musiclist.takeItem(row)
            del item
            self.playlist.removeMedia(row)
        else:
            message = QMessageBox.warning(self, 'Warning!', 'There is no more stations to remove!')

    # Method for playing a track if double clicked in the music list
    def _doubleclick(self):
        self.playlist.setCurrentIndex(self.musiclist.currentRow())
        self.player.play()

    # Method for updating slider position
    def _slider_pos(self, position):
            self.track_slider.setValue(position)

    # Method for setting the range of the duration
    def _duration(self, duration):
        self.track_slider.setRange(0, duration)

    # Updates the duration timer
    def _timer(self):
        total_milliseconds = self.player.duration()
        total_seconds, total_milliseconds = divmod(total_milliseconds,1000)
        total_minutes, total_seconds = divmod(total_seconds,60)
        total_hours, total_minutes = divmod(total_minutes, 60)

        elapsed_milliseconds = self.track_slider.value()
        elapsed_seconds, elapsed_milliseconds = divmod(elapsed_milliseconds,1000)
        elapsed_minutes, elapsed_seconds = divmod(elapsed_seconds, 60)
        elapsed_hours, elapsed_minutes = divmod(elapsed_minutes, 60)

        self.duration_timer.setText(f'{elapsed_hours:02d}:{elapsed_minutes:02d}:{elapsed_seconds:02d} / {total_hours:02d}:{total_minutes:02d}:{total_seconds:02d}')

    # Checks player state and updates accorrdingly
    def _state(self):
        if self.player.state() == self.player.PlayingState:
            self.play_btn.setIcon(self.play_btn.style().standardIcon(QStyle.SP_MediaPause))
            self.play_btn.released.connect(self.player.pause)
            self.status_label.setText('Now Playing')
        elif self.player.state() == self.player.PausedState:
            self.play_btn.setIcon(self.play_btn.style().standardIcon(QStyle.SP_MediaPlay))
            self.play_btn.released.connect(self.player.play)
            self.status_label.setText('Now Paused')
        else:
            self.play_btn.setIcon(self.play_btn.style().standardIcon(QStyle.SP_MediaPlay))
            self.play_btn.released.connect(self.player.play)
            self.status_label.setText('Now Stopped')

    # Methods for the next/prev actions
    def _prev(self):
        if self.playlist.previousIndex() == -1:
            self.playlist.setCurrentIndex(self.playlist.mediaCount()-1)
        else:
            self.playlist.previous()
        if self.playlist.currentIndexChanged:
            self.musiclist.setCurrentRow(self.playlist.currentIndex())
        self.player.play()

    def _next(self):
        self.playlist.next()
        if self.playlist.currentIndex() == -1:
            self.playlist.setCurrentIndex(0)
        self.musiclist.setCurrentRow(self.playlist.currentIndex())
        self.player.play()

    # Method for getting the music files
    def _files(self):
        files = QFileDialog.getOpenFileNames(None, 'Get Audio Files',
                                         filter='Audio Files (*.mp3 *.ogg *.wav)')

        for file in files[0]:
            self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(file)))
            try:
                self.track = MP3(file)
                self.musiclist.addItem(str(self.track['TIT2']))
            except:
                self.track = self._truncate(file.rpartition('/')[2].rpartition('.')[0])
                self.musiclist.addItem(self.track)

        self.musiclist.setCurrentRow(0)
        self.playlist.setCurrentIndex(0)

    # Method for updating much of the text information
    def _update(self):
        try:
            self.musiclist.setCurrentRow(self.playlist.currentIndex())
        except:
            pass
        try:
            if self.player.isMetaDataAvailable():
                if self.player.metaData(QMediaMetaData.AlbumArtist):
                    self.artist.setText(self.player.metaData(QMediaMetaData.AlbumArtist))

                if self.player.metaData(QMediaMetaData.Title):
                    if self.radio_btn.isChecked():
                        info = self.player.metaData(QMediaMetaData.Title).split('-')
                        self.artist.setText(self._truncate(info[0]))
                        self.title.setText(self._truncate(info[1]))
                        self.track_label.setText(info[1])
                    else:
                        self.title.setText(self._truncate(self.player.metaData(QMediaMetaData.Title)))
                        self.track_label.setText(self._truncate(self.player.metaData(QMediaMetaData.Title)))

                if self.player.metaData(QMediaMetaData.AlbumTitle):
                    self.album.setText(self._truncate(self.player.metaData(QMediaMetaData.AlbumTitle)))
                if self.player.metaData(QMediaMetaData.Year):
                    self.released.setText(f'{self.player.metaData(QMediaMetaData.Year)}')
                if self.player.metaData(QMediaMetaData.Genre):
                    self.genre.setText(self.player.metaData(QMediaMetaData.Genre))
                if self.player.metaData(QMediaMetaData.CoverArtImage):
                    pixmap = QPixmap(self.player.metaData(QMediaMetaData.CoverArtImage))
                    pixmap = pixmap.scaled(328, 295)
                    self.cover_art.setPixmap(pixmap)
                    self.cover_art.setStyleSheet('padding: 5px;')
            else:
                self.artist.setText('')
                self.title.setText('')
                self.album.setText('')
                self.released.setText('')
                self.genre.setText('')
                self.cover_art.setPixmap(QPixmap())
                self.track_label.setText('')

        except TypeError:
            pass

    # Method for shortening text
    def _truncate(self, text, length=25):
        if text:
            if len(text) <= length:
                return text
            else:
                return f"{' '.join(text[:length+1].split(' ')[0:-1])} ...."

    # Method for playing either radio or local audio files
    def _music(self):
        if self.radio_btn.isChecked():
            self.station_frame.show()
            self.entry.setFocus()
            self.musiclist.clear()
            self.playlist.clear()
            self.get_btn.setEnabled(False)
            self.get_btn.setStyleSheet('''QPushButton{background-color: lightgray; color: black;
                        font-size: 10pt; font-weight: 50; padding: 6px;
                        margin-left: 6px;}''')

            stations = [
                'http://us4.internet-radio.com:8258/stream',
                'http://us5.internet-radio.com:8267/stream',
                'https://us9.maindigitalstream.com/ssl/bigrock991',
                'https://playerservices.streamtheworld.com/api/livestream-redirect/KGFKAM.mp3',
                'https://cob-ais.leanstream.co/CFJBFM-MP3',
                'http://37.59.195.28:8045',
                'https://playerservices.streamtheworld.com/api/livestream-redirect/WIRLAM.mp3',
                'https://ais-sa2.cdnstream1.com/2383_128'
            ]

            for station in stations:
                self.playlist.addMedia(QMediaContent(QUrl(station)))
                self.musiclist.addItem(station)
            self.player.play()

        else:
            self._clear()
            self.get_btn.setEnabled(True)
            self.get_btn.setStyleSheet(self.btn_style)
            self.station_frame.hide()

    # Method for updating the volume
    def _volume(self, value):
        self.volume_label.setText(f'Volume: {value}')
        self.player.setVolume(value)

    # Method for clearing playlist, musiclist, and other data
    def _clear(self):
        self.playlist.clear()
        self.musiclist.clear()
        self.status_label.setText('Now Stopped')
        self.track_label.setText('')
        self._local_file_btn.setChecked(True)
        self.player.setVolume(70)
        self.volume_slider.setValue(70)
        self._update()

def main():
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags


Reply


Messages In This Thread
PyQt5 Music Player - by menator01 - Oct-14-2021, 09:38 AM
RE: PyQt5 Music Player - by Larz60+ - Oct-14-2021, 08:49 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-14-2021, 09:16 PM
RE: PyQt5 Music Player - by menator01 - Oct-15-2021, 05:58 AM
RE: PyQt5 Music Player - by menator01 - Oct-16-2021, 09:44 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-16-2021, 10:49 PM
RE: PyQt5 Music Player - by menator01 - Oct-16-2021, 11:00 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-17-2021, 09:26 PM
RE: PyQt5 Music Player - by menator01 - Oct-17-2021, 10:03 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-18-2021, 04:09 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-18-2021, 05:12 PM
RE: PyQt5 Music Player - by menator01 - Oct-18-2021, 06:44 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Oct-19-2021, 05:21 PM
RE: PyQt5 Music Player - by menator01 - Oct-29-2021, 06:49 PM
RE: PyQt5 Music Player - by menator01 - Nov-02-2021, 09:34 AM
RE: PyQt5 Music Player - by Axel_Erfurt - Nov-02-2021, 08:04 PM
RE: PyQt5 Music Player - by menator01 - Nov-03-2021, 08:30 AM
RE: PyQt5 Music Player - by Axel_Erfurt - Nov-03-2021, 09:05 AM
RE: PyQt5 Music Player - by menator01 - Nov-03-2021, 09:06 AM
RE: PyQt5 Music Player - by Axel_Erfurt - Nov-03-2021, 09:11 AM
RE: PyQt5 Music Player - by menator01 - Nov-03-2021, 05:49 PM
RE: PyQt5 Music Player - by Axel_Erfurt - Nov-03-2021, 05:59 PM

Possibly Related Threads…
Thread Author Replies Views Last Post
  Basic Music Player with tkinter menator01 4 4,774 Jul-31-2021, 04:27 AM
Last Post: ndc85430

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020