Example #1
0
class Utility:
    """
    # Generic "glue" class that contains commonly used helper
    # functions and helps to keep track of objects
    """
    def __init__(self, path):
        self.path = path
        self.browsers = []
        self.browser = ""
        self.quitting = False
        self.configdir = None
        self.dir_root = self.setupConfigPath()
        self.MakeDir("torrent")
        self.MakeDir("database")
        self.MakeDir("tracker")
        self.MakeDir("ipfilter")
        self.MoveOldFiles()
        
        # Set Lang
        self.lang = Lang(self)

        # Reg Checker
        self.regchecker = RegChecker(self)
        
        # Keep track of torrents
        self.setupTorrentList()
        self.torrents = { "all": [], 
                          "active": {}, 
                          "inactive": {}, 
                          "paused": {}, 
                          "seeding": {}, 
                          "downloading": {} }
        self.activeGroup = "all"
                          
        # Keep track of all the "ManagedList" objects in use
        self.lists = {}
        self.imagelist = None
        self.images = {}

        # Set Config
        self.config         = self.setupConfig()
        self.webconfig      = self.setupWebConfig()
        self.makerconfig    = self.setupTorrentMakerConfig()
        self.lastdir = { "save" : self.config.Read('defaultfolder'), 
                         "open" : "", 
                         "log": "" }

        self.configdir.deleteOldCacheData(self.config.Read('expire_cache_data', "int"))

	# Install Open New command
        if os.name == "posix" and sys.platform != "darwin":
            self.install_open_new()
            
    def MoveOldFiles(self):            
        oldpath = self.getPath()
        newpath = self.getConfigPath()
        files = ["torrent.list",
                 "lh.conf",
                 "maker.conf",
                 "search.conf",
                 "torrent"]
        for name in files:
            oldname = os.path.join(oldpath, name)
            if existsAndIsReadable(oldname):
                newname = os.path.join(newpath, name)
                try:
                    move(oldname, newname)
                except:
                    pass
                
    def getLastDir(self, operation = "save"):
        lastdir = self.lastdir[operation]
        
        if operation == "save":
            if not os.access(lastdir, os.F_OK):
                lastdir = self.config.Read('defaultfolder')
        
        if not os.access(lastdir, os.F_OK):
            lastdir = ""
            
        return lastdir
    
    def MakeDir(self, strDir):
        path = os.path.join(self.getConfigPath(), strDir)
        pathexists = os.access(path, os.F_OK)
        # If the database directory doesn't exist, create it now
        if not pathexists:
            os.mkdir(path)
        
    def getDatabaseDir(self):
        return os.path.join(self.getConfigPath(), "database")
    
    def setupConfigPath(self):
        """
        Sets the config path at %appdata%
        """
        self.configdir = ConfigDir(local = wx.GetApp().local)
        return forceunicode(self.configdir.dir_root)

    def setupConfig(self):
        """
        Main config
        """
        if (sys.platform[:3] == 'win'):
            # Default directory for windows 
            defaultdir = "C:\\Downloads"
        else:
            # Default directory for unix
            defaultdir = os.path.join(os.path.expanduser('~'), "Downloads")
        defaults = {
                    # General
                    'lang': str(wx.LANGUAGE_DEFAULT),
                    'confirmonclose': '1',
                    'checkforupdates': '1',
                    'ipfilter': '1',
                    'filelogger': '0',
                    'rsstimer': '1800',
                    'rsstimerstatus': '1',
                    'hotkey': '0',
                    'hotkeymod': '0',
                    'hotkeykeycode': '0',
                    'hotkeywxkeycode': '0',
                    'associate' : '1',
                    'mintray_balloontips': '0',
                    'homepage': home_page,
                    
                    # Bandwidth
                    'maxupload': '5', 
                    'maxuploadrate': '0', 
                    'maxdownloadrate': '0', 
                    'maxseeduploadrate': '0', 
                    'numsimdownload': '2', 
                    'uploadtime': '0', 
                    'uploadratio': '150',
                    'maxuploadvolume': '0', 
                    'maxdownloadvolume': '0', 
                    'uploadvolume': '0', 
                    'downloadvolume': '0', 
                    
                    # Disk
                    'movecompleted': '0', 
                    'setdefaultfolder': '0', 
                    'defaultfolder': defaultdir,
                    'scandir': '',
                    'scandirfreq': '300',
                    'scandirmovetor': '1',
                    'defrentorwithdest': '1',
                    'diskfullthreshold': '0', 
                    'buffer_write' : '4', 
                    'buffer_read' : '1', 
                    'auto_flush' : '0',
                    'addlabeltomovedir': '0',
                    
                    # Security
                    'kickban': '1', 
                    'notsameip': '1', 
                    'scrape': '1',
                    'scrapequeued': '1',
                    
                    # BitTornado
                    'dht': '1',
                    'tracker': '0',
                    'pex_allowed': '0',
                    'expire_cache_data': '10',
                    'minport': "49152", 
                    'maxport': "65535",
                    'useport': "-1",
                    'ipv6': '0', 
                    'ipv6_binds_v4': '1', 
                    'min_peers': '20', 
                    'max_initiate': '40', 
                    'alloc_type': 'normal', 
                    'alloc_rate': '2', 
                    'max_files_open': '50', 
                    'max_connections': '60',    # no need for more than that 
                    'lock_files': '1', 
                    'lock_while_reading': '0', 
                    'double_check': '1', 
                    'triple_check': '0', 
                    'crypto_mode': str(int(CRYPTO_OK)),
                    'display_interval': '0.8',
                    'peercache': '60',

                    # Queue
                    'consideractive': '2',
                    'preferuncompleted': '0',
                    'timeouttracker': '15', 
                    'timeoutdownload': '30', 
                    'timeoutupload': '1', 
                    'defaultpriority': '2',
                    'defaultstatus': '1',
                    'autostart_upload': '0',
                    'autostart_upload_threshold': '10',
                    'autostart_upload_delay': '300',
                    'autostart_download': '0',
                    'autostart_download_threshold': '0',
                    'autostart_download_delay': '300',
                    'autostart_scrape': '0',
                    'autostart_scrape_value': '1',

                    # Constant
                    'fastresume': '1', 
                    'removetorrent': '1', 
                    'savecolumnwidth': '1',

                    # Network
                    'useportrange': '0', 
                    'upnp_nat_access': '0', 
                    'natpmp': '0',
                    'trackeraddress': '',
                    
                    # View
                    'peers_list_sort_colid': '-1',
                    'peers_list_sort_flag': '0',
                    'listfont': '', 
                    'allowmenubitmaps': '1',
                    'progress_bars': '0',
                    'show_statusbar': '1',
                    'show_toolbar': '1',
                    'show_inspector': '1',
                    'tb_size': '1',
                    'tb_style': '2',
                    'mintray': str(int(wx.Platform == "__WXMSW__")),
                    'maximized': '0',
                    'window_x': '',
                    'window_y': '',
                    'window_width': '792',
                    'window_height':'550',
                    'splitter_pos': '500',
                    'rss_column0_rank': '0',
                    'rss_column1_rank': '1',
                    'rss_column2_rank': '2',
                    'rss_column0_width': '335',
                    'rss_column1_width': '225',
                    'rss_column2_width': '200',
                    'statusbar1_rank': '1',
                    'statusbar2_rank': '1',
                    'statusbar3_rank': '1',
                    'statusbar4_rank': '1',
                    'column0_rank': '0',
                    'column1_rank': '1',
                    'column2_rank': '-1',
                    'column3_rank': '2',
                    'column4_rank': '3',
                    'column5_rank': '4',
                    'column6_rank': '5',
                    'column7_rank': '6',
                    'column8_rank': '7',
                    'column9_rank': '-1',
                    'column10_rank': '-1',
                    'column11_rank': '-1',
                    'column12_rank': '-1',
                    'column13_rank': '-1',
                    'column14_rank': '-1',
                    'column15_rank': '-1',
                    'column16_rank': '-1',
                    'column17_rank': '-1',
                    'column0_width': '235',
                    'column1_width': '85',
                    'column3_width': '75',
                    'column4_width': '75',
                    'column6_width': '80',
                    'column7_width': '75',
                    'column8_width': '75',
                    'column13_width': '80',
                    'column14_width': '80',
                    'column15_width': '80',
                    'column16_width': '80',
                    'column17_width': '80',
                    'fileinfo0_rank': '0',
                    'fileinfo0_width': '300', 
                    'fileinfo1_rank': '1',
                    'fileinfo1_width': '100', 
                    'fileinfo2_rank': '2',
                    'fileinfo2_width': '60', 
                    'fileinfo3_rank': '3',
                    'fileinfo3_width': '200', 
                    'fileinfo4_rank': '-1',
                    'fileinfo4_width': '200', 
                    'fileinfo5_rank': '-1',
                    'fileinfo5_width': '200', 
                    'fileinfo6_rank': '-1',
                    'fileinfo6_width': '200',
                    'icons_toolbartop':[ACTION_VIEWTRANSFER,
                                        ACTION_VIEWSEARCH,
                                        ACTION_VIEWRSS,
                                        ACTION_VIEWLOG,
                                        -1,
                                        ACTION_RESUME,
                                        ACTION_PAUSE,
                                        ACTION_STOP,
                                        ACTION_QUEUE,
                                        ACTION_REMOVE,
                                        -1,                                    
                                        ACTION_TORRENTDETAILS,
                                        ],
                    
                    'rightclickmenu': [ACTION_RESUME,
                                       ACTION_PAUSE,
                                       ACTION_STOP,
                                       ACTION_QUEUE,
                                       -1,
                                       ACTION_HASHCHECK,
                                       ACTION_SUPERSEED,
                                       ACTION_SCRAPE,
                                       -1,
                                       ACTION_REMOVE,
                                       ACTION_REMOVEFILE,
                                       -1,
                                       ACTION_SET_PRIORITY,
                                       ACTION_OPENDEST,
                                       ACTION_CHANGEDEST],
                    'taskbarmenu': [ACTION_EXIT]
                    }

        configfilepath = os.path.join(self.getConfigPath(), "lh.conf")
        return ConfigReader(configfilepath, "LH", defaults)

    def setupWebConfig(self):
        defaults = {
            'username': '******', 
            'password': '',
            'authorization': 'digest',
            'host': '127.0.0.1',
            'port': 56667,
            'webautostart': '0',
            'allow_quit': '1',
            'allow_version': '1',
            'allow_query': '1',
            'allow_getfiles': '1',
            'allow_setfileprio': '1',
            'allow_getsettings': '0',
            'allow_setsettings': '0',
            'allow_getprops': '1',
            'allow_setprops': '1',
            'allow_setpriority': '1',
            'allow_addurl': '1',
            'allow_addfile': '1',
            'allow_remove': '1',
            'allow_removedata': '0',
            'allow_queue': '1', 
            'allow_pause': '1', 
            'allow_stop': '1', 
            'allow_start': '1', 
            'allow_move': '1',
            'allow_superseed': '1',
            'allow_recheck': '1',
        }

        webconfigfilepath = os.path.join(self.getConfigPath(), "webservice.conf")
        return ConfigReader(webconfigfilepath, "LH/Webservice", defaults)

    def setupTorrentMakerConfig(self):
        """
        Torrent maker config
        """
        defaults = {
            'piece_size': '0', 
            'comment': '', 
            'created_by': '',
            'announcedefault': '', 
            'announcehistory': '', 
            'announce-list': '', 
            'dhtnodes': '', 
            'httpseeds': '', 
            'makehash_md5': '0', 
            'makehash_crc32': '0', 
            'makehash_sha1': '0', 
            'startnow': '0', 
            'savetorrent': '2',
            'privatetorrent': '0'
        }

        torrentmakerconfigfilepath = os.path.join(self.getConfigPath(), "maker.conf")
        return ConfigReader(torrentmakerconfigfilepath, "LH/TorrentMaker", defaults)
            
    def getConfigPath(self):
        """
        Returns the main config path
        """
        return self.dir_root
           
    def setupTorrentList(self):
        torrentfilepath = os.path.join(self.getConfigPath(), "torrent.list")
        self.torrentconfig = ConfigReader(torrentfilepath, "list0")
               
    def postAppInit(self):
        """
        Called at app initialization
        Sets the icon and all the actions
        """
        # Set icon
        if wx.Platform == "__WXGTK__":
            img = wx.Image(os.path.join(self.getSharePath(), 'icon_bt.png'), wx.BITMAP_TYPE_PNG)
            self.icon = wx.IconFromBitmap(img.Scale(24, 24).ConvertToBitmap())
            self.smallIcon = wx.IconFromBitmap(img.Scale(20, 21).ConvertToBitmap())
        else:
            iconPath = os.path.join(self.getSharePath(), 'icon_bt.ico')
            self.icon = wx.Icon(iconPath, wx.BITMAP_TYPE_ICO)
            self.smallIcon = self.icon
            
##        iconPath = os.path.join(self.getSharePath(), 'icon_bt.ico')
##        self.icon = wx.Icon(iconPath, wx.BITMAP_TYPE_ICO)
##        if wx.Platform == "__WXGTK__":
##            icons = wx.IconBundle()
##            icons.AddIconFromFile(iconPath, wx.BITMAP_TYPE_ICO)
##            self.smallIcon = icons.GetIcon((16,16))
##        else:
##            self.smallIcon = self.icon
            
        # Load supported languages
        self.lang.loadLanguages()
        
        # Load Actions
        self.makeactionlist()

    def makeactionlist(self):
        """
        (Re)creates all actions
        """
        makeActionList()
            
    def getPath(self):
        """
        Returns the application path
        """
        return self.path.decode(sys.getfilesystemencoding())

    def getSharePath(self):
        path = os.path.join(sys.prefix, 'share', 'lh-abc')
        if not existsAndIsReadable(path):
            path = self.getPath()
        return path
	
    def makeBitmap(self, section, bitmap, trans_color = None):
        """
        Returns a new bitmap from the icons directory
        """
        if bitmap.endswith('.png'):
            bmpType = wx.BITMAP_TYPE_PNG
        else:
            bmpType = wx.BITMAP_TYPE_BMP
        button_bmp = wx.Bitmap(os.path.join(self.getSharePath(), "icons", section, bitmap), bmpType)
        if trans_color is not None:
            button_mask = wx.Mask(button_bmp, trans_color)
            button_bmp.SetMask(button_mask)
        return button_bmp

    def makeBitmapButton(self, parent, bitmap, tooltip, event, trans_color = wx.Colour(200, 200, 200),
                         btnstyle = None, ext = (10, 10), section = 'misc'):
        """
        Returns a bitmap button using an icon from the icons directory
        """
        if btnstyle is None:
            btnstyle = wx.BU_AUTODRAW
            
        if not isinstance(bitmap, wx.Bitmap):
            bitmap = self.makeBitmap(section, bitmap, trans_color)

        ID_BUTTON = wx.NewId()
        button_btn = wx.BitmapButton(parent, ID_BUTTON, bitmap, size=wx.Size(bitmap.GetWidth()+ext[0], bitmap.GetHeight()+ext[1]), style=btnstyle)  
        button_btn.SetToolTipString(tooltip)
        parent.Bind(wx.EVT_BUTTON, event, button_btn)
        return button_btn
    
    def makePopup(self, menu, event = None, label = "", extralabel = "", bindto = None, kind = wx.ITEM_NORMAL, ID = None):
        """
        Make an entry for a popup menu
        """
        text = ""
        if label != "":
            text = label
        text += extralabel

        if ID:
            newid = ID
        else:
            newid = wx.NewId()

        if event is not None:
            if bindto is None:
                bindto = menu
            bindto.Bind(wx.EVT_MENU, event, id = newid)
        menu.Append(newid, text, kind = kind)
        return newid
   
    def eta_value(self, n, truncate = 3):
        if n == -1:
            return '<unknown>'
        if not n:
            return ''
        n = int(n)
        week, r1 = divmod(n, 60 * 60 * 24 * 7)
        day, r2 = divmod(r1, 60 * 60 * 24)
        hour, r3 = divmod(r2, 60 * 60)
        minute, sec = divmod(r3, 60)
    
        if week > 1000:
            return '<unknown>'
    
        weekstr = '%d' % (week) + _('w')
        daystr = '%d' % (day) + _('d')
        hourstr = '%d' % (hour) + _('h')
        minutestr = '%02d' % (minute) + _('m')
        secstr = '%02d' % (sec) + _('s')
            
        if week > 0:
            text = weekstr
            if truncate > 1:
                text += ":" + daystr
            if truncate > 2:
                text += "-" + hourstr
        elif day > 0:
            text = daystr
            if truncate > 1:
                text += "-" + hourstr
            if truncate > 2:
                text += ":" + minutestr
        elif hour > 0:
            text = hourstr
            if truncate > 1:
                text += ":" + minutestr
            if truncate > 2:
                text += ":" + secstr   
        else:
            text = minutestr
            if truncate > 1:
                text += ":" + secstr

        return  text
                    
    def speed_format(self, s, truncate = 1, stopearly = None):
        return self.size_format(s, truncate, stopearly) + "/" + _('s')

    def size_format(self, s, truncate = None, stopearly = None, applylabel = True, rawsize = False, showbytes = False, labelonly = False, textonly = False):
        size = 0.0
        label = ""
        
        if truncate is None:
            truncate = 2
        
        if ((s < 1024) and showbytes and stopearly is None) or stopearly == "Byte":
            truncate = 0
            size = s
            text = _("Byte")
        elif ((s < 1048576) and stopearly is None) or stopearly == "KB":
            size = (s/1024.0)
            text = _("KB")
        elif ((s < 1073741824L) and stopearly is None) or stopearly == "MB":
            size = (s/1048576.0)
            text = _("MB")
        elif ((s < 1099511627776L) and stopearly is None) or stopearly == "GB":
            size = (s/1073741824.0)
            text = _("GB")
        else:
            size = (s/1099511627776.0)
            text = _("TB")

        if textonly:
            return text
        
        label = text
        if labelonly:
            return label
            
        if rawsize:
            return size
                        
        truncatestring = '%.' + str(truncate) + 'f'
        text = (truncatestring % size)
            
        if applylabel:
            text += ' ' + label
            
        return text
    
    def getBTParams(self, skipcheck = False):
        # Construct BT params
        ###########################
        btparams = []
        
        btparams.append("--display_interval")
        btparams.append(self.config.Read('display_interval'))


        # ipv6
        if sys.version_info >= (2,3) and socket.has_ipv6 and \
           self.config.Read('ipv6', "boolean"):
            btparams.append("--ipv6_enabled")
            btparams.append("1")
            btparams.append("--ipv6_binds_v4")
            btparams.append(self.config.Read('ipv6_binds_v4'))

        # See if we use a fixed port, or pick a random port from a range
        useportrange = self.config.Read('useportrange', 'boolean')
        minport = self.config.Read('minport', "int")
        maxport = self.config.Read('maxport', "int")
        useport = self.config.Read('useport', "int")
        
        # Pick a random port between minport and maxport
        if useportrange or useport < 0:
            useport = str(random.randint(minport, maxport))
        else:
            useport = str(useport)
        self.config.Write('useport', useport)
       
        # Use single port only
        btparams.append("--minport")
        btparams.append(useport)
        btparams.append("--maxport")
        btparams.append(useport)

        # Fast resume
        btparams.append("--selector_enabled")
        btparams.append(self.config.Read('fastresume'))
        
        btparams.append("--auto_kick")
        btparams.append(self.config.Read('kickban'))
        btparams.append("--security")
        btparams.append(self.config.Read('notsameip'))

        btparams.append("--max_upload_rate")
        btparams.append("0")

        # crypto_mode:
        #  @0 disabled
        #  @1 enabled
        #  @2 forced
        #  @3 stealth
        crypto_mode = self.config.Read('crypto_mode', "int")
        btparams.append("--crypto_stealth")
        btparams.append(str(int(crypto_mode==3)))
        btparams.append("--crypto_only")
        btparams.append(str(int(crypto_mode>=2)))
        btparams.append("--crypto_allowed")
        btparams.append(str(int(crypto_mode>=1)))
            
        paramlist = [ "ip", 
                      "bind", 
                      "alloc_rate", 
                      "alloc_type", 
                      "double_check", 
                      "triple_check", 
                      "lock_while_reading", 
                      "lock_files", 
                      "min_peers", 
                      "max_files_open", 
                      "max_connections",
                      "upnp_nat_access",
                      "natpmp",
                      "auto_flush",
                      "pex_allowed",
                      "expire_cache_data"]
        for param in paramlist:
            value = self.config.Read(param)
            if value != "":
                btparams.append("--" + param)
                btparams.append(value)

        config, args = parseargs(btparams, BTDefaults)
        
        config["dht"] = self.config.Read('dht', "boolean")
        config["tracker"] = self.config.Read('tracker', "boolean")
        
        return config

    def checkWinPath(self, parent, pathtocheck):
        if pathtocheck and pathtocheck[-1] == '\\' and pathtocheck != '\\\\':
            pathitems = pathtocheck[:-1].split('\\')
        else:
            pathitems = pathtocheck.split('\\')
        nexttotest = 1
        if isPathRelative(pathtocheck):
            # Relative path
            # Empty relative path is allowed
            if pathtocheck == '':
                return True
            fixedname = fixInvalidName(pathitems[0])
            if fixedname != pathitems[0]:
                dlg = wx.MessageDialog(parent, 
                                       pathitems[0] + '\n' + \
                                       _('This name cannot be used as a Windows file or folder name.') + '\n'+ \
                                       _('Suggested corrected name :') + '\n\n' + \
                                       fixedname, 
                                       _('Error'), wx.ICON_ERROR)
                dlg.ShowModal()
                dlg.Destroy()
                return False
        else:
            # Absolute path
            # An absolute path must have at least one '\'
            if not '\\' in pathtocheck:
                dlg = wx.MessageDialog(parent, pathitems[0] + '\n' + _('Invalid syntax in the path. Try adding a \\'), 
                                       _('Error'), wx.ICON_ERROR)
                dlg.ShowModal()
                dlg.Destroy()
                return False
            if pathtocheck[:2] != '\\\\':
                # Not a network path
                fixedname = fixWindowsName(pathitems[0], unit = True)
                if fixedname:
                    dlg = wx.MessageDialog(parent, 
                                           pathitems[0] + '\n' + \
                                           _('Invalid syntax in the path. Try adding a \\') + \
                                           fixedname, 
                                           _('Error'), wx.ICON_ERROR)
                    dlg.ShowModal()
                    dlg.Destroy()
                    return False
            else:
                # Network path
                nexttotest = 2

        for name in pathitems[nexttotest:]:
            fixedname = fixInvalidName(name)
            if fixedname != name:
                dlg = wx.MessageDialog(parent,
                                       name + '\n' + _('This name cannot be used as a Windows file or folder name.') + fixedname, 
                                       _('Error'), wx.ICON_ERROR)
                dlg.ShowModal()
                dlg.Destroy()
                return False

        return True
    
    def shutdownComputer(self):
        if (sys.platform == 'win32'):
            winver = sys.getwindowsversion()[3]
            if winver == VER_PLATFORM_WIN32_NT:
                shutdownthread = Thread(target = self.shutdown2k)
                shutdownthread.setDaemon(True)
                shutdownthread.start()
            elif winver == VER_PLATFORM_WIN32_WINDOWS:
                os.system('rundll32 shell32.dll,SHExitWindowsEx 9')
        else:
            func = utility.config.Read("shutdowncommand")
            os.execvp(func, [])
            
    def shutdown2k(self):
        AdjustTokenPrivileges(OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS), False,
                              [(LookupPrivilegeValue(None, SE_SHUTDOWN_NAME), SE_PRIVILEGE_ENABLED)])
        ExitWindowsEx(9)
        
    def setShutdownCommand(self, event = None):
        dialog = wx.TextEntryDialog(None, 
                                    _('Enter the shutdown command for your OS'), 
                                    _('Set Shutdown Command'), 
                                    utility.config.Read("shutdowncommand"))
        result = dialog.ShowModal()
        shutdowncommand = dialog.GetValue()
        dialog.Destroy()
        
        if result == wx.ID_OK:
            utility.config.Write("shutdowncommand", shutdowncommand)

    def install_open_new(self):
        browsers = ('xdg-open', 'gnome-open', 'nautilus', 'konqueror')
        userbrowser = self.config.Read('filemanager')
	
        # Detect Available Browsers
        for browser in browsers:
            if os.system("which " + browser + " >/dev/null 2>&1") == 0:
                self.browsers.append(browser)
        
        # Add Custom Browser
        if userbrowser:
            if userbrowser in self.browsers:
                self.browsers.remove(userbrowser)
                self.browsers.insert(0, userbrowser)
        
        # Set Browser To Use
        if self.browsers:
            self.browser = self.browsers[0]
        else:
            self.browser = ""

    def open_new(self, dest):
        # Fix Path
        if sys.platform == 'darwin':
            dest = 'file://%s' % dest
        elif sys.platform[:5] == 'linux' and not dest.startswith('/'):
            dest = self.path + '/' + dest

        # Known browser
        if self.browser:
            os.spawnlp(os.P_NOWAIT, self.browser, self.browser, dest)
        # Let python handle it
        else:
            open_new(dest)