def __createTree(self): """ Create the main tree, add it to the scrolled window """ from gui.extTreeview import ExtTreeView txtRdr = gtk.CellRendererText() pixbufRdr = gtk.CellRendererPixbuf() txtRdrAlbumLen = gtk.CellRendererText() columns = (('', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRdrAlbumLen, TYPE_STRING), (txtRdr, TYPE_STRING)], False), (None, [(None, TYPE_INT)], False), (None, [(None, TYPE_STRING)], False), (None, [(None, TYPE_PYOBJECT)], False)) self.tree = ExtTreeView(columns, True) # The first text column (ROW_ALBUM_LEN) is not the one to search for # set_search_column(ROW_NAME) should work, but it doesn't... self.tree.set_search_equal_func(self.__searchFunc) self.tree.get_column(0).set_cell_data_func(txtRdr, self.__drawCell) self.tree.get_column(0).set_cell_data_func(pixbufRdr, self.__drawCell) self.tree.get_column(0).set_cell_data_func(txtRdrAlbumLen, self.__drawAlbumLenCell) # The album length is written in a smaller font, with a lighter color txtRdrAlbumLen.set_property('scale', 0.85) txtRdrAlbumLen.set_property('foreground-gdk', self.tree.get_style().text[gtk.STATE_INSENSITIVE]) self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]]) # GTK handlers self.tree.connect('drag-data-get', self.onDragDataGet) self.tree.connect('key-press-event', self.onKeyPressed) self.tree.connect('exttreeview-row-expanded', self.onRowExpanded) self.tree.connect('exttreeview-row-collapsed', self.onRowCollapsed) self.tree.connect('exttreeview-button-pressed', self.onButtonPressed) # Add the tree to the scrolled window self.scrolled.add(self.tree)
class Library(modules.Module): def __init__(self): """ Constructor """ handlers = { consts.MSG_EVT_APP_QUIT: self.onModUnloaded, consts.MSG_EVT_MOD_LOADED: self.onModLoaded, consts.MSG_EVT_APP_STARTED: self.onModLoaded, consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded, consts.MSG_EVT_EXPLORER_CHANGED: self.onExplorerChanged, } modules.Module.__init__(self, handlers) def __createTree(self): """ Create the main tree, add it to the scrolled window """ from gui.extTreeview import ExtTreeView txtRdr = gtk.CellRendererText() pixbufRdr = gtk.CellRendererPixbuf() txtRdrAlbumLen = gtk.CellRendererText() columns = (('', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRdrAlbumLen, TYPE_STRING), (txtRdr, TYPE_STRING)], False), (None, [(None, TYPE_INT)], False), (None, [(None, TYPE_STRING)], False), (None, [(None, TYPE_PYOBJECT)], False)) self.tree = ExtTreeView(columns, True) # The first text column (ROW_ALBUM_LEN) is not the one to search for # set_search_column(ROW_NAME) should work, but it doesn't... self.tree.set_search_equal_func(self.__searchFunc) self.tree.get_column(0).set_cell_data_func(txtRdr, self.__drawCell) self.tree.get_column(0).set_cell_data_func(pixbufRdr, self.__drawCell) self.tree.get_column(0).set_cell_data_func(txtRdrAlbumLen, self.__drawAlbumLenCell) # The album length is written in a smaller font, with a lighter color txtRdrAlbumLen.set_property('scale', 0.85) txtRdrAlbumLen.set_property('foreground-gdk', self.tree.get_style().text[gtk.STATE_INSENSITIVE]) self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]]) # GTK handlers self.tree.connect('drag-data-get', self.onDragDataGet) self.tree.connect('key-press-event', self.onKeyPressed) self.tree.connect('exttreeview-row-expanded', self.onRowExpanded) self.tree.connect('exttreeview-row-collapsed', self.onRowCollapsed) self.tree.connect('exttreeview-button-pressed', self.onButtonPressed) # Add the tree to the scrolled window self.scrolled.add(self.tree) def __searchFunc(self, model, column, key, iter): """ Check whether the given key matches the current candidate (iter) """ return model.get_value(iter, ROW_NAME)[:len(key)].lower() != key.lower() def __drawCell(self, column, cell, model, iter): """ Use a different background color for alphabetical headers """ if model.get_value(iter, ROW_TYPE) == TYPE_HEADER: cell.set_property('cell-background-gdk', self.tree.style.base[gtk.STATE_PRELIGHT]) else: cell.set_property('cell-background', None) def __drawAlbumLenCell(self, column, cell, model, iter): """ Use a different background color for alphabetical headers """ if model.get_value(iter, ROW_ALBUM_LEN) is None: cell.set_property('visible', False) else: cell.set_property('visible', True) def __createEmptyLibrary(self, name): """ Create bootstrap files for a new library """ import shutil # Make sure that the root directory of all libraries exists if not isdir(ROOT_PATH): os.mkdir(ROOT_PATH) # Start from an empty library libPath = os.path.join(ROOT_PATH, name) if isdir(libPath): shutil.rmtree(libPath) os.mkdir(libPath) pickleSave(os.path.join(libPath, 'files'), {}) def refreshLibrary(self, parent, libName, path, creation=False): """ Refresh the given library, must be called through idle_add() """ import collections, shutil from gui import progressDlg # First show a progress dialog if creation: header = _('Creating library') else: header = _('Refreshing library') progress = progressDlg.ProgressDlg(parent, header, _('The directory is scanned for media files. This can take some time.\nPlease wait.')) yield True libPath = os.path.join(ROOT_PATH, libName) # Location of the library # If the version number has changed or does not exist, don't reuse any existing file and start from scratch if not os.path.exists(os.path.join(libPath, 'VERSION_%u' % VERSION)): self.__createEmptyLibrary(libName) db = {} # The dictionnary used to create the library queue = collections.deque((path,)) # Faster structure for appending/removing elements mediaFiles = [] # All media files found newLibrary = {} # Reflect the current file structure of the library oldLibrary = pickleLoad(os.path.join(libPath, 'files')) # Previous file structure of the same library # Make sure the root directory still exists if not os.path.exists(path): queue.pop() while len(queue) != 0: currDir = queue.pop() currDirMTime = os.stat(currDir).st_mtime # Retrieve previous information on the current directory, if any if currDir in oldLibrary: oldDirMTime, oldDirectories, oldFiles = oldLibrary[currDir] else: oldDirMTime, oldDirectories, oldFiles = -1, [], {} # If the directory has not been modified, keep old information if currDirMTime == oldDirMTime: files, directories = oldFiles, oldDirectories else: files, directories = {}, [] for (filename, fullPath) in tools.listDir(currDir): if isdir(fullPath): directories.append(fullPath) elif isfile(fullPath) and media.isSupported(filename): if filename in oldFiles: files[filename] = oldFiles[filename] else: files[filename] = [-1, FileTrack(fullPath)] # Determine which files need to be updated for filename, (oldMTime, track) in files.iteritems(): mTime = os.stat(track.getFilePath()).st_mtime if mTime != oldMTime: files[filename] = [mTime, media.getTrackFromFile(track.getFilePath())] newLibrary[currDir] = (currDirMTime, directories, files) mediaFiles.extend([track for mTime, track in files.itervalues()]) queue.extend(directories) # Update the progress dialog try: text = ngettext('Scanning directories (one track found)', 'Scanning directories (%(nbtracks)u tracks found)', len(mediaFiles)) progress.pulse(text % {'nbtracks': len(mediaFiles)}) yield True except progressDlg.CancelledException: progress.destroy() if creation: shutil.rmtree(libPath) yield False # From now on, the process should not be cancelled progress.setCancellable(False) if creation: progress.pulse(_('Creating library...')) else: progress.pulse(_('Refreshing library...')) yield True # Create the database for track in mediaFiles: album = track.getExtendedAlbum() genre = track.getGenre().lower() if track.hasAlbumArtist(): artist = track.getAlbumArtist() else: artist = track.getArtist() if artist in db: allAlbums = db[artist] try: albumNfo = allAlbums[album] albumNfo[0][genre] = None albumNfo[1].append(track) except: allAlbums[album] = ({genre: None}, [track]) else: db[artist] = {album: ({genre: None}, [track])} progress.pulse() yield True # If an artist name begins with a known prefix, put it at the end (e.g., Future Sound of London (The)) prefixes = prefs.get(__name__, 'prefixes', PREFS_DEFAULT_PREFIXES) for artist in db.keys(): artistLower = artist.lower() for prefix in prefixes: if artistLower.startswith(prefix): db[artist[len(prefix):] + ' (%s)' % artist[:len(prefix)-1]] = db[artist] del db[artist] progress.pulse() yield True # Load favorites before removing the files from the disk if self.currLib == libName: favorites = self.favorites else: favorites = self.loadFavorites(libName) # Re-create the library structure on the disk if isdir(libPath): shutil.rmtree(libPath) os.mkdir(libPath) # Put a version number tools.touch(os.path.join(libPath, 'VERSION_%u' % VERSION)) overallNbAlbums = 0 overallNbTracks = 0 overallNbArtists = len(db) # The 'artists' file contains all known artists with their index, the 'files' file contains the file structure of the root path allArtists = sorted([(artist, str(indexArtist), len(albums)) for indexArtist, (artist, albums) in enumerate(db.iteritems())], key = lambda a: a[0].lower()) pickleSave(os.path.join(libPath, 'files'), newLibrary) pickleSave(os.path.join(libPath, 'artists'), allArtists) # Keep track of genre through nested dictionaries genres -> artists -> albums allGenres = {} for (artist, indexArtist, nbAlbums) in allArtists: artistPath = os.path.join(libPath, indexArtist) overallNbAlbums += nbAlbums os.mkdir(artistPath) albums = [] for index, (name, (albumGenres, tracks)) in enumerate(db[artist].iteritems()): length = sum([track.getLength() for track in tracks]) overallNbTracks += len(tracks) albums.append((name, str(index), len(tracks), length)) pickleSave(os.path.join(artistPath, str(index)), sorted(tracks, key = lambda track: track.getNumber())) # Update the dictionary with the genres for genre in albumGenres: try: allGenres[genre][artist][name] = None except: try: allGenres[genre][artist] = {name: None} except: allGenres[genre] = {artist: {name: None}} albums.sort() pickleSave(os.path.join(artistPath, 'albums'), albums) progress.pulse() yield True pickleSave(os.path.join(libPath, 'genres'), allGenres) self.libraries[libName] = (path, overallNbArtists, overallNbAlbums, overallNbTracks) self.fillLibraryList() if creation: modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': libName, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled}) progress.destroy() # Trim favorites and save them newFavorites = {} for (artist, albums) in favorites.iteritems(): if artist in db: newFavorites[artist] = {} for album in albums: if album in db[artist][1]: newFavorites[artist][album] = None self.saveFavorites(libName, newFavorites) # If the refreshed library is currently displayed, refresh the treeview as well if self.currLib == libName: self.saveTreeState() self.favorites = newFavorites self.loadArtists(self.tree, self.currLib) self.restoreTreeState() yield False def __getTracksFromPaths(self, tree, paths): """ Return the list of tracks extracted from: * The list 'paths' if it is not None * The currently selected rows if 'paths' is None """ from sys import maxint tracks = [] if paths is None: paths = tree.getSelectedPaths() for currPath in paths: row = tree.getRow(currPath) if row[ROW_TYPE] == TYPE_TRACK: tracks.append(row[ROW_DATA]) elif row[ROW_TYPE] == TYPE_ALBUM: tracks.extend(pickleLoad(row[ROW_FULLPATH])) elif row[ROW_TYPE] == TYPE_ARTIST: for album in pickleLoad(os.path.join(row[ROW_FULLPATH], 'albums')): tracks.extend(pickleLoad(os.path.join(row[ROW_FULLPATH], album[ALB_INDEX]))) elif row[ROW_TYPE] == TYPE_HEADER: for path in xrange(currPath[0]+1, maxint): if not tree.isValidPath(path): break row = tree.getRow(path) if row[ROW_TYPE] == TYPE_HEADER: break for album in pickleLoad(os.path.join(row[ROW_FULLPATH], 'albums')): tracks.extend(pickleLoad(os.path.join(row[ROW_FULLPATH], album[ALB_INDEX]))) return tracks def playPaths(self, tree, paths, replace): """ Replace/extend the tracklist If the list 'paths' is None, use the current selection """ tracks = self.__getTracksFromPaths(tree, paths) if replace: modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': True}) else: modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': False}) def pickAlbumArtist(self, tree, artistPath): """ Pick an album at random of the given artist and play it """ import random # Expanding the artist row populates it, so that we can then pick an album at random tree.expandRow(artistPath) albumPath = artistPath + (random.randint(0, tree.getNbChildren(artistPath)-1), ) # Select the random album and play it tree.get_selection().unselect_all() tree.get_selection().select_path(albumPath) tree.scroll(albumPath) self.playPaths(tree, [albumPath], True) def pickAlbumLibrary(self, tree): """ Pick an album at random in the library and play it """ import random # Pick an artist at random (make sure not to select an alphabetical header) path = (random.randint(0, tree.getCount()-1), ) while tree.getItem(path, ROW_TYPE) != TYPE_ARTIST: path = (random.randint(0, tree.getCount()-1), ) self.pickAlbumArtist(tree, path) def switchFavoriteStateOfSelectedItems(self, tree): """ Add to/remove from favorites the selected items """ # Go through selected items and switch their state removed = False for path in tree.getSelectedPaths(): if tree.getItem(path, ROW_TYPE) == TYPE_ALBUM: album = tree.getItem(path, ROW_DATA) artist = tree.getItem(path[:-1], ROW_DATA) if self.isAlbumInFavorites(artist, album): removed = True tree.setItem(path, ROW_PIXBUF, icons.mediaDirMenuIcon()) self.removeFromFavorites(artist, album) else: tree.setItem(path, ROW_PIXBUF, icons.starDirMenuIcon()) self.addToFavorites(artist, album) # If some favorites were removed, we may have to reload the tree if self.showOnlyFavs and removed: self.saveTreeState() self.loadArtists(self.tree, self.currLib) self.restoreTreeState() def switchFavoritesView(self, tree): """ Show all/favorites """ self.saveTreeState() self.showOnlyFavs = not self.showOnlyFavs prefs.set(__name__, 'show-only-favorites', self.showOnlyFavs) self.loadArtists(self.tree, self.currLib) self.restoreTreeState() def filterByGenre(self, genre): """ Filter the library and keep only albums of the given genre """ self.saveTreeState() self.currGenre = genre self.saveGenreFilter(self.currLib, genre) self.loadArtists(self.tree, self.currLib) self.restoreTreeState() def showPopupMenu(self, tree, button, time, path): """ Show a popup menu """ popup = gtk.Menu() # Play play = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY) popup.append(play) if path is None: play.set_sensitive(False) else: play.connect('activate', lambda widget: self.playPaths(tree, None, True)) # Add add = gtk.ImageMenuItem(gtk.STOCK_ADD) popup.append(add) if path is None: add.set_sensitive(False) else: add.connect('activate', lambda widget: self.playPaths(tree, None, False)) # Separator popup.append(gtk.SeparatorMenuItem()) # Add to/remove from favorites favCpt = 0 nonFavCpt = 0 for node in tree.getSelectedPaths(): if tree.getItem(node, ROW_TYPE) != TYPE_ALBUM: favCpt = 1 nonFavCpt = 1 break elif tree.getItem(node, ROW_PIXBUF) == icons.mediaDirMenuIcon(): nonFavCpt += 1 else: favCpt += 1 if favCpt == 0: favorite = gtk.ImageMenuItem(_('Add to Favorites')) elif nonFavCpt == 0: favorite = gtk.ImageMenuItem(_('Remove from Favorites')) else: favorite = gtk.ImageMenuItem(_('Favorites')) favorite.get_image().set_from_pixbuf(icons.starMenuIcon()) popup.append(favorite) if TYPE_ALBUM in [tree.getItem(path, ROW_TYPE) for path in tree.getSelectedPaths()]: favorite.connect('activate', lambda widget: self.switchFavoriteStateOfSelectedItems(tree)) else: favorite.set_sensitive(False) # Show only favorites showFavorites = gtk.CheckMenuItem(_('Show Only Favorites')) showFavorites.set_active(self.showOnlyFavs) showFavorites.connect('toggled', lambda widget: self.switchFavoritesView(tree)) popup.append(showFavorites) # Separator popup.append(gtk.SeparatorMenuItem()) # Filter by genre genresMenu = gtk.Menu() filterGenre = gtk.MenuItem(_("Filter by Genre")); filterGenre.set_submenu(genresMenu) popup.append(filterGenre) # Need to keep the mapping widget -> genre to handle activate events self.genreItemToName = {} for genre in sorted(self.allGenres): genreItem = gtk.CheckMenuItem(genre.capitalize()) genreItem.set_active(genre == self.currGenre) self.genreItemToName[genreItem] = genre genreItem.connect('activate', lambda widget: self.filterByGenre(self.genreItemToName[widget])) genresMenu.append(genreItem) genresMenu.append(gtk.SeparatorMenuItem()) genreItem = gtk.CheckMenuItem(_('Unfiltered')) genreItem.set_active(self.currGenre == None) genreItem.connect('activate', lambda widget: self.filterByGenre(None)) genresMenu.append(genreItem) # Separator popup.append(gtk.SeparatorMenuItem()) # Collapse all nodes collapse = gtk.ImageMenuItem(_('Collapse all')) collapse.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU)) popup.append(collapse) enabled = False for child in self.tree.iterChildren(None): if self.tree.row_expanded(child): enabled = True break if enabled: collapse.connect('activate', lambda widget: self.tree.collapse_all()) else: collapse.set_sensitive(False) # Refresh the library refresh = gtk.ImageMenuItem(gtk.STOCK_REFRESH) refresh.connect('activate', lambda widget: idle_add(self.refreshLibrary(None, self.currLib, self.libraries[self.currLib][LIB_PATH]).next)) popup.append(refresh) # Randomness randomness = gtk.Menu() randomnessItem = gtk.ImageMenuItem(_('Randomness')) randomnessItem.get_image().set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_MENU) randomnessItem.set_submenu(randomness) popup.append(randomnessItem) # Random album of the selected artist if path is not None and tree.getItem(path, ROW_TYPE) == TYPE_ARTIST: album = gtk.MenuItem(_('Pick an album of %(artist)s' % {'artist': tree.getItem(path, ROW_NAME).replace('&', '&')})) album.connect('activate', lambda widget: self.pickAlbumArtist(tree, path)) randomness.append(album) # Random album in the entire library album = gtk.MenuItem(_('Pick an album in the library')) album.connect('activate', lambda widget: self.pickAlbumLibrary(tree)) randomness.append(album) popup.show_all() popup.popup(None, None, None, button, time) # --== Populating the tree ==-- def loadArtists(self, tree, name): """ Load the given library """ libPath = os.path.join(ROOT_PATH, name) # Make sure the version number is the good one if not os.path.exists(os.path.join(libPath, 'VERSION_%u' % VERSION)): logger.error('[%s] Version number does not match, loading of library "%s" aborted' % (MOD_INFO[modules.MODINFO_NAME], name)) error = _('This library is deprecated, please refresh it.') tree.replaceContent([(icons.errorMenuIcon(), None, error, TYPE_NONE, None, None)]) return rows = [] icon = icons.dirMenuIcon() prevChar = '' allArtists = pickleLoad(os.path.join(libPath, 'artists')) self.allGenres = pickleLoad(os.path.join(libPath, 'genres')) # Filter artists by genre if needed if self.currGenre is not None: allArtists = [artist for artist in allArtists if artist[ART_NAME] in self.allGenres[self.currGenre]] rows.append((icons.infoMenuIcon(), None, '<b>%s</b>' % self.currGenre.capitalize(), TYPE_GENRE_BANNER, None, None)) else: rows.append((icons.infoMenuIcon(), None, '<b>%s</b>' % _('All genres'), TYPE_GENRE_BANNER, None, None)) # Filter artists by favorites if needed if self.showOnlyFavs: allArtists = [artist for artist in allArtists if self.isArtistInFavorites(artist[ART_NAME])] rows.append((icons.starMenuIcon(), None, '<b>%s</b>' % _('My Favorites'), TYPE_FAVORITES_BANNER, None, None)) # Create the rows for artist in allArtists: if len(artist[ART_NAME]) != 0: currChar = unicode(artist[ART_NAME], errors='replace')[0].lower() else: currChar = prevChar if prevChar != currChar and not (prevChar.isdigit() and currChar.isdigit()): prevChar = currChar if currChar.isdigit(): rows.append((None, None, '<b>0 - 9</b>', TYPE_HEADER, None, None)) else: rows.append((None, None, '<b>%s</b>' % currChar.upper(), TYPE_HEADER, None, None)) rows.append((icon, None, htmlEscape(artist[ART_NAME]), TYPE_ARTIST, os.path.join(libPath, artist[ART_INDEX]), artist[ART_NAME])) # Insert all rows, and then add a fake child to each artist tree.replaceContent(rows) for node in tree.iterChildren(None): if tree.getItem(node, ROW_TYPE) == TYPE_ARTIST: tree.appendRow(FAKE_CHILD, node) def loadAlbums(self, tree, node, fakeChild): """ Initial load of the albums of the given node, assuming it is of type TYPE_ARTIST """ rows = [] path = tree.getItem(node, ROW_FULLPATH) artist = tree.getItem(node, ROW_DATA) allAlbums = pickleLoad(os.path.join(tree.getItem(node, ROW_FULLPATH), 'albums')) # Filter albums if only favorites should be shown if self.showOnlyFavs: allAlbums = [album for album in allAlbums if self.isAlbumInFavorites(artist, album[ALB_NAME])] # Filter artists by genre if needed if self.currGenre is not None: allAlbums = [album for album in allAlbums if album[ALB_NAME] in self.allGenres[self.currGenre][artist]] # The icon depends on whether the album is in the favorites for album in allAlbums: if self.isAlbumInFavorites(artist, album[ALB_NAME]): icon = icons.starDirMenuIcon() else: icon = icons.mediaDirMenuIcon() rows.append((icon, '[%s]' % tools.sec2str(album[ALB_LENGTH], True), '%s' % htmlEscape(album[ALB_NAME]), TYPE_ALBUM, os.path.join(path, album[ALB_INDEX]), album[ALB_NAME])) # Add all the rows, and then add a fake child to each of them tree.appendRows(rows, node) tree.removeRow(fakeChild) for child in tree.iterChildren(node): tree.appendRow(FAKE_CHILD, child) def loadTracks(self, tree, node, fakeChild): """ Initial load of all tracks of the given node, assuming it is of type TYPE_ALBUM """ allTracks = pickleLoad(tree.getItem(node, ROW_FULLPATH)) icon = icons.mediaFileMenuIcon() rows = [(icon, None, '%02u. %s' % (track.getNumber(), htmlEscape(track.getTitle())), TYPE_TRACK, track.getFilePath(), track) for track in allTracks] tree.appendRows(rows, node) tree.removeRow(fakeChild) # --== Manage tree state ==-- def saveTreeState(self): """ Save the current tree state """ state = self.tree.saveState(ROW_NAME) try: self.treeStates[self.currLib][(self.currGenre, self.showOnlyFavs)] = state except: self.treeStates[self.currLib] = {(self.currGenre, self.showOnlyFavs): state} def restoreTreeState(self): """ Restore the tree state """ try: self.tree.restoreState(self.treeStates[self.currLib][(self.currGenre, self.showOnlyFavs)], ROW_NAME) except: pass def removeTreeStates(self, libName): """ Remove the tree states associated to the given library """ if libName in self.treeStates: del self.treeStates[libName] def renameTreeStates(self, oldLibName, newLibName): """ Rename the tree states associated with oldLibName """ if oldLibName in self.treeStates: self.treeStates[newLibName] = self.treeStates[oldLibName] del self.treeStates[oldLibName] # --== Filtering by genre ==-- def loadGenreFilter(self, libName): """ Load the last genre used to filter the given library """ savedGenreFilters = prefs.get(__name__, 'genre-filters', PREFS_DEFAULT_GENRE_FILTERS) try: return savedGenreFilters[libName] except: return None def saveGenreFilter(self, libName, genre): """ Save the current genre used to filter the given library """ savedGenreFilters = prefs.get(__name__, 'genre-filters', PREFS_DEFAULT_GENRE_FILTERS) savedGenreFilters[libName] = genre prefs.set(__name__, 'genre-filters', savedGenreFilters) # --== Favorites ==-- def loadFavorites(self, libName): """ Load favorites from the disk """ try: return pickleLoad(os.path.join(ROOT_PATH, libName, 'favorites')) except: return {} def saveFavorites(self, libName, favorites): """ Save favorites to the disk """ pickleSave(os.path.join(ROOT_PATH, libName, 'favorites'), favorites) def isArtistInFavorites(self, artist): """ Return whether the given artist is in the favorites (at least one album) """ return artist in self.favorites def isAlbumInFavorites(self, artist, album): """ Return whether the given album is in the favorites """ return artist in self.favorites and album in self.favorites[artist] def addToFavorites(self, artist, album): """ Add the given album to the favorites """ if artist in self.favorites: self.favorites[artist][album] = None else: self.favorites[artist] = {album: None} def removeFromFavorites(self, artist, album): """ Remove the given album from the favorites """ del self.favorites[artist][album] if len(self.favorites[artist]) == 0: del self.favorites[artist] # --== GTK handlers ==-- def onRowExpanded(self, tree, node): """ Populate the expanded row """ if tree.getItem(node, ROW_TYPE) == TYPE_ARTIST: self.loadAlbums(tree, node, tree.getChild(node, 0)) else: self.loadTracks(tree, node, tree.getChild(node, 0)) def onRowCollapsed(self, tree, node): """ Replace all children of the node by a fake child """ tree.removeAllChildren(node) tree.appendRow(FAKE_CHILD, node) def onButtonPressed(self, tree, event, path): """ A mouse button has been pressed """ if event.button == 3: self.showPopupMenu(tree, event.button, event.time, path) elif path is not None and tree.getItem(path, ROW_TYPE) != TYPE_NONE: if event.button == 2: self.playPaths(tree, [path], False) elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS: if tree.getItem(path, ROW_PIXBUF) != icons.dirMenuIcon(): self.playPaths(tree, None, True) elif tree.row_expanded(path): tree.collapse_row(path) else: tree.expand_row(path, False) def onKeyPressed(self, tree, event): """ A key has been pressed """ keyname = gtk.gdk.keyval_name(event.keyval) if keyname == 'F5': idle_add(self.refreshLibrary(None, self.currLib, self.libraries[self.currLib][LIB_PATH]).next) elif keyname == 'plus': tree.expandRows() elif keyname == 'Left': tree.collapseRows() elif keyname == 'Right': tree.expandRows() elif keyname == 'minus': tree.collapseRows() elif keyname == 'space': tree.switchRows() elif keyname == 'Return': self.playPaths(tree, None, True) def onDragDataGet(self, tree, context, selection, info, time): """ Provide information about the data being dragged """ serializedTracks = '\n'.join([track.serialize() for track in self.__getTracksFromPaths(tree, None)]) selection.set(consts.DND_TARGETS[consts.DND_DAP_TRACKS][0], 8, serializedTracks) def addAllExplorers(self): """ Add all libraries to the Explorer module """ for (name, (path, nbArtists, nbAlbums, nbTracks)) in self.libraries.iteritems(): modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled}) def removeAllExplorers(self): """ Remove all libraries from the Explorer module """ for (name, (path, nbArtists, nbAlbums, nbTracks)) in self.libraries.iteritems(): modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': name}) # --== Message handlers ==-- def onModLoaded(self): """ This is the real initialization function, called when the module has been loaded """ self.tree = None self.currLib = None self.allGenres = {} self.currGenre = None self.cfgWindow = None self.libraries = prefs.get(__name__, 'libraries', PREFS_DEFAULT_LIBRARIES) self.favorites = None self.treeStates = prefs.get(__name__, 'tree-states-2', PREFS_DEFAULT_TREE_STATE) self.showOnlyFavs = prefs.get(__name__, 'show-only-favorites', PREFS_DEFAULT_SHOW_ONLY_FAVORITES) # Scroll window self.scrolled = gtk.ScrolledWindow() self.scrolled.set_shadow_type(gtk.SHADOW_IN) self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrolled.show() idle_add(self.addAllExplorers) def onModUnloaded(self): """ The module has been unloaded """ if self.currLib is not None: self.saveTreeState() self.saveFavorites(self.currLib, self.favorites) prefs.set(__name__, 'tree-states-2', self.treeStates) prefs.set(__name__, 'libraries', self.libraries) self.removeAllExplorers() def onExplorerChanged(self, modName, expName): """ A new explorer has been selected """ if modName == MOD_L10N and expName != self.currLib: # Create the tree if needed if self.tree is None: self.__createTree() # Save the state of the current library if self.currLib is not None: self.saveTreeState() self.saveFavorites(self.currLib, self.favorites) # Switch to the new library self.currLib = expName self.favorites = self.loadFavorites(self.currLib) self.currGenre = self.loadGenreFilter(self.currLib) self.loadArtists(self.tree, self.currLib) self.restoreTreeState() # --== Configuration ==-- def configure(self, parent): """ Show the configuration dialog """ if self.cfgWindow is None: from gui import extListview, window self.cfgWindow = window.Window('Library.ui', 'vbox1', __name__, MOD_L10N, 370, 400) # Create the list of libraries txtRdr = gtk.CellRendererText() pixRdr = gtk.CellRendererPixbuf() columns = ((None, [(txtRdr, TYPE_STRING)], 0, False, False), ('', [(pixRdr, gtk.gdk.Pixbuf), (txtRdr, TYPE_STRING)], 2, False, True)) self.cfgList = extListview.ExtListView(columns, sortable=False, useMarkup=True, canShowHideColumns=False) self.cfgList.set_headers_visible(False) self.cfgWindow.getWidget('scrolledwindow1').add(self.cfgList) # Connect handlers self.cfgList.connect('key-press-event', self.onCfgKeyboard) self.cfgList.get_selection().connect('changed', self.onCfgSelectionChanged) self.cfgWindow.getWidget('btn-add').connect('clicked', self.onAddLibrary) self.cfgWindow.getWidget('btn-rename').connect('clicked', self.onRenameLibrary) self.cfgWindow.getWidget('btn-remove').connect('clicked', lambda btn: self.removeSelectedLibraries(self.cfgList)) self.cfgWindow.getWidget('btn-refresh').connect('clicked', self.onRefresh) self.cfgWindow.getWidget('btn-ok').connect('clicked', lambda btn: self.cfgWindow.hide()) self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide()) self.cfgWindow.getWidget('btn-help').connect('clicked', self.onHelp) if not self.cfgWindow.isVisible(): self.fillLibraryList() self.cfgWindow.getWidget('btn-ok').grab_focus() self.cfgWindow.show() def onRefresh(self, btn): """ Refresh the first selected library """ name = self.cfgList.getSelectedRows()[0][0] idle_add(self.refreshLibrary(self.cfgWindow, name, self.libraries[name][LIB_PATH]).next) def onAddLibrary(self, btn): """ Let the user create a new library """ from gui.selectPath import SelectPath result = SelectPath(MOD_L10N, self.cfgWindow, self.libraries.keys(), ['/']).run() if result is not None: name, path = result idle_add(self.refreshLibrary(self.cfgWindow, name, path, True).next) def renameLibrary(self, oldName, newName): """ Rename a library """ import shutil self.libraries[newName] = self.libraries[oldName] del self.libraries[oldName] oldPath = os.path.join(ROOT_PATH, oldName) newPath = os.path.join(ROOT_PATH, newName) shutil.move(oldPath, newPath) # Rename tree states as well self.renameTreeStates(oldName, newName) # Is it the current library? if self.currLib == oldName: self.currLib = newName modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName}) def onRenameLibrary(self, btn): """ Let the user rename a library """ from gui.selectPath import SelectPath name = self.cfgList.getSelectedRows()[0][0] forbidden = [libName for libName in self.libraries if libName != name] pathSelector = SelectPath(MOD_L10N, self.cfgWindow, forbidden, ['/']) pathSelector.setPathSelectionEnabled(False) result = pathSelector.run(name, self.libraries[name][LIB_PATH]) if result is not None and result[0] != name: self.renameLibrary(name, result[0]) self.fillLibraryList() def fillLibraryList(self): """ Fill the list of libraries """ if self.cfgWindow is not None: rows = [(name, icons.dirToolbarIcon(), '<b>%s</b>\n<small>%s - %u %s</small>' % (htmlEscape(name), htmlEscape(path), nbTracks, htmlEscape(_('tracks')))) for name, (path, nbArtists, nbAlbums, nbTracks) in sorted(self.libraries.iteritems())] self.cfgList.replaceContent(rows) def removeSelectedLibraries(self, list): """ Remove all selected libraries """ import shutil from gui import questionMsgBox if list.getSelectedRowsCount() == 1: remark = _('You will be able to recreate this library later on if you wish so.') question = _('Remove the selected library?') else: remark = _('You will be able to recreate these libraries later on if you wish so.') question = _('Remove all selected libraries?') if questionMsgBox(self.cfgWindow, question, '%s %s' % (_('Your media files will not be removed.'), remark)) == gtk.RESPONSE_YES: for row in list.getSelectedRows(): libName = row[0] if self.currLib == libName: self.currLib = None # Remove the library from the disk libPath = os.path.join(ROOT_PATH, libName) if isdir(libPath): shutil.rmtree(libPath) # Remove the corresponding explorer modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': libName}) del self.libraries[libName] # Remove tree states self.removeTreeStates(libName) # Clean up the listview list.removeSelectedRows() def onCfgKeyboard(self, list, event): """ Remove the selection if possible """ if gtk.gdk.keyval_name(event.keyval) == 'Delete': self.removeSelectedLibraries(list) def onCfgSelectionChanged(self, selection): """ The selection has changed, update the status of the buttons """ self.cfgWindow.getWidget('btn-remove').set_sensitive(selection.count_selected_rows() != 0) self.cfgWindow.getWidget('btn-rename').set_sensitive(selection.count_selected_rows() == 1) self.cfgWindow.getWidget('btn-refresh').set_sensitive(selection.count_selected_rows() == 1) def onHelp(self, btn): """ Display a small help message box """ from gui import help helpDlg = help.HelpDlg(MOD_L10N) helpDlg.addSection(_('Description'), _('This module organizes your media files by tags instead of using the file structure of your drive. ' 'Loading tracks is also faster because their tags are already known and do not have to be read again.')) helpDlg.addSection(_('Usage'), _('When you add a new library, you have to give the full path to the root directory of that library. ' 'Then, all directories under this root path are recursively scanned for media files whose tags are read ' 'and stored in a database.') + '\n\n' + _('Upon refreshing a library, the file structure under the root ' 'directory and all media files are scanned for changes, to update the database accordingly.')) helpDlg.show(self.cfgWindow)