class Lycra(object): """ This class does the main lyrics management. Args: config: MusicDB Configuration object. Raises: TypeError: when *config* is not of type :class:`~lib.cfg.musicdb.MusicDBConfig` """ def __init__(self, config): if type(config) != MusicDBConfig: logging.error("Config-class of unknown type!") raise TypeError("config argument not of type MusicDBConfig") logging.debug("Crawler path is %s", CRAWLERPATH) self.config = config self.lycradb = LycraDatabase(self.config.lycra.dbpath) self.fs = Filesystem(CRAWLERPATH) self.crawlers = None def LoadCrawlers(self): """ This method loads all crawlers inside the crawler directory. .. warning:: Changes at crawler may not be recognized until the whole application gets restarted. Only new added crawler gets loaded. Already loaded crawler are stuck at Pythons module cache. Returns: ``None`` """ # Get a list of all modules crawlerfiles = self.fs.GetFiles(".") modulenames = [ self.fs.GetFileName(x) for x in crawlerfiles if self.fs.GetFileExtension(x) == "py" ] if len(modulenames) == 0: logging.warning( "No modules found in \"%s\"! \033[1;30m(… but crawler cache is still usable.)", self.fs.AbsolutePath(CRAWLERPATH)) self.crawlers = None return None # load all modules self.crawlers = [] for modulename in modulenames: modfp, modpath, moddesc = imp.find_module(modulename, [CRAWLERPATH]) try: logging.debug("Loading %s …", str(modpath)) module = imp.load_module(modulename, modfp, modpath, moddesc) except Exception as e: logging.error( "Loading Crawler %s failed with error: %s! \033[1;30m(Ignoring this specific Crawler)", str(e), str(modpath)) finally: # Since we may exit via an exception, close fp explicitly. if modfp: modfp.close() crawler = {} crawler["module"] = module crawler["modulename"] = modulename self.crawlers.append(crawler) if len(self.crawlers) == 0: logging.warning( "No crawler loaded from \"%s\"! \033[1;30m(… but crawler cache is still usable.)", self.fs.AbsolutePath(CRAWLERPATH)) self.crawlers = None return None def RunCrawler(self, crawler, artistname, albumname, songname, songid): """ This method runs a specific crawler. This crawler gets all information available to search for a specific songs lyric. This method is for class internal use. When using this class, call :meth:`~mdbapi.lycra.Lycra.CrawlForLyrics` instead of calling this method directly. Before calling this method, :meth:`~mdbapi.lycra.Lycra.LoadCrawlers` must be called. The crawler base class :class:`lib.crawlerapi.LycraCrawler` catches all exceptions so that they do not net to be executed in an try-except environment. Args: crawler (str): Name of the crawler. If it addresses the file ``lib/crawler/example.py`` the name is ``example`` artistname (str): The name of the artist as stored in the MusicDatabase albumname (str): The name of the album as stored in the MusicDatabase songname (str): The name of the song as stored in the MusicDatabase songid (int): The ID of the song to associate the lyrics with the song Returns: ``None`` """ crawlerclass = getattr(crawler["module"], crawler["modulename"]) crawlerentity = crawlerclass(self.lycradb) crawlerentity.Crawl(artistname, albumname, songname, songid) return None def CrawlForLyrics(self, artistname, albumname, songname, songid): """ Loads all crawler from the crawler directory via :meth:`~mdbapi.lycra.Lycra.LoadCrawlers` and runs them via :meth:`~mdbapi.lycra.Lycra.RunCrawler`. Args: artistname (str): The name of the artist as stored in the music database albumname (str): The name of the album as stored in the music database songname (str): The name of the song as stored in the music database songid (int): The ID of the song to associate the lyrics with the song Returns: ``False`` if something went wrong. Otherwise ``True``. (This is *no* indication that there were lyrics found!) """ # Load / Reload crawlers try: self.LoadCrawlers() except Exception as e: logging.error( "Loading Crawlers failed with error \"%s\"! \033[1;30m(… but crawler cache is still usable.)", str(e)) return False if not self.crawlers: return False for crawler in self.crawlers: self.RunCrawler(crawler, artistname, albumname, songname, songid) return True def GetLyrics(self, songid): """ This method returns the lyrics of a song. See :meth:`lib.db.lycradb.LycraDatabase.GetLyricsFromCache` """ return self.lycradb.GetLyricsFromCache(songid)
class SongView(ListView, ButtonView): def __init__(self, config, albumpath, title, x, y, w, h): ListView.__init__(self, title, x, y, w, h) ButtonView.__init__(self, align="center") self.AddButton("↑", "Go up") self.AddButton("↓", "Go down") self.AddButton("c", "Clean name") self.AddButton("e", "Edit name") #self.AddButton("␣", "Toggle") #self.AddButton("↵", "Commit") #self.AddButton("␛", "Cancel") # elements are a tuple (original path, new path) self.cfg = config self.fs = Filesystem(self.cfg.music.path) self.albumpath = albumpath self.nameinput = FileNameInput() self.numberinput = TextInput() self.cdnuminput = TextInput() dialogh = 2 + 3 self.dialog = Dialog("Rename Song", self.x, self.y + 1, self.w, dialogh) self.dialog.AddInput("Song name:", self.nameinput, "Correct name only") self.dialog.AddInput("Song number:", self.numberinput, "Song number only") self.dialog.AddInput("CD number:", self.cdnuminput, "CD number or nothing") self.dialogmode = False def FindSongs(self): files = self.fs.GetFiles(self.albumpath, self.cfg.music.ignoresongs) songs = [] # Only take audio files into account - ignore images and booklets for f in files: extension = self.fs.GetFileExtension(f) if extension in ["mp3", "flac", "m4a", "aac"]: songs.append((f, f)) return songs def CleanFileNames(self): for index, element in enumerate(self.elements): origpath = element[0] path = element[1] directory = self.fs.GetDirectory(path) filename = self.fs.GetFileName(path) extension = self.fs.GetFileExtension(path) seg = self.FileNameSegments(filename) newfilename = filename[seg["number"]:seg["gap"]] newfilename += filename[seg["name"]:] newfilename = unicodedata.normalize("NFC", newfilename) newpath = os.path.join(directory, newfilename + "." + extension) self.elements[index] = (origpath, newpath) # no path, no file extension! # returns indices of name segments def FileNameSegments(self, filename): seg = {} # Start of song number m = re.search("\d", filename) if m: seg["number"] = m.start() else: seg["number"] = 0 # End of song number (1 space is necessary) m = re.search("\s", filename[seg["number"]:]) if m: seg["gap"] = seg["number"] + 1 + m.start() else: seg["gap"] = seg["number"] + 1 # Find start of song name m = re.search("\w", filename[seg["gap"]:]) if m: seg["name"] = seg["gap"] + m.start() else: seg["name"] = seg["gap"] return seg def UpdateUI(self): newsongs = self.FindSongs() self.SetData(newsongs) def onDrawElement(self, element, number, maxwidth): oldpath = element[0] path = element[1] width = maxwidth filename = self.fs.GetFileName(path) extension = self.fs.GetFileExtension(path) analresult = self.fs.AnalyseSongFileName(filename + "." + extension) # Render validation if not analresult: validation = "\033[1;31m ✘ " else: validation = "\033[1;32m ✔ " width -= 3 # Render file name renderedname = "" width -= len(filename) seg = self.FileNameSegments(filename) renderedname += "\033[1;31m\033[4m" + filename[ 0:seg["number"]] + "\033[24m" renderedname += "\033[1;34m" + filename[seg["number"]:seg["gap"]] renderedname += "\033[1;31m\033[4m" + filename[ seg["gap"]:seg["name"]] + "\033[24m" renderedname += "\033[1;34m" + filename[seg["name"]:] # Render file extension fileextension = "." + extension fileextension = fileextension[:width] fileextension = fileextension.ljust(width) return validation + "\033[1;34m" + renderedname + "\033[1;30m" + fileextension def Draw(self): if self.dialogmode == True: pass else: ListView.Draw(self) x = self.x + 1 y = self.y + self.h - 1 w = self.w - 2 ButtonView.Draw(self, x, y, w) def HandleKey(self, key): if self.dialogmode == True: if key == "enter": # Commit dialog inputs songname = self.nameinput.GetData() songnumber = self.numberinput.GetData() cdnumber = self.cdnuminput.GetData() element = self.dialog.oldelement path = element[ 1] # the editable path is 1, 0 is the original path directory = self.fs.GetDirectory(path) extension = self.fs.GetFileExtension(path) if len(songnumber) == 1: songnumber = "0" + songnumber if cdnumber: songnumber = cdnumber + "-" + songnumber newpath = os.path.join( directory, songnumber + " " + songname + "." + extension) self.SetSelectedData((element[0], newpath)) self.dialogmode = False self.Draw() # show list view instead of dialog elif key == "escape": self.dialogmode = False self.dialog.oldname = None # prevent errors by leaving a clean state self.Draw() # show list view instead of dialog # reject changes else: self.dialog.HandleKey(key) else: if key == "up" or key == "down": ListView.HandleKey(self, key) elif key == "c": self.CleanFileNames() elif key == "e": # edit name element = self.GetSelectedData() editpath = element[1] filename = self.fs.GetFileName(editpath) seg = self.FileNameSegments(filename) songnumber = filename[seg["number"]:seg["gap"]].strip() songname = filename[seg["name"]:].strip() if "-" in songnumber: cdnumber = songnumber.split("-")[0].strip() songnumber = songnumber.split("-")[1].strip() else: cdnumber = "" self.nameinput.SetData(songname) self.numberinput.SetData(songnumber) self.cdnuminput.SetData(cdnumber) self.dialog.oldelement = element self.dialog.Draw() self.dialogmode = True