class GenreView(ListView, MusicDBTags): def __init__(self, config, database, title, x, y, w, h): ListView.__init__(self, title, x, y, w, h) MusicDBTags.__init__(self, config, database) self.UpdateView() self.dialog = Dialog("Edit Genre", self.x, self.y+1, self.w, self.h-1) self.dialogmode = False self.nameinput = TextInput() self.posxinput = TextInput() self.dialog.AddInput("Name:", self.nameinput, "Visibly for user") self.dialog.AddInput("Position:", self.posxinput, "Position in WebUI list (positive integer)") def UpdateView(self): self.SetData(self.GetAllGenres()) def Draw(self): # Only draw the list view when not in dialog mode if self.dialogmode == False: ListView.Draw(self) def onDrawElement(self, element, number, maxwidth): # Render Position posx = element["posx"] posy = element["posy"] if posx == None: posx = "--" else: posx = "%2d"%(posx) pos = " (" + posx + ")" # Render Name width = maxwidth - len(pos) name = element["name"] name = name[:width] name = name.ljust(width) return name + "\033[1;30m" + pos def onAction(self, element, key): if key == "e": # Edit tag name = element["name"] posx = element["posx"] # Initialize dialog with element self.nameinput.SetData(name) self.posxinput.SetData(str(posx)) # show dialog self.dialog.oldname = name # trace Tag so that changes can be associated to a specific tag self.dialog.Draw() self.dialogmode = True elif key == "r": # Remove Tag self.DeleteGenre(element["name"]) self.elements.remove(element) self.UpdateView() self.Draw() return None return element def HandleKey(self, key): if self.dialogmode == True: if key == "enter": # Commit dialog inputs self.dialogmode = False self.Draw() # show list view instead of dialog # Get data from dialog name = self.nameinput.GetData() posx = self.posxinput.GetData() oldname = self.dialog.oldname try: posx = int(posx) assert posx >= 0 except: posx = None # do not update an invalid position # Update database with new data if oldname == None: self.CreateGenre(name) tagname = name newname = None else: tagname = oldname # Was the tag renamed? if oldname != name: newname = name else: newname = None self.ModifyGenre(tagname, newname, newposx=posx) self.UpdateView() self.Draw() 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 == "a": # Add new tag self.nameinput.SetData("") self.posxinput.SetData("") self.dialog.oldname = None # new tag has no old name self.dialog.Draw() self.dialogmode = True else: ListView.HandleKey(self, key)
class SubGenreView(ListView, MusicDBTags): def __init__(self, config, database, genreview, title, x, y, w, h): ListView.__init__(self, title, x, y, w, h) MusicDBTags.__init__(self, config, database) if type(genreview) != GenreView: raise TypeError("genreview must be of type GenreView!") self.genreview = genreview self.UpdateView() self.dialog = Dialog("Edit Subgenre", self.x, self.y+1, self.w, self.h-1) self.dialogmode = False self.nameinput = TextInput() self.dialog.AddInput("Name:", self.nameinput, "Visibly for user") def UpdateView(self): # Only show subgenres of the selected genre genre = self.genreview.GetSelectedData() subgenres = self.GetAllSubgenres() elements = [ subgenre for subgenre in subgenres if subgenre["parentid"] == genre["id"] ] self.SetData(elements) def Draw(self): # Only draw the list view when not in dialog mode if self.dialogmode == False: ListView.Draw(self) def onDrawElement(self, element, number, maxwidth): name = element["name"] name = name[:maxwidth] name = name.ljust(maxwidth) return name def onAction(self, element, key): if key == "e": # Edit tag name = element["name"] # Initialize dialog with element self.nameinput.SetData(name) # show dialog parent = self.genreview.GetSelectedData() parentname = parent["name"] self.dialog.oldname = name # trace Tag so that changes can be associated to a specific tag self.dialog.parentname = parentname self.dialog.Draw() self.dialogmode = True elif key == "r": # Remove Tag self.DeleteSubgenre(element["name"]) self.elements.remove(element) self.UpdateView() self.Draw() return None return element def HandleKey(self, key): if self.dialogmode == True: if key == "enter": # Commit dialog inputs self.dialogmode = False self.Draw() # show list view instead of dialog # Get data from dialog name = self.nameinput.GetData() oldname = self.dialog.oldname parentname = self.dialog.parentname # Update database with new data if oldname == None: self.CreateSubgenre(name, parentname) tagname = name newname = None else: tagname = oldname # Was the tag renamed? if oldname != name: newname = name else: newname = None self.ModifySubgenre(tagname, newname) self.UpdateView() self.Draw() elif key == "escape": self.dialogmode = False self.dialog.oldname = None # prevent errors by leaving a clean state self.dialog.parentname = None self.Draw() # show list view instead of dialog # reject changes else: self.dialog.HandleKey(key) else: if key == "a": # Add new tag self.nameinput.SetData("") parent = self.genreview.GetSelectedData() parentname = parent["name"] self.dialog.oldname = None # new tag has no old name self.dialog.parentname = parentname self.dialog.Draw() self.dialogmode = True else: ListView.HandleKey(self, key)
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
def ShowImportDialog(self, albumpath): self.cli.ShowCursor(False) self.cli.ClearScreen() self.cli.SetColor("1;30", "40") self.cli.SetCursor(1, 1) self.cli.PrintText("Press Ctrl-D to exit without any changes.") self.cli.SetCursor(1, 2) self.cli.PrintText( "\033[1;31m✘\033[1;30m marks invalid data, \033[1;32m✔\033[1;30m marks valid data." ) self.cli.SetCursor(1, 3) self.cli.PrintText("MusicDB file naming scheme:") self.cli.SetCursor(3, 4) self.cli.PrintText( "{artistname}/{albumrelease} - {albumname}/{songnumber} {songname}.{extension}" ) self.cli.SetCursor(3, 5) self.cli.PrintText( "{artistname}/{albumrelease} - {albumname}/{cdnumber}-{songnumber} {songname}.{extension}" ) # Calculate the positions of the UI element headh = 5 # n lines high headline pathh = 2 dialogx = 1 dialogy = 2 + headh dialogw = self.maxw - 2 dialogh = 9 listx = 1 listy = dialogy + dialogh + 1 listw = self.maxw - 2 listh = self.maxh - (headh + dialogh + pathh + 3 * 2) pathx = dialogx + 1 pathw = dialogw - 2 pathy = listy + listh + 1 # List Views songview = SongView(self.cfg, albumpath, "New Songs", listx, listy, listw, listh) albumdialog = Dialog("Album Import", dialogx, dialogy, dialogw, dialogh) albumdialog.RemoveButton("Cancel") albumdialog.RemoveButton("Commit") artistinput = FileNameInput() nameinput = FileNameInput() releaseinput = TextInput() origininput = TextInput() artworkinput = BoolInput() musicaiinput = BoolInput() lyricsinput = BoolInput() albumdialog.AddInput("Artist name:", artistinput, "Correct name of the album artist") albumdialog.AddInput("Album name:", nameinput, "Correct name of the album (no release year)") albumdialog.AddInput("Release date:", releaseinput, "Year with 4 digits like \"2017\"") albumdialog.AddInput( "Origin:", origininput, "\"iTunes\", \"bandcamp\", \"CD\", \"internet\", \"music163\"") albumdialog.AddInput("Import artwork:", artworkinput, "Import the artwork to MusicDB") albumdialog.AddInput("Import lyrics:", lyricsinput, "Try to import lyrics from file") albumdialog.AddInput("Predict genre:", musicaiinput, "Runs MusicAI to predict the song genres") # Initialize dialog albumdirname = os.path.split(albumpath)[1] artistdirname = os.path.split(albumpath)[0] metadata = self.GetAlbumMetadata(albumpath) if metadata: origin = str(metadata["origin"]) else: origin = "Internet" analresult = self.fs.AnalyseAlbumDirectoryName(albumdirname) if analresult: release = str(analresult["release"]) albumname = analresult["name"] else: # suggest the release date from meta data - the user can change when it's wrong if metadata: release = str(metadata["releaseyear"]) else: release = "20??" albumname = albumdirname artistinput.SetData(artistdirname) nameinput.SetData(albumname) releaseinput.SetData(release) origininput.SetData(origin) artworkinput.SetData(True) if self.cfg.debug.disableai: musicaiinput.SetData(False) else: musicaiinput.SetData(True) if metadata["lyrics"]: lyricsinput.SetData(True) else: lyricsinput.SetData(False) # Initialize list songview.UpdateUI() # Buttons buttons = ButtonView(align="middle") buttons.AddButton("↹", "Select list") buttons.AddButton("W", "Rename files Write to database") # Composition tabgroup = TabGroup() tabgroup.AddPane(albumdialog) tabgroup.AddPane(songview) # Draw once buttons.Draw(0, self.maxh - 2, self.maxw) while True: artistname = artistinput.GetData() albumname = nameinput.GetData() releasedate = releaseinput.GetData() origin = origininput.GetData() artwork = artworkinput.GetData() lyrics = lyricsinput.GetData() musicai = musicaiinput.GetData() # Show everything songview.Draw() albumdialog.Draw() self.ShowArtistValidation(artistname, pathx, pathy, pathw) self.ShowAlbumValidation(artistname, albumname, releasedate, pathx, pathy + 1, pathw) self.cli.FlushScreen() # Handle keys key = self.cli.GetKey() if key == "Ctrl-D": return None elif key == "W": # Returns a dicrionary data = {} data["oldalbumpath"] = albumpath data["artistname"] = artistname data["albumname"] = albumname data["releasedate"] = releasedate data["origin"] = origin data["runartwork"] = artwork data["runlyrics"] = lyrics data["runmusicai"] = musicai data["songs"] = songview.GetData() return data else: tabgroup.HandleKey(key)
class MoodView(ListView, MusicDBTags): def __init__(self, config, database, title, x, y, w, h): ListView.__init__(self, title, x, y, w, h) MusicDBTags.__init__(self, config, database) self.UpdateView() self.dialog = Dialog("Edit Mood", self.x, self.y + 1, self.w, self.h - 1) self.dialogmode = False self.nameinput = TextInput() self.iconinput = TextInput() self.colorinput = TextInput() self.posxinput = TextInput() self.posyinput = TextInput() self.varselector = BoolInput() self.dialog.AddInput("Name:", self.nameinput, "Visibly for user") self.dialog.AddInput("Icon:", self.iconinput, "Unicode char") self.dialog.AddInput("U+FE0E:", self.varselector, "Do not replace with emoji") self.dialog.AddInput("Color:", self.colorinput, "In HTML notation (#RRGGBB)") self.dialog.AddInput("X:", self.posxinput, "X coordinate on grid (positive integer)") self.dialog.AddInput("Y:", self.posyinput, "X coordinate on grid (positive integer)") self.moodgridcrossref = None def UpdateView(self): self.SetData(self.GetAllMoods()) def onDrawElement(self, element, number, maxwidth): # prints the following information # Icon Name Position Type width = maxwidth # trace left width during rendering. Start with maximum # Render Icon if element["icontype"] == 1: # unicode icon icon = element["icon"] else: icon = "?" # there may be a modifier "\xef\xb8\x8e" at the end to prevent some silly browser # to replace the Unicode characters with ugly emoji images that totally f**k up the design if len(icon) > 1: icon = icon[0] icon += " " width -= 2 # Render icon type (currently, only type 1 is supported by MusicDB) if element["icontype"] == 1: # Unicode icontype = " type:txt" elif element["icontype"] == 2: # HTML tag icontype = " type:htm" elif element["icontype"] == 3: # png image icontype = " type:png" elif element["icontype"] == 4: # svg graphic icontype = " type:svg" width -= len(icontype) # Render Icon Color if element["color"]: color = HTMLColorToANSI(element["color"]) else: color = HTMLColorToANSI("#CCCCCC") # Render Position posx = element["posx"] posy = element["posy"] if posx == None: posx = "--" else: posx = "%2d" % (posx) if posy == None: posy = "--" else: posy = "%2d" % (posy) pos = " (" + posx + ";" + posy + ")" width -= len(pos) # Render Name name = element["name"] name = name[:width] name = name.ljust(width) return color + icon + "\033[1;34m" + name + "\033[1;30m" + pos + icontype def Draw(self): # Only draw the list view when not in dialog mode if self.dialogmode == False: ListView.Draw(self) def onAction(self, element, key): if key == "e": # Edit tag name = element["name"] posx = element["posx"] posy = element["posy"] if element["icon"] == None: icon = "" else: icon = element["icon"][0] # Check if variant selector 15 is append to the Unicode character if len(element["icon"]) > 1: varsec15 = True else: varsec15 = False # Check if there is a color defined if element["color"]: color = element["color"] else: color = "" # Initialize dialog with element self.nameinput.SetData(name) self.iconinput.SetData(icon) self.varselector.SetData(varsec15) self.colorinput.SetData(color) self.posxinput.SetData(str(posx)) self.posyinput.SetData(str(posy)) # show dialog self.dialog.oldname = element[ "name"] # trace Tag so that changes can be associated to a specific tag self.dialog.Draw() self.dialogmode = True elif key == "r": # Remove Tag self.DeleteMood(element["name"]) self.elements.remove(element) self.UpdateView() self.Draw() # Synchronize new state with mood grid self.moodgridcrossref.UpdateView() self.moodgridcrossref.Draw() return None return element def HandleKey(self, key): if self.dialogmode == True: if key == "enter": # Commit dialog inputs self.dialogmode = False self.Draw() # show list view instead of dialog # Get data from dialog name = self.nameinput.GetData() icon = self.iconinput.GetData() posx = self.posxinput.GetData() posy = self.posyinput.GetData() try: posx = int(posx) assert posx >= 0 except: posx = None # do not update an invalid position try: posy = int(posy) assert posy >= 0 except: posy = None # do not update an invalid position if self.varselector.GetData() == True: icon += b"\xef\xb8\x8e".decode() color = self.colorinput.GetData() if len(color) != 7 or color[0] != "#": color = None oldname = self.dialog.oldname # Update database with new data if oldname == None: self.CreateMood(name) tagname = name newname = None else: tagname = oldname # Was the tag renamed? if oldname != name: newname = name else: newname = None self.ModifyMood(tagname, newname, icon, color, posx, posy) self.UpdateView() self.Draw() # Synchronize new state with mood grid self.moodgridcrossref.UpdateView() self.moodgridcrossref.Draw() 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 == "a": # Add new tag self.nameinput.SetData("") self.iconinput.SetData("") self.varselector.SetData(False) self.colorinput.SetData("") self.dialog.oldname = None # new tag has no old name self.dialog.Draw() self.dialogmode = True else: ListView.HandleKey(self, key)