Example #1
0
class Randy(object):
    """
    This class provides methods to get a random song under certain constraints.

    Args:
        config: :class:`~lib.cfg.musicdb.MusicDBConfig` object holding the MusicDB Configuration
        database: A :class:`~lib.db.musicdb.MusicDatabase` instance

    Raises:
        TypeError: When the arguments are not of the correct type.
    """
    def __init__(self, config, database):
        if type(config) != MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(database) != MusicDatabase:
            raise TypeError("database argument not of type MusicDatabase")

        self.db = database
        self.cfg = config
        self.mdbstate = MDBState(self.cfg.server.statedir, self.db)
        self.blacklist = BlacklistInterface(self.cfg, self.db)

        # Load most important keys
        self.nodisabled = self.cfg.randy.nodisabled
        self.nohated = self.cfg.randy.nohated
        self.minlen = self.cfg.randy.minsonglen

    def GetSong(self):
        """
        This method chooses a random song in a two-stage process as described in the module description.

        Returns:
            A song from the :class:`~lib.db.musicdb.MusicDatabase` or ``None`` if an error occurred.
        """
        global BlacklistLock
        global Blacklist

        filterlist = self.mdbstate.GetFilterList()
        if not filterlist:
            logging.warning(
                "No Genre selected! \033[1;30m(Selecting random song from the whole collection)"
            )

        # Get Random Song - this may take several tries
        logging.debug("Randy starts looking for a random song …")
        t_start = datetime.datetime.now()
        song = None
        while not song:
            # STAGE 1: Get Mathematical random song (under certain constraints)
            try:
                song = self.db.GetRandomSong(filterlist, self.nodisabled,
                                             self.nohated, self.minlen)
            except Exception as e:
                logging.error("Getting random song failed with error: \"%s\"!",
                              str(e))
                return None

            if not song:
                logging.error(
                    "There is no song fulfilling the constraints! \033[1;30m(Check the stage 1 constraints)"
                )
                return None

            logging.debug("Candidate for next song: \033[0;35m" + song["path"])

            # GetRandomSong only looks for album genres.
            # The song genre may be different and not in the set of the filerlist.
            try:

                songgenres = self.db.GetTargetTags(
                    "song", song["id"], MusicDatabase.TAG_CLASS_GENRE)
                # Create a set of tagnames if there are tags for this song.
                # Ignore AI set tags because they may be wrong
                if songgenres:
                    tagnames = {
                        songgenre["name"]
                        for songgenre in songgenres
                        if songgenre["approval"] >= 1
                    }
                else:
                    tagnames = {}

                # If the tag name set was successfully created, compare it with the selected genres
                if tagnames:
                    if not tagnames & set(filterlist):
                        logging.debug(
                            "song is of different genre than album and not in activated genres. (Song genres: %s)",
                            str(tagnames))
                        song = None
                        continue

            except Exception as e:
                logging.error("Song tag check failed with exception: \"%s\"!",
                              str(e))
                return None

            # STAGE 2: Make randomness feeling random by checking if the song was recently played
            if self.blacklist.CheckAllLists(song):
                song = None
                continue

        # New song found \o/
        t_stop = datetime.datetime.now()
        logging.debug("Randy found the following song after %s : \033[0;36m%s",
                      str(t_stop - t_start), song["path"])
        return song

    def GetSongFromAlbum(self, albumid):
        """
        Get a random song from a specific album.

        If the selected song is listed in the blacklist for songs, a new one will be selected.
        Entries in the album and artist blacklist will be ignored because the artist and album is forced by the user.
        But the song gets added to the blacklist for songs, as well as the album and artist gets added.

        The genre of the song gets completely ignored.
        The user wants to have a song from the given album, so it gets one.

        .. warning::

            This is a dangerous method.
            An album only has a very limited set of songs.

            If all the songs are listed in the blacklist, the method would get caught in an infinite loop.
            To avoid this, there are only 10 tries to find a random song.
            If after the tenth try, the method leaves returning ``None``

        Args:
            albumid (int): ID of the album the song shall come from

        Returns:
            A song from the :class:`~lib.db.musicdb.MusicDatabase` or ``None`` if an error occurred.
        """
        global BlacklistLock
        global Blacklist

        # Get parameters
        song = None
        tries = 0  # there is just a very limited set of possible songs. Avoid infinite loop when all songs are on the blacklist

        while not song and tries <= 10:
            tries += 1
            # STAGE 1: Get Mathematical random song (under certain constraints)
            try:
                song = self.db.GetRandomSong(None, self.nodisabled,
                                             self.nohated, self.minlen,
                                             albumid)
            except Exception as e:
                logging.error("Getting random song failed with error: \"%s\"!",
                              str(e))
                return None
            logging.debug("Candidate for next song: \033[0;35m" + song["path"])

            # STAGE 2: Make randomness feeling random by checking if the song was recently played
            # only check, if that song is in the blacklist. Artist and album is forced by the user
            if self.blacklist.CheckSongList(song):
                song = None
                continue

        if not song:
            logging.warning(
                "The loop that should find a new random song did not deliver a song! \033[1;30m(This happens when there are too many songs of the given album are already on the blacklist)"
            )
            return None

        # Add song to queue
        logging.debug(
            "Randy adds the following song after %s tries: \033[0;36m%s",
            tries, song["path"])
        return song
Example #2
0
class Randy(object):
    """
    This class provides methods to get a random song under certain constraints.

    This class is made to access the thread form all over the code simultaneously.

    Args:
        config: :class:`~lib.cfg.musicdb.MusicDBConfig` object holding the MusicDB Configuration
        database: A :class:`~lib.db.musicdb.MusicDatabase` instance

    Raises:
        TypeError: When the arguments are not of the correct type.
    """
    def __init__(self, config, database):
        if type(config) != MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(database) != MusicDatabase:
            raise TypeError("database argument not of type MusicDatabase")

        self.db = database
        self.cfg = config
        self.mdbstate = MDBState(self.cfg.server.statedir, self.db)

        # Load most important keys
        self.nodisabled = self.cfg.randy.nodisabled
        self.nohated = self.cfg.randy.nohated
        self.minlen = self.cfg.randy.minsonglen
        self.songbllen = self.cfg.randy.songbllen
        self.albumbllen = self.cfg.randy.albumbllen
        self.artistbllen = self.cfg.randy.artistbllen

        # Check blacklist and create new one if there is none yet
        global Blacklist
        global BlacklistLock

        with BlacklistLock:
            if not Blacklist:
                # try to load the blacklist from MusicDB State
                loadedlists = self.mdbstate.LoadBlacklists()

                # First, create a clean blacklist
                Blacklist = {}
                Blacklist["songs"] = [None] * self.songbllen
                Blacklist["albums"] = [None] * self.albumbllen
                Blacklist["artists"] = [None] * self.artistbllen

                # Now fill the blacklist considering changes in their size
                for key in loadedlists:
                    if key not in ["songs", "albums", "artists"]:
                        logging.error(
                            "Unexpected key \"%s\" in loaded blacklist dictionary! \033[1;30(Will be discard)",
                            str(key))
                        continue

                    dst = Blacklist[key]
                    src = loadedlists[key]
                    if not src:
                        continue  # when there are no entries loaded, keep the generated list of None-entries

                    # The following python magic inserts as much of src at the end of dst, as fits.
                    # if src must be cut, the first elements get removed
                    # So, case 1: len(dst) > len(src)
                    #   dst = [None, None, None, None, None]
                    #   src = [1, 2, 3]
                    #   Results in [None, None, 1, 2, 3]
                    #
                    # Case 2: len(dst) < len(src)
                    #   dst = [None, None]
                    #   src = [1, 2, 3]
                    #   Results in [2, 3]
                    #
                    # >>> l = [1,2,3]
                    # >>> d = [0]*3
                    # >>> d[-len(l):] = l[-len(d):]
                    # >>> l
                    # [1, 2, 3]
                    # >>> d
                    # [1, 2, 3]
                    # >>> d = [0]*2
                    # >>> d[-len(l):] = l[-len(d):]
                    # >>> l
                    # [1, 2, 3]
                    # >>> d
                    # [2, 3]
                    # >>> d = [0]*4
                    # >>> d[-len(l):] = l[-len(d):]
                    # >>> l
                    # [1, 2, 3]
                    # >>> d
                    # [0, 1, 2, 3]
                    # >>>
                    dst = dst[-len(src):] = src[-len(dst):]

                    Blacklist[key] = dst

    def AddSongToBlacklist(self, song):
        """
        This method pushes a song onto the blacklists.
        If the song is ``None`` nothing happens.

        This method should be the only place where the blacklist gets changed.
        After adding a song, the lists get stored in the MusicDB State Directory to be persistent

        Args:
            song (dict): A song from the :class:`~lib.db.musicdb.MusicDatabase`

        Returns:
            *Nothing*
        """
        if not song:
            return

        global BlacklistLock
        global Blacklist

        with BlacklistLock:
            if self.artistbllen > 0:
                Blacklist["artists"].pop(0)
                Blacklist["artists"].append(song["artistid"])
            if self.albumbllen > 0:
                Blacklist["albums"].pop(0)
                Blacklist["albums"].append(song["albumid"])
            if self.songbllen > 0:
                Blacklist["songs"].pop(0)
                Blacklist["songs"].append(song["id"])

            # Save blacklists to files
            self.mdbstate.SaveBlacklists(Blacklist)

    def GetSong(self):
        """
        This method chooses a random song in a two-stage process as described in the module description.

        Returns:
            A song from the :class:`~lib.db.musicdb.MusicDatabase` or ``None`` if an error occurred.
        """
        global BlacklistLock
        global Blacklist

        filterlist = self.mdbstate.GetFilterList()
        if not filterlist:
            logging.warning(
                "No Genre selected! \033[1;30m(Selecting random song from the whole collection)"
            )

        # Get Random Song - this may take several tries
        logging.debug("Randy starts looking for a random song …")
        t_start = datetime.datetime.now()
        song = None
        while not song:
            # STAGE 1: Get Mathematical random song (under certain constraints)
            try:
                song = self.db.GetRandomSong(filterlist, self.nodisabled,
                                             self.nohated, self.minlen)
            except Exception as e:
                logging.error("Getting random song failed with error: \"%s\"!",
                              str(e))
                return None

            if not song:
                logging.error(
                    "There is no song fulfilling the constraints! \033[1;30m(Check the stage 1 constraints)"
                )
                return None

            logging.debug("Candidate for next song: \033[0;35m" + song["path"])

            # GetRandomSong only looks for album genres.
            # The song genre may be different and not in the set of the filerlist.
            try:

                songgenres = self.db.GetTargetTags(
                    "song", song["id"], MusicDatabase.TAG_CLASS_GENRE)
                # Create a set of tagnames if there are tags for this song.
                # Ignore AI set tags because they may be wrong
                if songgenres:
                    tagnames = {
                        songgenre["name"]
                        for songgenre in songgenres
                        if songgenre["approval"] >= 1
                    }
                else:
                    tagnames = {}

                # If the tag name set was successfully created, compare it with the selected genres
                if tagnames:
                    if not tagnames & set(filterlist):
                        logging.debug(
                            "song is of different genre than album and not in activated genres. (Song genres: %s)",
                            str(tagnames))
                        song = None
                        continue

            except Exception as e:
                logging.error("Song tag check failed with exception: \"%s\"!",
                              str(e))
                return None

            # STAGE 2: Make randomness feeling random by checking if the song was recently played
            with BlacklistLock:
                if self.artistbllen > 0 and song["artistid"] in Blacklist[
                        "artists"]:
                    logging.debug("artist on blacklist")
                    song = None
                    continue
                if self.albumbllen > 0 and song["albumid"] in Blacklist[
                        "albums"]:
                    logging.debug("album on blacklist")
                    song = None
                    continue
                if self.songbllen > 0 and song["id"] in Blacklist["songs"]:
                    logging.debug("song on blacklist")
                    song = None
                    continue

        # New song found \o/
        # Add song into blacklists
        self.AddSongToBlacklist(song)

        t_stop = datetime.datetime.now()
        logging.debug("Randy found the following song after %s : \033[0;36m%s",
                      str(t_stop - t_start), song["path"])
        return song

    def GetSongFromAlbum(self, albumid):
        """
        Get a random song from a specific album.

        If the selected song is listed in the blacklist for songs, a new one will be selected.
        Entries in the album and artist blacklist will be ignored because the artist and album is forced by the user.
        But the song gets added to the blacklist for songs, as well as the album and artist gets added.

        The genre of the song gets completely ignored.
        The user wants to have a song from the given album, so it gets one.

        .. warning::

            This is a dangerous method.
            An album only has a very limited set of songs.

            If all the songs are listed in the blacklist, the method would get caught in an infinite loop.
            To avoid this, there are only 10 tries to find a random song.
            If after the tenth try, the method leaves returning ``None``

        Args:
            albumid (int): ID of the album the song shall come from

        Returns:
            A song from the :class:`~lib.db.musicdb.MusicDatabase` or ``None`` if an error occurred.
        """
        global BlacklistLock
        global Blacklist

        # Get parameters
        song = None
        tries = 0  # there is just a very limited set of possible songs. Avoid infinite loop when all songs are on the blacklist

        while not song and tries <= 10:
            tries += 1
            # STAGE 1: Get Mathematical random song (under certain constraints)
            try:
                song = self.db.GetRandomSong(None, self.nodisabled,
                                             self.nohated, self.minlen,
                                             albumid)
            except Exception as e:
                logging.error("Getting random song failed with error: \"%s\"!",
                              str(e))
                return None
            logging.debug("Candidate for next song: \033[0;35m" + song["path"])

            # STAGE 2: Make randomness feeling random by checking if the song was recently played
            # only check, if that song is in the blacklist. Artist and album is forced by the user
            with BlacklistLock:
                if self.songbllen > 0 and song["id"] in Blacklist["songs"]:
                    logging.debug("song on blacklist")
                    song = None
                    continue

        if not song:
            logging.warning(
                "The loop that should find a new random song did not deliver a song! \033[1;30m(This happens when there are too many songs of the given album are already on the blacklist)"
            )
            return None

        # maintain blacklists
        self.AddSongToBlacklist(song)

        # Add song to queue
        logging.debug(
            "Randy adds the following song after %s tries: \033[0;36m%s",
            tries, song["path"])
        return song