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
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 __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 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
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
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)
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
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