예제 #1
0
    def RemoveSong(self, songid):
        """
        This method removed a song from the database.
        The file gets not touched, and so it does not matter if it even exists.
        All related information will also be removed.

        .. warning::

            This is not a *"set the deleted flag"* method.
            The data gets actually removed from the database.
            No recovery possible!

        Args:
            songid (int): ID of the song that shall be removed from database

        Return:
            ``None``
        """
        tracker = TrackerDatabase(self.cfg.tracker.dbpath)

        # remove from music.db
        self.db.RemoveSong(songid)
        # remove from tracker.db
        tracker.RemoveSong(songid)

        return None
예제 #2
0
    def __init__(self, config, musicdb):
        if type(config) != MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(musicdb) != MusicDatabase:
            raise TypeError("database argument not of type MusicDatabase")

        self.disabled = config.debug.disabletracker
        self.lastsongid = None
        self.lastaction = time.time()

        # When tracking is disabled, don't even instantiate the databases.
        # Tracking is disabled for a reason, so protect the databases as good as possible!
        if not self.disabled:
            self.trackerdb = TrackerDatabase(config.tracker.dbpath)
            if musicdb:
                self.musicdb = musicdb
            else:
                self.musicdb = MusicDatabase(config.database.path)
예제 #3
0
    def __init__(self, config, target):
        if type(config) != MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(target) != str:
            raise TypeError("target argument not of type str")

        if not target in ["song", "video"]:
            raise ValueError("target must be \"song\" or \"video\"")

        self.config = config
        self.disabled = config.debug.disabletracker
        self.target = target
        self.lastid = None
        self.lastaction = time.time()

        # When tracking is disabled, don't even instantiate the databases.
        # Tracking is disabled for a reason, so protect the databases as good as possible!
        if not self.disabled:
            self.trackerdb = TrackerDatabase(config.tracker.dbpath)
예제 #4
0
    def UpgradeLycraDB(self):
        self.PrintCheckFile("lycra.db")
        lycradb = TrackerDatabase(self.cfg.lycra.dbpath)
        newversion = 2

        # Check current version
        version = self.GetDatabaseVersion(lycradb)

        # Check if good
        if version == newversion:
            self.PrintGood()
            return True

        # Upgrade if too old
        self.PrintUpgrade(version, newversion)
        if version == 1:
            retval = self.AddMetaTableToDatabase(lycradb)
            if not retval:
                return False
            version = 2

        self.PrintGood()
        return True
예제 #5
0
class Tracker(object):
    """
    This class tracks songs that were played after each other.
    So it gets tracked what songs the user put together into the queue because their style fit to each other.
    It should also track randomly added songs assuming the user skips or removes songs that don't fit.
    Only completely played songs should considered.
    Skipped songs should be ignored.
    Beside the songs, also the artist relations get tracked.
    The artist relation gets derived from the song relations.

    .. warning::

        It tracks the played songs using a local state.
        Creating a new instance of this class also creates a further independent tracker.
        This could mess up the database with relations that were counted twice!

    Args:
        config: :class:`~lib.cfg.musicdb.MusicDBConfig` object holding the MusicDB Configuration
        musicdb: Optional a :class:`~lib.db.musicdb.MusicDatabase` instance. When ``None`` a new instance will be created

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

        self.disabled = config.debug.disabletracker
        self.lastsongid = None
        self.lastaction = time.time()

        # When tracking is disabled, don't even instantiate the databases.
        # Tracking is disabled for a reason, so protect the databases as good as possible!
        if not self.disabled:
            self.trackerdb = TrackerDatabase(config.tracker.dbpath)
            if musicdb:
                self.musicdb = musicdb
            else:
                self.musicdb = MusicDatabase(config.database.path)

    def AddSong(self, songid):
        """
        This method tracks the relation to the given song with the last added song.
        This new song should be a song that was recently and completely played.

        If the time between this song, and the previous one exceeds 30 minutes, it gets ignored and the internal state gets reset.
        So the chain of songs get cut if the time between playing them is too long.
        The chain of songs gets also cut, if *songid* is ``None`` or invalid.

        If the given song is the same as the last song, then it gets ignored.

        After adding a song, the method checks for a new relation between two songs.
        This is the case when there was previously a song added.

        If there is a relation to track then the songs get loaded from the :class:`lib.db.musicdb.MusicDatabase` to get the artist IDs.
        The relation gets added to the tracker database by calling :meth:`lib.db.trackerdb.TrackerDatabase.AddRelation`

        Args:
            songid: song ID of the song that gets currently played, ``None`` to cut the chain of consecutive songs.

        Returns:
            ``True`` on success. ``False`` in case an error occurred.
        """
        # Check argument (A situation where songID was None leads to chaos.)
        if type(songid) != int:
            self.lastsongid = None
            if song == None:
                return True  # None is allowed to cut a song chain.
            logging.warning(
                "Song ID of new song is not an integer! The type was $s. \033[0;33m(Ignoring the NewSong-Call and clearing tracking list)",
                str(type(songid)))
            return False

        # If there is a 30 Minute gap, do not associate this song with the previous -> clear list
        timestamp = time.time()
        timediff = int(timestamp - self.lastaction)

        if timediff > 30 * 60:  # TODO: make configurable!
            logging.debug(
                "Resetting tracker history because of a time gap greater than %i minutes.",
                timediff // 60)
            self.queue = []

        self.lastaction = timestamp

        if self.lastsongid == songid:
            logging.debug(
                "The new song to track (%i) is the same as the previous one - so it gets ignored",
                songid)
            return True

        # Adding new song to the history
        logging.debug("Tracking new song with ID %i", songid)

        # If there was no previous song, initialize the tracker.
        if not self.lastsongid:
            self.lastsongid = songid
            return True

        if self.disabled:
            # do not do anything further when tracer is deactivated
            logging.info(
                "Updating tracker disabled. \033[1;33m!! \033[1;30m(Will not process relationship between %i and %i)",
                self.lastsongid, songid)
            self.lastsongid = songid  # fake the last step for better debugging
            return True

        # Get songs
        try:
            songa = self.musicdb.GetSongById(self.lastsongid)
            songb = self.musicdb.GetSongById(songid)
            artistida = songa["artistid"]
            artistidb = songb["artistid"]
        except Exception as e:
            # If one of the IDs is invalid, then here it will become noticed the first time.
            logging.error("musicdb.GetSongById failed with error \"%s\"!",
                          str(e))
            return False

        # store relation
        try:
            self.trackerdb.AddRelation("song", self.lastsongid, songid)
            self.trackerdb.AddRelation("artist", artistida, artistidb)
        except Exception as e:
            logging.error("trackerdb.AddRelation failed with error \"%s\"!",
                          str(e))
            return False

        logging.debug("New relation added: \033[0;35m%i → %i", self.lastsongid,
                      songid)

        # Rotate the chain
        self.lastsongid = songid

        return True
예제 #6
0
 def __init__(self, config, database):
     self.config = config
     self.musicdb = database
     self.fs = Filesystem(self.config.music.path)
     self.trackerdb = TrackerDatabase(self.config.tracker.dbpath)
예제 #7
0
class tracker(MDBModule):
    def __init__(self, config, database):
        self.config = config
        self.musicdb = database
        self.fs = Filesystem(self.config.music.path)
        self.trackerdb = TrackerDatabase(self.config.tracker.dbpath)

    def GenerateDotFile(self, target, targetid, relations, dotfile):
        """
        This method generates a dot file visualizing the relations between the target and the related songs or artists.
        Also, the weights get visualized by the thickness of the edges of the generated graph.

        .. warning::

            If the file exists, its content gets replaced!

        Args:
            target (str): The target all IDs apply to. Can be ``"song"`` or ``"artist"``.
            targetid (int): ID of the song or artists, the relations belong to
            relations: A list of relations as returned by :meth:`lib.db.tracker.TrackerDatabase.GetRelations`
            dotfile (str): A path to write the dotfile to.

        Returns:
            ``True`` on success. If there is any error, ``False`` gets returned.
        """
        if target not in ["song", "artist"]:
            return False

        # give the IDs a name
        if target == "song":
            targetname = self.musicdb.GetSongById(targetid)["name"]
        elif target == "artist":
            targetname = self.musicdb.GetArtistById(targetid)["name"]
        else:
            return False

        for relation in relations:
            if target == "song":
                relation["name"] = self.musicdb.GetSongById(
                    relation["id"])["name"]
            elif target == "artist":
                relation["name"] = self.musicdb.GetArtistById(
                    relation["id"])["name"]
            else:
                return False

        dot = open(dotfile, "w")
        dot.write("digraph songenv {\n")

        # Related Song
        dot.write("\tsubgraph {\n")
        dot.write("\t\trank   = same; ")
        for relation in relations:
            dot.write("\"" + relation["name"] + "\"; ")
        dot.write("\n\t}\n")

        # center
        dot.write("\tsubgraph {\n")
        dot.write("\t\trank = same; " + targetname + ";\n")
        dot.write("\t}\n")

        dot.write("\tedge [ arrowhead=\"none\" ; len=7 ];\n\n")

        # edges
        for relation in relations:
            penwidth = max(1, int(relation["weight"] / 10))

            dot.write("\t\"" + relation["name"] + "\" -> \"" + targetname +
                      "\" [penwidth=" + str(penwidth) + "];\n")

        dot.write("}\n\n")
        dot.close()
        return True

    def ShowRelations(self, target, targetid, relations):
        """
        This method lists all entries in the relations list returned by the database for the given target ID

        Args:
            target (str): The target all IDs apply to. Can be ``"song"`` or ``"artist"``.
            targetid (int): ID of the song or artists, the relations belong to
            relations: A list of relations as returned by :meth:`lib.db.tracker.TrackerDatabase.GetRelations`

        Returns:
            ``True`` on success. If there is any error, ``False`` gets returned.
        """
        if target not in ["song", "artist"]:
            return False

        for relation in relations:
            # Get Weight
            weight = relation["weight"]

            # Get Name
            if target == "song":
                name = self.musicdb.GetSongById(relation["id"])["name"]
            elif target == "artist":
                name = self.musicdb.GetArtistById(relation["id"])["name"]
            else:
                return False

            # Get Color
            if target == "song":
                colorweight = weight
            elif target == "artist":
                colorweight = int(weight / 5)
            else:
                return False

            if colorweight <= 1:
                color = "\033[1;30m"
            elif colorweight == 2:
                color = "\033[1;34m"
            elif colorweight == 3:
                color = "\033[1;36m"
            else:
                color = "\033[1;37m"

            # Print
            print(" \033[1;35m[%2d] %s%s" % (weight, color, name))

        return True

    @staticmethod
    def MDBM_CreateArgumentParser(parserset, modulename):
        parser = parserset.add_parser(
            modulename, help="access information from the song tracker")
        parser.set_defaults(module=modulename)
        parser.add_argument("-s",
                            "--show",
                            action="store_true",
                            help="Show the related songs or artists")
        parser.add_argument(
            "-d",
            "--dot",
            action="store",
            metavar="dotfile",
            type=str,
            help=
            "if this option is given, a dot-file will be generated with the results"
        )
        parser.add_argument("--test",
                            action="store_true",
                            help="for testing - it's magic! read the code")
        parser.add_argument(
            "path",
            help=
            "Path to the song or artist on that the previos options will be applied"
        )

    # return exit-code
    def MDBM_Main(self, args):

        if args.test:
            from tqdm import tqdm
            print("\033[1;35mTranslating old table to new table …\033[0m")

            # # Translate old table to new table
            # sql = "SELECT song, successor, weight FROM graph"
            # results = self.trackerdb.GetFromDatabase(sql)
            # for result in results:
            #     for _ in range(result[2]):
            #         self.trackerdb.AddRelation("song", result[0], result[1])

            # # Generate artistrelations out of songrelations
            # sql = "SELECT songida, songidb, weight FROM songrelations"
            # results = self.trackerdb.GetFromDatabase(sql)
            # for result in tqdm(results):

            #     artista = self.musicdb.GetSongById(result[0])["artistid"]
            #     artistb = self.musicdb.GetSongById(result[1])["artistid"]

            #     for _ in range(result[2]):
            #         self.trackerdb.AddRelation("artist", artista, artistb)

            print("\033[1;32mdone!\033[0m")
            return 0

        # Genrate path relative to the music root directory - if possible
        try:
            path = self.fs.AbsolutePath(
                args.path)  # Be sure the path is absolute (resolve "./")
            path = self.fs.RemoveRoot(
                path)  # Now make a relative artist or song path
        except Exception as e:
            print(
                "\033[1;31mInvalid path. Determin relative path to the music root directory failed with error: %s",
                str(e))
            return 1

        # Identify target by path and get target ID
        if self.fs.IsFile(path):
            mdbsong = self.musicdb.GetSongByPath(path)
            if not mdbsong:
                print(
                    "\033[1;31mPath %s is a file, but it is not a song file!\033[0m"
                    % (path))
            target = "song"
            targetid = mdbsong["id"]

        elif self.fs.IsDirectory(path):
            mdbartist = self.musicdb.GetArtistByPath(path)
            if not mdbartist:
                print(
                    "\033[1;31mPath %s is a directory, but it is not an artist directory!\033[0m"
                    % (path))
            target = "artist"
            targetid = mdbartist["id"]

        else:
            print("\033[1;31mPath %s does not exist!\033[0m" % (path))
            return 1

        # Get target relation
        print(
            "\033[1;34mGetting \033[1;36m%s\033[1;34m relations from database … \033[0m"
            % (target))
        relations = self.trackerdb.GetRelations(target, targetid)
        print("\033[1;36m%d\033[1;34m entries found.\033[0m" %
              (len(relations)))

        # Apply parameters
        if args.show:
            self.ShowRelations(target, targetid, relations)

        if args.dot:
            rootfs = Filesystem()
            dotfile = rootfs.AbsolutePath(args.dot)
            self.GenerateDotFile(target, targetid, relations, dotfile)

        return 0
예제 #8
0
class Tracker(object):
    """
    This class tracks music (songs and videos) that were played after each other.
    So it gets tracked what songs or videos the user put together into the queue because their style fit to each other.
    
    Only completely played music should considered.
    Skipped music should be ignored.

    .. warning::

        It tracks the played songs and videos using a local state.
        Creating a new instance of this class also creates a further independent tracker.
        This could mess up the database with relations that were counted twice!

    .. note::

        For better readability it is recommended to use the derived classes :class:`~mdbapi.tracker.SongTracker` and :class:`~mdbapi.tracker.VideoTracker`.

    Args:
        config: :class:`~lib.cfg.musicdb.MusicDBConfig` object holding the MusicDB Configuration
        target (str): ``"song"`` or ``"video"`` depending what kind of music will be tracked

    Raises:
        TypeError: When the arguments are not of the correct type.
        ValueError: When ``target`` is not ``"song"`` or ``"video"``
    """
    def __init__(self, config, target):
        if type(config) != MusicDBConfig:
            raise TypeError("config argument not of type MusicDBConfig")
        if type(target) != str:
            raise TypeError("target argument not of type str")

        if not target in ["song", "video"]:
            raise ValueError("target must be \"song\" or \"video\"")

        self.config = config
        self.disabled = config.debug.disabletracker
        self.target = target
        self.lastid = None
        self.lastaction = time.time()

        # When tracking is disabled, don't even instantiate the databases.
        # Tracking is disabled for a reason, so protect the databases as good as possible!
        if not self.disabled:
            self.trackerdb = TrackerDatabase(config.tracker.dbpath)

    def Track(self, targetid):
        """
        This method tracks the relation to the given target with the last added target.
        A target can be a song or a video.
        This new target should be a target that was recently and completely played.

        If the time between this target, and the previous one exceeds *N* minutes, it gets ignored and the internal state gets reset.
        So the chain of targets get cut if the time between playing them is too long.
        The chain of targets gets also cut, if ``targetid`` is ``None`` or invalid.
        The amount of time until this cut takes place can be configured: :doc:`/basics/config`

        If the given target is the same as the last target, then it gets ignored.

        After adding a target, the method checks for a new relation between two targets.
        This is the case when there was previously a target added.

        The relation gets added to the tracker database by calling :meth:`lib.db.trackerdb.TrackerDatabase.AddRelation`

        Args:
            targetid: ID of the song or video that gets currently played, ``None`` to cut the chain of consecutive targets.

        Returns:
            ``True`` on success. ``False`` in case an error occurred.
        """
        # Check argument (A situation where ID was None leads to chaos.)
        if type(targetid) != int:
            # Cut chain
            self.lastid = None

            if targetid == None:
                logging.debug(self.target + " ID of new " + self.target +
                              " is None! \033[0;33m(Clearing tracking chain)")
                return True  # None is allowed to cut the chain.

            logging.warning(
                self.target + " ID of new " + self.target +
                " is not an integer! The type was %s. \033[0;33m(Ignoring the Track-Call and clearing tracking list)",
                str(type(targetid)))
            return False

        # If there is a *cuttime* Minute gap, do not associate this target with the previous -> clear list
        timestamp = time.time()
        timediff = int(timestamp - self.lastaction)

        if timediff > self.config.tracker.cuttime * 60:
            logging.debug(
                "Resetting tracker history because of a time gap greater than %i minutes.",
                timediff // 60)
            self.queue = []

        self.lastaction = timestamp

        if self.lastid == targetid:
            logging.debug(
                "The new " + self.target +
                " to track (%i) is the same as the previous one - so it gets ignored",
                targetid)
            return True

        # Adding new target to the history
        logging.debug("Tracking new " + self.target + " with ID %i", targetid)

        # If there was no previous target, initialize the tracker.
        if not self.lastid:
            self.lastid = targetid
            logging.debug(
                "Starting new tracking chain with " + self.target + " ID %i.",
                targetid)
            return True

        if self.disabled:
            # do not do anything further when tracer is deactivated
            logging.info(
                "Updating tracker disabled. \033[1;33m!! \033[1;30m(Will not process relationship between %i and %i)",
                self.lastid, targetid)
            self.lastid = targetid  # fake the last step for better debugging
            return True

        # store relation
        try:
            self.trackerdb.AddRelation(self.target, self.lastid, targetid)
        except Exception as e:
            logging.error("trackerdb.AddRelation failed with error \"%s\"!",
                          str(e))
            return False

        logging.debug(
            "New " + self.target + " relation added: \033[0;35m%i → %i",
            self.lastid, targetid)

        # Rotate the chain
        self.lastid = targetid

        return True