def onAppStarted(self): """ The module has been loaded """ self.tree = None self.cfgWin = None self.folders = prefs.get(__name__, 'media-folders', PREFS_DEFAULT_MEDIA_FOLDERS) self.scrolled = gtk.ScrolledWindow() self.currRoot = None self.treeState = prefs.get(__name__, 'saved-states', {}) self.addByFilename = prefs.get(__name__, 'add-by-filename', PREFS_DEFAULT_ADD_BY_FILENAME) self.showHiddenFiles = prefs.get(__name__, 'show-hidden-files', PREFS_DEFAULT_SHOW_HIDDEN_FILES) self.scrolled.set_shadow_type(gtk.SHADOW_IN) self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrolled.show() for name in self.folders: modules.postMsg( consts.MSG_CMD_EXPLORER_ADD, { 'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled })
def onAppStarted(self): """ Real initialization function, called when this module has been loaded """ self.currTrackLength = 0 self.sclBeingDragged = False # Widgets wTree = prefs.getWidgetsTree() # self.btnStop = wTree.get_widget('btn-stop') self.btnPlay = wTree.get_widget('btn-play') # self.btnNext = wTree.get_widget('btn-next') self.btnPrev = wTree.get_widget('btn-previous') self.sclSeek = wTree.get_widget('scl-position') self.btnVolume = wTree.get_widget('btn-volume') self.lblElapsed = wTree.get_widget('lbl-elapsedTime') self.lblRemaining = wTree.get_widget('lbl-remainingTime') # Initial state self.onStopped() self.btnPlay.set_sensitive(False) # GTK handlers # self.btnStop.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_STOP)) # self.btnNext.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_NEXT)) self.btnPrev.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_PREVIOUS)) self.btnPlay.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)) self.sclSeek.connect('change-value', self.onSeekChangeValue) # We need to keep the handler ID for this one to be able to disconnect it when needed self.seekHandler = self.sclSeek.connect('value-changed', self.onSeekValueChanged) self.volumeHandler = self.btnVolume.connect('value-changed', self.onVolumeValueChanged) # We must make sure that the handler will be called: this is not the case if the new value is the same as the old one volumeValue = prefs.get(__name__, 'volume', PREFS_DEFAULT_VOLUME) if self.btnVolume.get_value() != volumeValue: self.btnVolume.set_value(volumeValue) else: self.onVolumeValueChanged(self.btnVolume, volumeValue)
def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.bufferedTrack = None # Retrieve widgets self.window = wTree.get_object('win-main') columns = (('', [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (gtk.CellRendererText(), TYPE_STRING)], True), (None, [(None, TYPE_PYOBJECT)], False), ) self.tree = TrackTreeView(columns, use_markup=True) self.tree.enableDNDReordering() self.tree.setDNDSources([DND_INTERNAL_TARGET]) wTree.get_object('scrolled-tracklist').add(self.tree) # GTK handlers self.tree.connect('exttreeview-button-pressed', self.onMouseButton) self.tree.connect('tracktreeview-dnd', self.onDND) self.tree.connect('key-press-event', self.onKeyboard) self.tree.get_model().connect('row-deleted', self.onRowDeleted) (options, args) = prefs.getCmdLine() self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist') self.paused = False # Populate the playlist with the saved playlist dump = None if os.path.exists(self.savedPlaylist): try: dump = pickleLoad(self.savedPlaylist) except: msg = '[%s] Unable to restore playlist from %s\n\n%s' log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME], self.savedPlaylist, traceback.format_exc())) if dump: self.restoreTreeDump(dump) log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME]) self.tree.collapse_all() self.select_last_played_track() self.onListModified() commands, args = tools.separate_commands_and_tracks(args) # Add commandline tracks to the playlist if args: log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME]) tracks = media.getTracks([os.path.abspath(arg) for arg in args]) playNow = not 'stop' in commands and not 'pause' in commands modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow}) elif 'play' in commands: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) # Automatically save the content at regular intervals gobject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)
def onSearch(self, query): self.should_stop = False self.found_something = False # Transform whitespace-separated query into OR-regex. regex = re.compile('|'.join(tools.get_pattern(word) for word in query.split()), re.IGNORECASE) for dir in self.get_search_paths() + [consts.dirBaseUsr]: # Check if search has been aborted during filtering if self.should_stop: break # Only search in home folder if we haven't found anything yet. if dir == consts.dirBaseUsr and self.found_something: break results = self.search_dir(dir, query) # Check if search has been aborted during searching if results is None or self.should_stop: break dirs, files = self.filter_results(results, dir, regex) if not self.should_stop and (dirs or files): self.found_something = True modules.postMsg(consts.MSG_EVT_SEARCH_APPEND, {'results': (dirs, files), 'query': query}) modules.postMsg(consts.MSG_EVT_SEARCH_END)
def onSearch(self, query): self.should_stop = False self.found_something = False # Transform whitespace-separated query into OR-regex. regex = re.compile( '|'.join(tools.get_pattern(word) for word in query.split()), re.IGNORECASE) for dir in self.get_search_paths() + [consts.dirBaseUsr]: # Check if search has been aborted during filtering if self.should_stop: break # Only search in home folder if we haven't found anything yet. if dir == consts.dirBaseUsr and self.found_something: break results = self.search_dir(dir, query) # Check if search has been aborted during searching if results is None or self.should_stop: break dirs, files = self.filter_results(results, dir, regex) if not self.should_stop and (dirs or files): self.found_something = True modules.postMsg(consts.MSG_EVT_SEARCH_APPEND, { 'results': (dirs, files), 'query': query }) modules.postMsg(consts.MSG_EVT_SEARCH_END)
def timerFunc(self): """ Move a bit the scales to their target value """ isFinished = True # Move the scales a bit for i in xrange(10): currLvl = self.scales[i].get_value() targetLvl = self.targetLvls[i] difference = targetLvl - currLvl if abs(difference) <= 0.25: newLvl = targetLvl else: newLvl = currLvl + (difference / 8.0) isFinished = False self.lvls[i] = newLvl self.scales[i].set_value(newLvl) # Set the equalizer to the new levels modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls}) if isFinished: self.timer = None prefs.set(__name__, 'levels', self.lvls) # Make sure labels are up to date (sometimes they aren't when we're done with the animation) # Also unblock the handlers for i in xrange(10): self.scales[i].queue_draw() self.scales[i].handler_unblock_by_func(self.onScaleValueChanged) return False return True
def onPopupMenu(self, statusIcon, button, time): """ The user asks for the popup menu """ if self.popupMenu is None: wTree = loadGladeFile('StatusIconMenu.ui') self.menuPlay = wTree.get_object('item-play') self.menuStop = wTree.get_object('item-stop') self.menuNext = wTree.get_object('item-next') self.popupMenu = wTree.get_object('menu-popup') self.menuPause = wTree.get_object('item-pause') self.menuPrevious = wTree.get_object('item-previous') self.menuSeparator = wTree.get_object('item-separator') # Connect handlers wTree.get_object('item-quit').connect('activate', lambda btn: modules.postQuitMsg()) wTree.get_object('item-preferences').connect('activate', lambda btn: modules.showPreferences()) self.menuPlay.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)) self.menuStop.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_STOP)) self.menuNext.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_NEXT)) self.menuPrevious.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_PREVIOUS)) self.menuPause.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)) self.popupMenu.show_all() # Enable only relevant menu entries self.menuStop.set_sensitive(self.isPlaying) self.menuNext.set_sensitive(self.isPlaying and self.trackHasNext) self.menuPause.set_sensitive(self.isPlaying and not self.isPaused) self.menuPrevious.set_sensitive(self.isPlaying and self.trackHasPrev) self.menuPlay.set_sensitive((not (self.isPlaying or self.emptyTracklist)) or self.isPaused) self.popupMenu.popup(None, None, gtk.status_icon_position_menu, button, time, statusIcon)
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 timerFunc(self): """ Move a bit the scales to their target value """ isFinished = True # Disconnect handlers before moving the scales for i in xrange(10): self.scales[i].disconnect(self.handlers[i]) # Move the scales a bit for i in xrange(10): currLvl = self.scales[i].get_value() targetLvl = self.targetLvls[i] difference = targetLvl - currLvl if abs(difference) <= 0.25: newLvl = targetLvl else: newLvl = currLvl + (difference / 8.0) isFinished = False self.lvls[i] = newLvl self.scales[i].set_value(newLvl) # Reconnect the handlers for i in xrange(10): self.handlers[i] = self.scales[i].connect('value-changed', self.onScaleValueChanged, i) # Set the equalizer to the new levels prefs.set(__name__, 'levels', self.lvls) modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls}) return not isFinished
def onModLoaded(self): """ The module has been loaded """ txtRdrLen = gtk.CellRendererText() columns = (('', [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (txtRdrLen, gobject.TYPE_STRING), (gtk.CellRendererText(), gobject.TYPE_STRING)], True), (None, [(None, gobject.TYPE_PYOBJECT)], False)) # The album length is written in a smaller font, with a lighter color txtRdrLen.set_property('scale', 0.85) txtRdrLen.set_property('foreground', '#909090') self.tree = extTreeview.ExtTreeView(columns, True) self.popup = None self.cfgWin = None self.expName = MOD_L10N self.scrolled = gtk.ScrolledWindow() self.cacheDir = os.path.join(consts.dirCfg, MOD_INFO[modules.MODINFO_NAME]) # Explorer self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]]) self.scrolled.add(self.tree) self.scrolled.set_shadow_type(gtk.SHADOW_IN) self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrolled.show() # GTK handlers self.tree.connect('drag-data-get', self.onDragDataGet) self.tree.connect('key-press-event', self.onKeyPressed) self.tree.connect('exttreeview-button-pressed', self.onButtonPressed) modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': self.expName, 'icon': consts.icoCdrom, 'widget': self.scrolled}) # Hide the album length when not drawing the root node self.tree.get_column(0).set_cell_data_func(txtRdrLen, self.__drawAlbumLenCell)
def tryToRestore(self, tracklist): """ Check whether it's possible to restore playback """ (options, args) = prefs.getCmdLine() # Ignore if the user provided its own tracks on the command line if len(args) != 0: return # Ignore if no track was being played last time if not self.playing or self.currTrack is None: return # Make sure the playlist is the same one if len(tracklist) != len(self.currTracklist): return trackIdx = -1 for i in xrange(len(tracklist)): if tracklist[i] != self.currTracklist[i]: return if tracklist[i] == self.currTrack: trackIdx = i # Once here, we know playback can be resumed if trackIdx != -1: if self.paused: modules.postMsg(consts.MSG_CMD_TRACKLIST_PLAY_PAUSE, {'idx': trackIdx, 'seconds': self.currPos}) else: modules.postMsg(consts.MSG_CMD_TRACKLIST_PLAY, {'idx': trackIdx, 'seconds': self.currPos})
def updateTree(self, discInfo): """ Update the tree using disc information from the cache, if any """ cddb = self.getDiscFromCache(discInfo) # Create fake CDDB information if needed if cddb is None: cddb = {"DTITLE": "%s / %s" % (consts.UNKNOWN_ARTIST, consts.UNKNOWN_ALBUM)} for i in xrange(discInfo[DISC_NB_TRACKS]): cddb["TTITLE%u" % i] = consts.UNKNOWN_TITLE # Compute the length of each track trackLen = [ int(round((discInfo[DISC_FRAME1 + i + 1] - discInfo[DISC_FRAME1 + i]) / 75.0)) for i in xrange(discInfo[DISC_NB_TRACKS] - 1) ] trackLen.append(discInfo[DISC_LENGTH] - int(round(discInfo[DISC_FRAMEn] / 75.0))) # Update the root of the tree disc = cddb["DTITLE"].strip().decode("iso-8859-15", "replace") artist, album = disc.split(" / ") self.tree.setItem((0,), ROW_NAME, "%s" % tools.htmlEscape(disc)) self.tree.setItem((0,), ROW_LENGTH, "[%s]" % sec2str(sum(trackLen))) # Update the explorer name modules.postMsg( consts.MSG_CMD_EXPLORER_RENAME, {"modName": MOD_L10N, "expName": self.expName, "newExpName": disc} ) self.expName = disc # Optional information try: date = int(cddb["DYEAR"].strip().decode("iso-8859-15", "replace")) except: date = None try: genre = cddb["DGENRE"].strip().decode("iso-8859-15", "replace") except: genre = None # Update each track for i, child in enumerate(self.tree.iterChildren((0,))): title = cddb["TTITLE%u" % i].strip().decode("iso-8859-15", "replace") # Create the corresponding Track object track = CDTrack(str(i + 1)) track.setTitle(title) track.setAlbum(album) track.setArtist(artist) track.setLength(trackLen[i]) track.setNumber(i + 1) # Optional information if date is not None: track.setDate(date) if genre is not None: track.setGenre(genre) # Fill the tree self.tree.setItem(child, ROW_NAME, "%02u. %s" % (i + 1, tools.htmlEscape(title))) self.tree.setItem(child, ROW_TRACK, track)
def remove(self, iter=None): """ Remove the given track, or the selection if iter is None """ hadMark = self.tree.hasMark() iters = [iter] if iter else list(self.tree.iterSelectedRows()) prev_iter = self.tree.get_prev_iter_or_parent(iters[0]) # reverse list, so that we remove children before their fathers for iter in reversed(iters): track = self.tree.getTrack(iter) if track: self.playtime -= track.getLength() self.tree.removeRow(iter) self.tree.selection.unselect_all() if hadMark and not self.tree.hasMark(): modules.postMsg(consts.MSG_CMD_STOP) # Select new track when old selected is deleted if prev_iter: self.tree.select(prev_iter) else: first_iter = self.tree.get_first_iter() if first_iter: self.tree.select(first_iter) self.onListModified()
def onMediaKey(self, appName, action): """ A media key has been pressed """ if appName == consts.appName: # if action == 'Stop': modules.postMsg(consts.MSG_CMD_STOP) # elif action == 'Next': modules.postMsg(consts.MSG_CMD_NEXT) if action == 'Previous': modules.postMsg(consts.MSG_CMD_PREVIOUS) elif action in ['Play', 'Pause']: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
def onRemoveSelectedFolder(self, list): """ Remove the selected media folder """ import gui if list.getSelectedRowsCount() == 1: remark = _('You will be able to add this root folder again later on if you wish so.') question = _('Remove the selected entry?') else: remark = _('You will be able to add these root folders again later on if you wish so.') question = _('Remove all selected entries?') if gui.questionMsgBox(self.cfgWin, question, '%s %s' % (_('Your media files will not be deleted.'), remark)) == gtk.RESPONSE_YES: for row in self.cfgList.getSelectedRows(): name = row[0] modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': name}) del self.folders[name] # Remove the tree, if any, from the scrolled window if self.currRoot == name: self.currRoot = None # Remove the saved state of the tree, if any if name in self.treeState: del self.treeState[name] self.cfgList.removeSelectedRows()
def onScroll(self, statusIcon, scrollEvent): """ The mouse is scrolled on the status icon """ if scrollEvent.direction == gtk.gdk.SCROLL_UP or scrollEvent.direction == gtk.gdk.SCROLL_RIGHT: self.volume = min(1.0, self.volume + 0.05) else: self.volume = max(0.0, self.volume - 0.05) modules.postMsg(consts.MSG_CMD_SET_VOLUME, {'value': self.volume})
def onAddFolder(self, btn): """ Let the user add a new folder to the list """ result = selectPath.SelectPath(MOD_L10N, self.cfgWin, self.folders.keys()).run() if result is not None: name, path = result self.folders[name] = path self.populateFolderList() modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': name, 'icon': None, 'widget': self.scrolled})
def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.bufferedTrack = None self.previousTracklist = None # Retrieve widgets self.window = wTree.get_object('win-main') self.btnClear = wTree.get_object('btn-tracklistClear') self.btnRepeat = wTree.get_object('btn-tracklistRepeat') self.btnShuffle = wTree.get_object('btn-tracklistShuffle') self.btnClear.set_sensitive(False) self.btnShuffle.set_sensitive(False) # Create the list and its columns txtLRdr = gtk.CellRendererText() txtRRdr = gtk.CellRendererText() pixbufRdr = gtk.CellRendererPixbuf() txtRRdr.set_property('xalign', 1.0) # 'columns-visibility' may be broken, we should not use it (#311293) visible = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY) for (key, value) in PREFS_DEFAULT_COLUMNS_VISIBILITY.iteritems(): if key not in visible: visible[key] = value columns = (('#', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRRdr, TYPE_INT)], (ROW_NUM, ROW_TIT), False, visible[COL_TRCK_NUM]), (_('Title'), [(txtLRdr, TYPE_STRING)], (ROW_TIT,), True, visible[COL_TITLE]), (_('Artist'), [(txtLRdr, TYPE_STRING)], (ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ARTIST]), (_('Album'), [(txtLRdr, TYPE_STRING)], (ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ALBUM]), (_('Length'), [(txtRRdr, TYPE_INT)], (ROW_LEN,), False, visible[COL_LENGTH]), (_('Bit Rate'), [(txtRRdr, TYPE_STRING)], (ROW_BTR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_BITRATE]), (_('Genre'), [(txtLRdr, TYPE_STRING)], (ROW_GNR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_GENRE]), (_('Date'), [(txtLRdr, TYPE_INT)], (ROW_DAT, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_DATE]), (_('Filename'), [(txtLRdr, TYPE_STRING)], (ROW_FIL,), False, visible[COL_FILENAME]), (_('Path'), [(txtLRdr, TYPE_STRING)], (ROW_PTH,), False, visible[COL_PATH]), (None, [(None, TYPE_PYOBJECT)], (None,), False, False)) self.list = ExtListView(columns, sortable=True, dndTargets=consts.DND_TARGETS.values(), useMarkup=False, canShowHideColumns=True) self.list.get_column(1).set_cell_data_func(txtLRdr, self.__fmtColumnColor) self.list.get_column(4).set_cell_data_func(txtRRdr, self.__fmtLengthColumn) self.list.enableDNDReordering() wTree.get_object('scrolled-tracklist').add(self.list) # GTK handlers self.list.connect('extlistview-dnd', self.onDND) self.list.connect('key-press-event', self.onKeyboard) self.list.connect('extlistview-modified', self.onListModified) self.list.connect('extlistview-button-pressed', self.onButtonPressed) self.list.connect('extlistview-selection-changed', self.onSelectionChanged) self.list.connect('extlistview-column-visibility-changed', self.onColumnVisibilityChanged) self.btnClear.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) self.btnRepeat.connect('toggled', self.onButtonRepeat) self.btnShuffle.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE)) # Restore preferences self.btnRepeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS)) # Set icons wTree.get_object('img-repeat').set_from_icon_name('stock_repeat', gtk.ICON_SIZE_BUTTON) wTree.get_object('img-shuffle').set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_BUTTON)
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})
def renameLibrary(self, oldName, newName): """ Rename a library """ 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) modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName})
def crop(self): """ Remove the unselected tracks """ hadMark = self.list.hasMark() self.previousTracklist = [row[ROW_TRK] for row in self.list] self.playtime = sum([row[ROW_LEN] for row in self.list.iterSelectedRows()]) self.list.cropSelectedRows() if hadMark and not self.list.hasMark(): modules.postMsg(consts.MSG_CMD_STOP)
def onStop(self): """ Stop playing """ self.__stopUpdateTimer() self.player.stop() self.nextURI = None if self.playbackTimer is not None: gobject.source_remove(self.playbackTimer) modules.postMsg(consts.MSG_EVT_STOPPED)
def renameFolder(self, oldName, newName): """ Rename a folder """ self.folders[newName] = self.folders[oldName] del self.folders[oldName] if oldName in self.treeState: self.treeState[newName] = self.treeState[oldName] del self.treeState[oldName] modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName})
def play(self, replace, path=None): """ Replace/extend the tracklist If 'path' is None, use the current selection """ if path is None: tracks = media.getTracks([row[ROW_FULLPATH] for row in self.tree.getSelectedRows()], self.addByFilename, not self.showHiddenFiles) else: tracks = media.getTracks([self.tree.getRow(path)[ROW_FULLPATH]], self.addByFilename, not self.showHiddenFiles) 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 onStop(self): """ Stop playing """ self.__stopUpdateTimer() self.player.stop() self.nextURI = None if self.playbackTimer is not None: GObject.source_remove(self.playbackTimer) modules.postMsg(consts.MSG_EVT_STOPPED)
def on_searchbox_activate(self, entry): self.stop_searches() query = self.searchbox.get_text().strip() if len(query) < MIN_CHARS: msg = 'Search term has to have at least %d characters' % MIN_CHARS logging.info(msg) return query = self.searchbox.get_text().decode('utf-8') logging.info('Query: %s' % query) modules.postMsg(consts.MSG_EVT_SEARCH_START, {'query': query})
def on_searchbox_activate(self, entry): self.stop_searches() query = self.searchbox.get_text().strip() if len(query) < MIN_CHARS: msg = 'Search term has to have at least %d characters' % MIN_CHARS logging.info(msg) return query = self.searchbox.get_text() logging.info('Query: %s' % query) modules.postMsg(consts.MSG_EVT_SEARCH_START, {'query': query})
def play(self, path=None): """ Replace/extend the tracklist If 'path' is None, use the current selection """ if path is None: track_paths = [row[ROW_FULLPATH] for row in self.tree.getSelectedRows()] else: track_paths = [self.tree.getRow(path)[ROW_FULLPATH]] modules.postMsg(consts.MSG_EVT_LOAD_TRACKS, {'paths': track_paths})
def onListModified(self): """ Some rows have been added/removed/moved """ # Getting the trackdir takes virtually no time, so we can do it on every # paylist change tracks = self.getTrackDir() self.playtime = tracks.get_playtime() modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': tracks, 'playtime': self.playtime}) if self.tree.hasMark(): modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def updateTimerHandler(self): """ Regularly called during playback (can be paused) """ if self.player.isPlaying(): position = self.player.getPosition() remaining = self.player.getDuration() - position modules.postMsg(consts.MSG_EVT_TRACK_POSITION, {'seconds': int(position / 1000000000)}) if remaining < 4000000000 and self.nextURI is None: modules.postMsg(consts.MSG_EVT_NEED_BUFFER) return True
def updateTimerHandler(self): """ Regularly called during playback (can be paused) """ if self.player.isPlaying(): position = self.player.getPosition() remaining = self.player.getDuration() - position modules.postMsg(consts.MSG_EVT_TRACK_POSITION, {'seconds': int(position // 1000000000)}) if remaining < 5000000000 and self.nextURI is None: modules.postMsg(consts.MSG_EVT_NEED_BUFFER) return True
def playPaths(self, tree, paths, replace): """ Replace/extend the tracklist If the list 'paths' is None, use the current selection """ if self.tree.getNbChildren((0,)) != 0: 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 onListModified(self, list): """ Some rows have been added/removed/moved """ # self.btnClear.set_sensitive(len(list) != 0) self.btnShuffle.set_sensitive(len(list) != 0) # Update playlist length and playlist position for all tracks for position, row in enumerate(self.list.getAllRows()): row[ROW_TRK].setPlaylistPos(position + 1) row[ROW_TRK].setPlaylistLen(len(self.list)) modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': self.getAllTracks(), 'playtime': self.playtime}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__getPreviousTrackIdx() != -1, 'hasNext': self.__getNextTrackIdx() != -1})
def renameFolder(self, oldName, newName): """ Rename a folder """ self.folders[newName] = self.folders[oldName] del self.folders[oldName] savedStates = prefs.get(__name__, 'saved-states', {}) if oldName in savedStates: savedStates[newName] = savedStates[oldName] del savedStates[oldName] prefs.set(__name__, 'saved-states', savedStates) modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName})
def realStartup(): """ Perform all the initialization stuff which is not mandatory to display the window This function should be called within the GTK main loop, once the window has been displayed """ import atexit, dbus.mainloop.glib, gui.about, modules, webbrowser def onDelete(win, event): """ Use our own quit sequence, that will itself destroy the window """ window.hide() modules.postQuitMsg() return True def onResize(win, rect): """ Save the new size of the window """ if win.window is not None and not win.window.get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED: prefs.set(__name__, 'win-width', rect.width) prefs.set(__name__, 'win-height', rect.height) if prefs.get(__name__, 'view-mode', DEFAULT_VIEW_MODE)in (consts.VIEW_MODE_FULL, consts.VIEW_MODE_PLAYLIST): prefs.set(__name__, 'full-win-height', rect.height) def onState(win, evt): """ Save the new state of the window """ prefs.set(__name__, 'win-is-maximized', bool(evt.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED)) def atExit(): """ Final function, called just before exiting the Python interpreter """ prefs.save() log.logger.info('Stopped') # D-Bus dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) # Make sure to perform a few actions before exiting the Python interpreter atexit.register(atExit) signal.signal(signal.SIGTERM, lambda sig, frame: onDelete(window, None)) # GTK handlers window.connect('delete-event', onDelete) window.connect('size-allocate', onResize) window.connect('window-state-event', onState) paned.connect('size-allocate', lambda win, rect: prefs.set(__name__, 'paned-pos', paned.get_position())) wTree.get_widget('menu-mode-mini').connect('activate', onViewMode, consts.VIEW_MODE_MINI) wTree.get_widget('menu-mode-full').connect('activate', onViewMode, consts.VIEW_MODE_FULL) wTree.get_widget('menu-mode-playlist').connect('activate', onViewMode, consts.VIEW_MODE_PLAYLIST) wTree.get_widget('menu-quit').connect('activate', lambda item: onDelete(window, None)) wTree.get_widget('menu-about').connect('activate', lambda item: gui.about.show(window)) wTree.get_widget('menu-help').connect('activate', lambda item: webbrowser.open(consts.urlHelp)) wTree.get_widget('menu-preferences').connect('activate', lambda item: modules.showPreferences()) # Let's go modules.postMsg(consts.MSG_EVT_APP_STARTED)
def renameFolder(self, oldName, newName): """ Rename a folder """ self.folders[newName] = self.folders[oldName] del self.folders[oldName] if oldName in self.treeState: self.treeState[newName] = self.treeState[oldName] del self.treeState[oldName] modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, { 'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName })
def onMediaKey(self, appName, action): """ A media key has been pressed """ if action == 'Stop': modules.postMsg(consts.MSG_CMD_STOP) elif action == 'Next': modules.postMsg(consts.MSG_CMD_NEXT) elif action == 'Previous': modules.postMsg(consts.MSG_CMD_PREVIOUS) elif action in ['Play', 'Pause']: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
def onScaleValueChanged(self, scale, idx): """ The user has moved one of the scales """ # Add a 'custom' entry to the presets if needed if self.preset is not None: self.preset = None prefs.set(__name__, 'preset', self.preset) self.combo.handler_block_by_func(self.onPresetChanged) self.comboStore.insert(0, (False, _('Custom'), None)) self.comboStore.insert(1, (True, '', None)) self.combo.set_active(0) self.combo.handler_unblock_by_func(self.onPresetChanged) self.lvls[idx] = scale.get_value() prefs.set(__name__, 'levels', self.lvls) modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
def onTogglePause(self): """ Switch between play/pause """ if self.player.isPaused(): if self.queuedSeek is not None: self.player.seek(self.queuedSeek*1000000000) self.queuedSeek = None self.player.play() modules.postMsg(consts.MSG_EVT_UNPAUSED) elif self.player.isPlaying(): if self.playbackTimer is not None: GObject.source_remove(self.playbackTimer) self.player.pause() modules.postMsg(consts.MSG_EVT_PAUSED)
def onShowPopupMenu(self, tree, button, time, path): # Keep reference after method exits. self.popup_menu = Gtk.Menu() # Remove remove = Gtk.MenuItem.new_with_label(_('Remove')) self.popup_menu.append(remove) if path is None: remove.set_sensitive(False) else: remove.connect('activate', lambda item: self.remove()) # Clear clear = Gtk.MenuItem.new_with_label(_('Clear Playlist')) self.popup_menu.append(clear) # Save to m3u export_m3u = Gtk.MenuItem.new_with_label(_('Export playlist to file')) self.popup_menu.append(export_m3u) # Save to dir export_dir = Gtk.MenuItem.new_with_label(_('Export playlist to directory')) self.popup_menu.append(export_dir) if len(tree.store) == 0: clear.set_sensitive(False) export_m3u.set_sensitive(False) export_dir.set_sensitive(False) else: clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR)) export_m3u.connect('activate', lambda item: self.export_playlist_to_m3u()) export_dir.connect('activate', lambda item: self.export_playlist_to_dir()) self.popup_menu.show_all() self.popup_menu.popup(None, None, None, None, button, time)
def onKeyboard(self, list, event): """ Keyboard shortcuts """ keyname = Gdk.keyval_name(event.keyval) if keyname == 'Delete': self.remove() elif keyname == 'Return': self.jumpTo(self.tree.getFirstSelectedRow()) elif keyname == 'space': modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP) elif keyname == 'Left': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5}) elif keyname == 'Right': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})
def onAddFolder(self, btn): """ Let the user add a new folder to the list """ from gui import selectPath result = selectPath.SelectPath(MOD_L10N, self.cfgWin, self.folders.keys()).run() if result is not None: name, path = result self.folders[name] = path self.populateFolderList() modules.postMsg( consts.MSG_CMD_EXPLORER_ADD, { 'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled })
def onModUnloaded(self): """ The module has been unloaded """ if self.currTrack is not None: modules.postMsg( consts.MSG_CMD_SET_COVER, { 'track': self.currTrack, 'pathThumbnail': None, 'pathFullSize': None }) # Delete covers that have been generated by this module for covers in self.coverMap.values(): if os.path.exists(covers[CVR_THUMB]): os.remove(covers[CVR_THUMB]) if os.path.exists(covers[CVR_FULL]): os.remove(covers[CVR_FULL]) self.coverMap = None # Delete blacklist self.coverBlacklist = None
def set(self, tracks, playNow): """ Replace the tracklist, clear it if tracks is None """ self.playtime = 0 if type(tracks) == list: trackdir = media.TrackDir(None, flat=True) trackdir.tracks = tracks tracks = trackdir if self.tree.hasMark() and ((not playNow) or (tracks is None) or tracks.empty()): modules.postMsg(consts.MSG_CMD_STOP) self.tree.clear() if tracks is not None and not tracks.empty(): self.insert(tracks, playNow=playNow) self.tree.collapse_all() self.onListModified()
def onTrackEnded(self, withError): """ The current track has ended, jump to the next one if any """ current_iter = self.tree.getMark() # If an error occurred with the current track, flag it as such if withError and current_iter: self.tree.setItem(current_iter, ROW_ICO, icons.errorMenuIcon()) # Find the next 'playable' track (not already flagged) next = self.__getNextTrackIter() if next: send_play_msg = True if current_iter: track_name = self.tree.getTrack(current_iter).getURI() send_play_msg = (track_name != self.bufferedTrack) self.jumpTo(next, sendPlayMsg=send_play_msg, forced=False) self.bufferedTrack = None return self.bufferedTrack = None modules.postMsg(consts.MSG_CMD_STOP)
def jumpTo(self, iter, sendPlayMsg=True, forced=True): """ Jump to the track located at the given iter """ if not iter: return mark = self.tree.getMark() if mark: self.set_track_playing(mark, False) self.tree.setMark(iter) self.tree.scroll(iter) # Check track track = self.tree.getTrack(iter) if not track: # Row may be a directory self.jumpTo(self.__getNextTrackIter()) return self.set_track_playing(iter, True) self.paused = False if sendPlayMsg: modules.postMsg(consts.MSG_CMD_PLAY, {'uri': track.getURI(), 'forced': forced}) modules.postMsg(consts.MSG_EVT_NEW_TRACK, {'track': track}) modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def timerFunc(self): """ Move a bit the scales to their target value """ isFinished = True # Move the scales a bit for i in range(10): currLvl = self.scales[i].get_value() targetLvl = self.targetLvls[i] difference = targetLvl - currLvl if abs(difference) <= 0.25: newLvl = targetLvl else: newLvl = currLvl + (difference / 8.0) isFinished = False self.lvls[i] = newLvl self.scales[i].set_value(newLvl) # Set the equalizer to the new levels modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls}) if isFinished: self.timer = None prefs.set(__name__, 'levels', self.lvls) # Make sure labels are up to date (sometimes they aren't when we're done with the animation) # Also unblock the handlers for i in range(10): self.scales[i].queue_draw() self.scales[i].handler_unblock_by_func( self.onScaleValueChanged) return False return True
def play(self, replace, path=None): """ Replace/extend the tracklist If 'path' is None, use the current selection """ if path is None: tracks = media.getTracks( [row[ROW_FULLPATH] for row in self.tree.getSelectedRows()], self.addByFilename, not self.showHiddenFiles) else: tracks = media.getTracks([self.tree.getRow(path)[ROW_FULLPATH]], self.addByFilename, not self.showHiddenFiles) 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 onRemoveSelectedFolder(self, list): """ Remove the selected media folder """ import gui if list.getSelectedRowsCount() == 1: remark = _( 'You will be able to add this root folder again later on if you wish so.' ) question = _('Remove the selected entry?') else: remark = _( 'You will be able to add these root folders again later on if you wish so.' ) question = _('Remove all selected entries?') if gui.questionMsgBox( self.cfgWin, question, '%s %s' % (_('Your media files will not be deleted.'), remark)) == gtk.RESPONSE_YES: for row in self.cfgList.getSelectedRows(): name = row[0] modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, { 'modName': MOD_L10N, 'expName': name }) del self.folders[name] # Remove the tree, if any, from the scrolled window if self.currRoot == name: self.currRoot = None # Remove the saved state of the tree, if any if name in self.treeState: del self.treeState[name] self.cfgList.removeSelectedRows()
def __onTrackEnded(self, error): """ Called to signal eos and errors """ self.nextURI = None if error: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_ERROR) else: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_OK)
def onSkipTrack(self, notification, action): """ The user wants to skip the current track """ if self.hasNext: modules.postMsg(consts.MSG_CMD_NEXT) else: modules.postMsg(consts.MSG_CMD_STOP)
def onAppStarted(self): """ The application has started """ self.modInit() modules.postMsg(consts.MSG_CMD_ENABLE_EQZ) modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
def onNewTrack(self, track): """ A new track is being played, try to retrieve the corresponding cover """ # Make sure we have enough information if track.getArtist() == consts.UNKNOWN_ARTIST or track.getAlbum( ) == consts.UNKNOWN_ALBUM: modules.postMsg(consts.MSG_CMD_SET_COVER, { 'track': track, 'pathThumbnail': None, 'pathFullSize': None }) return album = track.getAlbum().lower() artist = track.getArtist().lower() rawCover = None self.currTrack = track # Let's see whether we already have the cover if (artist, album) in self.coverMap: covers = self.coverMap[(artist, album)] pathFullSize = covers[CVR_FULL] pathThumbnail = covers[CVR_THUMB] # Make sure the files are still there if os.path.exists(pathThumbnail) and os.path.exists(pathFullSize): modules.postMsg( consts.MSG_CMD_SET_COVER, { 'track': track, 'pathThumbnail': pathThumbnail, 'pathFullSize': pathFullSize }) return # Should we check for a user cover? if not prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS) \ or prefs.get(__name__, 'prefer-user-covers', PREFS_DFT_PREFER_USER_COVERS): rawCover = self.getUserCover(os.path.dirname(track.getFilePath())) # Is it in our cache? if rawCover is None: rawCover = self.getFromCache(artist, album) # If we still don't have a cover, maybe we can try to download it if rawCover is None: modules.postMsg(consts.MSG_CMD_SET_COVER, { 'track': track, 'pathThumbnail': None, 'pathFullSize': None }) if prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS): rawCover = self.getFromInternet(artist, album) # If we still don't have a cover, too bad # Otherwise, generate a thumbnail and a full size cover, and add it to our cover map if rawCover is not None: import tempfile thumbnail = tempfile.mktemp() + '.png' fullSizeCover = tempfile.mktemp() + '.png' self.generateThumbnail(rawCover, thumbnail, 'PNG') self.generateFullSizeCover(rawCover, fullSizeCover, 'PNG') if os.path.exists(thumbnail) and os.path.exists(fullSizeCover): self.coverMap[(artist, album)] = (thumbnail, fullSizeCover) modules.postMsg( consts.MSG_CMD_SET_COVER, { 'track': track, 'pathThumbnail': thumbnail, 'pathFullSize': fullSizeCover }) else: modules.postMsg(consts.MSG_CMD_SET_COVER, { 'track': track, 'pathThumbnail': None, 'pathFullSize': None })
def onBufferingNeeded(self): """ The current track is close to its end, so we try to buffer the next one to avoid gaps """ where = self.__getNextTrackIter() if where: self.bufferedTrack = self.tree.getItem(where, ROW_TRK).getURI() modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})
def onLoadTracks(self, paths): modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, { 'tracks': media.getTracks(paths), 'playNow': True })
def on_searchbox_changed(self, entry): if self.searchbox.get_text().strip() == '': self.stop_searches() modules.postMsg(consts.MSG_EVT_SEARCH_RESET, {})
def onAppStarted(self): """ This is the real initialization function, called when the module has been loaded """ wTree = tools.prefs.getWidgetsTree() self.playtime = 0 self.bufferedTrack = None # Retrieve widgets self.window = wTree.get_object('win-main') columns = (('', [(Gtk.CellRendererPixbuf(), GdkPixbuf.Pixbuf), (Gtk.CellRendererText(), GObject.TYPE_STRING)], True), (None, [(None, GObject.TYPE_PYOBJECT)], False), ) self.tree = TrackTreeView(columns, use_markup=True) self.tree.enableDNDReordering() target = Gtk.TargetEntry.new(*DND_INTERNAL_TARGET) targets = Gtk.TargetList.new([target]) self.tree.setDNDSources(targets) wTree.get_object('scrolled-tracklist').add(self.tree) # GTK handlers self.tree.connect('exttreeview-button-pressed', self.onMouseButton) self.tree.connect('tracktreeview-dnd', self.onDND) self.tree.connect('key-press-event', self.onKeyboard) self.tree.get_model().connect('row-deleted', self.onRowDeleted) (options, args) = prefs.getCmdLine() self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist') self.paused = False # Populate the playlist with the saved playlist dump = None if os.path.exists(self.savedPlaylist): try: dump = pickleLoad(self.savedPlaylist) except (EOFError, IOError): msg = '[%s] Unable to restore playlist from %s\n\n%s' log.logger.error(msg % (MOD_INFO[modules.MODINFO_NAME], self.savedPlaylist, traceback.format_exc())) if dump: self.restoreTreeDump(dump) log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME]) self.tree.collapse_all() self.select_last_played_track() self.onListModified() commands, args = tools.separate_commands_and_tracks(args) # Add commandline tracks to the playlist if args: log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME]) tracks = media.getTracks([os.path.abspath(arg) for arg in args]) playNow = not 'stop' in commands and not 'pause' in commands modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': playNow}) elif 'play' in commands: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE) # Automatically save the content at regular intervals GObject.timeout_add_seconds(SAVE_INTERVAL, self.save_track_tree)