示例#1
0
    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.songbllen = self.cfg.randy.songbllen
        self.albumbllen = self.cfg.randy.albumbllen
        self.artistbllen = self.cfg.randy.artistbllen
        self.videobllen = self.cfg.randy.videobllen

        # 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
                Blacklist = self.mdbstate.LoadBlacklists(
                    self.songbllen, self.albumbllen, self.artistbllen,
                    self.videobllen)
示例#2
0
    def __init__(self, config, database):
        if type(config) is not MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(database) is not 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)
        self.randy = Randy(self.cfg, self.db)

        global Queue
        global QueueLock

        with QueueLock:
            if Queue == None:
                logging.debug("Loading Video Queue…")
                try:
                    self.Load()
                except Exception as e:
                    logging.warning(
                        "Loading video queue failed with error: %s. \033[1;30m(Creating an empty one)",
                        str(e))
                    Queue = []
示例#3
0
文件: randy.py 项目: fat84/musicdb-1
    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
示例#4
0
class VideoQueue(object):
    """
    This class implements a queue to manage videos to play.
    Whenever the queue changes, its data gets stored in the MusicDB State Directory

    When the constructor detects that there is no queue yet (not even an empty one),
    it tries to load the stored queue.
    If this fails, a new empty queue gets created.

    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) is not MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(database) is not 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)
        self.randy = Randy(self.cfg, self.db)

        global Queue
        global QueueLock

        with QueueLock:
            if Queue == None:
                logging.debug("Loading Video Queue…")
                try:
                    self.Load()
                except Exception as e:
                    logging.warning(
                        "Loading video queue failed with error: %s. \033[1;30m(Creating an empty one)",
                        str(e))
                    Queue = []

    #####################################################################
    # Event Management                                                  #
    #####################################################################

    def RegisterCallback(self, function):
        """
        Register a callback function that reacts on Video Queue related events.
        For more details see the module description at the top of this document.

        Args:
            function: A function that shall be called on an event.

        Returns:
            *Nothing*
        """
        global Callbacks
        Callbacks.append(function)

    def RemoveCallback(self, function):
        """
        Removes a function from the list of callback functions.

        Args:
            function: A function that shall be called removed.

        Returns:
            *Nothing*
        """
        global Callbacks

        # Not registered? Then do nothing.
        if not function in Callbacks:
            logging.warning(
                "A Video Queue callback function should be removed, but did not exist in the list of callback functions!"
            )
            return

        Callbacks.remove(function)

    def TriggerEvent(self, name, arg=None):
        """
        This function triggers an event.
        It iterates through all registered callback functions and calls them.

        The arguments to the functions are the name of the even (``name``) and addition arguments (``arg``).
        That argument will be ``None`` if there is no argument.

        More details about events can be found in the module description at the top of this document.

        Args:
            name (str): Name of the event
            arg: Additional arguments to the event, or ``None``

        Returns:
            *Nothing*
        """
        global Callbacks
        for callback in Callbacks:
            try:
                callback(name, arg)
            except Exception as e:
                logging.exception(
                    "A Video Queue event callback function crashed!")

    def Event_VideoQueueChanged(self):
        """
        See :meth:`~TriggerEvent` with event name ``"VideoQueueChanged"``.
        More details in the module description at the top of this document.

        This method also tries to save the queue into the MusicDB State Directory.
        """
        try:
            self.Save()
        except Exception as e:
            logging.warning(
                "Saving the current video queue failed with error: %s. \033[1;30m(Continuing without saving)",
                str(e))
        self.TriggerEvent("VideoQueueChanged")

    def Event_VideoChanged(self):
        """
        See :meth:`~TriggerEvent` with event name ``"VideoChanged"``
        More details in the module description at the top of this document.
        """
        self.TriggerEvent("VideoChanged")

    #####################################################################
    # Queue Management                                                  #
    #####################################################################

    def Save(self):
        """
        Save the current queue into a csv file in the MusicDB State Directory.
        Therefor the :meth:`lib.cfg.mdbstate.MDBState.SaveVideoQueue` gets used.

        Returns:
            *Nothing*
        """
        global Queue
        global QueueLock

        with QueueLock:
            self.mdbstate.SaveVideoQueue(Queue)

    def Load(self):
        """
        This method loads the last stored video queue for the MusicDB State Directory
        via :meth:`lib.cfg.mdbstate.MDBState.LoadVideoQueue`.

        Returns:
            *Nothing*
        """
        global Queue
        global QueueLock

        with QueueLock:
            Queue = self.mdbstate.LoadVideoQueue()

    def GenerateID(self):
        """
        This method generate a unique ID.
        In detail, it is a `Version 4 Universally Unique Identifier (UUID) <https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)>`_ .
        It will be returned as an integer.

        This method is build for the internal use in this class.

        Returns:
            A UUID to be used as entry ID

        Example:

            .. code-block:: python

                queue = VideoQueue()
                uuid  = queue.GenerateID()
                print(type(uuid))   # int
        """
        return uuid.uuid4().int

    def CurrentVideo(self):
        """
        This method returns the current video in the queue.

        The method returns element 0 from the queue which is the current video
        that can be streamed or gets already streamed.
        The video shall remain in the queue until it got completely streamed.
        Then it can be removed by calling :meth:`~mdbapi.videoqueue.VideoQueue.NextVideo`.

        When the queue is empty, a new random video gets added.
        This is the exact same video that then will be returned by this method.
        If adding a new video fails, ``None`` gets returned.
        This method triggers the ``VideoQueueChanged`` event when the queue was empty and a new random video got added.

        Returns:
            A dictionary as described in the module description

        Example:

            .. code-block:: python

                queue = VideoQueue()

                # Queue will be empty after creating a VideoQueue object
                entry = queue.CurrentVideo()
                if entry:
                    print("Random VideoID: %s" % (str(entry["videoid"])))
                else
                    print("Queue is empty! - Adding random video failed!")

                # Adds two new video with ID 7357 and 1337. 
                # Then the current video is the first video added.
                queue.AddVideo(7357)
                queue.AddVideo(1337)
                entry = queue.CurrentVideo()
                if entry:
                    print("VideoID: %s" % (str(entry["videoid"]))) # 7357
                else
                    # will not be reached because we explicitly added videos.
                    print("Queue is empty! - Adding random videofailed!")

        """
        global Queue
        global QueueLock

        with QueueLock:
            # Empty Queue? Add a random video!
            if len(Queue) == 0:
                self.AddRandomVideo()
                self.Event_VideoQueueChanged()

            # Still empty (no random video found)? Then return None. Nothing to do…
            if len(Queue) == 0:
                logging.critical(
                    "Queue run empty! \033[1;30m(Check constraints for random video selection and check if there are videos at all)"
                )
                return None

            # Select first video from queue
            entry = Queue[0]

        return entry

    def NextVideo(self):
        """
        This method returns the next video in the queue.
        This entry will be the next current video.

        The method pops the last current element from the queue.
        Then the new element at position 0, the new current element, will be returned.

        If the queue is empty, ``None`` gets returned.

        .. warning::

            In context of streaming, this method may not be the one you want to call.
            This Method drops the current video and sets the next video on top of the queue.

            The stream will not notice this, so that it continues streaming the previous video. (See :doc:`/mdbapi/videostream`).
            If you want to stream the next video, call :meth:`mdbapi.videostream.VideoStreamManager.PlayNextVideo`.

            The :meth:`mdbapi.videostream.VideoStreamManager.PlayNextVideo` then makes the Streaming Thread calling this method.

        This method triggers the ``VideoChanged`` and ``VideoQueueChanged`` event when the queue was not empty.
        The ``VideoChanged`` event gets also triggered when there was no next video.

        When there is only one entry left in the queue - the current video - then a new one gets add via :meth:`AddRandomVideo`

        Returns:
            The new current video entry in the queue as dictionary described in the module description

        Example:

            .. code-block:: python

                queue = VideoQueue()

                # Adds two new video with ID 7357 and 1337. 
                queue.AddVideo(7357)
                queue.AddVideo(1337)
            
                entry = queue.CurrentVideo()
                print("VideoID: %s" % (str(entry["videoid"]))) # 7357

                entry = queue.NextVideo()
                print("VideoID: %s" % (str(entry["videoid"]))) # 1337
                entry = queue.CurrentVideo()
                print("VideoID: %s" % (str(entry["videoid"]))) # 1337

        """
        global Queue
        global QueueLock

        with QueueLock:
            if len(Queue) == 0:
                return None

            # Get next video
            if len(Queue) == 1:
                Queue.pop(0)
                entry = None
            else:  # > 1
                Queue.pop(0)
                entry = Queue[0]

            # Make sure the queue never runs empty
            if len(Queue) < 2:
                self.AddRandomVideo()

        self.Event_VideoChanged()
        self.Event_VideoQueueChanged()
        return entry

    def GetQueue(self):
        """
        This method returns a copy of the video queue.

        The queue is a list of dictionaries.
        The content of the dictionary is described in the description of this module.

        Returns:
            The current video queue. ``[None]`` if there is no queue yet.

        Example:

            .. code-block:: python

                queue = videoqueue.GetQueue()

                if not queue:
                    print("There are no videos in the queue")
                else:
                    for entry in queue:
                        print("Element with ID %i holds the video with ID %i" 
                                % (entry["entryid"], entry["videoid"]))

        """
        global Queue
        return list(Queue)

    def AddVideo(self, videoid, position="last", israndom=False):
        """
        With this method, a new video can be insert into the queue.

        The position in the queue, where the video gets insert can be changed by setting the ``position`` argument:

            * ``"last"`` (default): Appends the video at the end of the queue
            * ``"next"``: Inserts the video right after the current playing video.
            * *Integer*: Entry-ID after that the video shall be inserted.

        On success, this method triggers the ``VideoQueueChanged`` event.

        When the video shall be put at the beginning of the queue, then it gets set to index 1 not index 0.
        So the current playing video (index 0) remains!

        The new video gets added to the :mod:`~mdbapi.blacklist` via :meth:`mdbapi.blacklist.BlacklistInterface.AddVideo`
        The method also triggers the ``VideoQueueChanged`` event.

        Args:
            videoid (int): The ID of the video that shall be added to the queue
            position (str/int): Defines the position where the video gets inserted
            israndom (bool): Defines whether the video is randomly selected or not

        Returns:
            *Nothing*

        Raises:
            TypeError: When ``videoid`` is not of type ``int``
        """
        if type(videoid) is not int:
            raise TypeError("Video ID must be an integer!")

        entryid = self.GenerateID()

        newentry = {}
        newentry["entryid"] = entryid
        newentry["videoid"] = videoid
        newentry["israndom"] = israndom

        global Queue
        global QueueLock

        with QueueLock:
            if position == "next":
                Queue.insert(1, newentry)

            elif position == "last":
                Queue.append(newentry)

            elif type(position) == int:
                for index, entry in enumerate(Queue):
                    if entry["entryid"] == position:
                        Queue.insert(index + 1, newentry)
                        break
                else:
                    logging.warning(
                        "Queue Entry ID %s does not exist. \033[1;30m(Doing nothing)",
                        str(position))
            else:
                logging.warning(
                    "Position must have the value \"next\" or \"last\" or an Queue Entry ID. Given was \"%s\". \033[1;30m(Doing nothing)",
                    str(position))
                return

        # add to blacklist
        self.blacklist.AddVideo(videoid)

        self.Event_VideoQueueChanged()
        return

    def AddRandomVideo(self, position="last"):
        """
        This method adds a random video into the queue.

        The position in the queue, where the video gets insert can be changed by setting the ``position`` argument:

            * ``"last"`` (default): Appends the video at the end of the queue
            * ``"next"``: Inserts the video right after the current playing video.

        The method :meth:`mdbapi.randy.Randy.GetVideo` will be used to get a random video from the activated genres.

        After selecting the random video, the :meth:`~AddVideo` method gets used to insert the new video into the queue.
        If there is no video found by Randy, then nothing gets added to the queue and ``False`` will be returned.

        Args:
            position (str): Defines the position where the video gets inserted.
            albumid (int/NoneType): ID of the album from that the video will be selected, or ``None`` for selecting a video from the activated genres.

        Returns:
            ``True`` when a random video got added to the queue. Otherwise ``False``.

        Raises:
            TypeError: When one of the types of the arguments are not correct
        """
        if type(position) != str:
            raise TypeError("Position must be a string!")

        mdbvideo = self.randy.GetVideo()

        if not mdbvideo:
            return False

        self.AddVideo(mdbvideo["id"], position, israndom=True)
        return True

    def GetVideo(self, entryid):
        """
        Returns the video ID of the entry addressed by the entry ID

        Args:
            entryid (int): ID of the entry that video ID shall be returned

        Returns:
            The video ID of the entry, or ``None`` if the entry does not exists

        Raises:
            TypeError: When ``entryid`` is not of type ``int``
        """
        if type(entryid) is not int:
            raise TypeError("Entry ID must be an integer!")

        global Queue
        global QueueLock

        with QueueLock:
            for entry in Queue:
                if entry["entryid"] == entryid:
                    return entry["videoid"]

        logging.debug(
            "Cannot find the requested entry %s! \033[1;30m(Returning None)",
            str(entryid))
        return None

    def RemoveVideo(self, entryid):
        """
        Removes the entry with the ID ``entryid`` from the queue.
        Removing the current video is not allowed!
        Call :meth:`~NextVideo` instead.

        When there is only one entry left in the queue - the current video - then a new one gets add via :meth:`~AddRandomVideo`

        On success, this method triggers the ``VideoQueueChanged`` event.

        Args:
            entryid (int): Entry to remove

        Returns:
            ``True`` on success, otherwise ``False``

        Raises:
            TypeError: When ``entryid`` is not of type ``int``
        """
        if type(entryid) is not int:
            raise TypeError("Entry ID must be an integer!")

        global Queue
        global QueueLock

        with QueueLock:
            if len(Queue) < 2:
                logging.warning(
                    "The queue has only %i element. There must be at least 2 entries to be able to remove one.",
                    len(Queue))
                return False

            if Queue[0]["entryid"] == entryid:
                logging.warning(
                    "The entry ID addresses the current video. This entry cannot be removed!"
                )
                return False

            Queue = [entry for entry in Queue if entry["entryid"] != entryid]

            # Make sure the queue never runs empty
            if len(Queue) < 2:
                self.AddRandomVideo()

        self.Event_VideoQueueChanged()
        return True

    def MoveVideo(self, entryid, afterid):
        """
        This method moves an entry, addressed by ``entryid`` behind another entry addressed by ``afterid``.
        If both IDs are the same, the method returns immediately without doing anything.
        When ``entryid`` addresses the current video, the method returns with value ``False``

        On success, the method triggers the ``VideoQueueChanged`` event.

        Args:
            entryid (int):
            afterid (int):

        Returns:
            ``True`` when the entry was moved, otherwise ``False``

        Raises:
            TypeError: When ``entryid`` or ``afterid`` is not of type int
        """
        if entryid == afterid:
            return False

        # First check, if everything is OK with the arguments
        if type(entryid) is not int or type(afterid) is not int:
            raise TypeError("Queue entry IDs must be of type int!")

        global Queue
        global QueueLock

        with QueueLock:
            if Queue[0]["entryid"] == entryid:
                logging.warning(
                    "The entry ID addresses the current video. This entry cannot be moved!"
                )
                return False

            # Get Positions
            frompos = [
                pos for pos, entry in enumerate(Queue)
                if entry["entryid"] == entryid
            ]
            topos = [
                pos for pos, entry in enumerate(Queue)
                if entry["entryid"] == afterid
            ]

            if not frompos:
                logging.warning(
                    "Cannot find element with entryid %i in the queue!\033[1;30m (Doing nothing)",
                    entryid)
                return False
            if not topos:
                logging.warning(
                    "Cannot find element with afterid %i in the queue!\033[1;30m (Doing nothing)",
                    afterid)
                return False

            frompos = frompos[0]
            topos = topos[0]

            # When topos is behind frompos, decrement topos because if shifts one entry down due to popping the frompos-element from the list
            if topos < frompos:
                topos += 1

            # Move element
            entry = Queue.pop(frompos)
            Queue.insert(topos, entry)

        self.Event_VideoQueueChanged()
        return True
示例#5
0
class BlacklistInterface(object):
    """
    This class provides methods to manage the blacklists used by :class:`~mdbapi.randy.Randy`.

    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.songbllen = self.cfg.randy.songbllen
        self.albumbllen = self.cfg.randy.albumbllen
        self.artistbllen = self.cfg.randy.artistbllen
        self.videobllen = self.cfg.randy.videobllen

        # 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
                Blacklist = self.mdbstate.LoadBlacklists(
                    self.songbllen, self.albumbllen, self.artistbllen,
                    self.videobllen)

    def ValidateBlacklist(self, blacklistname):
        """
        This method validates a specific blacklist.
        It checks each entry in the list if its time stamp is still in the configured time range.
        If not, the entry gets set to ``None``.

        Args:
            blacklistname (str): A valid name for the blacklist. Valid names are: ``"songs"``, ``"albums"``, ``"artists"``

        Returns:
            *Nothing*

        Raises:
            ValueError: When blacklistname is not ``"videos"``, ``"songs"``, ``"albums"`` or ``"artists"``
        """
        if blacklistname not in ["songs", "albums", "artists", "videos"]:
            raise ValueError(
                "blacklistname must be \"videos\", \"songs\", \"albums\" or \"artists\"!"
            )

        timelimit = time.time() - self.cfg.randy.maxblage * 60 * 60

        global BlacklistLock
        global Blacklist

        removed = 0
        with BlacklistLock:
            for entry in Blacklist[blacklistname]:
                if entry["timestamp"] == None:
                    continue

                if entry["timestamp"] < timelimit:
                    entry["id"] = None
                    entry["timestamp"] = None
                    removed += 1

        if removed > 0:
            logging.debug(
                "%d entries from blacklist \"%s\" removed because their lifetime exceeded",
                removed, blacklistname)
        return

    def GetIDsFromBlacklist(self, blacklistname):
        """
        This method returns a list of IDs (as Integer) from the blacklist.
        It checks each entry in the list if it is ``None``.
        If it is ``None``, it gets ignored.
        
        .. note::

            This method does not check if the IDs time stamp is in a valid range.
            Call :meth:`~mdbapi.blacklist.BlacklistInterface.ValidateBlacklist` before to make sure
            all entries in the blacklist are valid.

        Args:
            blacklistname (str): A valid name for the blacklist. Valid names are: ``"songs"``, ``"albums"``, ``"artists"``

        Returns:
            A list of valid IDs as integers.

        Raises:
            ValueError: When blacklistname is not ``"videos"``, ``"songs"``, ``"albums"`` or ``"artists"``
        """
        if blacklistname not in ["songs", "albums", "artists", "videos"]:
            raise ValueError(
                "blacklistname must be \"videos\", \"songs\", \"albums\" or \"artists\"!"
            )

        global BlacklistLock
        global Blacklist

        with BlacklistLock:
            idlist = [
                entry["id"] for entry in Blacklist[blacklistname]
                if entry["id"] != None
            ]

        return idlist

    def GetValidIDsFromBlacklists(self):
        """
        Returns all Video, Song, Album and Artist IDs from the blacklist.
        The IDs are separated into three lists.
        This method checks the time stamp and removes all IDs that are older than configured.

        This method combines the following methods for all three blacklists (songs, albums, artists):
        
            #. :meth:`~mdbapi.blacklist.BlacklistInterface.ValidateBlacklist`
            #. :meth:`~mdbapi.blacklist.BlacklistInterface.GetIDsFromBlacklist`

        Returns:
            A tupel ``(VideoIDs, SongIDs, AlbumIDs, ArtistIDs)`` of lists of IDs that are temporal still valid.
        """
        videoids = []
        songids = []
        albumids = []
        artistids = []

        self.ValidateBlacklist("videos")
        self.ValidateBlacklist("songs")
        self.ValidateBlacklist("albums")
        self.ValidateBlacklist("artists")

        videoids = self.GetIDsFromBlacklist("videos")
        songids = self.GetIDsFromBlacklist("songs")
        albumids = self.GetIDsFromBlacklist("albums")
        artistids = self.GetIDsFromBlacklist("artists")

        return (videoids, songids, albumids, artistids)

    def CheckAllListsForSong(self, song):
        """
        This method checks if a song, its album or artist is on one of the blacklists.
        If it is so, the method returns ``True``.
        If none are on the blacklists, ``False`` gets returned.

        If the song is ``None`` nothing happens.

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

        Returns:
            ``True`` if song, album or artist is on blacklist. ``False`` otherwise.

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        videobl, songbl, albumbl, artistbl = self.GetValidIDsFromBlacklists()

        if self.artistbllen > 0 and song["artistid"] in artistbl:
            logging.debug("artist on blacklist")
            return True
        if self.albumbllen > 0 and song["albumid"] in albumbl:
            logging.debug("album on blacklist")
            return True
        if self.songbllen > 0 and song["id"] in songbl:
            logging.debug("song on blacklist")
            return True
        return False

    def CheckSongList(self, song):
        """
        This method checks if a song is on the song blacklists.
        If it is so, the method returns ``True``.

        If the song is ``None`` nothing happens.

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

        Returns:
            ``True`` if song is on blacklist. ``False`` otherwise.

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        videobl, songbl, albumbl, artistbl = self.GetValidIDsFromBlacklists()

        if self.songbllen > 0 and song["id"] in songbl:
            logging.debug("song on blacklist")
            return True
        return False

    def AddSong(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
        
        If the length of the blacklist exceeds its limit, the oldest entry gets dropped.

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

        Returns:
            *Nothing*

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        global BlacklistLock
        global Blacklist

        logging.debug("Setting song \"%s\" onto the blacklist.", song["path"])
        with BlacklistLock:
            if self.artistbllen > 0:
                entry = {}
                entry["timestamp"] = int(time.time())
                entry["id"] = song["artistid"]
                Blacklist["artists"].pop(0)
                Blacklist["artists"].append(entry)
            if self.albumbllen > 0:
                entry = {}
                entry["timestamp"] = int(time.time())
                entry["id"] = song["albumid"]
                Blacklist["albums"].pop(0)
                Blacklist["albums"].append(entry)
            if self.songbllen > 0:
                entry = {}
                entry["timestamp"] = int(time.time())
                entry["id"] = song["id"]
                Blacklist["songs"].pop(0)
                Blacklist["songs"].append(entry)

            # Remove outdated entries before saving
            self.ValidateBlacklist("songs")
            self.ValidateBlacklist("albums")
            self.ValidateBlacklist("artists")

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

    def AddVideo(self, video):
        """
        This method pushes a video onto the video blacklist.
        If the video is ``None`` nothing happens.

        .. note::

            The video associated album and artist get not pushed on the album or artist blacklist.

        This method should be the only place where the blacklist gets changed.
        After adding a video, the lists get stored in the MusicDB State Directory to be persistent
        
        If the length of the blacklist exceeds its limit, the oldest entry gets dropped.

        Args:
            video (dict/int): A video from the :class:`~lib.db.musicdb.MusicDatabase` or the video ID

        Returns:
            *Nothing*

        Raises:
            TypeError: When ``video`` is not of type ``dict`` or ``int``
        """
        if not video:
            return

        if type(video) is int:
            video = self.db.GetVideoById(video)
        elif type(video) is not dict:
            raise TypeError(
                "video argument must be of type dict or a video ID (int). Actual type was %s!"
                % (str(type(video))))

        global BlacklistLock
        global Blacklist

        logging.debug("Setting video \"%s\" onto the blacklist.",
                      video["path"])
        with BlacklistLock:
            if self.videobllen > 0:
                entry = {}
                entry["timestamp"] = int(time.time())
                entry["id"] = video["id"]
                Blacklist["videos"].pop(0)
                Blacklist["videos"].append(entry)

            # Remove outdated entries before saving
            self.ValidateBlacklist("videos")

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

    def CheckAllListsForVideo(self, video):
        """
        This method checks if a video on the blacklists.
        If it is so, the method returns ``True``.
        If none are on the blacklists, ``False`` gets returned.

        If the video is ``None`` nothing happens.

        Args:
            video (dict/int): A video from the :class:`~lib.db.musicdb.MusicDatabase` or the video ID

        Returns:
            ``True`` if the video is on blacklist. ``False`` otherwise.

        Raises:
            TypeError: When ``video`` is not of type ``dict`` or ``int``
        """
        if not video:
            return

        if type(video) == int:
            video = self.db.GetSongById(video)
        elif type(video) != dict:
            raise TypeError(
                "video argument must be of type dict or a video ID (int). Actual type was %s!"
                % (str(type(video))))

        videobl, songbl, albumbl, artistbl = self.GetValidIDsFromBlacklists()

        if self.artistbllen > 0 and video["artistid"] in artistbl:
            logging.debug(
                "artist of video on artist-blacklist but this blacklist is ignored for videos"
            )
        if self.videobllen > 0 and video["id"] in videobl:
            logging.debug("video on blacklist")
            return True
        return False
示例#6
0
文件: randy.py 项目: fat84/musicdb-1
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
示例#7
0
    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
示例#8
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
示例#9
0
class BlacklistInterface(object):
    """
    This class provides methods to manage the blacklists used by :class:`~mdbapi.randy.Randy`.

    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.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 CheckAllLists(self, song):
        """
        This method checks if a song, its album or artist is on one of the blacklists.
        If it is so, the method returns ``True``.
        If none are on the blacklists, ``False`` gets returned.

        If the song is ``None`` nothing happens.

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

        Returns:
            ``True`` if song, album or artist is on blacklist. ``False`` otherwise.

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        global BlacklistLock
        global Blacklist

        with BlacklistLock:
            if self.artistbllen > 0 and song["artistid"] in Blacklist[
                    "artists"]:
                logging.debug("artist on blacklist")
                return True
            if self.albumbllen > 0 and song["albumid"] in Blacklist["albums"]:
                logging.debug("album on blacklist")
                return True
            if self.songbllen > 0 and song["id"] in Blacklist["songs"]:
                logging.debug("song on blacklist")
                return True
        return False

    def CheckSongList(self, song):
        """
        This method checks if a song is on the song blacklists.
        If it is so, the method returns ``True``.

        If the song is ``None`` nothing happens.

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

        Returns:
            ``True`` if song is on blacklist. ``False`` otherwise.

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        global BlacklistLock
        global Blacklist

        with BlacklistLock:
            if self.songbllen > 0 and song["id"] in Blacklist["songs"]:
                logging.debug("song on blacklist")
                return True
        return False

    def AddSong(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
        
        If the length of the blacklist exceeds its limit, the oldest entry gets dropped.

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

        Returns:
            *Nothing*

        Raises:
            TypeError: When ``song`` is not of type ``dict`` or ``int``
        """
        if not song:
            return

        if type(song) == int:
            song = self.db.GetSongById(song)
        elif type(song) != dict:
            raise TypeError(
                "song argument must be of type dict or a song ID (int). Actual type was %s!"
                % (str(type(song))))

        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)