diff --git a/installer/linux/freeze.py b/installer/linux/freeze.py index 595d64d436..2346760181 100644 --- a/installer/linux/freeze.py +++ b/installer/linux/freeze.py @@ -9,7 +9,7 @@ Create linux binary. ''' def freeze(): - import glob, sys, subprocess, tarfile, os, re, textwrap, shutil, cStringIO, bz2, codecs + import glob, sys, tarfile, os, textwrap, shutil from contextlib import closing from cx_Freeze import Executable, setup from calibre.constants import __version__, __appname__ @@ -18,13 +18,13 @@ def freeze(): from calibre.web.feeds.recipes import recipe_modules from calibre.ebooks.lrf.fonts import FONT_MAP import calibre - + QTDIR = '/usr/lib/qt4' QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit') - + binary_excludes = ['libGLcore*', 'libGL*', 'libnvidia*'] - + binary_includes = [ '/usr/bin/pdftohtml', '/usr/lib/libunrar.so', @@ -48,58 +48,60 @@ def freeze(): '/usr/lib/libMagickWand.so', '/usr/lib/libMagickCore.so', ] - + binary_includes += [os.path.join(QTDIR, 'lib%s.so.4'%x) for x in QTDLLS] - - + + d = os.path.dirname CALIBRESRC = d(d(d(os.path.abspath(calibre.__file__)))) CALIBREPLUGINS = os.path.join(CALIBRESRC, 'src', 'calibre', 'plugins') FREEZE_DIR = os.path.join(CALIBRESRC, 'build', 'cx_freeze') DIST_DIR = os.path.join(CALIBRESRC, 'dist') - + os.chdir(CALIBRESRC) - + print 'Freezing calibre located at', CALIBRESRC - + sys.path.insert(0, os.path.join(CALIBRESRC, 'src')) - + entry_points = entry_points['console_scripts'] + entry_points['gui_scripts'] - entry_points = ['calibre_postinstall=calibre.linux:binary_install', + entry_points = ['calibre_postinstall=calibre.linux:binary_install', 'calibre-parallel=calibre.parallel:main'] + entry_points executables = {} for ep in entry_points: executables[ep.split('=')[0].strip()] = (ep.split('=')[1].split(':')[0].strip(), ep.split(':')[-1].strip()) - + if os.path.exists(FREEZE_DIR): shutil.rmtree(FREEZE_DIR) os.makedirs(FREEZE_DIR) - + if not os.path.exists(DIST_DIR): os.makedirs(DIST_DIR) - + includes = [x[0] for x in executables.values()] includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()] - - excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk", - "ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so', + includes += ['email.iterators', 'email.generator'] + + + excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk", + "ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so', 'PyQt4.QtScript.so', 'PyQt4.QtSql.so', 'PyQt4.QtTest.so', 'qt', 'glib', 'gobject'] - - packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg', - 'dateutil'] - + + packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg', + 'dateutil', 'dns', 'email'] + includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules] - + LOADER = '/tmp/loader.py' open(LOADER, 'wb').write('# This script is never actually used.\nimport sys') - + INIT_SCRIPT = '/tmp/init.py' open(INIT_SCRIPT, 'wb').write(textwrap.dedent(''' ## Load calibre module specified in the environment variable CALIBRE_CX_EXE ## Also restrict sys.path to the executables' directory and add the - ## executables directory to LD_LIBRARY_PATH + ## executables directory to LD_LIBRARY_PATH import encodings import os import sys @@ -107,26 +109,26 @@ def freeze(): import zipimport import locale import codecs - + enc = locale.getdefaultlocale()[1] if not enc: enc = locale.nl_langinfo(locale.CODESET) enc = codecs.lookup(enc if enc else 'UTF-8').name sys.setdefaultencoding(enc) - + paths = os.environ.get('LD_LIBRARY_PATH', '').split(os.pathsep) if DIR_NAME not in paths or not sys.getfilesystemencoding(): paths.insert(0, DIR_NAME) os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) os.environ['PYTHONIOENCODING'] = enc os.execv(sys.executable, sys.argv) - + sys.path = sys.path[:3] sys.frozen = True sys.frozen_path = DIR_NAME - + executables = %(executables)s - + exe = os.environ.get('CALIBRE_CX_EXE', False) ret = 1 if not exe: @@ -141,7 +143,7 @@ def freeze(): module = __import__(module, fromlist=[1]) func = getattr(module, func) ret = func() - + module = sys.modules.get("threading") if module is not None: module._shutdown() @@ -162,35 +164,35 @@ def freeze(): 'init_script' : INIT_SCRIPT, 'copy_dependent_files' : True, 'create_shared_zip' : False, - } + } } ) - + def copy_binary(src, dest_dir): dest = os.path.join(dest_dir, os.path.basename(src)) if not os.path.exists(dest_dir): os.makedirs(dest_dir) shutil.copyfile(os.path.realpath(src), dest) shutil.copymode(os.path.realpath(src), dest) - + for f in binary_includes: copy_binary(f, FREEZE_DIR) - + for pat in binary_excludes: matches = glob.glob(os.path.join(FREEZE_DIR, pat)) for f in matches: os.remove(f) - + print 'Adding calibre plugins...' - os.makedirs(os.path.join(FREEZE_DIR, 'plugins')) + os.makedirs(os.path.join(FREEZE_DIR, 'plugins')) for f in glob.glob(os.path.join(CALIBREPLUGINS, '*.so')): copy_binary(f, os.path.join(FREEZE_DIR, 'plugins')) - + print 'Adding Qt plugins...' plugdir = os.path.join(QTDIR, 'plugins') for dirpath, dirnames, filenames in os.walk(plugdir): for f in filenames: - if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath: + if not f.endswith('.so') or 'designer' in dirpath or 'codecs' in dirpath or 'sqldrivers' in dirpath: continue f = os.path.join(dirpath, f) dest_dir = dirpath.replace(plugdir, os.path.join(FREEZE_DIR, 'qtplugins')) @@ -198,7 +200,7 @@ def freeze(): print 'Creating launchers' for exe in executables: - path = os.path.join(FREEZE_DIR, exe) + path = os.path.join(FREEZE_DIR, exe) open(path, 'wb').write(textwrap.dedent('''\ #!/bin/sh export CALIBRE_CX_EXE=%s @@ -209,15 +211,15 @@ def freeze(): $loader "$@" ''')%exe) os.chmod(path, 0755) - + exes = list(executables.keys()) exes.remove('calibre_postinstall') exes.remove('calibre-parallel') open(os.path.join(FREEZE_DIR, 'manifest'), 'wb').write('\n'.join(exes)) - + print 'Creating archive...' dist = open(os.path.join(DIST_DIR, 'calibre-%s-i686.tar.bz2'%__version__), 'wb') - with closing(tarfile.open(fileobj=dist, mode='w:bz2', + with closing(tarfile.open(fileobj=dist, mode='w:bz2', format=tarfile.PAX_FORMAT)) as tf: for f in walk(FREEZE_DIR): name = f.replace(FREEZE_DIR, '')[1:] diff --git a/installer/windows/freeze.py b/installer/windows/freeze.py index fce5a2c923..aca1481fa8 100644 --- a/installer/windows/freeze.py +++ b/installer/windows/freeze.py @@ -14,12 +14,11 @@ IMAGEMAGICK_DIR = 'C:\\ImageMagick' FONTCONFIG_DIR = 'C:\\fontconfig' VC90 = r'C:\VC90.CRT' -import sys, os, py2exe, shutil, zipfile, glob, subprocess, re +import sys, os, py2exe, shutil, zipfile, glob, re from distutils.core import setup -from distutils.filelist import FileList BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) sys.path.insert(0, BASE_DIR) -from setup import VERSION, APPNAME, entry_points, scripts, basenames +from setup import VERSION, APPNAME, scripts, basenames sys.path.remove(BASE_DIR) ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')] @@ -33,7 +32,7 @@ WINVER = VERSION+'.0' PY2EXE_DIR = os.path.join(BASE_DIR, 'build','py2exe') class BuildEXE(py2exe.build_exe.py2exe): - + def run(self): py2exe.build_exe.py2exe.run(self) print 'Adding plugins...' @@ -61,19 +60,19 @@ class BuildEXE(py2exe.build_exe.py2exe): if os.path.exists(tg): shutil.rmtree(tg) shutil.copytree(imfd, tg) - - print + + print print 'Adding main scripts' f = zipfile.ZipFile(os.path.join(PY2EXE_DIR, 'library.zip'), 'a', zipfile.ZIP_DEFLATED) for i in scripts['console'] + scripts['gui']: f.write(i, i.partition('\\')[-1]) f.close() - + print print 'Copying icons' for icon in ICONS: shutil.copyfile(icon, os.path.join(PY2EXE_DIR, os.path.basename(icon))) - + print print 'Adding third party dependencies' print '\tAdding devcon' @@ -96,18 +95,18 @@ class BuildEXE(py2exe.build_exe.py2exe): shutil.copytree(f, tgt) else: shutil.copyfile(f, tgt) - - print + + print print 'Doing DLL redirection' # See http://msdn.microsoft.com/en-us/library/ms682600(VS.85).aspx for f in glob.glob(os.path.join(PY2EXE_DIR, '*.exe')): open(f + '.local', 'w').write('\n') - + print print 'Adding Windows runtime dependencies...' for f in glob.glob(os.path.join(VC90, '*')): shutil.copyfile(f, os.path.join(PY2EXE_DIR, os.path.basename(f))) - - + + def exe_factory(dest_base, script, icon_resources=None): exe = { 'dest_base' : dest_base, @@ -144,7 +143,9 @@ def main(args=sys.argv): 'includes' : [ 'sip', 'pkg_resources', 'PyQt4.QtSvg', 'mechanize', 'ClientForm', 'wmi', - 'win32file', 'pythoncom', + 'win32file', 'pythoncom', + 'email.iterators', + 'email.generator', 'win32process', 'win32api', 'msvcrt', 'win32event', 'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*', @@ -155,14 +156,14 @@ def main(args=sys.argv): 'PyQt4.QtWebKit', 'PyQt4.QtNetwork', ], 'packages' : ['PIL', 'lxml', 'cherrypy', - 'dateutil'], + 'dateutil', 'dns'], 'excludes' : ["Tkconstants", "Tkinter", "tcl", "_imagingtk", "ImageTk", "FixTk" ], 'dll_excludes' : ['mswsock.dll'], }, }, - + ) return 0 diff --git a/src/calibre/ebooks/metadata/fetch.py b/src/calibre/ebooks/metadata/fetch.py index 57d4368883..f950e1282c 100644 --- a/src/calibre/ebooks/metadata/fetch.py +++ b/src/calibre/ebooks/metadata/fetch.py @@ -59,8 +59,8 @@ class FetchISBNDB(Thread): args.extend(['--author', self.author]) if self.publisher: args.extend(['--publisher', self.publisher]) - if self.verbose: - args.extend(['--verbose']) + if self.verbose: + args.extend(['--verbose']) args.append(self.key) try: opts, args = option_parser().parse_args(args) diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index 1fce2c07ca..7ab2f16949 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -28,7 +28,8 @@ def fetch_metadata(url, max=100, timeout=5.): raw = urlopen(url).read() except Exception, err: raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err)) - soup = BeautifulStoneSoup(raw) + soup = BeautifulStoneSoup(raw, + convertEntities=BeautifulStoneSoup.XML_ENTITIES) book_list = soup.find('booklist') if book_list is None: errmsg = soup.find('errormessage').string @@ -41,13 +42,13 @@ def fetch_metadata(url, max=100, timeout=5.): return books finally: socket.setdefaulttimeout(timeout) - + class ISBNDBMetadata(MetaInformation): - + def __init__(self, book): MetaInformation.__init__(self, None, []) - + self.isbn = book['isbn'] self.title = book.find('titlelong').string if not self.title: @@ -59,7 +60,7 @@ class ISBNDBMetadata(MetaInformation): for au in temp: if not au: continue self.authors.extend([a.strip() for a in au.split('&')]) - + try: self.author_sort = book.find('authors').find('person').string if self.authors and self.author_sort == self.authors[0]: @@ -67,12 +68,12 @@ class ISBNDBMetadata(MetaInformation): except: pass self.publisher = book.find('publishertext').string - + summ = book.find('summary') if summ and hasattr(summ, 'string') and summ.string: self.comments = 'SUMMARY:\n'+summ.string - - + + def build_isbn(base_url, opts): return base_url + 'index1=isbn&value1='+opts.isbn @@ -85,11 +86,11 @@ def build_combined(base_url, opts): query = query.strip() if len(query) == 0: raise ISBNDBError('You must specify at least one of --author, --title or --publisher') - + query = re.sub(r'\s+', '+', query) if isinstance(query, unicode): query = query.encode('utf-8') - return base_url+'index1=combined&value1='+quote(query, '+') + return base_url+'index1=combined&value1='+quote(query, '+') def option_parser(): @@ -97,7 +98,7 @@ def option_parser(): _(''' %prog [options] key -Fetch metadata for books from isndb.com. You can specify either the +Fetch metadata for books from isndb.com. You can specify either the books ISBN ID or its title and author. If you specify the title and author, then more than one book may be returned. @@ -112,11 +113,11 @@ key is the account key you generate after signing up for a free account from isb default=None, help=_('The title of the book to search for.')) parser.add_option('-p', '--publisher', default=None, dest='publisher', help=_('The publisher of the book to search for.')) - parser.add_option('-v', '--verbose', default=False, + parser.add_option('-v', '--verbose', default=False, action='store_true', help=_('Verbose processing')) - + return parser - + def create_books(opts, args, timeout=5.): base_url = BASE_URL%dict(key=args[1]) @@ -124,10 +125,10 @@ def create_books(opts, args, timeout=5.): url = build_isbn(base_url, opts) else: url = build_combined(base_url, opts) - + if opts.verbose: print ('ISBNDB query: '+url) - + return [ISBNDBMetadata(book) for book in fetch_metadata(url, timeout=timeout)] def main(args=sys.argv): @@ -137,10 +138,10 @@ def main(args=sys.argv): parser.print_help() print ('You must supply the isbndb.com key') return 1 - + for book in create_books(opts, args): print unicode(book).encode('utf-8') - + return 0 if __name__ == '__main__': diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 50b2923d84..782a705ee9 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -337,7 +337,7 @@ class DeviceMenu(QMenu): class Emailer(Thread): - def __init__(self, timeout=10): + def __init__(self, timeout=60): Thread.__init__(self) self.setDaemon(True) self.job_lock = RLock() @@ -369,6 +369,7 @@ class Emailer(Thread): def _send_mails(self, jobnames, callback, attachments, to_s, subjects, texts, attachment_names): opts = email_config().parse() + opts.verbose = 3 if os.environ.get('CALIBRE_DEBUG_EMAIL', False) else 0 from_ = opts.from_ if not from_: from_ = 'calibre ' @@ -380,7 +381,8 @@ class Emailer(Thread): attachment_name = attachment_names[i]) efrom, eto = map(extract_email_address, (from_, to_s[i])) eto = [eto] - sendmail(msg, efrom, eto, localhost=None, verbose=0, + sendmail(msg, efrom, eto, localhost=None, + verbose=opts.verbose, timeout=self.timeout, relay=opts.relay_host, username=opts.relay_username, password=opts.relay_password, port=opts.relay_port, diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py index 0dc5474b78..805cc1c3bf 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.py +++ b/src/calibre/gui2/dialogs/fetch_metadata.py @@ -11,12 +11,12 @@ from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, QThread, \ from PyQt4.QtGui import QDialog, QItemSelectionModel from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata -from calibre.gui2 import error_dialog, NONE, info_dialog, warning_dialog +from calibre.gui2 import error_dialog, NONE, info_dialog from calibre.gui2.widgets import ProgressIndicator from calibre.utils.config import prefs class Fetcher(QThread): - + def __init__(self, title, author, publisher, isbn, key): QThread.__init__(self) self.title = title @@ -24,47 +24,47 @@ class Fetcher(QThread): self.publisher = publisher self.isbn = isbn self.key = key - + def run(self): from calibre.ebooks.metadata.fetch import search self.results, self.exceptions = search(self.title, self.author, - self.publisher, self.isbn, + self.publisher, self.isbn, self.key if self.key else None) - + class Matches(QAbstractTableModel): - + def __init__(self, matches): self.matches = matches self.matches.sort(cmp=lambda b, a: \ - cmp(len(a.comments if a.comments else ''), + cmp(len(a.comments if a.comments else ''), len(b.comments if b.comments else ''))) QAbstractTableModel.__init__(self) - + def rowCount(self, *args): return len(self.matches) - + def columnCount(self, *args): return 5 - + def headerData(self, section, orientation, role): if role != Qt.DisplayRole: return NONE text = "" - if orientation == Qt.Horizontal: + if orientation == Qt.Horizontal: if section == 0: text = _("Title") elif section == 1: text = _("Author(s)") elif section == 2: text = _("Author Sort") elif section == 3: text = _("Publisher") elif section == 4: text = _("ISBN") - + return QVariant(text) - else: + else: return QVariant(section+1) - + def summary(self, row): return self.matches[row].comments - + def data(self, index, role): row, col = index.row(), index.column() if role == Qt.DisplayRole: @@ -86,18 +86,18 @@ class Matches(QAbstractTableModel): return NONE class FetchMetadata(QDialog, Ui_FetchMetadata): - + def __init__(self, parent, isbn, title, author, publisher, timeout): QDialog.__init__(self, parent) Ui_FetchMetadata.__init__(self) self.setupUi(self) - + self.pi = ProgressIndicator(self) self.timeout = timeout QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata) - + self.key.setText(prefs['isbndb_com_key']) - + self.setWindowTitle(title if title else _('Unknown')) self.isbn = isbn self.title = title @@ -106,19 +106,19 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.previous_row = None self.warning.setVisible(False) self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen) - self.connect(self.matches, SIGNAL('entered(QModelIndex)'), + self.connect(self.matches, SIGNAL('entered(QModelIndex)'), self.show_summary) self.matches.setMouseTracking(True) self.fetch_metadata() - - + + def show_summary(self, current, *args): row = current.row() if row != self.previous_row: summ = self.model.summary(row) self.summary.setText(summ if summ else '') self.previous_row = row - + def fetch_metadata(self): self.warning.setVisible(False) key = str(self.key.text()) @@ -143,7 +143,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck) self.start_time = time.time() self._hangcheck.start(100) - + def hangcheck(self): if not (self.fetcher.isFinished() or time.time() - self.start_time > 75): return @@ -169,13 +169,13 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): _('No metadata found, try adjusting the title and author ' 'or the ISBN key.')).exec_() return - + self.matches.setModel(self.model) - QObject.connect(self.matches.selectionModel(), + QObject.connect(self.matches.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), self.show_summary) self.model.reset() - self.matches.selectionModel().select(self.model.index(0, 0), + self.matches.selectionModel().select(self.model.index(0, 0), QItemSelectionModel.Select | QItemSelectionModel.Rows) self.matches.setCurrentIndex(self.model.index(0, 0)) finally: @@ -183,14 +183,24 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.unsetCursor() self.matches.resizeColumnsToContents() self.pi.stop() - - + + def terminate(self): + if hasattr(self, 'fetcher') and self.fetcher.isRunning(): + self.fetcher.terminate() + + + def __enter__(self, *args): + return self + + def __exit__(self, *args): + self.terminate() + def selected_book(self): try: return self.matches.model().matches[self.matches.currentIndex().row()] except: return None - + def chosen(self, index): self.matches.setCurrentIndex(index) self.accept() diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 7c5523161e..a9d20905c6 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -1,7 +1,8 @@ +from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -''' -The dialog used to edit meta information for a book as well as +''' +The dialog used to edit meta information for a book as well as add/remove formats ''' import os, time, traceback @@ -26,7 +27,7 @@ from calibre.utils.config import prefs from calibre.customize.ui import run_plugins_on_import class CoverFetcher(QThread): - + def __init__(self, username, password, isbn, timeout): self.username = username self.password = password @@ -34,7 +35,7 @@ class CoverFetcher(QThread): self.isbn = isbn QThread.__init__(self) self.exception = self.traceback = self.cover_data = None - + def run(self): try: login(self.username, self.password, force=False) @@ -42,8 +43,8 @@ class CoverFetcher(QThread): except Exception, e: self.exception = e self.traceback = traceback.format_exc() - - + + class Format(QListWidgetItem): def __init__(self, parent, ext, size, path=None): @@ -51,25 +52,25 @@ class Format(QListWidgetItem): self.ext = ext self.size = float(size)/(1024*1024) text = '%s (%.2f MB)'%(self.ext.upper(), self.size) - QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), + QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), text, parent, QListWidgetItem.UserType) class AuthorCompleter(QCompleter): - + def __init__(self, db): all_authors = db.all_authors() all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) QCompleter.__init__(self, [x[1] for x in all_authors]) class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): - + def do_reset_cover(self, *args): pix = QPixmap(':/images/book.svg') self.cover.setPixmap(pix) self.cover_changed = True - + def select_cover(self, checked): - files = choose_images(self, 'change cover dialog', + files = choose_images(self, 'change cover dialog', u'Choose cover for ' + qstring_to_unicode(self.title.text())) if not files: return @@ -77,7 +78,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if _file: _file = os.path.abspath(_file) if not os.access(_file, os.R_OK): - d = error_dialog(self.window, _('Cannot read'), + d = error_dialog(self.window, _('Cannot read'), _('You do not have permission to read the file: ') + _file) d.exec_() return @@ -85,7 +86,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): try: cf = open(_file, "rb") cover = cf.read() - except IOError, e: + except IOError, e: d = error_dialog(self.window, _('Error reading file'), _("

There was an error reading from file:
") + _file + "


"+str(e)) d.exec_() @@ -99,15 +100,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.cover_path.setText(_file) self.cover.setPixmap(pix) self.cover_changed = True - self.cpixmap = pix - - + self.cpixmap = pix + + def add_format(self, x): - files = choose_files(self, 'add formats dialog', + files = choose_files(self, 'add formats dialog', "Choose formats for " + qstring_to_unicode((self.title.text())), [('Books', BOOK_EXTENSIONS)]) - if not files: - return + if not files: + return for _file in files: _file = os.path.abspath(_file) if not os.access(_file, os.R_OK): @@ -124,13 +125,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): break Format(self.formats, ext, size, path=_file) self.formats_changed = True - + def remove_format(self, x): rows = self.formats.selectionModel().selectedRows(0) for row in rows: self.formats.takeItem(row.row()) self.formats_changed = True - + def set_cover(self): row = self.formats.currentRow() fmt = self.formats.item(row) @@ -155,19 +156,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): elif mi.cover_data[1] is not None: cdata = mi.cover_data[1] if cdata is None: - error_dialog(self, _('Could not read cover'), + error_dialog(self, _('Could not read cover'), _('Could not read cover from %s format')%ext).exec_() return pix = QPixmap() pix.loadFromData(cdata) if pix.isNull(): - error_dialog(self, _('Could not read cover'), + error_dialog(self, _('Could not read cover'), _('The cover in the %s format is invalid')%ext).exec_() return self.cover.setPixmap(pix) self.cover_changed = True self.cpixmap = pix - + def sync_formats(self): old_extensions, new_extensions, paths = set(), set(), {} for row in range(self.formats.count()): @@ -187,7 +188,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): for ext in db_extensions: if ext not in extensions: self.db.remove_format(self.row, ext, notify=False) - + def __init__(self, window, row, db, accepted_callback=None): ResizableDialog.__init__(self, window) self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter) @@ -211,12 +212,12 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.add_format) QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \ self.remove_format) - QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'), + QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'), self.fetch_metadata) - - QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'), + + QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'), self.fetch_cover) - QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), + QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), self.edit_tags) QObject.connect(self.remove_series_button, SIGNAL('clicked()'), self.remove_unused_series) @@ -242,28 +243,28 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): tags = self.db.tags(row) self.tags.setText(tags if tags else '') rating = self.db.rating(row) - if rating > 0: + if rating > 0: self.rating.setValue(int(rating/2.)) comments = self.db.comments(row) self.comments.setPlainText(comments if comments else '') cover = self.db.cover(row) - + exts = self.db.formats(row) if exts: - exts = exts.split(',') + exts = exts.split(',') for ext in exts: if not ext: ext = '' size = self.db.sizeof_format(row, ext) Format(self.formats, ext, size) - - + + self.initialize_series_and_publisher() - + self.series_index.setValue(self.db.series_index(row)) QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index) - QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password) + QObject.connect(self.password_button, SIGNAL('clicked()'), self.change_password) self.show() height_of_rest = self.frameGeometry().height() - self.cover.height() @@ -274,14 +275,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if cover: pm = QPixmap() pm.loadFromData(cover) - if not pm.isNull(): + if not pm.isNull(): self.cover.setPixmap(pm) - + def deduce_author_sort(self): au = unicode(self.authors.text()) authors = string_to_authors(au) self.author_sort.setText(authors_to_sort_string(authors)) - + def swap_title_author(self): title = self.title.text() self.title.setText(self.authors.text()) @@ -290,7 +291,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): def cover_dropped(self): self.cover_changed = True - + def initialize_series(self): all_series = self.db.all_series() all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) @@ -302,19 +303,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): idx = c self.series.addItem(name) c += 1 - + self.series.lineEdit().setText('') if idx is not None: self.series.setCurrentIndex(idx) self.enable_series_index() - + pl = self.series.parentWidget().layout() for i in range(pl.count()): l = pl.itemAt(i).layout() if l: l.invalidate() l.activate() - + def initialize_series_and_publisher(self): self.initialize_series() all_publishers = self.db.all_publishers() @@ -327,33 +328,33 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): idx = c self.publisher.addItem(name) c += 1 - + self.publisher.setEditText('') if idx is not None: self.publisher.setCurrentIndex(idx) - - + + self.layout().activate() - + def edit_tags(self): d = TagEditor(self, self.db, self.row) d.exec_() if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) self.tags.setText(tag_string) - + def lt_password_dialog(self): - return PasswordDialog(self, 'LibraryThing account', + return PasswordDialog(self, 'LibraryThing account', _('

Enter your username and password for LibraryThing.com.
If you do not have one, you can register for free!.

')) - + def change_password(self): - d = self.lt_password_dialog() + d = self.lt_password_dialog() d.exec_() - + def fetch_cover(self): isbn = qstring_to_unicode(self.isbn.text()) if isbn: - d = self.lt_password_dialog() + d = self.lt_password_dialog() if not d.username() or not d.password(): d.exec_() if d.result() != PasswordDialog.Accepted: @@ -369,31 +370,31 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.pi.start(_('Downloading cover...')) self._hangcheck.start(100) else: - error_dialog(self, _('Cannot fetch cover'), + error_dialog(self, _('Cannot fetch cover'), _('You must specify the ISBN identifier for this book.')).exec_() - + def hangcheck(self): if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150): return - + self._hangcheck.stop() try: if self.cover_fetcher.isRunning(): self.cover_fetcher.terminate() - error_dialog(self, _('Cannot fetch cover'), + error_dialog(self, _('Cannot fetch cover'), _('Could not fetch cover.
')+ _('The download timed out.')).exec_() return if self.cover_fetcher.exception is not None: err = self.cover_fetcher.exception - error_dialog(self, _('Cannot fetch cover'), + error_dialog(self, _('Cannot fetch cover'), _('Could not fetch cover.
')+repr(err)).exec_() return - + pix = QPixmap() pix.loadFromData(self.cover_fetcher.cover_data) if pix.isNull(): - error_dialog(self.window, _('Bad cover'), + error_dialog(self.window, _('Bad cover'), _('The cover is not a valid picture')).exec_() else: self.cover.setPixmap(pix) @@ -403,16 +404,17 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.fetch_cover_button.setEnabled(True) self.unsetCursor() self.pi.stop() - - + + def fetch_metadata(self): isbn = qstring_to_unicode(self.isbn.text()) title = qstring_to_unicode(self.title.text()) author = string_to_authors(unicode(self.authors.text()))[0] - publisher = qstring_to_unicode(self.publisher.currentText()) + publisher = qstring_to_unicode(self.publisher.currentText()) if isbn or title or author or publisher: d = FetchMetadata(self, isbn, title, author, publisher, self.timeout) - d.exec_() + with d: + d.exec_() if d.result() == QDialog.Accepted: book = d.selected_book() if book: @@ -428,13 +430,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): prefix += '\n' self.comments.setText(prefix + summ) else: - error_dialog(self, _('Cannot fetch metadata'), + error_dialog(self, _('Cannot fetch metadata'), _('You must specify at least one of ISBN, Title, ' 'Authors or Publisher')) - + def enable_series_index(self, *args): self.series_index.setEnabled(True) - + def remove_unused_series(self): self.db.remove_unused_series() idx = qstring_to_unicode(self.series.currentText()) @@ -445,15 +447,15 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if qstring_to_unicode(self.series.itemText(i)) == idx: self.series.setCurrentIndex(i) break - - + + def accept(self): if self.formats_changed: self.sync_formats() title = qstring_to_unicode(self.title.text()) self.db.set_title(self.id, title, notify=False) au = unicode(self.authors.text()) - if au: + if au: self.db.set_authors(self.id, string_to_authors(au), notify=False) aus = qstring_to_unicode(self.author_sort.text()) if aus: diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py index a050cb6ea2..2f6dce0573 100644 --- a/src/calibre/trac/plugins/download.py +++ b/src/calibre/trac/plugins/download.py @@ -5,7 +5,7 @@ import re, textwrap DEPENDENCIES = [ #(Generic, version, gentoo, ubuntu, fedora) - ('python', '2.5', None, None, None), + ('python', '2.6', None, None, None), ('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'), ('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'), ('libusb', '0.1.12', None, None, None), @@ -20,10 +20,10 @@ DEPENDENCIES = [ ('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'), ('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'), ] - + class CoolDistro: - + def __init__(self, name, title, prefix=''): self.title = title url = prefix + '/chrome/dl/images/%s_logo.png' @@ -39,7 +39,7 @@ def get_linux_data(version='1.0.0'): ('foresight', 'Foresight 2.1'), ('ubuntu', 'Ubuntu Jaunty Jackalope'), ]: - data['supported'].append(CoolDistro(name, title, + data['supported'].append(CoolDistro(name, title, prefix='http://calibre.kovidgoyal.net')) data['dependencies'] = DEPENDENCIES return data @@ -54,51 +54,51 @@ if __name__ == '__main__': return MarkupTemplate(raw).generate(**get_linux_data()).render('xhtml') index.exposed = True t = Test() - t.index() + t.index() cherrypy.quickstart(t) else: from pkg_resources import resource_filename - + from trac.core import Component, implements from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet from trac.web.main import IRequestHandler from trac.util import Markup - - - + + + DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads' MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' - + class OS(dict): """Dictionary with a default value for unknown keys.""" def __init__(self, dict): self.update(dict) if not dict.has_key('img'): self['img'] = self['name'] - - + + class Download(Component): implements(INavigationContributor, IRequestHandler, ITemplateProvider) - + request_pat = re.compile(r'\/download$|\/download_\S+') - + # INavigationContributor methods def get_active_navigation_item(self, req): return 'download' - + def get_navigation_items(self, req): yield 'mainnav', 'download', Markup('Get %s'%(__appname__,)) - + def get_templates_dirs(self): return [resource_filename(__name__, 'templates')] - + def get_htdocs_dirs(self): return [('dl', resource_filename(__name__, 'htdocs'))] - + # IRequestHandler methods def match_request(self, req): return self.__class__.request_pat.match(req.path_info) - + def process_request(self, req): add_stylesheet(req, 'dl/css/download.css') if req.path_info == '/download': @@ -115,29 +115,29 @@ else: return self.osx(req) elif os == 'linux': return self.linux(req) - + def top_level(self, req): operating_systems = [ OS({'name' : 'windows', 'title' : 'Windows'}), OS({'name' : 'osx', 'title' : 'OS X'}), OS({'name' : 'linux', 'title' : 'Linux'}), ] - data = dict(title='Get ' + __appname__, + data = dict(title='Get ' + __appname__, operating_systems=operating_systems, width=200, font_size='xx-large', top_level=True) return 'download.html', data, None - + def version_from_filename(self): try: return open(DOWNLOAD_DIR+'/latest_version', 'rb').read().strip() except: return '0.0.0' - + def windows(self, req): version = self.version_from_filename() - file = '%s-%s.exe'%(__appname__, version,) + file = '%s-%s.exe'%(__appname__, version,) data = dict(version = version, name='windows', - installer_name='Windows installer', + installer_name='Windows installer', title='Download %s for windows'%(__appname__), compatibility='%s works on Windows XP and Windows Vista.'%(__appname__,), path=MOBILEREAD+file, app=__appname__, @@ -146,7 +146,7 @@ else:

If you are using the SONY PRS-500 and %(appname)s does not detect your reader, read on:

- If you are using 64-bit windows, you're out of luck. + If you are using 64-bit windows, you're out of luck.

There may be a conflict with the USB driver from SONY. In windows, you cannot install two drivers @@ -160,7 +160,7 @@ else: You can uninstall a driver by right clicking on it and selecting uninstall. -

  • Once the drivers have been uninstalled, find the file prs500.inf (it will be in the +
  • Once the drivers have been uninstalled, find the file prs500.inf (it will be in the driver folder in the folder in which you installed %(appname)s. Right click on it and select Install.
  • @@ -168,12 +168,12 @@ else:
    '''%dict(appname=__appname__))) return 'binary.html', data, None - + def osx(self, req): version = self.version_from_filename() - file = 'calibre-%s.dmg'%(version,) + file = 'calibre-%s.dmg'%(version,) data = dict(version = version, name='osx', - installer_name='OS X universal dmg', + installer_name='OS X universal dmg', title='Download %s for OS X'%(__appname__), compatibility='%s works on OS X Tiger and above.'%(__appname__,), path=MOBILEREAD+file, app=__appname__, @@ -181,57 +181,57 @@ else: u'''
    1. Before trying to use the command line tools, you must run the app at least once. This will ask you for you password and then setup the symbolic links for the command line tools.
    2. -
    3. The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).
    4. +
    5. The app cannot be run from within the dmg. You must drag it to a folder on your filesystem (The Desktop, Applications, wherever).
    6. In order for localization of the user interface in your language, select your language in the preferences (by pressing u\2318+P) and select your language.
    ''')) return 'binary.html', data, None - + def linux(self, req): data = get_linux_data(version=self.version_from_filename()) return 'linux.html', data, None - - + + LINUX_INSTALLER = textwrap.dedent(r''' import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat - + MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' - + class TerminalController: BOL = '' #: Move the cursor to the beginning of the line UP = '' #: Move the cursor up one line DOWN = '' #: Move the cursor down one line LEFT = '' #: Move the cursor left one char RIGHT = '' #: Move the cursor right one char - + # Deletion: CLEAR_SCREEN = '' #: Clear the screen and move to home position CLEAR_EOL = '' #: Clear to the end of the line. CLEAR_BOL = '' #: Clear to the beginning of the line. CLEAR_EOS = '' #: Clear to the end of the screen - + # Output modes: BOLD = '' #: Turn on bold mode BLINK = '' #: Turn on blink mode DIM = '' #: Turn on half-bright mode REVERSE = '' #: Turn on reverse-video mode NORMAL = '' #: Turn off all modes - + # Cursor display: HIDE_CURSOR = '' #: Make the cursor invisible SHOW_CURSOR = '' #: Make the cursor visible - + # Terminal size: COLS = None #: Width of the terminal (None for unknown) LINES = None #: Height of the terminal (None for unknown) - + # Foreground colors: BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' - + # Background colors: BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = '' BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = '' - + _STRING_CAPABILITIES = """ BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold @@ -239,29 +239,29 @@ else: HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split() _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() - + def __init__(self, term_stream=sys.stdout): # Curses isn't available on all platforms try: import curses except: return - + # If the stream isn't a tty, then assume it has no capabilities. if not hasattr(term_stream, 'isatty') or not term_stream.isatty(): return - + # Check the terminal type. If we fail, then assume that the # terminal has no capabilities. try: curses.setupterm() except: return - + # Look up numeric capabilities. self.COLS = curses.tigetnum('cols') self.LINES = curses.tigetnum('lines') - + # Look up string capabilities. for capability in self._STRING_CAPABILITIES: (attrib, cap_name) = capability.split('=') setattr(self, attrib, self._tigetstr(cap_name) or '') - + # Colors set_fg = self._tigetstr('setf') if set_fg: @@ -279,7 +279,7 @@ else: if set_bg_ansi: for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '') - + def _tigetstr(self, cap_name): # String capabilities can include "delays" of the form "$<2>". # For any modern terminal, we should be able to just ignore @@ -287,19 +287,19 @@ else: import curses cap = curses.tigetstr(cap_name) or '' return re.sub(r'\$<\d+>[/*]?', '', cap) - + def render(self, template): return re.sub(r'\$\$|\${\w+}', self._render_sub, template) - + def _render_sub(self, match): s = match.group() if s == '$$': return s else: return getattr(self, s[2:-1]) - + class ProgressBar: BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n' HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n' - + def __init__(self, term, header): self.term = term if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL): @@ -309,7 +309,7 @@ else: self.bar = term.render(self.BAR) self.header = self.term.render(self.HEADER % header.center(self.width)) self.cleared = 1 #: true if we haven't drawn the bar yet. - + def update(self, percent, message=''): if isinstance(message, unicode): message = message.encode('utf-8', 'ignore') @@ -323,14 +323,14 @@ else: (self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) + self.term.CLEAR_EOL + msg) sys.stdout.flush() - + def clear(self): if not self.cleared: sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL + self.term.UP + self.term.CLEAR_EOL) self.cleared = 1 - + def download_tarball(): try: pb = ProgressBar(TerminalController(sys.stdout), 'Downloading calibre...') @@ -355,14 +355,14 @@ else: print '%d%%, '%int(percent*100), f.seek(0) return f - + def extract_tarball(tar, destdir): print 'Extracting application files...' if hasattr(tar, 'read'): subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir]) else: subprocess.check_call(['tar', 'xjf', tar, '-C', destdir]) - + def main(): defdir = '/opt/calibre' destdir = raw_input('Enter the installation directory for calibre (Its contents will be deleted!)[%s]: '%defdir).strip() @@ -372,12 +372,12 @@ else: if os.path.exists(destdir): shutil.rmtree(destdir) os.makedirs(destdir) - + f = download_tarball() - + print 'Extracting files to %s ...'%destdir extract_tarball(f, destdir) pi = os.path.join(destdir, 'calibre_postinstall') subprocess.call(pi, shell=True) return 0 - ''') \ No newline at end of file + ''') diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py index 04a4231e13..63629c8394 100644 --- a/src/calibre/utils/smtp.py +++ b/src/calibre/utils/smtp.py @@ -32,7 +32,10 @@ def create_mail(from_, to, subject, text=None, attachment_data=None, if attachment_data is not None: from email.mime.base import MIMEBase assert attachment_data and attachment_name - maintype, subtype = attachment_type.split('/', 1) + try: + maintype, subtype = attachment_type.split('/', 1) + except AttributeError: + maintype, subtype = 'application', 'octet-stream' msg = MIMEBase(maintype, subtype) msg.set_payload(attachment_data) encoders.encode_base64(msg) @@ -50,9 +53,11 @@ def get_mx(host): def sendmail_direct(from_, to, msg, timeout, localhost, verbose): import smtplib + hosts = get_mx(to.split('@')[-1].strip()) + if 'darwin' in sys.platform: + timeout=None # Non blocking sockets dont work on OS X s = smtplib.SMTP(timeout=timeout, local_hostname=localhost) s.set_debuglevel(verbose) - hosts = get_mx(to.split('@')[-1].strip()) if not hosts: raise ValueError('No mail server found for address: %s'%to) last_error = last_traceback = None @@ -76,6 +81,8 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30, return sendmail_direct(from_, x, msg, timeout, localhost, verbose) import smtplib cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL + if 'darwin' in sys.platform: + timeout = None # Non-blocking sockets in OS X don't work s = cls(timeout=timeout, local_hostname=localhost) s.set_debuglevel(verbose) if port < 0: diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 13a79201e2..5df0a24efe 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -36,131 +36,131 @@ class BasicNewsRecipe(object, LoggingInterface): ''' Abstract base class that contains logic needed in all feed fetchers. ''' - + #: The title to use for the ebook title = _('Unknown News Source') - + #: A couple of lines that describe the content this recipe downloads. #: This will be used primarily in a GUI that presents a list of recipes. description = '' - + #: The author of this recipe - __author__ = __appname__ - + __author__ = __appname__ + #: The language that the news is in language = _('Unknown') - + #: Maximum number of articles to download from each feed. This is primarily #: useful for feeds that don't have article dates. For most feeds, you should #: use :attr:`BasicNewsRecipe.oldest_article` max_articles_per_feed = 100 - + #: Oldest article to download from this news source. In days. - oldest_article = 7.0 - - #: Number of levels of links to follow on article webpages + oldest_article = 7.0 + + #: Number of levels of links to follow on article webpages recursions = 0 - + #: Delay between consecutive downloads in seconds delay = 0 - + #: Number of simultaneous downloads. Set to 1 if the server is picky. #: Automatically reduced to 1 if :attr:`BasicNewsRecipe.delay` > 0 simultaneous_downloads = 5 - + #: If False the remote server is contacted by only one thread at a time multithreaded_fetch = False - + #: Timeout for fetching files from server in seconds timeout = 120.0 - + #: The format string for the date shown on the first page. #: By default: Day_Name, Day_Number Month_Name Year timefmt = ' [%a, %d %b %Y]' - + #: List of feeds to download #: Can be either ``[url1, url2, ...]`` or ``[('title1', url1), ('title2', url2),...]`` feeds = None - + #: Max number of characters in the short description summary_length = 500 - + #: Convenient flag to disable loading of stylesheets for websites #: that have overly complex stylesheets unsuitable for conversion #: to ebooks formats #: If True stylesheets are not downloaded and processed no_stylesheets = False - + #: Convenient flag to strip all javascript tags from the downloaded HTML remove_javascript = True - - #: If True the GUI will ask the user for a username and password + + #: If True the GUI will ask the user for a username and password #: to use while downloading #: @type: boolean needs_subscription = False - + #: If True the navigation bar is center aligned, otherwise it is left aligned center_navbar = True - + #: Specify an override encoding for sites that have an incorrect #: charset specification. The most common being specifying ``latin1`` and - #: using ``cp1252``. If None, try to detect the encoding. + #: using ``cp1252``. If None, try to detect the encoding. encoding = None - + #: Normally we try to guess if a feed has full articles embedded in it #: based on the length of the embedded content. If `None`, then the - #: default guessing is used. If `True` then the we always assume the feeds has + #: default guessing is used. If `True` then the we always assume the feeds has #: embedded content and if `False` we always assume the feed does not have #: embedded content. use_embedded_content = None - + #: Set to True and implement :method:`get_obfuscated_article` to handle #: websites that try to make it difficult to scrape content. articles_are_obfuscated = False - + #: Specify any extra :term:`CSS` that should be addded to downloaded :term:`HTML` files #: It will be inserted into `