예제 #1
0
def test_answer():
    cont = []

    osc_1 = OSCThreadServer()
    osc_1.listen(default=True)

    @osc_1.address(b'/ping')
    def ping(*values):
        if True in values:
            osc_1.answer(b'/zap', [True], port=osc_3.getaddress()[1])
        else:
            osc_1.answer(bundle=[(b'/pong', [])])

    osc_2 = OSCThreadServer()
    osc_2.listen(default=True)

    @osc_2.address(b'/pong')
    def pong(*values):
        osc_2.answer(b'/ping', [True])

    osc_3 = OSCThreadServer()
    osc_3.listen(default=True)

    @osc_3.address(b'/zap')
    def zap(*values):
        if True in values:
            cont.append(True)

    osc_2.send_message(b'/ping', [], *osc_1.getaddress())

    with pytest.raises(RuntimeError) as e_info:  # noqa
        osc_1.answer(b'/bing', [])

    timeout = time() + 2
    while not cont:
        if time() > timeout:
            raise OSError('timeout while waiting for success message.')
        sleep(10e-9)
예제 #2
0
class SocketP2P(object):
    '''
    A peer to peer socket server base on OSCPy
    '''
    server = None
    allow_finding = False
    is_host = False
    is_running = False

    config_data = ''

    _my_name = ''

    def get_my_name(self):
        return self._my_name

    def set_my_name(self, name):
        if name:
            self._my_name = name

    my_name = property(get_my_name, set_my_name)

    __events__ = ('on_create_server', 'on_scan_device', 'on_found_device',
                  'on_device_hided', 'on_connect_user',
                  'on_accepted_connection', 'on_request_config',
                  'on_got_config', 'on_new_device', 'on_delete_device')
    _callback_collec = {}

    def __init__(self, port, bind_collection={}, **kwargs):
        # super(SocketP2P, self).__init__(**kwargs)

        self._port = port
        self._bind_collection = bind_collection
        self._bind_collection.update({
            '/probe': self._probe,
            '/found': self._found,
            '/hided': self._hided_host,
            '/get_conf': self._send_conf,
            '/conf': self._got_conf,
            '/new_device': self._new_device,
            '/delete_device': self._delete_device
        })
        for event in SocketP2P.__events__:
            self._callback_collec[event] = [getattr(self, event)]

        sock = socket.socket(AF_INET, SOCK_DGRAM)
        sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
        sock.settimeout(1)
        self._scan_client = OSCClient(address='255.255.255.255',
                                      port=self._port,
                                      sock=sock,
                                      encoding='utf8')

        self.create_server()

    @_catch_exception
    def _identify_me(self, time):
        global MY_IP

        if time == self._time:
            self.server.unbind('/identify_me', self._identify_me)
            _, ip_address, _ = self.server.get_sender()
            MY_IP = ip_address
            if self._my_name == '':
                self.my_name = ip_address
            Log_P2P(f'P2P: IP: {self.myip}')

    def stop(self):
        if self.server:
            try:
                self.server.stop_all()
            except Exception as e:
                Logger.exception('P2P: On close')
        self.is_running = False

    def bind(self, **kwargs):
        for event, cb in kwargs.items():
            if event in SocketP2P.__events__:
                self._callback_collec[event].append(cb)

    def unbind(self, event, callback):
        if event in SocketP2P.__events__ and\
                callback in self._callback_collec[event]:
            self._callback_collec[event].remove(callback)

    def bind_address(self, address, callback):
        self.server.bind(address, callback)
        self._bind_collection[address] = callback

    def unbind_address(self, address):
        self.server.unbind(address, self._bind_collection.pop(address, None))

    def dispatch(self, event, *args, **kwargs):
        if event in SocketP2P.__events__:
            args = [self] + list(args)
            for cb in self._callback_collec[event]:
                try:
                    cb(*args, **kwargs)
                except TypeError as tye:
                    Logger.exception(
                        f'TypeError: {cb.__qualname__} with {args} {kwargs}')
                except Exception as e:
                    Logger.exception(f'P2P: Error in {cb.__qualname__}')

    @property
    def myip(self):
        return MY_IP

    @_catch_exception
    def create_server(self):
        self.stop()
        self.reset_data()

        # Fix [Errno 98] Address already in use
        sleep(0.01)

        self.server = OSCThreadServer(encoding='utf8')
        self.server.listen(address='0.0.0.0', port=self._port, default=True)

        for address, cb in self._bind_collection.items():
            self.server.bind(address, cb)

        self.server.bind('/identify_me', self._identify_me)
        self._time = str(datetime.now())
        try:
            self._scan_client.send_message('/identify_me', [self._time])
        except OSError:
            Logger.exception('P2P: No network')
            self.server.unbind('/identify_me', self._identify_me)
            return

        self.dispatch('on_create_server', self.server)
        self.is_running = True

    def reset_data(self):
        self.list_device = []
        self.list_name = {}
        self.waiting_list = []

    @_catch_exception
    def scan_device(self):
        self.dispatch('on_scan_device')
        self._scan_client.send_message(b'/probe', [])

    @_catch_exception
    def _probe(self):
        if self.allow_finding:
            Log_P2P('P2P: /probe')
            self.server.answer(b'/found',
                               values=[MY_IP, self.my_name],
                               port=self._port)
        else:
            self.server.answer(b'/hided', values=[MY_IP], port=self._port)

    @_catch_exception
    def _found(self, ip, ip_name):
        if ip != MY_IP and ip not in self.list_device\
                and ip not in self.waiting_list:
            Log_P2P(f'P2P: /found {ip}')
            self.waiting_list.append(ip)
            self.dispatch('on_found_device', ip, ip_name)

    @_catch_exception
    def _hided_host(self, ip):
        if ip in self.waiting_list:
            Log_P2P(f'P2P: /hided {ip}')
            self.waiting_list.remove(ip)
            self.dispatch('on_device_hided', ip)

    def connect_user(self, ip):
        if ip in self.waiting_list:
            self.dispatch('on_connect_user', ip)
            self.get_config(ip)

    @_catch_exception
    def send_to_all(self, address, data, list_ip=None, and_myip=False):
        if not isinstance(data, list):
            data - list(data)
        if not list_ip:
            list_ip = self.list_device.copy()
        if and_myip:
            list_ip += [self.myip]

        Log_P2P(f'P2P: send {address}\nData: {data}\nTo: {list_ip}')
        for other in list_ip:
            self.server.send_message(address, data, other, self._port)

    def generate_token(self, *args):
        return '1234'

    def handle_token(self, token):
        if token == '1234':
            return True
        else:
            return False

    def get_config(self, ip):
        self.server.send_message(
            b'/get_conf', [MY_IP, self.generate_token(), self.my_name], ip,
            self._port)

    @_catch_exception
    def _send_conf(self, ip, token, ip_name):
        if self.handle_token(token) and self.allow_finding:
            Log_P2P(f'P2P: /get_conf {ip}')

            if ip not in self.list_device:
                for other in self.list_device:
                    self.send_to_all(
                        '/new_device',
                        [MY_IP, self.generate_token(), ip, ip_name])
                self.list_device.append(ip)
                self.list_name[ip] = ip_name
                self.dispatch('on_accepted_connection', ip, ip_name)

            list_send = {MY_IP: self.my_name}
            list_send.update(self.list_name)
            self.server.answer(b'/conf', [
                MY_IP,
                self.generate_token(), self.config_data,
                json.dumps(list_send)
            ],
                               port=self._port)
            self.dispatch('on_request_config', ip)

    @_catch_exception
    def _got_conf(self, ip, token, config_data, list_device):
        if self.handle_token(token):
            Log_P2P(f'P2P: /conf {ip} with {list_device}')
            self.list_device = []
            for i, name in json.loads(list_device).items():
                if i != MY_IP:
                    self.list_device.append(i)
                    self.list_name[i] = name

            if ip in self.waiting_list:
                self.waiting_list.remove(ip)

            self.dispatch('on_got_config', ip, config_data)

    @_catch_exception
    def _new_device(self, ip, token, new_ip, ip_name):
        if self.handle_token(token) and new_ip not in self.list_device:
            Log_P2P(f'P2P: /new_device {new_ip} {ip_name}')
            if ip in self.waiting_list:
                self.waiting_list.remove(new_ip)
            self.list_device.append(new_ip)
            self.list_name[new_ip] = ip_name
            self.dispatch('on_new_device', new_ip, ip_name)

    def remove_device(self):
        self.send_to_all('/delete_device',
                         [MY_IP, self.generate_token(), MY_IP],
                         and_myip=True)
        self.reset_data()

    @_catch_exception
    def _delete_device(self, ip, token, del_ip):
        if self.handle_token(token):
            Log_P2P(f'P2P: /delete_device {del_ip}')
            if del_ip in self.waiting_list:
                self.waiting_list.remove(del_ip)
            if del_ip in self.list_device:
                self.list_device.remove(del_ip)
            self.list_name.pop(del_ip, None)
            self.dispatch('on_delete_device', ip, del_ip)

    def on_create_server(self, ins, server):
        pass

    def on_scan_device(self, ins):
        pass

    def on_found_device(self, ins, ip, ip_name):
        pass

    def on_device_hided(self, ins, ip):
        pass

    def on_connect_user(self, ins, ip):
        pass

    def on_accepted_connection(self, ins, ip, ip_name):
        pass

    def on_request_config(self, ins, ip):
        pass

    def on_got_config(self, ins, ip, config_data):
        pass

    def on_new_device(self, ins, new_ip, ip_name):
        pass

    def on_delete_device(self, ins, ip, del_ip):
        pass
예제 #3
0
class OBSRemote:
    def __init__(self,
                 osc_port,
                 osc_client_host,
                 osc_client_port,
                 host,
                 port,
                 password=''):
        self.client = obsws(host, port, password)
        self.register_callbacks()

        self.osc = OSCThreadServer(
            encoding='utf8',
            advanced_matching=True,
        )

        self.socket = None
        self.osc_client_host = osc_client_host
        self.osc_client_port = osc_client_port
        self.osc_port = osc_port
        self.volume_changed = False
        self.levels = [0, 0]

        self.scenes = []

    def start(self):
        self.client.connect()
        self.socket = self.osc.listen(address='0.0.0.0',
                                      port=self.osc_port,
                                      default=True)

        self.update_audio_sources()
        self.update_audio_levels()
        self.update_scenes()
        self.update_mute_status()
        self.register_osc_addresses()

        self.client.call(requests.SetHeartbeat(True))

    def stop(self):
        self.client.disconnect()
        self.osc.stop()
        self.socket = None

    def register_callbacks(self):
        self.client.register(self.scene_changed, event=events.SwitchScenes)
        self.client.register(self.mute_changed,
                             event=events.SourceMuteStateChanged)
        self.client.register(self.update_scenes, event=events.ScenesChanged)
        self.client.register(self.update_scenes,
                             event=events.SceneCollectionChanged)
        self.client.register(self.update_audio_sources,
                             event=events.SourceRenamed)
        self.client.register(self.update_audio_levels,
                             event=events.SourceVolumeChanged)
        self.client.register(self.status_update, event=events.Heartbeat)

    def register_osc_addresses(self):
        @self.osc.address('/scene/?/?', get_address=True)
        def scene_cb(address, values):
            if values < 1.0:
                return

            scene_id = int(address.split(b'/')[-1]) - 1
            if scene_id < len(self.scenes):
                self.osc.answer(address, [1.0])
                self.client.call(
                    requests.SetCurrentScene(self.scenes[scene_id]))

        @self.osc.address('/mic')
        def mic_cb(values):
            self.osc.answer('/mic', [values])
            self.client.call(
                requests.SetMute(self.audio_sources['mic'], values < 1.0))

        @self.osc.address('/audio')
        def audio_cb(values):
            self.osc.answer('/audio', [values])
            self.client.call(
                requests.SetMute(self.audio_sources['desktop'], values < 1.0))

        @self.osc.address('/cam')
        def cam_cb(values):
            self.osc.answer('/cam', [values])
            self.client.call(
                requests.SetSceneItemProperties('Webcam',
                                                visible=values > 0.0))
            self.client.call(
                requests.SetSceneItemProperties('Overlay: Webcam',
                                                visible=values > 0.0))

        @self.osc.address('/audio_level/?', get_address=True)
        def audio_level_cb(address, values):
            slider_id = int(address.split(b'/')[-1]) - 1
            self.levels[slider_id] = values
            self.volume_changed = True

        @self.osc.address('/rec')
        def rec_cb(values):
            if values > 0.0:
                self.client.call(requests.StartRecording())
            else:
                self.client.call(requests.StopRecording())
            self.osc.answer('/rec', [values])

        @self.osc.address('/stream')
        def stream_cb(values):
            if values > 0.0:
                self.client.call(requests.StartStreaming())
            else:
                self.client.call(requests.StopStreaming())
            self.osc.answer('/stream', [values])

    def mute_changed(self, event):
        name = event.getSourceName()
        muted = event.getMuted()
        if name == self.audio_sources['mic']:
            self._send_osc('/mic', 1.0 if muted is False else 0.0)
        if name == self.audio_sources['desktop']:
            self._send_osc('/audio', 1.0 if muted is False else 0.0)

    def scene_changed(self, event):
        name = event.getSceneName()
        for idx, scene_name in enumerate(self.scenes):
            if name == scene_name:
                self._send_osc('/scene/1/{}'.format(idx + 1), 1.0)
        self.update_mute_status(sources=event.getSources())

    def status_update(self, event: events.Heartbeat):
        streaming = event.getStreaming()
        recording = event.getRecording()
        try:
            stream_time = timedelta(seconds=event.getTotalStreamTime())
        except KeyError:
            stream_time = 0
        try:
            rec_time = timedelta(seconds=event.getTotalRecordTime())
        except KeyError:
            rec_time = 0
        self._send_osc('/stream', 1.0 if streaming else 0.0)
        self._send_osc('/rec', 1.0 if recording else 0.0)
        self._send_osc('/stream_time', str(stream_time))
        self._send_osc('/rec_time', str(rec_time))

    def update_mute_status(self, sources=None):
        if sources is None:
            sources = self.client.call(requests.GetCurrentScene()).getSources()
        webcam_found = False
        for source in sources:
            if source['name'] in ['Webcam', 'Overlay: Webcam']:
                webcam_found = True
                self._send_osc('/cam', 1.0 if source['render'] else 0.0)
        if not webcam_found:
            self._send_osc('/cam', 0.0)

    def update_scenes(self, *args):
        if len(args) > 0:
            self.scenes = [
                s['name'] for s in args[0].getScenes()
                if not s['name'].lower().startswith('overlay:')
            ]
        else:
            self.scenes = [
                s['name']
                for s in self.client.call(requests.GetSceneList()).getScenes()
                if not s['name'].lower().startswith('overlay:')
            ]

        for idx in range(0, 8):
            try:
                name = self.scenes[idx]
            except IndexError:
                name = ''

            self._send_osc('/scene_label_{}'.format(idx), name)
        self.update_mute_status()

    def update_audio_sources(self, *args):
        sources = self.client.call(requests.GetSpecialSources())
        self.audio_sources = {
            "mic": sources.getMic1(),
            "desktop": sources.getDesktop1()
        }

    def update_audio_levels(self, *args):
        if len(args) > 0:
            event = args[0]
            name = event.getSourceName()
            volume = None
            if isinstance(event, events.SourceVolumeChanged):
                volume = event.getVolume()
            muted = None
            if isinstance(event, events.SourceMuteStateChanged):
                muted = event.getMuted()

            if name == self.audio_sources['mic']:
                if volume is not None:
                    self._send_osc('/audio_level/1', volume)
                    self.levels[0] = volume
                if muted is not None:
                    self._send_osc('/mic', 1.0 if not muted else 0.0)
            elif name == self.audio_sources['desktop']:
                if volume is not None:
                    self._send_osc('/audio_level/2', volume)
                if muted is not None:
                    self._send_osc('/audio', 1.0 if not muted else 0.0)
        else:
            desktop = self.client.call(
                requests.GetVolume(self.audio_sources['desktop']))
            mic = self.client.call(
                requests.GetVolume(self.audio_sources['mic']))

            self._send_osc('/audio_level/2', desktop.getVolume())
            self.levels[1] = desktop.getVolume()
            self._send_osc('/audio', 1.0 if not desktop.getMuted() else 0.0)
            self._send_osc('/audio_level/1', mic.getVolume())
            self.levels[0] = mic.getVolume()
            self._send_osc('/mic', 1.0 if not mic.getMuted() else 0.0)

    def _send_osc(self, address, value):
        self.osc.send_message(address, [value], self.osc_client_host,
                              self.osc_client_port)

    def tick(self):
        if self.volume_changed:
            self.volume_changed = False
            self.client.call(
                requests.SetVolume(
                    list(self.audio_sources.values())[0], self.levels[0]))
            self.client.call(
                requests.SetVolume(
                    list(self.audio_sources.values())[1], self.levels[1]))
예제 #4
0
class OSCSever:
    def __init__(self, activity_server_address, port):
        self.song = SoundAndroidPlayer(self.on_complete)
        self.osc = OSCThreadServer()
        self.osc.listen(port=port, default=True)
        self.activity_server_address = activity_server_address
        self.osc.bind(b'/get_pos', self.get_pos)
        self.osc.bind(b'/load', self.load)
        self.osc.bind(b'/play', self.play)
        self.osc.bind(b'/load_play', self.load_play)
        self.osc.bind(b'/seek', self.seek)
        self.osc.bind(b'/play_new_playlist', self.play_next)
        self.osc.bind(b'/set_volume', self.set_volume)
        self.osc.bind(b'/stop', self.pause)
        self.osc.bind(b'/unload', self.unload)

        self.api = API()
        self.db = Database()
        user = self.db.get_user()
        self.genres = user['genres']
        self.artists = user['artists']
        self.volume = user['volume']
        self.songs_path = user['songs_path']
        self.playlist = self.db.get_playlist()
        self.waiting_for_load = False
        self.seek_pos = 0
        self.downloading = None
        self.waiting_for_download = False
        self.downloads = []

    def check_pos(self, *args):
        if self.song.state == "play" and self.song.length - self.song.get_pos(
        ) < 20:
            next_song = self.playlist.preview_next()
            if next_song is None or next_song.preview_file is None:
                if next_song is None:
                    self.playlist = self.get_new_playlist()
                    next_song = self.playlist.preview_next()
                Logger.debug('SERVICE: Preloading next song.')
                self.download_song(next_song)

    def thread_download_song(self, song):
        self.downloads.append(song.id)
        try:
            res = self.api.download_preview(song)
        except Exception as e:
            Logger.error("SERVICE: Download failed. Reason: %s", e)
        else:
            song.preview_file = save_song(self.songs_path, song, res)
            self.db.update_track(song, 'preview_file', song.preview_file)
            Logger.debug('SERVICE: Downloading song finished.')
        self.downloads.remove(song.id)

    def download_song(self, song):
        if song.id not in self.downloads and song.preview_file is None:
            Logger.debug('SERVICE: Downloading %s.', song.id)
            t = threading.Thread(target=self.thread_download_song,
                                 args=(song, ))
            t.daemon = True
            t.start()
        else:
            Logger.debug(
                'SERVICE: Skipped downloading %s. Already in progress.',
                song.id)

    def get_new_playlist(self):
        Logger.debug('SERVICE: getting new playlist.')
        req = self.api.get_recommendations(
            self.genres,
            self.artists,
            song_type='preview',
        )
        playlist = Playlist(req)
        self.osc.send_message(b'/update_playlist', [],
                              *self.activity_server_address)
        # clean up playlist songs
        favorites = self.db.get_favorites()
        for song in self.playlist.tracks:
            if (song.preview_file and song not in favorites
                    and song != self.song.song_object):
                Logger.debug("Service: Removed %s", song.id)
                os.remove(song.preview_file)
        return playlist

    def get_pos(self, *values):
        pos = self.song.get_pos() if self.song and self.song.is_prepared else 0
        Logger.debug('SERVICE -> ACTIVITY: /pos %s', pos)
        self.osc.answer(b'/pos', [pos])

    def getaddress(self):
        return self.osc.getaddress()

    def load(self, id):
        self.first_load = not getattr(self.song, "id", 0)
        self.song.id = id
        self.song.is_prepared = False
        Logger.debug('SERVICE: Loading %d.', id)
        song = self.db.get_track(id)
        if song.preview_file is None and self.downloading != song.id:
            Logger.debug('SERVICE: %d is not downloaded.', id)
            self.download_song(song)
        if song.id in self.downloads:
            Logger.debug('SERVICE: %d is downloading. Returning.', id)
            self.waiting_for_download = song.id
            return
        Logger.debug('SERVICE: %d file is available.', id)
        self.waiting_for_download = None
        if not self.first_load:
            self.song.reset()
        self.song.load(song.preview_file)
        self.song.song_object = song
        self.db.update_current_track(song)
        self.playlist = self.db.get_playlist()
        self.song.is_prepared = True
        self.update_notification()
        Logger.debug('SERVICE: Song loaded.')

    def load_play(self, id, volume=None):
        Logger.debug('SERVICE: Loading and playing %d.', id)
        self.pause()
        self.load(id)
        self.play(0, volume if volume is not None else self.volume)

    def play(self, seek, volume):
        if not self.song.is_prepared:
            song_id = getattr(self.song, "id", None)
            Logger.debug('SERVICE: %s is not prepared.',
                         song_id if song_id else "Song")
            if not self.waiting_for_download:
                self.load(self.playlist.current_track.id)
            else:
                self.waiting_for_load = True
                self.seek_pos = seek
                self.volume = volume
                return
        else:
            self.waiting_for_load = False
            self.song.is_prepared = True
        self.song.play()
        self.song.seek(seek)
        self.song.volume = volume
        pos = self.song.get_pos()
        Logger.debug('SERVICE -> ACTIVITY: /playing %s.', pos)
        values = [self.song.song_object.id, pos]
        self.osc.send_message(b'/playing', values,
                              *self.activity_server_address)
        Logger.debug('SERVICE: Playing %d.', self.song.song_object.id)
        self.update_notification()

    def stop(self, *values):
        Logger.debug('SERVICE: stopping %d.', self.song.song_object.id)
        self.waiting_for_load = False
        if self.song.is_prepared and self.song.state == 'play':
            self.song.is_prepared = False
            self.song.stop()
        self.update_notification()

    def pause(self, *values):
        Logger.debug('SERVICE: pausing Song.')
        self.waiting_for_load = False
        if self.song.is_prepared and self.song.state == 'play':
            self.song.pause()
        self.update_notification()

    def seek(self, value):
        Logger.debug('SERVICE: seeking %s.', value)
        if self.song.is_prepared:
            self.song.seek(value)

    def set_volume(self, value):
        Logger.debug('SERVICE: setting song volume %s.', value)
        self.volume = value
        if self.song.is_prepared:
            self.song.volume = value

    def on_complete(self, *values):
        Logger.debug('SERVICE -> ACTIVITY: /set_complete')
        self.osc.send_message(b'/set_complete', [True],
                              *self.activity_server_address)
        self.play_next()

    def play_next(self):
        Logger.debug('SERVICE: Playing next.')
        self.stop()
        if self.playlist.is_last:
            self.playlist = self.get_new_playlist()
        song = self.playlist.next()
        self.db.update_playlist(self.playlist)
        self.load_play(song.id)

    def play_previous(self):
        Logger.debug('SERVICE: Playing previous.')
        if not self.playlist.is_first:
            self.stop()
            self.load_play(self.playlist.previous().id)

    def on_state(self, instance, value):
        Logger.debug('SERVICE -> ACTIVITY: /set_state %s', value)
        self.osc.send_message(b'/set_state', [value.encode()],
                              *self.activity_server_address)
        if self.song.is_prepared and self.song.length - self.song.get_pos(
        ) < 0.2:
            self.on_complete()

    def unload(self, *values):
        self.song.unload()

    def update_notification(self):
        notification = self.create_notification()
        notification_manager = context.getSystemService(
            Context.NOTIFICATION_SERVICE)
        notification_manager.notify(1, notification)

    def create_notification(self):
        song = getattr(self.song, "song_object", None)
        if api_version >= 26:
            builder = NotificationCompatBuilder(context, "gtplayer")
        else:
            builder = NotificationCompatBuilder(context)
        (builder.setContentTitle(song.name if song else "GTPlayer").
         setContentText(song.artist if song else "GTPlayer").setContentIntent(
             controller.getSessionActivity()).setDeleteIntent(
                 MediaButtonReceiver.buildMediaButtonPendingIntent(
                     context, PlaybackStateCompat.ACTION_STOP)).setVisibility(
                         NotificationCompat.VISIBILITY_PUBLIC).setSmallIcon(
                             icon))
        style = MediaStyle().setShowActionsInCompactView(0)
        builder.setStyle(style)

        if song is not None and not self.playlist.is_first:
            previous_intent = MediaButtonReceiver.buildMediaButtonPendingIntent(
                context, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
            action = NotificationCompatAction(RDrawable.ic_media_previous,
                                              "Previous", previous_intent)
            builder.addAction(action)

        if self.song.state == "play":
            intent = MediaButtonReceiver.buildMediaButtonPendingIntent(
                context, PlaybackStateCompat.ACTION_PAUSE)
            action = NotificationCompatAction(RDrawable.ic_media_pause,
                                              "Pause", intent)
        else:
            intent = MediaButtonReceiver.buildMediaButtonPendingIntent(
                context, PlaybackStateCompat.ACTION_PLAY)
            action = NotificationCompatAction(RDrawable.ic_media_play, "Play",
                                              intent)
        builder.addAction(action)

        coverart = None
        if song is not None:
            next_intent = MediaButtonReceiver.buildMediaButtonPendingIntent(
                context, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)
            action = NotificationCompatAction(RDrawable.ic_media_next, "Next",
                                              next_intent)
            builder.addAction(action)

            path = os.path.join(images_path, f"{song.id}.png")
            if os.path.isfile(path):
                coverart = path

        coverart = coverart if coverart is not None else "images/empty_coverart.png"
        builder.setLargeIcon(BitmapFactory.decodeFile(coverart))

        return builder.build()