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 """ 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_TAGS]) 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, sys.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 loadLibrary(self, tree, name): """ Load the given library """ rows = [] path = os.path.join(ROOT_PATH, name) prevChar = '' # Make sure the version number is the good one if not os.path.exists(os.path.join(path, 'VERSION_%u' % VERSION)): logger.error('[%s] Version number does not match, loading of library "%s" aborted' % (MOD_NAME, name)) error = _('This library is deprecated, please refresh it.') tree.replaceContent([(consts.icoError, error, TYPE_NONE, None, None)]) return # Create the rows, with alphabetical header if needed for artist in pickleLoad(os.path.join(path, 'artists')): if len(artist[ART_NAME]) != 0: currChar = unicode(artist[ART_NAME], errors='replace')[0] 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, TYPE_HEADER, None, None)) rows.append((consts.icoDir, None, cgi.escape(artist[ART_NAME]), TYPE_ARTIST, os.path.join(path, artist[ART_INDEX]), None)) # 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((None, None, '', TYPE_NONE, '', None), node)
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)) rows = [(consts.icoMediaFile, None, '%02u. %s' % (track.getNumber(), cgi.escape(track.getTitle())), TYPE_TRACK, track.getFilePath(), track) for track in allTracks] tree.appendRows(rows, node) tree.removeRow(fakeChild)
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 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 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 all albums of the given node, assuming it is of type TYPE_ARTIST """ allAlbums = pickleLoad(os.path.join(tree.getItem(node, ROW_FULLPATH), 'albums')) path = tree.getItem(node, ROW_FULLPATH) rows = [(consts.icoMediaDir, '[%s]' % tools.sec2str(album[ALB_LENGTH], True), '%s' % cgi.escape(album[ALB_NAME]), TYPE_ALBUM, os.path.join(path, album[ALB_INDEX]), None) for album in allAlbums] # Add all the rows, and then add a fake child to each of them tree.freeze_child_notify() tree.appendRows(rows, node) tree.removeRow(fakeChild) for child in tree.iterChildren(node): tree.appendRow((None, None, '', TYPE_NONE, '', None), child) tree.thaw_child_notify()
def getFromCache(self, artist, album): """ Return the path to the cached cover, or None if it's not cached """ cachePath = os.path.join(self.cacheRootPath, str(abs(hash(artist)))) cacheIdxPath = os.path.join(cachePath, 'INDEX') try: cacheIdx = tools.pickleLoad(cacheIdxPath) cover = os.path.join(cachePath, cacheIdx[artist + album]) if os.path.exists(cover): return cover except: pass return None
def onAppStarted(self): """ Try to fill the playlist by using the files given on the command line or by restoring the last playlist """ (options, args) = prefs.getCmdLine() self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist.txt') if len(args) != 0: log.logger.info('[%s] Filling playlist with files given on command line' % MOD_NAME) modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': media.getTracks(args), 'playNow': True}) else: try: tracks = [media.track.unserialize(serialTrack) for serialTrack in tools.pickleLoad(self.savedPlaylist)] modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': False}) log.logger.info('[%s] Restored playlist' % MOD_NAME) except: log.logger.error('[%s] Unable to restore playlist from %s\n\n%s' % (MOD_NAME, self.savedPlaylist, traceback.format_exc()))
def refreshLibrary(self, parent, libName, path, creation=False): """ Refresh the given library, must be called through idle_add() """ # First show a progress dialog if creation: header = _('Creating library') else: header = _('Refreshing library') progress = 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() if track.hasAlbumArtist(): artist = track.getAlbumArtist() else: artist = track.getArtist() if artist in db: allAlbums = db[artist] if album in allAlbums: allAlbums[album].append(track) else: allAlbums[album] = [track] else: db[artist] = {album: [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 # 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(db[artist])) for indexArtist, artist in enumerate(db)], key = lambda a: a[0]) pickleSave(os.path.join(libPath, 'files'), newLibrary) pickleSave(os.path.join(libPath, 'artists'), allArtists) for (artist, indexArtist, nbAlbums) in allArtists: artistPath = os.path.join(libPath, indexArtist) overallNbAlbums += nbAlbums os.mkdir(artistPath) albums = [] for index, (name, 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())) albums.sort(cmp = lambda a1, a2: cmp(db[artist][a1[0]][0], db[artist][a2[0]][0])) pickleSave(os.path.join(artistPath, 'albums'), albums) progress.pulse() yield True self.libraries[libName] = (path, overallNbArtists, overallNbAlbums, overallNbTracks) self.fillLibraryList() if creation: modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': libName, 'icon': None, 'widget': self.scrolled}) progress.destroy() # If the refreshed library is currently displayed, refresh the treeview as well if self.currLib == libName: treeState = self.tree.saveState(ROW_NAME) self.loadLibrary(self.tree, self.currLib) self.tree.restoreState(treeState, ROW_NAME) yield False
def getDiscFromCache(self, discInfo): """ Return CDDB information from the cache, or None if that disc is not cached """ try: return tools.pickleLoad(os.path.join(self.cacheDir, str(discInfo[DISC_CHECKSUM]))) except: return None
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)
data = stream.read() if len(data) < 1024: raise Exception, 'The cover image seems incorrect (%u bytes is too small)' % len(data) except: logger.error('[%s] Cover image request failed\n\n%s' % (MOD_NAME, traceback.format_exc())) return None # So far, so good: let's cache the image cachePath = os.path.join(self.cacheRootPath, str(abs(hash(artist)))) cacheIdxPath = os.path.join(cachePath, 'INDEX') if not os.path.exists(cachePath): os.mkdir(cachePath) try: cacheIdx = tools.pickleLoad(cacheIdxPath) except: cacheIdx = {} nextInt = len(cacheIdx) + 1 filename = str(nextInt) + coverFormat coverPath = os.path.join(cachePath, filename) cacheIdx[artist + album] = filename tools.pickleSave(cacheIdxPath, cacheIdx) try: output = open(coverPath, 'wb') output.write(data) output.close() return coverPath except:
def loadFavorites(self, libName): """ Load favorites from the disk """ try: return pickleLoad(os.path.join(ROOT_PATH, libName, 'favorites')) except: return {}
# (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os, threading, tools # Load user preferences from the disk try: __usrPrefs = tools.pickleLoad(tools.consts.filePrefs) # UnicodeDecodeError happens when loading Pogo 0.8.7 files with newer versions. except (IOError, EOFError, UnicodeDecodeError): __usrPrefs = {} # Prevent concurrent calls to functions __mutex = threading.Lock() # Some global values shared by all the components of the application __appGlobals = {} def save(): """ Save user preferences to the disk """ with __mutex: tools.pickleSave(tools.consts.filePrefs, __usrPrefs)
def __getFromInternet(self, artist, album): """ Try to download the cover from the Internet If successful, add it to the cache and return the path to it Otherwise, return None """ import socket, urllib.request, urllib.error, urllib.parse # Make sure to not be blocked by the request socket.setdefaulttimeout(consts.socketTimeout) # Request information to Last.fm # Beware of UTF-8 characters: we need to percent-encode all characters try: url = 'http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=%s&artist=%s&album=%s' % ( AS_API_KEY, tools.percentEncode(artist), tools.percentEncode(album)) request = urllib.request.Request( url, headers={'User-Agent': USER_AGENT}) stream = urllib.request.urlopen(request) data = stream.read().decode('utf-8') except urllib.error.HTTPError as err: if err.code == 400: logger.error('[%s] No known cover for %s / %s' % (MOD_NAME, artist, album)) else: logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc())) return None except urllib.error.URLError: logger.info('[%s] Could not fetch cover. No internet connection.' % MOD_NAME) return None except: logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc())) return None # Extract the URL to the cover image malformed = True startIdx = data.find(AS_TAG_START) endIdx = data.find(AS_TAG_END, startIdx) if startIdx != -1 and endIdx != -1: coverURL = data[startIdx + len(AS_TAG_START):endIdx] coverFormat = os.path.splitext(coverURL)[1].lower() if coverURL.startswith( ('http://', 'https://')) and coverFormat in ACCEPTED_FILE_FORMATS: malformed = False if malformed: ## Do not show the data in the log every time no cover is found if coverURL: logger.error('[%s] Received malformed data\n\n%s' % (MOD_NAME, data)) return None # Download the cover image try: request = urllib.request.Request( coverURL, headers={'User-Agent': USER_AGENT}) stream = urllib.request.urlopen(request) data = stream.read() if len(data) < 1024: raise Exception( 'The cover image seems incorrect (%u bytes is too small)' % len(data)) except: logger.error('[%s] Cover image request failed' % MOD_NAME) return None # So far, so good: let's cache the image cachePath = os.path.join(self.cacheRootPath, str(abs(hash(artist)))) cacheIdxPath = os.path.join(cachePath, 'INDEX') if not os.path.exists(cachePath): os.mkdir(cachePath) try: cacheIdx = tools.pickleLoad(cacheIdxPath) except: cacheIdx = {} nextInt = len(cacheIdx) + 1 filename = str(nextInt) + coverFormat coverPath = os.path.join(cachePath, filename) cacheIdx[artist + album] = filename tools.pickleSave(cacheIdxPath, cacheIdx) try: output = open(coverPath, 'wb') output.write(data) output.close() return coverPath except: logger.error('[%s] Could not save the downloaded cover\n\n%s' % (MOD_NAME, traceback.format_exc())) return None
# (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os, threading, tools # Load user preferences from the disk try: __usrPrefs = tools.pickleLoad(tools.consts.filePrefs) except: __usrPrefs = {} __mutex = threading.Lock() # Prevent concurrent calls to functions __appGlobals = {} # Some global values shared by all the components of the application def save(): """ Save user preferences to the disk """ __mutex.acquire() tools.pickleSave(tools.consts.filePrefs, __usrPrefs) os.chmod(tools.consts.filePrefs, 0600) __mutex.release() def set(module, name, value):