(self)
The main loop of the player.
        Takes a song from the queue and plays it until it is finished.
The main loop of the player.
        Takes a song from the queue and plays it until it is finished.
The main loop of the player . Takes a song from the queue and plays it until it is finished .
def loop(self) -> None:
        """The main loop of the player.
        Takes a song from the queue and plays it until it is finished."""
        while True:

            if redis.get("stop_playback_loop"):
                break

            catch_up = None
            self.playback_started.clear()

            if models.CurrentSong.objects.exists():
                # recover interrupted song from database
                current_song = models.CurrentSong.objects.get()

                # continue with the current song (approximately) where we last left
                if storage.get("paused"):
                    catch_up = round(
                        (
                            current_song.last_paused - current_song.created
                        ).total_seconds()
                        * 1000
                    )
                else:
                    catch_up = round(
                        (timezone.now() - current_song.created).total_seconds() * 1000
                    )
                if catch_up > current_song.duration * 1000:
                    catch_up = -1
            else:
                if queue.count() == 0:
                    queue_changed.wait()
                    queue_changed.clear()

                    # restart the loop to check again whether songs are available
                    # in case of a false wakeup this causes as to wait again
                    continue

                # select the next song depending on settings
                song: Optional[models.QueuedSong]
                if storage.get("voting_enabled"):
                    with transaction.atomic():
                        song = queue.confirmed().order_by("-votes", "index")[0]
                        song_id = song.id
                        queue.remove(song.id)
                elif storage.get("shuffle"):
                    confirmed = queue.confirmed()
                    index = random.randint(0, confirmed.count() - 1)
                    song_id = confirmed[index].id
                    song = queue.remove(song_id)
                else:
                    # move the first song in the queue into the current song
                    song_id, song = queue.dequeue()

                if song is None:
                    # either the semaphore didn't match up with the actual count
                    # of songs in the queue or a race condition occured
                    logging.warning("dequeued on empty list")
                    continue

                if song.internal_url == "alarm":
                    self.play_alarm()
                    continue

                # stop backup stream.
                # when the dequeued song starts playing, the backup stream playback is stopped
                redis.set("backup_playing", False)

                current_song = models.CurrentSong.objects.create(
                    queue_key=song_id,
                    manually_requested=song.manually_requested,
                    votes=song.votes,
                    internal_url=song.internal_url,
                    external_url=song.external_url,
                    stream_url=song.stream_url,
                    artist=song.artist,
                    title=song.title,
                    duration=song.duration,
                )

                handle_autoplay()

                try:
                    archived_song = models.ArchivedSong.objects.get(
                        url=current_song.external_url
                    )
                    votes: Optional[int]
                    if storage.get("voting_enabled"):
                        votes = current_song.votes
                    else:
                        votes = None
                    if storage.get("logging_enabled"):
                        models.PlayLog.objects.create(
                            song=archived_song,
                            manually_requested=current_song.manually_requested,
                            votes=votes,
                        )
                except (
                    models.ArchivedSong.DoesNotExist,
                    models.ArchivedSong.MultipleObjectsReturned,
                ):
                    pass

            with mopidy_command(important=True):
                # after a restart consume may be set to False again, so make sure it is on
                self.player.tracklist.clear()
                self.player.tracklist.set_consume(True)
                self.player.tracklist.add(uris=[current_song.internal_url])
                # temporarily mute mopidy in case we need to seek but mopidy does not react directly
                # this allows us to seek first and then unmute, preventing audible skips
                volume = self.player.mixer.get_volume()
                if catch_up is not None and catch_up >= 0:
                    self.player.mixer.set_volume(0)
                # mopidy can only seek when the song is playing
                # also we do not continue without the playing state properly set.
                # otherwise waiting might exit before the song started
                self.player.playback.play()
                if not self.playback_started.wait(timeout=1):
                    # mopidy did not acknowledge that it started the song
                    # to make sure it is not in an error state,
                    # restart the loop and retry to start the song
                    # also prevents "queue-eating" bug,
                    # where mopidy in a failed state would refuse to play any song,
                    # but raveberry keeps on sending songs from the queue
                    logging.warning("playback_started event did not trigger")
                    set_playback_error(True)
                    self.player.mixer.set_volume(volume)
                    continue
                set_playback_error(False)
                if catch_up is not None and catch_up >= 0:
                    self.player.playback.seek(catch_up)
                    if storage.get("paused"):
                        self.player.playback.pause()
                    self.player.mixer.set_volume(volume)
            redis.set("playing", True)

            # needs some more testing but could prevent "eating the queue" bug
            # if not started_playing and not settings.DOCKER:
            #    logging.error(
            #        "Mopidy did not start playing the song in the timeout. Error assumed.\n"
            #        "Usually faulty outputs are the reason, config is reset to local output.\n"
            #        "Consult mopidy's log for more information.\n"
            #    )
            #    # reset to pulse device 0
            #    core.settings.sound._set_output("0")
            #    # recover the current song by restarting the loop
            #    continue

            musiq.update_state()

            # don't wait for the song to end if catch_up is negative (=the song should be skipped)
            if catch_up is None or catch_up >= 0:
                if not self.wait_until_song_end():
                    # there was an error while waiting for the song to end
                    # This happens when we could not connect to mopidy (ConnectionError)
                    # or when an interrupting alarm was initiated.
                    # we do not delete the current song but recover its state by restarting the loop
                    storage.set("paused", False)
                    redis.set("playing", False)
                    continue
            # Allowing new songs to start playing while paused introduces many edge cases
            # Instead of dealing with them, always start playback when skipping a paused song
            storage.set("paused", False)
            redis.set("playing", False)

            current_song.delete()

            if storage.get("repeat"):
                queue.enqueue(
                    {
                        "artist": current_song.artist,
                        "title": current_song.title,
                        "duration": current_song.duration,
                        "internal_url": current_song.internal_url,
                        "external_url": current_song.external_url,
                        "stream_url": current_song.stream_url,
                    },
                    False,
                )
                queue_changed.set()

            if user_manager.partymode_enabled() and random.random() < storage.get(
                "alarm_probability"
            ):
                self.play_alarm()

            if not queue.exists() and storage.get("backup_stream"):
                redis.set("backup_playing", True)
                # play backup stream
                self.player.tracklist.add(uris=[storage.get("backup_stream")])
                self.player.playback.play()

            musiq.update_state()
def loop ( self ) -> None : while True : if redis . get ( "stop_playback_loop" ) : break catch_up = None self . playback_started . clear ( ) if models . CurrentSong . objects . exists ( ) : # recover interrupted song from database current_song = models . CurrentSong . objects . get ( ) # continue with the current song (approximately) where we last left if storage . get ( "paused" ) : catch_up = round ( ( current_song . last_paused - current_song . created ) . total_seconds ( ) * 1000 ) else : catch_up = round ( ( timezone . now ( ) - current_song . created ) . total_seconds ( ) * 1000 ) if catch_up > current_song . duration * 1000 : catch_up = - 1 else : if queue . count ( ) == 0 : queue_changed . wait ( ) queue_changed . clear ( ) # restart the loop to check again whether songs are available # in case of a false wakeup this causes as to wait again continue # select the next song depending on settings song : Optional [ models . QueuedSong ] if storage . get ( "voting_enabled" ) : with transaction . atomic ( ) : song = queue . confirmed ( ) . order_by ( "-votes" , "index" ) [ 0 ] song_id = song . id queue . remove ( song . id ) elif storage . get ( "shuffle" ) : confirmed = queue . confirmed ( ) index = random . randint ( 0 , confirmed . count ( ) - 1 ) song_id = confirmed [ index ] . id song = queue . remove ( song_id ) else : # move the first song in the queue into the current song song_id , song = queue . dequeue ( ) if song is None : # either the semaphore didn't match up with the actual count # of songs in the queue or a race condition occured logging . warning ( "dequeued on empty list" ) continue if song . internal_url == "alarm" : self . play_alarm ( ) continue # stop backup stream. # when the dequeued song starts playing, the backup stream playback is stopped redis . set ( "backup_playing" , False ) current_song = models . CurrentSong . objects . create ( queue_key = song_id , manually_requested = song . manually_requested , votes = song . votes , internal_url = song . internal_url , external_url = song . external_url , stream_url = song . stream_url , artist = song . artist , title = song . title , duration = song . duration , ) handle_autoplay ( ) try : archived_song = models . ArchivedSong . objects . get ( url = current_song . external_url ) votes : Optional [ int ] if storage . get ( "voting_enabled" ) : votes = current_song . votes else : votes = None if storage . get ( "logging_enabled" ) : models . PlayLog . objects . create ( song = archived_song , manually_requested = current_song . manually_requested , votes = votes , ) except ( models . ArchivedSong . DoesNotExist , models . ArchivedSong . MultipleObjectsReturned , ) : pass with mopidy_command ( important = True ) : # after a restart consume may be set to False again, so make sure it is on self . player . tracklist . clear ( ) self . player . tracklist . set_consume ( True ) self . player . tracklist . add ( uris = [ current_song . internal_url ] ) # temporarily mute mopidy in case we need to seek but mopidy does not react directly # this allows us to seek first and then unmute, preventing audible skips volume = self . player . mixer . get_volume ( ) if catch_up is not None and catch_up >= 0 : self . player . mixer . set_volume ( 0 ) # mopidy can only seek when the song is playing # also we do not continue without the playing state properly set. # otherwise waiting might exit before the song started self . player . playback . play ( ) if not self . playback_started . wait ( timeout = 1 ) : # mopidy did not acknowledge that it started the song # to make sure it is not in an error state, # restart the loop and retry to start the song # also prevents "queue-eating" bug, # where mopidy in a failed state would refuse to play any song, # but raveberry keeps on sending songs from the queue logging . warning ( "playback_started event did not trigger" ) set_playback_error ( True ) self . player . mixer . set_volume ( volume ) continue set_playback_error ( False ) if catch_up is not None and catch_up >= 0 : self . player . playback . seek ( catch_up ) if storage . get ( "paused" ) : self . player . playback . pause ( ) self . player . mixer . set_volume ( volume ) redis . set ( "playing" , True ) # needs some more testing but could prevent "eating the queue" bug # if not started_playing and not settings.DOCKER: #    logging.error( #        "Mopidy did not start playing the song in the timeout. Error assumed.\n" #        "Usually faulty outputs are the reason, config is reset to local output.\n" #        "Consult mopidy's log for more information.\n" #    ) #    # reset to pulse device 0 #    core.settings.sound._set_output("0") #    # recover the current song by restarting the loop #    continue musiq . update_state ( ) # don't wait for the song to end if catch_up is negative (=the song should be skipped) if catch_up is None or catch_up >= 0 : if not self . wait_until_song_end ( ) : # there was an error while waiting for the song to end # This happens when we could not connect to mopidy (ConnectionError) # or when an interrupting alarm was initiated. # we do not delete the current song but recover its state by restarting the loop storage . set ( "paused" , False ) redis . set ( "playing" , False ) continue # Allowing new songs to start playing while paused introduces many edge cases # Instead of dealing with them, always start playback when skipping a paused song storage . set ( "paused" , False ) redis . set ( "playing" , False ) current_song . delete ( ) if storage . get ( "repeat" ) : queue . enqueue ( { "artist" : current_song . artist , "title" : current_song . title , "duration" : current_song . duration , "internal_url" : current_song . internal_url , "external_url" : current_song . external_url , "stream_url" : current_song . stream_url , } , False , ) queue_changed . set ( ) if user_manager . partymode_enabled ( ) and random . random ( ) < storage . get ( "alarm_probability" ) : self . play_alarm ( ) if not queue . exists ( ) and storage . get ( "backup_stream" ) : redis . set ( "backup_playing" , True ) # play backup stream self . player . tracklist . add ( uris = [ storage . get ( "backup_stream" ) ] ) self . player . playback . play ( ) musiq . update_state ( )