Commit b056a4ea authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 0.6.10

parent a00539b2
*******************************************
*** This is SABnzbd 0.6.9 ***
*** This is SABnzbd 0.6.10 ***
*******************************************
SABnzbd is an open-source cross-platform binary newsreader.
It simplifies the process of downloading from Usenet dramatically,
......
-------------------------------------------------------------------------------
0.6.10Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Convert ambiguous Windows paths like D: and D:folder to D:\ and D:\folder
- Fix file name encoding problems when verifying using SFV files
- Add GNTP module to source distribution
- Prevent reading newzbin bookmarks when newzbin credentials are not set
-------------------------------------------------------------------------------
0.6.10RC1 by The SABnzbd-Team
-------------------------------------------------------------------------------
- Allow saving of category paths ending in a *
This is will prevent the creation of job folders in the final folder
- Fix incompatibility with unrar 4.01 regarding detection of encrypted files
- Create .bak (backup) file for sabnzbd.ini before modifying it
- OSX: Compatible with Growl 1.2.2 and 1.3
- OSX: Prevent changes to SABnzbd.app folder which confused the OSX Firewall
- OSX: Fix access rights of SABnzbs.app so that restricted users can run SABnzbd
- OSX: Combined SnowLeopard/Lion DMG and separate Leopard DMG
-------------------------------------------------------------------------------
0.6.9Final by The SABnzbd-Team
-------------------------------------------------------------------------------
......
SABnzbd 0.6.9
SABnzbd 0.6.10
-------------------------------------------------------------------------------
0) LICENSE
......@@ -85,7 +85,7 @@ Embedded modules (only use the included version)
Unpack the ZIP-file containing the SABnzbd sources to any folder of your liking.
If fou want multiple languages, you need to compile the translations.
If you want multiple languages, you need to compile the translations.
Start this from a shell terminal (or command prompt):
python tools/make_mo.py
......@@ -125,6 +125,6 @@ Visit the WIKI site:
6) CREDITS
-------------------------------------------------------------------------------
Serveral parts of SABnzbd were built by other people, illustrating the
Several parts of SABnzbd were built by other people, illustrating the
wonderful world of Free Open Source Software.
See the licences folder of the main program and of the skin folders.
Metadata-Version: 1.0
Name: SABnzbd
Version: 0.6.9
Summary: SABnzbd-0.6.9
Version: 0.6.10
Summary: SABnzbd-0.6.10
Home-page: http://sourceforge.net/projects/sabnzbdplus
Author: The SABnzbd Team
Author-email: team@sabnzbd.org
......
************************ SABnzbd 0.6.9 ************************
************************ SABnzbd 0.6.10 ************************
What's new:
- Update Plush to solve minor browser incompatibilities
- On Windows the 64bit versions of par2 and unrar were never used
- Updated unrar to 4.01
- Using the "Download" button in newzbin.com RSS feeds produced malformed names.
- When removing job folders in the "temporary download folder", remove everything.
This is needed because some operating systems add spurious files and folders.
- Generic Sorter failed to uppercase first letter of title when starting with "the/a/to" etc.
- Add "hidden" option allow_64bit_tools (lost when going from 0.5.6 to 0.6.0)
- OSX has now a Leopard/SnowLeopard DMG and a Lion-only DMG
You can see the difference in the DMG's background image
- Allow saving of category paths ending in a *
This feature (*) will prevent the creation of job folders in the final folder
- Fix incompatibility with unrar 4.01 regarding detection of encrypted files
- Create .bak (backup) file for sabnzbd.ini before modifying it
- Convert ambiguous Windows paths like D: and D:folder to D:\ and D:\folder
- Fix file name encoding problems when verifying using SFV files
- Prevent reading newzbin bookmarks when newzbin credentials are not set
- OSX: Compatible with Growl 1.2.2 and 1.3
- OSX: Prevent changes to SABnzbd.app folder which confused the OSX Firewall
- OSX: Fix access rights of SABnzbs.app so that restricted users can run SABnzbd
- OSX: Combined SnowLeopard/Lion DMG and separate Leopard DMG
About:
......
This diff is collapsed.
"""
A Python module that uses GNTP to post messages
Mostly mirrors the Growl.py file that comes with Mac Growl
http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py
"""
import gntp
import socket
import logging
logger = logging.getLogger(__name__)
class GrowlNotifier(object):
applicationName = 'Python GNTP'
notifications = []
defaultNotifications = []
applicationIcon = None
passwordHash = 'MD5'
#GNTP Specific
password = None
hostname = 'localhost'
port = 23053
def __init__(self, applicationName=None, notifications=None, defaultNotifications=None, applicationIcon=None, hostname=None, password=None, port=None):
if applicationName:
self.applicationName = applicationName
assert self.applicationName, 'An application name is required.'
if notifications:
self.notifications = list(notifications)
assert self.notifications, 'A sequence of one or more notification names is required.'
if defaultNotifications is not None:
self.defaultNotifications = list(defaultNotifications)
elif not self.defaultNotifications:
self.defaultNotifications = list(self.notifications)
if applicationIcon is not None:
self.applicationIcon = self._checkIcon(applicationIcon)
elif self.applicationIcon is not None:
self.applicationIcon = self._checkIcon(self.applicationIcon)
#GNTP Specific
if password:
self.password = password
if hostname:
self.hostname = hostname
assert self.hostname, 'Requires valid hostname'
if port:
self.port = int(port)
assert isinstance(self.port,int), 'Requires valid port'
def _checkIcon(self, data):
'''
Check the icon to see if it's valid
@param data:
@todo Consider checking for a valid URL
'''
return data
def register(self):
'''
Send GNTP Registration
'''
logger.info('Sending registration to %s:%s',self.hostname,self.port)
register = gntp.GNTPRegister()
register.add_header('Application-Name',self.applicationName)
for notification in self.notifications:
enabled = notification in self.defaultNotifications
register.add_notification(notification,enabled)
if self.applicationIcon:
register.add_header('Application-Icon',self.applicationIcon)
if self.password:
register.set_password(self.password,self.passwordHash)
response = self.send('register',register.encode())
if isinstance(response,gntp.GNTPOK): return True
logger.debug('Invalid response %s',response.error())
return response.error()
def notify(self, noteType, title, description, icon=None, sticky=False, priority=None):
'''
Send a GNTP notifications
'''
logger.info('Sending notification [%s] to %s:%s',noteType,self.hostname,self.port)
assert noteType in self.notifications
notice = gntp.GNTPNotice()
notice.add_header('Application-Name',self.applicationName)
notice.add_header('Notification-Name',noteType)
notice.add_header('Notification-Title',title)
if self.password:
notice.set_password(self.password,self.passwordHash)
if sticky:
notice.add_header('Notification-Sticky',sticky)
if priority:
notice.add_header('Notification-Priority',priority)
if icon:
notice.add_header('Notification-Icon',self._checkIcon(icon))
if description:
notice.add_header('Notification-Text',description)
response = self.send('notify',notice.encode())
if isinstance(response,gntp.GNTPOK): return True
logger.debug('Invalid response %s',response.error())
return response.error()
def subscribe(self,id,name,port):
sub = gntp.GNTPSubscribe()
sub.add_header('Subscriber-ID',id)
sub.add_header('Subscriber-Name',name)
sub.add_header('Subscriber-Port',port)
if self.password:
sub.set_password(self.password,self.passwordHash)
response = self.send('subscribe',sub.encode())
if isinstance(response,gntp.GNTPOK): return True
logger.debug('Invalid response %s',response.error())
return response.error()
def send(self,type,data):
'''
Send the GNTP Packet
'''
#logger.debug('To : %s:%s <%s>\n%s',self.hostname,self.port,type,data)
#Less verbose please
logger.debug('To : %s:%s <%s>',self.hostname,self.port,type)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
s.connect((self.hostname,self.port))
s.send(data.encode('utf-8', 'replace'))
response = gntp.parse_gntp(s.recv(1024))
s.close()
#logger.debug('From : %s:%s <%s>\n%s',self.hostname,self.port,response.__class__,response)
#Less verbose please
logger.debug('From : %s:%s <%s>',self.hostname,self.port,response.__class__)
return response
......@@ -129,6 +129,9 @@
$T('explain-ampm')<br>
<br/>
<!--#end if#-->
<label><input type="checkbox" name="growl_enable" value="1" <!--#if $growl_enable > 0 then "checked=1" else ""#--> /> <strong>$T('opt-growl_enable')</strong></label><br>
$T('explain-growl_enable')<br>
<br/>
<strong>$T('opt-ignore_samples'):</strong><br>
$T('explain-ignore_samples')<br>
<input class="radio" type="radio" name="ignore_samples" value="0" <!--#if $ignore_samples == 0 then 'checked="1"' else ""#--> /> $T('igsam-off')
......
......@@ -35,6 +35,13 @@
</label>
</div>
<!--#end if#-->
<div class="field-pair">
<input type="checkbox" name="growl_enable" id="growl_enable" value="1" <!--#if $growl_enable > 0 then "checked=1" else ""#--> />
<label class="clearfix" for="growl_enable">
<span class="component-title">$T('opt-growl_enable')</span>
<span class="component-desc">$T('explain-growl_enable')</span>
</label>
</div>
</fieldset>
</div><!-- /component-group1 -->
......
......@@ -175,6 +175,12 @@
<br class="clear" />
<!--#end if#-->
<label><span class="label">$T('opt-growl_enable'):</span>
<input class="radio" type="checkbox" name="growl_enable" value="1" <!--#if $growl_enable > 0 then 'checked="1"' else ""#--> /></label>
<span class="tips">$T('explain-growl_enable')</span>
<br class="clear" />
<span class="label">$T('opt-ignore_samples'):</span>
<input class="radio" type="radio" name="ignore_samples" value="0" <!--#if $ignore_samples == 0 then 'checked="1"' else ""#--> /> $T('igsam-off')
<input class="radio" type="radio" name="ignore_samples" value="1" <!--#if $ignore_samples == 1 then 'checked="1"' else ""#--> /> $T('igsam-del')
......
The module gntp is (C) Paul Traylor
Home of the module:
https://github.com/kfdm/gntp/
It is covered by the following license.
-------------------------------------------------------------------------
Copyright (c) 2011 Paul Traylor
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------
......@@ -204,6 +204,8 @@ ssl_type = OptionStr('misc', 'ssl_type', 'v23')
unpack_check = OptionBool('misc', 'unpack_check', True)
no_penalties = OptionBool('misc', 'no_penalties', False)
growl_enable = OptionBool('growl', 'growl_enable', True)
# Internal options, not saved in INI file
debug_delay = OptionNumber('misc', 'debug_delay', 0, add=False)
......
......@@ -725,28 +725,42 @@ def save_config(force=False):
filename = CFG.filename
try:
# Check if file is writable
if not sabnzbd.misc.is_writable(filename):
logging.error(Ta('Cannot write to INI file %s'), filename)
modified = False
return False
# Read current content
f = open(CFG.filename)
f = open(filename)
data = f.read()
f.close()
# Write to temp file
CFG.filename = filename + '.tmp'
f = open(CFG.filename, 'w')
tmpname = filename + '.tmp'
bakname = filename + '.bak'
# Write new file
f = open(tmpname, 'w')
f.write(data)
f.close()
# Update temp file content
CFG.filename = tmpname
CFG.write()
# Rename to backup
if os.path.isfile(bakname):
os.remove(bakname)
os.rename(filename, bakname)
# Rename temp file, overwriting old one
os.remove(filename)
os.rename(CFG.filename, filename)
os.rename(tmpname, filename)
modified = False
res = True
except:
logging.error(Ta('Cannot create temp file for %s'), CFG.filename)
logging.error(Ta('Cannot create backup file for %s'), filename)
logging.info("Traceback: ", exc_info = True)
res = False
CFG.filename = filename
return res
......
......@@ -1129,7 +1129,7 @@ SWITCH_LIST = \
'safe_postproc', 'no_dupes', 'replace_spaces', 'replace_dots', 'replace_illegal', 'auto_browser',
'ignore_samples', 'pause_on_post_processing', 'quick_check', 'nice', 'ionice',
'ssl_type', 'pre_script', 'pause_on_pwrar', 'ampm', 'sfv_check', 'folder_rename',
'unpack_check'
'unpack_check', 'growl_enable'
)
#------------------------------------------------------------------------------
......@@ -1151,7 +1151,10 @@ class ConfigSwitches(object):
conf['have_ionice'] = bool(sabnzbd.newsunpack.IONICE_COMMAND)
for kw in SWITCH_LIST:
conf[kw] = config.get_config('misc', kw)()
if kw == 'growl_enable':
conf[kw] = config.get_config('growl', kw)()
else:
conf[kw] = config.get_config('misc', kw)()
conf['script_list'] = list_scripts() or ['None']
conf['have_ampm'] = HAVE_AMPM
......@@ -1166,7 +1169,10 @@ class ConfigSwitches(object):
if msg: return msg
for kw in SWITCH_LIST:
item = config.get_config('misc', kw)
if kw == 'growl_enable':
item = config.get_config('growl', kw)
else:
item = config.get_config('misc', kw)
value = platform_encode(kwargs.get(kw))
msg = item.set(value)
if msg:
......@@ -2046,10 +2052,7 @@ class ConfigCats(object):
name = newname.lower()
if kwargs.get('dir'):
kwargs['dir'] = platform_encode(kwargs['dir'])
folder = config.ConfigCat(name, kwargs).dir
msg = folder.set(folder(), create=True)
if msg:
return badParameterResponse(msg)
config.ConfigCat(name, kwargs)
config.save_config()
raise dcRaiser(self.__root, kwargs)
......
......@@ -30,6 +30,7 @@ import subprocess
import socket
import time
import glob
import stat
import sabnzbd
from sabnzbd.decorators import synchronized
......@@ -301,7 +302,10 @@ def real_path(loc, path):
if not sabnzbd.WIN32 and path.startswith('~/'):
path = path.replace('~', sabnzbd.DIR_HOME, 1)
if sabnzbd.WIN32:
if path[0] not in '/\\' and not (len(path) > 1 and path[0].isalpha() and path[1] == ':'):
if path[0].isalpha() and len(path) > 1 and path[1] == ':':
if len(path) == 2 or path[2] not in '\\/':
path = path.replace(':', ':\\', 1)
else:
path = os.path.join(loc, path)
elif path[0] != '/':
path = os.path.join(loc, path)
......@@ -1166,3 +1170,10 @@ def remove_all(path, pattern='*', keep_folder=False, recursive=False):
except:
logging.info('Cannot remove folder %s', path)
def is_writable(path):
""" Return True is file is writable (also when non-existent) """
if os.path.isfile(path):
return bool(os.stat(path).st_mode & stat.S_IWUSR)
else:
return True
......@@ -28,7 +28,8 @@ from time import time
import binascii
import sabnzbd
from sabnzbd.encoding import TRANS, UNTRANS, unicode2local, name_fixer, reliable_unpack_names, unicoder, latin1
from sabnzbd.encoding import TRANS, UNTRANS, unicode2local, name_fixer, \
reliable_unpack_names, unicoder, latin1, platform_encode
from sabnzbd.utils.rarfile import RarFile, is_rarfile
from sabnzbd.misc import format_time_string, find_on_path, make_script_path
from sabnzbd.tvsort import SeriesSorter
......@@ -588,8 +589,16 @@ def rar_extract_core(rarfile, numrars, one_folder, nzo, setname, extraction_path
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
fail = 1
elif line.startswith('Encrypted file: CRC failed'):
filename = TRANS(line[31:-23].strip())
elif 'ncrypted file' in line and 'CRC failed' in line:
# unrar 4.x syntax
m = re.search('encrypted file (.+)\. Corrupt file', line)
if not m:
# unrar 3.x syntax
m = re.search('Encrypted file: CRC failed in (.+) \(password', line)
if m:
filename = TRANS(m.group(1)).strip()
else:
filename = '???'
nzo.fail_msg = T('Unpacking failed, archive requires a password')
msg = ('[%s][%s] '+Ta('Unpacking failed, archive requires a password')) % (setname, latin1(filename))
nzo.set_unpack_info('Unpack', unicoder(msg), set=setname)
......@@ -1330,7 +1339,7 @@ def sfv_check(sfv_path):
x = line.rfind(' ')
filename = line[:x].strip()
checksum = line[x:].strip()
path = os.path.join(root, filename)
path = os.path.join(root, platform_encode(filename))
if os.path.exists(path):
if crc_check(path, checksum):
logging.debug('File %s passed SFV check', path)
......
......@@ -274,6 +274,9 @@ class Bookmarks(object):
@synchronized(BOOK_LOCK)
def run(self, delete=None):
if not (cfg.newzbin_bookmarks() and cfg.newzbin_username() and cfg.newzbin_password()):
return
headers = { 'User-Agent': 'SABnzbd+/%s' % sabnzbd.__version__, }
# Connect to Newzbin
......
......@@ -15,51 +15,154 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
#"""
#TO FIX : Translations are not working with this implementation
# Growl Registration may only be done once per run ?
# Registration is made too early, the language module has not read the text file yet
#NOTIFICATION = {'startup':'grwl-notif-startup','download':'grwl-notif-dl','pp':'grwl-notif-pp','other':'grwl-notif-other'}
NOTIFICATION = {'startup':'1. On Startup/Shutdown','download':'2. On adding NZB','pp':'3. On post-processing','complete':'4. On download terminated','other':'5. Other Messages'}
"""
sabnzbd.growler - Send notifications to Growl
"""
#------------------------------------------------------------------------------
import os.path
import logging
import socket
# For a future release, make texts translatable.
if 0:
#------------------------------------------------------------------------------
# Define translatable message table
TT = lambda x:x
_NOTIFICATION = {
'startup' : TT('Startup/Shutdown'), #: Message class for Growl server
'download' : TT('Added NZB'), #: Message class for Growl server
'pp' : TT('Post-processing started'), #: Message class for Growl server
'complete' : TT('Job finished'), #: Message class for Growl server
'other' : TT('Other Messages') #: Message class for Growl server
}
import sabnzbd
from sabnzbd.encoding import unicoder, latin1
import gntp
import gntp.notifier
try:
import Growl
import os.path
import logging
import platform
# If running on OSX-Lion and classic Growl (older than 1.3) is absent, assume GNTP-only
if [int(n) for n in platform.mac_ver()[0].split('.')] >= [10, 7, 0]:
_HAVE_OSX_GROWL = os.path.isfile('/Library/PreferencePanes/Growl.prefPane/Contents/MacOS/Growl')
else:
_HAVE_OSX_GROWL = True
except ImportError:
_HAVE_OSX_GROWL = False
#------------------------------------------------------------------------------
# Define translatable message table
NOTIFICATION = {'startup':'1. On Startup/Shutdown','download':'2. On adding NZB','pp':'3. On post-processing','complete':'4. On download terminated','other':'5. Other Messages'}
#------------------------------------------------------------------------------
# Setup platform dependent Growl support
#
_GROWL_ICON = None # Platform-dependant icon path
_GROWL = None # Instance of the Notifier after registration
_GROWL_REG = False # Succesful registration
#------------------------------------------------------------------------------
def get_icon():
icon = os.path.join(sabnzbd.DIR_PROG, 'sabnzbd.ico')
if not os.path.isfile(icon):
icon = None
return icon
#------------------------------------------------------------------------------
def register_growl():
""" Register this app with Growl
"""
error = None
# Clean up persistent data in GNTP to make re-registration work
gntp.GNTPRegister.notifications = []
gntp.GNTPRegister.headers = {}
growler = gntp.notifier.GrowlNotifier(
applicationName = 'SABnzbd',
applicationIcon = get_icon(),
notifications = sorted(NOTIFICATION.values()),
hostname = None,
port = 23053,
password = None
)
try:
ret = growler.register()
if ret is None or isinstance(ret, bool):
logging.info('Registered with Growl')
ret = growler
else:
error = 'Cannot register with Growl %s' % ret
logging.debug(error)
del growler
ret = None
except socket.error, err:
error = 'Cannot register with Growl %s' % err
logging.debug(error)
del growler
ret = None
return ret, error
#------------------------------------------------------------------------------
def sendGrowlMsg(title , msg, gtype):
""" Send Growl message
"""
global _GROWL, _GROWL_REG
if not sabnzbd.cfg.growl_enable() or not sabnzbd.DARWIN:
return
if _HAVE_OSX_GROWL:
res = send_local_growl(title, msg, gtype)
return res
for n in (0, 1):
if not _GROWL_REG: _GROWL = None
if not _GROWL:
_GROWL, error = register_growl()
if _GROWL:
assert isinstance(_GROWL, gntp.notifier.GrowlNotifier)
_GROWL_REG = True
#logging.debug('Send to Growl: %s %s %s', gtype, latin1(title), latin1(msg))
try:
ret = _GROWL.notify(
noteType = gtype,
title = title,
description = unicoder(msg),
#icon = options.icon,
#sticky = options.sticky,
#priority = options.priority
)
if ret is None or isinstance(ret, bool):
return None
elif ret[0] == '401':
_GROWL = False
else:
logging.debug('Growl error %s', ret)
return 'Growl error %s', ret
except socket.error, err:
logging.debug('Growl error %s', err)
return 'Growl error %s', err
else:
return error
return None
#------------------------------------------------------------------------------
# Local OSX Growl support
#
if _HAVE_OSX_GROWL:
_local_growl = None
if os.path.isfile('sabnzbdplus.icns'):
nIcon = Growl.Image.imageFromPath('sabnzbdplus.icns')
_OSX_ICON = Growl.Image.imageFromPath('sabnzbdplus.icns')
elif os.path.isfile('osx/resources/sabnzbdplus.icns'):
nIcon = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns')
_OSX_ICON = Growl.Image.imageFromPath('osx/resources/sabnzbdplus.icns')
else:
nIcon = Growl.Image.imageWithIconForApplication('Terminal')
def sendGrowlMsg(nTitle , nMsg, nType=NOTIFICATION['other']):
gnotifier = SABGrowlNotifier(applicationIcon=nIcon)
gnotifier.register()
#TO FIX
#gnotifier.notify(T(nType), nTitle, nMsg)
gnotifier.notify(nType, nTitle, nMsg)
_OSX_ICON = Growl.Image.imageWithIconForApplication('Terminal')
class SABGrowlNotifier(Growl.GrowlNotifier):
applicationName = "SABnzbd"
#TO FIX
#notifications = [T(notification) for notification in NOTIFICATION.values()]
notifications = NOTIFICATION.values()
except ImportError:
def sendGrowlMsg(nTitle , nMsg, nType):
pass
def send_local_growl(title , msg, gtype):
""" Send to local Growl server, OSX-only """
global _local_growl
if not _local_growl:
notes = sorted(NOTIFICATION.values())
_local_growl = Growl.GrowlNotifier(
applicationName = 'SABnzbd',
applicationIcon = _OSX_ICON,
notifications = notes,
defaultNotifications = notes
)
_local_growl.register()
_local_growl.notify(gtype, title, msg)
return None
......@@ -4,5 +4,5 @@
# You MUST use double quotes (so " and not ')
__version__ = "0.6.9"
__baseline__ = "713993140f07097cc5da6d2d233fc2bd37926961"
__version__ = "0.6.10"
__baseline__ = "19daeab0f359d27a92d6d0f5f09f3f4c644511be"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment