From d9a0a5f3cb9ec8a6ef8aa3cd6ea2a56d0c972324 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Apr 2009 08:10:51 -0700 Subject: [PATCH 01/27] Device drivers: Do not check for ebooks in subdirectories on devices that do not support subdirectories --- src/calibre/devices/usbms/driver.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 3bfb5a61d9..c24002dcdb 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -57,12 +57,17 @@ class USBMS(Device): prefix = self._card_prefix if oncard else self._main_prefix ebook_dir = self.EBOOK_DIR_CARD if oncard else self.EBOOK_DIR_MAIN - # Get all books in all directories under the root ebook_dir directory - for path, dirs, files in os.walk(os.path.join(prefix, ebook_dir)): - # Filter out anything that isn't in the list of supported ebook - # types - for book_type in self.FORMATS: - for filename in fnmatch.filter(files, '*.%s' % (book_type)): + # Get all books in the ebook_dir directory + if self.SUPPORTS_SUB_DIRS: + for path, dirs, files in os.walk(os.path.join(prefix, ebook_dir)): + # Filter out anything that isn't in the list of supported ebook types + for book_type in self.FORMATS: + for filename in fnmatch.filter(files, '*.%s' % (book_type)): + bl.append(self.__class__.book_from_path(os.path.join(path, filename))) + else: + path = os.path.join(prefix, ebook_dir) + for filename in os.listdir(path): + if path_to_ext(filename) in self.FORMATS: bl.append(self.__class__.book_from_path(os.path.join(path, filename))) return bl From eb83545a26cfcad159440a7da943b41950cfda39 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Apr 2009 08:16:58 -0700 Subject: [PATCH 02/27] New recipe for Moneynews by Darko Miletic --- src/calibre/web/feeds/recipes/__init__.py | 1 + .../web/feeds/recipes/recipe_moneynews.py | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/calibre/web/feeds/recipes/recipe_moneynews.py diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py index a2dbcd7d24..191bf905ca 100644 --- a/src/calibre/web/feeds/recipes/__init__.py +++ b/src/calibre/web/feeds/recipes/__init__.py @@ -39,6 +39,7 @@ recipe_modules = ['recipe_' + r for r in ( 'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs', 'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet', 'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en', + 'moneynews', )] import re, imp, inspect, time, os diff --git a/src/calibre/web/feeds/recipes/recipe_moneynews.py b/src/calibre/web/feeds/recipes/recipe_moneynews.py new file mode 100644 index 0000000000..96656e490d --- /dev/null +++ b/src/calibre/web/feeds/recipes/recipe_moneynews.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +moneynews.newsmax.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class MoneyNews(BasicNewsRecipe): + title = 'Moneynews.com' + __author__ = 'Darko Miletic' + description = 'Financial news worldwide' + publisher = 'moneynews.com' + category = 'news, finances, USA, business' + oldest_article = 2 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + encoding = 'cp1252' + + html2lrf_options = [ + '--comment', description + , '--category', category + , '--publisher', publisher + , '--ignore-tables' + ] + + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True' + + feeds = [ + (u'Street Talk' , u'http://moneynews.newsmax.com/xml/streettalk.xml' ) + ,(u'Finance News' , u'http://moneynews.newsmax.com/xml/FinanceNews.xml' ) + ,(u'Economy' , u'http://moneynews.newsmax.com/xml/economy.xml' ) + ,(u'Companies' , u'http://moneynews.newsmax.com/xml/companies.xml' ) + ,(u'Markets' , u'http://moneynews.newsmax.com/xml/Markets.xml' ) + ,(u'Investing & Analysis' , u'http://moneynews.newsmax.com/xml/investing.xml' ) + ] + + + keep_only_tags = [dict(name='table', attrs={'class':'copy'})] + + remove_tags = [ + dict(name='td' , attrs={'id':'article_fontsize'}) + ,dict(name='table', attrs={'id':'toolbox' }) + ,dict(name='tr' , attrs={'id':'noprint3' }) + ] + \ No newline at end of file From b52d7f60d96703e8710966d31586a3be3e240100 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Apr 2009 08:33:20 -0700 Subject: [PATCH 03/27] IGN:... --- src/calibre/gui2/__init__.py | 2 ++ src/calibre/gui2/main.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 225f7a9e33..1da5bb6851 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -67,6 +67,8 @@ def _config(): c.add_opt('default_send_to_device_action', default=None, help=_('Default action to perform when send to device button is ' 'clicked')) + c.add_opt('show_donate_button', default=True, + help='Show donation button') return ConfigProxy(c) config = _config() diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index bbe53cb7ef..4f65a5bc50 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -107,6 +107,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.donate_action = self.system_tray_menu.addAction( QIcon(':/images/donate.svg'), _('&Donate to support calibre')) self.donate_button.setDefaultAction(self.donate_action) + if not config['show_donate_button']: + self.donate_button.setVisible(False) self.addAction(self.quit_action) self.action_restart = QAction(_('&Restart'), self) self.addAction(self.action_restart) From 28a9c6868ab2691722dc5cfc8eed636ed47994c9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Apr 2009 09:18:47 -0700 Subject: [PATCH 04/27] Fix #2261 (Conversion metadata problem) --- src/calibre/gui2/dialogs/epub.py | 11 ++--- src/calibre/gui2/tools.py | 70 +++++++++++++++++--------------- src/calibre/library/__init__.py | 12 +++--- 3 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/calibre/gui2/dialogs/epub.py b/src/calibre/gui2/dialogs/epub.py index 0773440b01..e61d034642 100644 --- a/src/calibre/gui2/dialogs/epub.py +++ b/src/calibre/gui2/dialogs/epub.py @@ -176,19 +176,19 @@ class Config(ResizableDialog, Ui_Dialog): def get_metadata(self): title, authors = self.get_title_and_authors() mi = MetaInformation(title, authors) - publisher = unicode(self.publisher.text()) + publisher = unicode(self.publisher.text()).strip() if publisher: mi.publisher = publisher - author_sort = unicode(self.author_sort.text()) + author_sort = unicode(self.author_sort.text()).strip() if author_sort: mi.author_sort = author_sort - comments = unicode(self.comment.toPlainText()) + comments = unicode(self.comment.toPlainText()).strip() if comments: mi.comments = comments mi.series_index = int(self.series_index.value()) if self.series.currentIndex() > -1: - mi.series = unicode(self.series.currentText()) - tags = [t.strip() for t in unicode(self.tags.text()).split(',')] + mi.series = unicode(self.series.currentText()).strip() + tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')] if tags: mi.tags = tags @@ -267,6 +267,7 @@ class Config(ResizableDialog, Ui_Dialog): ).exec_() return mi = self.get_metadata() + self.user_mi = mi self.read_settings() self.cover_file = None if self.row is not None: diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index aca2da74e2..7185f73a37 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -52,10 +52,10 @@ def convert_single(fmt, parent, db, comics, others): temp_files.append(d.cover_file) opts.cover = d.cover_file.name temp_files.extend([d.opf_file, pt, of]) - jobs.append(('any2'+fmt, args, _('Convert book: ')+d.mi.title, + jobs.append(('any2'+fmt, args, _('Convert book: ')+d.mi.title, fmt.upper(), row_id, temp_files)) changed = True - + for row, row_id in zip(comics, comics_ids): mi = db.get_metadata(row) title = author = _('Unknown') @@ -72,7 +72,7 @@ def convert_single(fmt, parent, db, comics, others): try: data = db.format(row, _fmt.upper()) if data is not None: - break + break except: continue pt = PersistentTemporaryFile('.'+_fmt) @@ -84,12 +84,12 @@ def convert_single(fmt, parent, db, comics, others): opts.verbose = 2 args = [pt.name, opts] changed = True - jobs.append(('comic2'+fmt, args, _('Convert comic: ')+opts.title, + jobs.append(('comic2'+fmt, args, _('Convert comic: ')+opts.title, fmt.upper(), row_id, [pt, of])) - + return jobs, changed - - + + def convert_single_lrf(parent, db, comics, others): changed = False @@ -114,10 +114,10 @@ def convert_single_lrf(parent, db, comics, others): if d.cover_file: temp_files.append(d.cover_file) temp_files.extend([pt, of]) - jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(), + jobs.append(('any2lrf', [cmdline], _('Convert book: ')+d.title(), 'LRF', row_id, temp_files)) changed = True - + for row, row_id in zip(comics, comics_ids): mi = db.get_metadata(row) title = author = _('Unknown') @@ -134,7 +134,7 @@ def convert_single_lrf(parent, db, comics, others): try: data = db.format(row, fmt.upper()) if data is not None: - break + break except: continue if data is None: @@ -148,19 +148,20 @@ def convert_single_lrf(parent, db, comics, others): opts.verbose = 1 args = [pt.name, opts] changed = True - jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title, + jobs.append(('comic2lrf', args, _('Convert comic: ')+opts.title, 'LRF', row_id, [pt, of])) - + return jobs, changed def convert_bulk(fmt, parent, db, comics, others): if others: d = get_dialog(fmt)(parent, db) if d.exec_() != QDialog.Accepted: - others = [] + others, user_mi = [], None else: opts = d.opts opts.verbose = 2 + user_mi = d.user_mi if comics: comic_opts = ComicConf.get_bulk_conversion_options(parent) if not comic_opts: @@ -171,7 +172,7 @@ def convert_bulk(fmt, parent, db, comics, others): if total == 0: return parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) - + for i, row in enumerate(others+comics): row_id = db.id(row) if row in others: @@ -188,6 +189,11 @@ def convert_bulk(fmt, parent, db, comics, others): continue options = opts.copy() mi = db.get_metadata(row) + if user_mi is not None: + if user_mi.series_index == 1: + user_mi.series_index = None + mi.smart_update(user_mi) + db.set_metadata(db.id(row), mi) opf = OPFCreator(os.getcwdu(), mi) opf_file = PersistentTemporaryFile('.opf') opf.render(opf_file) @@ -223,10 +229,10 @@ def convert_bulk(fmt, parent, db, comics, others): try: data = db.format(row, _fmt.upper()) if data is not None: - break + break except: continue - + pt = PersistentTemporaryFile('.'+_fmt.lower()) pt.write(data) pt.close() @@ -236,17 +242,17 @@ def convert_bulk(fmt, parent, db, comics, others): options.verbose = 1 args = [pt.name, options] desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) - jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of])) - + jobs.append(('comic2'+fmt, args, desc, fmt.upper(), row_id, [pt, of])) + if bad_rows: res = [] for row in bad_rows: title = db.title(row) res.append('
  • %s
  • '%title) - + msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

      %s
    ')%(len(res), total, '\n'.join(res)) warning_dialog(parent, _('Could not convert some books'), msg).exec_() - + return jobs, False @@ -265,7 +271,7 @@ def convert_bulk_lrf(parent, db, comics, others): if total == 0: return parent.status_bar.showMessage(_('Starting Bulk conversion of %d books')%total, 2000) - + for i, row in enumerate(others+comics): row_id = db.id(row) if row in others: @@ -320,10 +326,10 @@ def convert_bulk_lrf(parent, db, comics, others): try: data = db.format(row, fmt.upper()) if data is not None: - break + break except: continue - + pt = PersistentTemporaryFile('.'+fmt.lower()) pt.write(data) pt.close() @@ -333,17 +339,17 @@ def convert_bulk_lrf(parent, db, comics, others): options.verbose = 1 args = [pt.name, options] desc = _('Convert book %d of %d (%s)')%(i+1, total, repr(mi.title)) - jobs.append(('comic2lrf', args, desc, 'LRF', row_id, [pt, of])) - + jobs.append(('comic2lrf', args, desc, 'LRF', row_id, [pt, of])) + if bad_rows: res = [] for row in bad_rows: title = db.title(row) res.append('
  • %s
  • '%title) - + msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

      %s
    ')%(len(res), total, '\n'.join(res)) warning_dialog(parent, _('Could not convert some books'), msg).exec_() - + return jobs, False def set_conversion_defaults_lrf(comic, parent, db): @@ -370,7 +376,7 @@ def _fetch_news(data, fmt): args.extend(['--password', data['password']]) args.append(data['script'] if data['script'] else data['title']) return 'feeds2'+fmt.lower(), [args], _('Fetch news from ')+data['title'], fmt.upper(), [pt] - + def fetch_scheduled_recipe(recipe, script): from calibre.gui2.dialogs.scheduler import config @@ -385,7 +391,7 @@ def fetch_scheduled_recipe(recipe, script): args.extend(['--username', x[0], '--password', x[1]]) args.append(script) return 'feeds2'+fmt, [args], _('Fetch news from ')+recipe.title, fmt.upper(), [pt] - + def convert_single_ebook(*args): fmt = prefs['output_format'].lower() @@ -393,14 +399,14 @@ def convert_single_ebook(*args): return convert_single_lrf(*args) elif fmt in ('epub', 'mobi'): return convert_single(fmt, *args) - + def convert_bulk_ebooks(*args): fmt = prefs['output_format'].lower() if fmt == 'lrf': return convert_bulk_lrf(*args) elif fmt in ('epub', 'mobi'): return convert_bulk(fmt, *args) - + def set_conversion_defaults(comic, parent, db): fmt = prefs['output_format'].lower() if fmt == 'lrf': @@ -410,4 +416,4 @@ def set_conversion_defaults(comic, parent, db): def fetch_news(data): fmt = prefs['output_format'].lower() - return _fetch_news(data, fmt) \ No newline at end of file + return _fetch_news(data, fmt) diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py index 6cec55c471..1bdb7d4f60 100644 --- a/src/calibre/library/__init__.py +++ b/src/calibre/library/__init__.py @@ -15,19 +15,19 @@ def title_sort(title): def server_config(defaults=None): desc=_('Settings to control the calibre content server') c = Config('server', desc) if defaults is None else StringConfig(defaults, desc) - - c.add_opt('port', ['-p', '--port'], default=8080, + + c.add_opt('port', ['-p', '--port'], default=8080, help=_('The port on which to listen. Default is %default')) - c.add_opt('timeout', ['-t', '--timeout'], default=120, + c.add_opt('timeout', ['-t', '--timeout'], default=120, help=_('The server timeout in seconds. Default is %default')) - c.add_opt('thread_pool', ['--thread-pool'], default=30, + c.add_opt('thread_pool', ['--thread-pool'], default=30, help=_('The max number of worker threads to use. Default is %default')) - c.add_opt('password', ['--password'], default=None, + c.add_opt('password', ['--password'], default=None, help=_('Set a password to restrict access. By default access is unrestricted.')) c.add_opt('username', ['--username'], default='calibre', help=_('Username for access. By default, it is: %default')) c.add_opt('develop', ['--develop'], default=False, help='Development mode. Server automatically restarts on file changes and serves code files (html, css, js) from the file system instead of calibre\'s resource system.') - c.add_opt('max_cover', ['--max-cover'], default='600x800', + c.add_opt('max_cover', ['--max-cover'], default='600x800', help=_('The maximum size for displayed covers. Default is %default.')) return c From 3c70e352df9eb25eb23ab6ae6718245d83fa857f Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 11 Apr 2009 22:04:25 -0400 Subject: [PATCH 05/27] Fix bug #2263 for mobi files. --- src/calibre/ebooks/mobi/reader.py | 109 ++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 22 deletions(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 07b8e153ba..65ff86173f 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -15,7 +15,8 @@ except ImportError: from lxml import html, etree -from calibre import entity_to_unicode +from calibre import entity_to_unicode, sanitize_file_name +from calibre.ptempfile import TemporaryDirectory from calibre.ebooks import DRMError from calibre.ebooks.chardet import ENCODING_PATS from calibre.ebooks.mobi import MobiError @@ -25,7 +26,6 @@ from calibre.ebooks.mobi.langcodes import main_language, sub_language from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.ebooks.metadata.toc import TOC -from calibre import sanitize_file_name class EXTHHeader(object): @@ -154,6 +154,62 @@ class BookHeader(object): self.exth.mi.language = self.language +class MetadataHeader(BookHeader): + def __init__(self, stream): + self.stream = stream + + self.ident = self.identity() + self.num_sections = self.section_count() + + if self.num_sections >= 2: + header = self.header() + BookHeader.__init__(self, header, self.ident, None) + else: + self.exth = None + + def identity(self): + self.stream.seek(60) + ident = self.stream.read(8).upper() + + if ident not in ['BOOKMOBI', 'TEXTREAD']: + raise MobiError('Unknown book type: %s' % ident) + return ident + + def section_count(self): + self.stream.seek(76) + return struct.unpack('>H', self.stream.read(2))[0] + + def section_offset(self, number): + self.stream.seek(78+number*8) + return struct.unpack('>LBBBB', self.stream.read(8))[0] + + def header(self): + section_headers = [] + + # First section with the metadata + section_headers.append(self.section_offset(0)) + # Second section used to get the lengh of the first + section_headers.append(self.section_offset(1)) + + end_off = section_headers[1] + off = section_headers[0] + + self.stream.seek(off) + return self.stream.read(end_off - off) + + def section_data(self, number): + start = self.section_offset(number) + + if number == self.num_sections -1: + end = os.stat(self.stream.name).st_size + else: + end = self.section_offset(number + 1) + + self.stream.seek(start) + + return self.stream.read(end - start) + + class MobiReader(object): PAGE_BREAK_PAT = re.compile(r'(<[/]{0,1}mbp:pagebreak\s*[/]{0,1}>)+', re.IGNORECASE) IMAGE_ATTRS = ('lowrecindex', 'recindex', 'hirecindex') @@ -562,26 +618,35 @@ class MobiReader(object): self.image_names.append(os.path.basename(path)) im.convert('RGB').save(open(path, 'wb'), format='JPEG') -def get_metadata(stream): - mr = MobiReader(stream) - if mr.book_header.exth is None: - mi = MetaInformation(mr.name, [_('Unknown')]) - else: - mi = mr.create_opf('dummy.html') - try: - if hasattr(mr.book_header.exth, 'cover_offset'): - cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset - data = mr.sections[int(cover_index)][0] - else: - data = mr.sections[mr.book_header.first_image_index][0] - buf = cStringIO.StringIO(data) - im = PILImage.open(buf) - obuf = cStringIO.StringIO() - im.convert('RGBA').save(obuf, format='JPEG') - mi.cover_data = ('jpg', obuf.getvalue()) - except: - import traceback - traceback.print_exc() +def get_metadata(stream): + mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) + try: + mh = MetadataHeader(stream) + + if mh.exth is not None: + if mh.exth.mi is not None: + mi = mh.exth.mi + else: + with TemporaryDirectory('_mobi_meta_reader') as tdir: + mr = MobiReader(stream) + mr.extract_content(tdir) + if mr.embedded_mi is not None: + mi = mr.embedded_mi + + if hasattr(mh.exth, 'cover_offset'): + cover_index = mh.first_image_index + mh.exth.cover_offset + data = mh.section_data(int(cover_index)) + else: + data = mh.section_data(mh.first_image_index) + buf = cStringIO.StringIO(data) + im = PILImage.open(buf) + obuf = cStringIO.StringIO() + im.convert('RGBA').save(obuf, format='JPEG') + mi.cover_data = ('jpg', obuf.getvalue()) + except: + import traceback + traceback.print_exc() + return mi From a9b6393b137520d0c86ddf377ae502527d8d005c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 11 Apr 2009 19:13:34 -0700 Subject: [PATCH 06/27] IGN:... --- src/calibre/gui2/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index de11366b3b..0ef4191b84 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -619,7 +619,9 @@ class DeviceGUI(object): bad = '\n'.join('
  • %s
  • '%(i,) for i in bad) d = warning_dialog(self, _('No suitable formats'), _('Could not upload the following books to the device, ' - 'as no suitable formats were found:
      %s
    ')%(bad,)) + 'as no suitable formats were found. Try changing the output ' + 'format in the upper right corner next to the red heart and ' + 're-converting.
      %s
    ')%(bad,)) d.exec_() def upload_booklists(self): From 0a9f038a4363ab123fc6832f8dabd857037c8723 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 12 Apr 2009 10:12:52 -0400 Subject: [PATCH 07/27] PDF metadata writer plugin enabled and working. --- src/calibre/customize/builtins.py | 11 ++++++++++ src/calibre/ebooks/metadata/pdf.py | 33 +++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index a087e7f36d..d4470b16fd 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -251,6 +251,17 @@ class MOBIMetadataWriter(MetadataWriterPlugin): def set_metadata(self, stream, mi, type): from calibre.ebooks.metadata.mobi import set_metadata set_metadata(stream, mi) + +class PDFMetadataWriter(MetadataWriterPlugin): + + name = 'Set PDF metadata' + file_types = set(['pdf']) + description = _('Set metadata in %s files') % 'PDF' + author = 'John Schember' + + def set_metadata(self, stream, mi, type): + from calibre.ebooks.metadata.pdf import set_metadata + set_metadata(stream, mi) plugins = [HTML2ZIP] diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py index ad59351248..80cdc82070 100644 --- a/src/calibre/ebooks/metadata/pdf.py +++ b/src/calibre/ebooks/metadata/pdf.py @@ -2,10 +2,10 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' '''Read meta information from PDF files''' -import sys, os, re +import sys, os, StringIO from calibre.ebooks.metadata import MetaInformation, authors_to_string, get_parser -from pyPdf import PdfFileReader +from pyPdf import PdfFileReader, PdfFileWriter def get_metadata(stream): """ Return metadata as a L{MetaInfo} object """ @@ -31,18 +31,27 @@ def get_metadata(stream): def set_metadata(stream, mi): stream.seek(0) - raw = stream.read() - if mi.title: - tit = mi.title.encode('utf-8') if isinstance(mi.title, unicode) else mi.title - raw = re.compile(r'<<.*?/Title\((.+?)\)', re.DOTALL).sub(lambda m: m.group().replace(m.group(1), tit), raw) - if mi.authors: - au = authors_to_string(mi.authors) - if isinstance(au, unicode): - au = au.encode('utf-8') - raw = re.compile(r'<<.*?/Author\((.+?)\)', re.DOTALL).sub(lambda m: m.group().replace(m.group(1), au), raw) + + # Use a StringIO object for the pdf because we will want to over + # write it later and if we are working on the stream directly it + # could cause some issues. + raw = StringIO.StringIO(stream.read()) + orig_pdf = PdfFileReader(raw) + + title = mi.title if mi.title else orig_pdf.documentInfo.title + author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author + + out_pdf = PdfFileWriter(title=title, author=author) + for page in orig_pdf.pages: + out_pdf.addPage(page) + + out_str = StringIO.StringIO() + out_pdf.write(out_str) + stream.seek(0) stream.truncate() - stream.write(raw) + out_str.seek(0) + stream.write(out_str.read()) stream.seek(0) def option_parser(): From 270806587084d835a391984abbc35576ecded919 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Apr 2009 10:25:17 -0700 Subject: [PATCH 08/27] Implement a --add-simple-plugin option for calibre-debug that makes it easy to add calibre plugins distributed as .py files --- src/calibre/customize/ui.py | 67 ++++++++++++++++++++----------------- src/calibre/debug.py | 23 +++++++++++++ 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 7374eaba11..e19c17a169 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -2,7 +2,7 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, shutil, traceback, functools, sys +import os, shutil, traceback, functools, sys, re from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \ MetadataWriterPlugin @@ -29,7 +29,7 @@ def _config(): c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins')) c.add_opt('plugin_customization', default={}, help=_('Local plugin customization')) c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins')) - + return ConfigProxy(c) config = _config() @@ -44,7 +44,7 @@ class PluginNotFound(ValueError): def load_plugin(path_to_zip_file): ''' Load plugin from zip file or raise InvalidPlugin error - + :return: A :class:`Plugin` instance. ''' print 'Loading plugin from', path_to_zip_file @@ -54,15 +54,22 @@ def load_plugin(path_to_zip_file): for name in zf.namelist(): if name.lower().endswith('plugin.py'): locals = {} - exec zf.read(name) in locals + raw = zf.read(name) + match = re.search(r'coding[:=]\s*([-\w.]+)', raw[:300]) + encoding = 'utf-8' + if match is not None: + encoding = match.group(1) + raw = raw.decode(encoding) + raw = re.sub('\r\n', '\n', raw) + exec raw in locals for x in locals.values(): if isinstance(x, type) and issubclass(x, Plugin): if x.minimum_calibre_version > version or \ platform not in x.supported_platforms: continue - + return x - + raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file) _initialized_plugins = [] @@ -112,10 +119,10 @@ def reread_metadata_plugins(): for ft in plugin.file_types: if not _metadata_writers.has_key(ft): _metadata_writers[ft] = [] - _metadata_writers[ft].append(plugin) - - - + _metadata_writers[ft].append(plugin) + + + def get_file_type_metadata(stream, ftype): mi = MetaInformation(None, None) ftype = ftype.lower().strip() @@ -141,21 +148,21 @@ def set_file_type_metadata(stream, mi, ftype): plugin.set_metadata(stream, mi, ftype.lower().strip()) break except: - print 'Failed to set metadata for', repr(getattr(mi, 'title', '')) + print 'Failed to set metadata for', repr(getattr(mi, 'title', '')) traceback.print_exc() - - + + def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'): - occasion = {'import':_on_import, 'preprocess':_on_preprocess, + occasion = {'import':_on_import, 'preprocess':_on_preprocess, 'postprocess':_on_postprocess}[occasion] customization = config['plugin_customization'] if ft is None: - ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '') + ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '') nfp = path_to_file for plugin in occasion.get(ft, []): if is_disabled(plugin): continue - plugin.site_customization = customization.get(plugin.name, '') + plugin.site_customization = customization.get(plugin.name, '') with plugin: try: nfp = plugin.run(path_to_file) @@ -168,13 +175,13 @@ def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'): nfp = path_to_file return nfp -run_plugins_on_import = functools.partial(_run_filetype_plugins, +run_plugins_on_import = functools.partial(_run_filetype_plugins, occasion='import') -run_plugins_on_preprocess = functools.partial(_run_filetype_plugins, +run_plugins_on_preprocess = functools.partial(_run_filetype_plugins, occasion='preprocess') -run_plugins_on_postprocess = functools.partial(_run_filetype_plugins, +run_plugins_on_postprocess = functools.partial(_run_filetype_plugins, occasion='postprocess') - + def initialize_plugin(plugin, path_to_zip_file): try: @@ -184,7 +191,7 @@ def initialize_plugin(plugin, path_to_zip_file): tb = traceback.format_exc() raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:') %tb) + '\n'+tb) - + def add_plugin(path_to_zip_file): make_config_dir() @@ -252,21 +259,21 @@ def initialize_plugins(): except: print 'Failed to initialize plugin...' traceback.print_exc() - _initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True) + _initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True) reread_filetype_plugins() reread_metadata_plugins() - + initialize_plugins() def option_parser(): parser = OptionParser(usage=_('''\ %prog options - + Customize calibre by loading external plugins. ''')) - parser.add_option('-a', '--add-plugin', default=None, + parser.add_option('-a', '--add-plugin', default=None, help=_('Add a plugin by specifying the path to the zip file containing it.')) - parser.add_option('-r', '--remove-plugin', default=None, + parser.add_option('-r', '--remove-plugin', default=None, help=_('Remove a custom plugin by name. Has no effect on builtin plugins')) parser.add_option('--customize-plugin', default=None, help=_('Customize plugin. Specify name of plugin and customization string separated by a comma.')) @@ -320,16 +327,16 @@ def main(args=sys.argv): print for plugin in initialized_plugins(): print fmt%( - plugin.type, plugin.name, - plugin.version, is_disabled(plugin), + plugin.type, plugin.name, + plugin.version, is_disabled(plugin), plugin_customization(plugin) ) print '\t', plugin.description if plugin.is_customizable(): print '\t', plugin.customization_help() print - + return 0 - + if __name__ == '__main__': sys.exit(main()) diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 45ce9987e0..6444eaa691 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -31,6 +31,11 @@ Run an embedded python interpreter. parser.add_option('--migrate', action='store_true', default=False, help='Migrate old database. Needs two arguments. Path ' 'to library1.db and path to new library folder.') + parser.add_option('--add-simple-plugin', default=None, + help='Add a simple plugin (i.e. a plugin that consists of only a ' + '.py file), by specifying the path to the py file containing the ' + 'plugin code.') + return parser def update_zipfile(zipfile, mod, path): @@ -115,6 +120,22 @@ def debug_device_driver(): print 'Total space:', d.total_space() break +def add_simple_plugin(path_to_plugin): + import tempfile, zipfile, shutil + tdir = tempfile.mkdtemp() + open(os.path.join(tdir, 'custom_plugin.py'), + 'wb').write(open(path_to_plugin, 'rb').read()) + odir = os.getcwd() + os.chdir(tdir) + zf = zipfile.ZipFile('plugin.zip', 'w') + zf.write('custom_plugin.py') + zf.close() + from calibre.customize.ui import main + main(['calibre-customize', '-a', 'plugin.zip']) + os.chdir(odir) + shutil.rmtree(tdir) + + def main(args=sys.argv): opts, args = option_parser().parse_args(args) @@ -137,6 +158,8 @@ def main(args=sys.argv): print 'You must specify the path to library1.db and the path to the new library folder' return 1 migrate(args[1], args[2]) + elif opts.add_simple_plugin is not None: + add_simple_plugin(opts.add_simple_plugin) else: from IPython.Shell import IPShellEmbed ipshell = IPShellEmbed() From a423691dd52475d8de2b0227b357fcf3fdbb68f1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 12 Apr 2009 12:09:38 -0700 Subject: [PATCH 09/27] Initial (untested) port of splitting code to OEBBook --- src/calibre/ebooks/oeb/base.py | 32 +- src/calibre/ebooks/oeb/iterator.py | 1 - src/calibre/ebooks/oeb/output.py | 12 +- .../ebooks/{epub => oeb/transforms}/split.py | 556 ++++++++---------- 4 files changed, 280 insertions(+), 321 deletions(-) rename src/calibre/ebooks/{epub => oeb/transforms}/split.py (51%) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 76a6648e8d..ed7981df4f 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -272,11 +272,26 @@ def XPath(expr): def xpath(elem, expr): return elem.xpath(expr, namespaces=XPNSMAP) -def xml2str(root, pretty_print=False): - return etree.tostring(root, encoding='utf-8', xml_declaration=True, +def _prepare_xml_for_serialization(root): + root.set('xmlns', XHTML_NS) + root.set('{%s}xlink'%XHTML_NS, XLINK_NS) + for x in root.iter(): + if hasattr(x.tag, 'rpartition') and x.tag.rpartition('}')[-1].lower() == 'svg': + x.set('xmlns', SVG_NS) + +def xml2str(root, pretty_print=False, strip_comments=False): + _prepare_xml_for_serialization(root) + ans = etree.tostring(root, encoding='utf-8', xml_declaration=True, pretty_print=pretty_print) + if strip_comments: + ans = re.compile(r'', re.DOTALL).sub('', ans) + + return ans + + def xml2unicode(root, pretty_print=False): + _prepare_xml_for_serialization(root) return etree.tostring(root, pretty_print=pretty_print) ASCII_CHARS = set(chr(x) for x in xrange(128)) @@ -826,6 +841,11 @@ class Manifest(object): return xml2str(data, pretty_print=self.oeb.pretty_print) if isinstance(data, unicode): return data.encode('utf-8') + if hasattr(data, 'cssText'): + data = data.cssText + if isinstance(data, unicode): + data = data.encode('utf-8') + return data return str(data) def __unicode__(self): @@ -834,6 +854,8 @@ class Manifest(object): return xml2unicode(data, pretty_print=self.oeb.pretty_print) if isinstance(data, unicode): return data + if hasattr(data, 'cssText'): + return data.cssText return unicode(data) def __eq__(self, other): @@ -1044,6 +1066,12 @@ class Spine(object): self.items[i].spine_position = i item.spine_position = None + def index(self, item): + for i, x in enumerate(self): + if item == x: + return i + return -1 + def __iter__(self): for item in self.items: yield item diff --git a/src/calibre/ebooks/oeb/iterator.py b/src/calibre/ebooks/oeb/iterator.py index ec0eda908a..8672d42e2b 100644 --- a/src/calibre/ebooks/oeb/iterator.py +++ b/src/calibre/ebooks/oeb/iterator.py @@ -162,7 +162,6 @@ class EbookIterator(object): s.pages = p start = 1 - for s in self.spine: s.start_page = start start += s.pages diff --git a/src/calibre/ebooks/oeb/output.py b/src/calibre/ebooks/oeb/output.py index ea986f49fa..480ca3776e 100644 --- a/src/calibre/ebooks/oeb/output.py +++ b/src/calibre/ebooks/oeb/output.py @@ -22,7 +22,6 @@ class OEBOutput(OutputFormatPlugin): if not os.path.exists(output_path): os.makedirs(output_path) from calibre.ebooks.oeb.base import OPF_MIME, NCX_MIME, PAGE_MAP_MIME - from calibre.ebooks.html import tostring as html_tostring with CurrentDir(output_path): results = oeb_book.to_opf2(page_map=True) for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME): @@ -38,16 +37,7 @@ class OEBOutput(OutputFormatPlugin): dir = os.path.dirname(path) if not os.path.exists(dir): os.makedirs(dir) - raw = item.data - if not isinstance(raw, basestring): - if hasattr(raw, 'cssText'): - raw = raw.cssText - else: - raw = html_tostring(raw, - pretty_print=opts.pretty_print) - if isinstance(raw, unicode): - raw = raw.encode('utf-8') with open(path, 'wb') as f: - f.write(raw) + f.write(str(item)) diff --git a/src/calibre/ebooks/epub/split.py b/src/calibre/ebooks/oeb/transforms/split.py similarity index 51% rename from src/calibre/ebooks/epub/split.py rename to src/calibre/ebooks/oeb/transforms/split.py index 8ff62a1c4b..20205e9c6d 100644 --- a/src/calibre/ebooks/epub/split.py +++ b/src/calibre/ebooks/oeb/transforms/split.py @@ -4,21 +4,25 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' ''' -Split the flows in an epub file to conform to size limitations. +Splitting of the XHTML flows. Splitting can happen on page boundaries or can be +forces at "likely" locations to conform to size limitations. This transform +assumes a prior call to the flatcss transform. ''' -import os, math, functools, collections, re, copy, sys +import os, math, functools, collections, re, copy from lxml.etree import XPath as _XPath from lxml import etree, html from lxml.cssselect import CSSSelector -from calibre.ebooks.metadata.opf2 import OPF +from calibre.ebooks.oeb.base import OEB_STYLES, XPNSMAP, urldefrag, \ + rewrite_links from calibre.ebooks.epub import tostring, rules -from calibre import CurrentDir -XPath = functools.partial(_XPath, namespaces={'re':'http://exslt.org/regular-expressions'}) -content = functools.partial(os.path.join, 'content') +NAMESPACES = dict(XPNSMAP) +NAMESPACES['re'] = 'http://exslt.org/regular-expressions' + +XPath = functools.partial(_XPath, namespaces=NAMESPACES) SPLIT_ATTR = 'cs' SPLIT_POINT_ATTR = 'csp' @@ -27,149 +31,166 @@ class SplitError(ValueError): def __init__(self, path, root): size = len(tostring(root))/1024. - ValueError.__init__(self, _('Could not find reasonable point at which to split: %s Sub-tree size: %d KB')% - (os.path.basename(path), size)) + ValueError.__init__(self, + _('Could not find reasonable point at which to split: ' + '%s Sub-tree size: %d KB')% + (path, size)) + +class Split(object): + + def __init__(self, split_on_page_breaks=True, page_breaks_xpath=None, + max_flow_size=0): + self.split_on_page_breaks = split_on_page_breaks + self.page_breaks_xpath = page_breaks_xpath + self.max_flow_size = max_flow_size + if self.page_breaks_xpath is not None: + self.page_breaks_xpath = XPath(self.page_breaks_xpath) + + def __call__(self, oeb, context): + self.oeb = oeb + self.log = oeb.log + self.map = {} + self.page_break_selectors = None + for item in self.oeb.manifest.items: + if etree.iselement(item.data): + self.split_item(item) + + self.fix_links() + + def split_item(self, item): + if self.split_on_page_breaks: + if self.page_breaks_xpath is None: + page_breaks, page_break_ids = self.find_page_breaks(item) + else: + page_breaks, page_break_ids = self.page_breaks_xpath(item.data) + + splitter = FlowSplitter(item, page_breaks, page_break_ids, + self.max_flow_size, self.oeb) + if splitter.was_split: + self.map[item.href] = dict(splitter.anchor_map) + + def find_page_breaks(self, item): + if self.page_break_selectors is None: + self.page_break_selectors = set([]) + stylesheets = [x.data for x in self.oeb.manifest if x.media_type in + OEB_STYLES] + page_break_selectors = set([]) + for rule in rules(stylesheets): + before = getattr(rule.style.getPropertyCSSValue( + 'page-break-before'), 'cssText', '').strip().lower() + after = getattr(rule.style.getPropertyCSSValue( + 'page-break-after'), 'cssText', '').strip().lower() + try: + if before and before != 'avoid': + page_break_selectors.add((CSSSelector(rule.selectorText), + True)) + except: + pass + try: + if after and after != 'avoid': + page_break_selectors.add((CSSSelector(rule.selectorText), + False)) + except: + pass + + page_breaks = set([]) + for selector, before in page_break_selectors: + for elem in selector(item.data): + elem.pb_before = before + page_breaks.add(elem) + + for i, elem in enumerate(item.data.iter()): + elem.pb_order = i + + page_breaks = list(page_breaks) + page_breaks.sort(cmp=lambda x,y : cmp(x.pb_order, y.pb_order)) + page_break_ids, page_breaks_ = [], [] + for i, x in enumerate(page_breaks): + x.set('id', x.get('id', 'calibre_pb_%d'%i)) + id = x.get('id') + page_breaks_.append((XPath('//*[@id="%s"]'%id), x.pb_before)) + page_break_ids.append(id) + + return page_breaks_, page_break_ids + + def fix_links(self, opf): + ''' + Fix references to the split files in other content files. + ''' + for item in self.oeb.manifest: + if etree.iselement(item.data): + self.current_item = item + rewrite_links(item.data, self.rewrite_links) + + def rewrite_links(self, url): + href, frag = urldefrag(url) + href = self.current_item.abshref(href) + if href in self.map: + anchor_map = self.map[href] + nhref = anchor_map[frag if frag else None] + if frag: + nhref = '#'.joinn(href, frag) + return nhref + return url -class Splitter(object): +class FlowSplitter(object): - def __init__(self, path, opts, stylesheet_map, opf): - self.setup_cli_handler(opts.verbose) - self.path = path - self.always_remove = not opts.preserve_tag_structure or \ - os.stat(content(path)).st_size > 5*opts.profile.flow_size - self.base = (os.path.splitext(path)[0].replace('%', '%%') + '_split_%d.html') - self.opts = opts - self.orig_size = os.stat(content(path)).st_size - self.log_info('\tSplitting %s (%d KB)', path, self.orig_size/1024.) - root = html.fromstring(open(content(path)).read()) + def __init__(self, item, page_breaks, page_break_ids, max_flow_size, oeb): + self.item = item + self.oeb = oeb + self.log = oeb.log + self.page_breaks = page_breaks + self.page_break_ids = page_break_ids + self.max_flow_size = max_flow_size + self.base = item.abshref(item.href) - self.page_breaks, self.trees = [], [] - self.split_size = 0 + base, ext = os.path.splitext(self.base) + self.base = base.replace('%', '%%')+'_split_%d'+ext - # Split on page breaks + self.trees = [self.item.data] self.splitting_on_page_breaks = True - if not opts.dont_split_on_page_breaks: - self.log_info('\tSplitting on page breaks...') - if self.path in stylesheet_map: - self.find_page_breaks(stylesheet_map[self.path], root) - self.split_on_page_breaks(root.getroottree()) - trees = list(self.trees) - else: - self.trees = [root.getroottree()] - trees = list(self.trees) - - # Split any remaining over-sized trees + if self.page_breaks: + self.split_on_page_breaks(self.item.data) self.splitting_on_page_breaks = False - if self.opts.profile.flow_size < sys.maxint: + + if self.max_flow_size > 0: lt_found = False - self.log_info('\tLooking for large trees...') - for i, tree in enumerate(list(trees)): + self.log('\tLooking for large trees...') + trees = list(self.trees) + for i, tree in enumerate(list(self.trees)): self.trees = [] size = len(tostring(tree.getroot())) if size > self.opts.profile.flow_size: lt_found = True - try: - self.split_to_size(tree) - except (SplitError, RuntimeError): # Splitting fails - if not self.always_remove: - self.always_remove = True - self.split_to_size(tree) - else: - raise + self.split_to_size(tree) trees[i:i+1] = list(self.trees) if not lt_found: self.log_info('\tNo large trees found') + self.trees = trees - self.trees = trees self.was_split = len(self.trees) > 1 - if self.was_split: - self.commit() - self.log_info('\t\tSplit into %d parts.', len(self.trees)) - if self.opts.verbose: - for f in self.files: - self.log_info('\t\t\t%s - %d KB', f, os.stat(content(f)).st_size/1024.) - self.fix_opf(opf) + self.commit() - self.trees = None + def split_on_page_breaks(self, orig_tree): + ordered_ids = [] + for elem in orig_tree.xpath('//*[@id]'): + id = elem.get('id') + if id in self.page_break_ids: + ordered_ids.append(self.page_breaks[self.page_break_ids.index(id)]) - - def split_text(self, text, root, size): - self.log_debug('\t\t\tSplitting text of length: %d'%len(text)) - rest = text.replace('\r', '') - parts = re.split('\n\n', rest) - self.log_debug('\t\t\t\tFound %d parts'%len(parts)) - if max(map(len, parts)) > size: - raise SplitError('Cannot split as file contains a
     tag with a very large paragraph', root)
    -        ans = []
    -        buf = ''
    -        for part in parts:
    -            if len(buf) + len(part) < size:
    -                buf += '\n\n'+part
    -            else:
    -                ans.append(buf)
    -                buf = part
    -        return ans
    -
    -
    -    def split_to_size(self, tree):
    -        self.log_debug('\t\tSplitting...')
    -        root = tree.getroot()
    -        # Split large 
     tags
    -        for pre in list(root.xpath('//pre')):
    -            text = u''.join(pre.xpath('descendant::text()'))
    -            pre.text = text
    -            for child in list(pre.iterchildren()):
    -                pre.remove(child)
    -            if len(pre.text) > self.opts.profile.flow_size*0.5:
    -                frags = self.split_text(pre.text, root, int(0.2*self.opts.profile.flow_size))
    -                new_pres = []
    -                for frag in frags:
    -                    pre2 = copy.copy(pre)
    -                    pre2.text = frag
    -                    pre2.tail = u''
    -                    new_pres.append(pre2)
    -                new_pres[-1].tail = pre.tail
    -                p = pre.getparent()
    -                i = p.index(pre)
    -                p[i:i+1] = new_pres
    -
    -        split_point, before = self.find_split_point(root)
    -        if split_point is None or self.split_size > 6*self.orig_size:
    -            if not self.always_remove:
    -                self.log_warn(_('\t\tToo much markup. Re-splitting without '
    -                                'structure preservation. This may cause '
    -                                'incorrect rendering.'))
    -            raise SplitError(self.path, root)
    -
    -        for t in self.do_split(tree, split_point, before):
    -            r = t.getroot()
    -            if self.is_page_empty(r):
    -                continue
    -            size = len(tostring(r))
    -            if size <= self.opts.profile.flow_size:
    -                self.trees.append(t)
    -                #print tostring(t.getroot(), pretty_print=True)
    -                self.log_debug('\t\t\tCommitted sub-tree #%d (%d KB)',
    -                               len(self.trees), size/1024.)
    -                self.split_size += size
    -            else:
    -                self.split_to_size(t)
    -
    -    def is_page_empty(self, root):
    -        body = root.find('body')
    -        if body is None:
    -            return False
    -        txt = re.sub(r'\s+', '', html.tostring(body, method='text', encoding=unicode))
    -        if len(txt) > 4:
    -            #if len(txt) < 100:
    -            #    print 1111111, html.tostring(body, method='html', encoding=unicode)
    -            return False
    -        for img in root.xpath('//img'):
    -            if img.get('style', '') != 'display:none':
    -                return False
    -        return True
    +        self.trees = []
    +        tree = orig_tree
    +        for pattern, before in ordered_ids:
    +            self.log.debug('\t\tSplitting on page-break')
    +            elem = pattern(tree)
    +            if elem:
    +                before, after = self.do_split(tree, elem[0], before)
    +                self.trees.append(before)
    +                tree = after
    +        self.trees.append(tree)
    +        self.trees = [t for t in self.trees if not self.is_page_empty(t.getroot())]
     
         def do_split(self, tree, split_point, before):
             '''
    @@ -190,7 +211,7 @@ class Splitter(object):
             split_point2 = root2.xpath(path)[0]
     
             def nix_element(elem, top=True):
    -            if self.always_remove:
    +            if True:
                     parent = elem.getparent()
                     index = parent.index(elem)
                     if top:
    @@ -198,7 +219,6 @@ class Splitter(object):
                     else:
                         index = parent.index(elem)
                         parent[index:index+1] = list(elem.iterchildren())
    -
                 else:
                     elem.text = u''
                     elem.tail = u''
    @@ -241,67 +261,76 @@ class Splitter(object):
     
             return tree, tree2
     
    +    def is_page_empty(self, root):
    +        body = root.find('body')
    +        if body is None:
    +            return False
    +        txt = re.sub(r'\s+', '', html.tostring(body, method='text', encoding=unicode))
    +        if len(txt) > 4:
    +            return False
    +        for img in root.xpath('//img'):
    +            if img.get('style', '') != 'display:none':
    +                return False
    +        return True
     
    -    def split_on_page_breaks(self, orig_tree):
    -        ordered_ids = []
    -        for elem in orig_tree.xpath('//*[@id]'):
    -            id = elem.get('id')
    -            if id in self.page_break_ids:
    -                ordered_ids.append(self.page_breaks[self.page_break_ids.index(id)])
    -
    -        self.trees = []
    -        tree = orig_tree
    -        for pattern, before in ordered_ids:
    -            self.log_info('\t\tSplitting on page-break')
    -            elem = pattern(tree)
    -            if elem:
    -                before, after = self.do_split(tree, elem[0], before)
    -                self.trees.append(before)
    -                tree = after
    -        self.trees.append(tree)
    -        self.trees = [t for t in self.trees if not self.is_page_empty(t.getroot())]
    +    def split_text(self, text, root, size):
    +        self.log.debug('\t\t\tSplitting text of length: %d'%len(text))
    +        rest = text.replace('\r', '')
    +        parts = re.split('\n\n', rest)
    +        self.log.debug('\t\t\t\tFound %d parts'%len(parts))
    +        if max(map(len, parts)) > size:
    +            raise SplitError('Cannot split as file contains a 
     tag '
    +                'with a very large paragraph', root)
    +        ans = []
    +        buf = ''
    +        for part in parts:
    +            if len(buf) + len(part) < size:
    +                buf += '\n\n'+part
    +            else:
    +                ans.append(buf)
    +                buf = part
    +        return ans
     
     
    +    def split_to_size(self, tree):
    +        self.log.debug('\t\tSplitting...')
    +        root = tree.getroot()
    +        # Split large 
     tags
    +        for pre in list(root.xpath('//pre')):
    +            text = u''.join(pre.xpath('descendant::text()'))
    +            pre.text = text
    +            for child in list(pre.iterchildren()):
    +                pre.remove(child)
    +            if len(pre.text) > self.max_flow_size*0.5:
    +                frags = self.split_text(pre.text, root, int(0.2*self.max_flow_size))
    +                new_pres = []
    +                for frag in frags:
    +                    pre2 = copy.copy(pre)
    +                    pre2.text = frag
    +                    pre2.tail = u''
    +                    new_pres.append(pre2)
    +                new_pres[-1].tail = pre.tail
    +                p = pre.getparent()
    +                i = p.index(pre)
    +                p[i:i+1] = new_pres
     
    -    def find_page_breaks(self, stylesheets, root):
    -        '''
    -        Find all elements that have either page-break-before or page-break-after set.
    -        Populates `self.page_breaks` with id based XPath selectors (for elements that don't
    -        have ids, an id is created).
    -        '''
    -        page_break_selectors = set([])
    -        for rule in rules(stylesheets):
    -            before = getattr(rule.style.getPropertyCSSValue('page-break-before'), 'cssText', '').strip().lower()
    -            after  = getattr(rule.style.getPropertyCSSValue('page-break-after'), 'cssText', '').strip().lower()
    -            try:
    -                if before and before != 'avoid':
    -                    page_break_selectors.add((CSSSelector(rule.selectorText), True))
    -            except:
    -                pass
    -            try:
    -                if after and after != 'avoid':
    -                    page_break_selectors.add((CSSSelector(rule.selectorText), False))
    -            except:
    -                pass
    -
    -        page_breaks = set([])
    -        for selector, before in page_break_selectors:
    -            for elem in selector(root):
    -                elem.pb_before = before
    -                page_breaks.add(elem)
    -
    -        for i, elem in enumerate(root.iter()):
    -            elem.pb_order = i
    -
    -        page_breaks = list(page_breaks)
    -        page_breaks.sort(cmp=lambda x,y : cmp(x.pb_order, y.pb_order))
    -        self.page_break_ids = []
    -        for i, x in enumerate(page_breaks):
    -            x.set('id', x.get('id', 'calibre_pb_%d'%i))
    -            id = x.get('id')
    -            self.page_breaks.append((XPath('//*[@id="%s"]'%id), x.pb_before))
    -            self.page_break_ids.append(id)
    +        split_point, before = self.find_split_point(root)
    +        if split_point is None:
    +            raise SplitError(self.item.href, root)
     
    +        for t in self.do_split(tree, split_point, before):
    +            r = t.getroot()
    +            if self.is_page_empty(r):
    +                continue
    +            size = len(tostring(r))
    +            if size <= self.max_flow_size:
    +                self.trees.append(t)
    +                #print tostring(t.getroot(), pretty_print=True)
    +                self.log.debug('\t\t\tCommitted sub-tree #%d (%d KB)',
    +                               len(self.trees), size/1024.)
    +                self.split_size += size
    +            else:
    +                self.split_to_size(t)
     
         def find_split_point(self, root):
             '''
    @@ -336,8 +365,7 @@ class Splitter(object):
                          '//br',
                          '//li',
                          ):
    -            elems = root.xpath(path,
    -                    namespaces={'re':'http://exslt.org/regular-expressions'})
    +            elems = root.xpath(path, namespaces=NAMESPACES)
                 elem = pick_elem(elems)
                 if elem is not None:
                     try:
    @@ -355,6 +383,8 @@ class Splitter(object):
             all anchors in the original tree. Internal links are re-directed. The
             original file is deleted and the split files are saved.
             '''
    +        if not self.was_split:
    +            return
             self.anchor_map = collections.defaultdict(lambda :self.base%0)
             self.files = []
     
    @@ -368,134 +398,46 @@ class Splitter(object):
                     elem.attrib.pop(SPLIT_ATTR, None)
                     elem.attrib.pop(SPLIT_POINT_ATTR, '0')
     
    -        for current, tree in zip(self.files, self.trees):
    -            for a in tree.getroot().xpath('//a[@href]'):
    +        spine_pos = self.item.spine_pos
    +        for current, tree in zip(map(reversed, (self.files, self.trees))):
    +            for a in tree.getroot().xpath('//h:a[@href]', namespaces=NAMESPACES):
                     href = a.get('href').strip()
                     if href.startswith('#'):
                         anchor = href[1:]
                         file = self.anchor_map[anchor]
                         if file != current:
                             a.set('href', file+href)
    -            open(content(current), 'wb').\
    -                write(tostring(tree.getroot(), pretty_print=self.opts.pretty_print))
     
    -        os.remove(content(self.path))
    +            new_id = self.oeb.manifest.generate(id=self.item.id)[0]
    +            new_item = self.oeb.manifest.add(new_id, current,
    +                    self.item.media_type, data=tree.getroot())
    +            self.oeb.spine.insert(spine_pos, new_item, self.item.linear)
    +
    +        if self.oeb.guide:
    +            for ref in self.oeb.guide:
    +                href, frag = urldefrag(ref.href)
    +                if href == self.item.href:
    +                    nhref = self.anchor_map[frag if frag else None]
    +                    if frag:
    +                        nhref = '#'.join(nhref, frag)
    +                    ref.href = nhref
    +
    +        def fix_toc_entry(toc):
    +            if toc.href:
    +                href, frag = urldefrag(toc.href)
    +                if href == self.item.href:
    +                    nhref = self.anchor_map[frag if frag else None]
    +                    if frag:
    +                        nhref = '#'.join(nhref, frag)
    +                    toc.href = nhref
    +            for x in toc:
    +                fix_toc_entry(x)
     
     
    -    def fix_opf(self, opf):
    -        '''
    -        Fix references to the split file in the OPF.
    -        '''
    -        items = [item for item in opf.itermanifest() if item.get('href') == 'content/'+self.path]
    -        new_items = [('content/'+f, None) for f in self.files]
    -        id_map = {}
    -        for item in items:
    -            id_map[item.get('id')] = opf.replace_manifest_item(item, new_items)
    +        if self.oeb.toc:
    +            fix_toc_entry(self.oeb.toc)
     
    -        for id in id_map.keys():
    -            opf.replace_spine_items_by_idref(id, id_map[id])
    -
    -        for ref in opf.iterguide():
    -            href = ref.get('href', '')
    -            if href.startswith('content/'+self.path):
    -                href = href.split('#')
    -                frag = None
    -                if len(href) > 1:
    -                    frag = href[1]
    -                if frag not in self.anchor_map:
    -                    self.log_warning('\t\tUnable to re-map OPF link', href)
    -                    continue
    -                new_file = self.anchor_map[frag]
    -                ref.set('href', 'content/'+new_file+('' if frag is None else ('#'+frag)))
    +        self.oeb.manifest.remove(self.item)
     
     
     
    -def fix_content_links(html_files, changes, opts):
    -    split_files = [f.path for f in changes]
    -    anchor_maps = [f.anchor_map for f in changes]
    -    files = list(html_files)
    -    for j, f in enumerate(split_files):
    -        try:
    -            i = files.index(f)
    -            files[i:i+1] = changes[j].files
    -        except ValueError:
    -            continue
    -
    -    for htmlfile in files:
    -        changed = False
    -        root = html.fromstring(open(content(htmlfile), 'rb').read())
    -        for a in root.xpath('//a[@href]'):
    -            href = a.get('href')
    -            if not href.startswith('#'):
    -                href = href.split('#')
    -                anchor = href[1] if len(href) > 1 else None
    -                href = href[0]
    -                if href in split_files:
    -                    try:
    -                        newf = anchor_maps[split_files.index(href)][anchor]
    -                    except:
    -                        print '\t\tUnable to remap HTML link:', href, anchor
    -                        continue
    -                    frag = ('#'+anchor) if anchor else ''
    -                    a.set('href', newf+frag)
    -                    changed = True
    -
    -        if changed:
    -            open(content(htmlfile), 'wb').write(tostring(root, pretty_print=opts.pretty_print))
    -
    -def fix_ncx(path, changes):
    -    split_files = [f.path for f in changes]
    -    anchor_maps = [f.anchor_map for f in changes]
    -    tree = etree.parse(path)
    -    changed = False
    -    for content in tree.getroot().xpath('//x:content[@src]',
    -                    namespaces={'x':"http://www.daisy.org/z3986/2005/ncx/"}):
    -        href = content.get('src')
    -        if not href.startswith('#'):
    -            href = href.split('#')
    -            anchor = href[1] if len(href) > 1 else None
    -            href = href[0].split('/')[-1]
    -            if href in split_files:
    -                try:
    -                    newf = anchor_maps[split_files.index(href)][anchor]
    -                except:
    -                    print 'Unable to remap NCX link:', href, anchor
    -                frag = ('#'+anchor) if anchor else ''
    -                content.set('src', 'content/'+newf+frag)
    -                changed = True
    -    if changed:
    -        open(path, 'wb').write(etree.tostring(tree.getroot(), encoding='UTF-8', xml_declaration=True))
    -
    -def find_html_files(opf):
    -    '''
    -    Find all HTML files referenced by `opf`.
    -    '''
    -    html_files = []
    -    for item in opf.itermanifest():
    -        if 'html' in item.get('media-type', '').lower():
    -            f = item.get('href').split('/')[-1]
    -            f2 = f.replace('&', '%26')
    -            if not os.path.exists(content(f)) and os.path.exists(content(f2)):
    -                f = f2
    -                item.set('href', item.get('href').replace('&', '%26'))
    -            if os.path.exists(content(f)):
    -                html_files.append(f)
    -    return html_files
    -
    -
    -def split(pathtoopf, opts, stylesheet_map):
    -    pathtoopf = os.path.abspath(pathtoopf)
    -    opf = OPF(open(pathtoopf, 'rb'), os.path.dirname(pathtoopf))
    -
    -    with CurrentDir(os.path.dirname(pathtoopf)):
    -        html_files = find_html_files(opf)
    -        changes = [Splitter(f, opts, stylesheet_map, opf) for f in html_files]
    -        changes = [c for c in changes if c.was_split]
    -
    -        fix_content_links(html_files, changes, opts)
    -        for item in opf.itermanifest():
    -            if item.get('media-type', '') == 'application/x-dtbncx+xml':
    -                fix_ncx(item.get('href'), changes)
    -                break
    -
    -        open(pathtoopf, 'wb').write(opf.render())
    
    From b23b32a7ea3bbbc18edc32808852a626b6f17da4 Mon Sep 17 00:00:00 2001
    From: Kovid Goyal 
    Date: Sun, 12 Apr 2009 19:00:31 -0700
    Subject: [PATCH 10/27] Handle periods better when reading metadata from
     filenames
    
    ---
     src/calibre/ebooks/metadata/meta.py | 26 +++++++++++++-------------
     upload.py                           |  1 +
     2 files changed, 14 insertions(+), 13 deletions(-)
    
    diff --git a/src/calibre/ebooks/metadata/meta.py b/src/calibre/ebooks/metadata/meta.py
    index de7ac8eeea..a176c12c2b 100644
    --- a/src/calibre/ebooks/metadata/meta.py
    +++ b/src/calibre/ebooks/metadata/meta.py
    @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal '
     import os, re, collections
     
     from calibre.utils.config import prefs
    - 
    +
     from calibre.ebooks.metadata.opf2 import OPF
     
     from calibre.customize.ui import get_file_type_metadata, set_file_type_metadata
    @@ -37,18 +37,18 @@ def metadata_from_formats(formats):
             mi2 = opf_metadata(opf)
             if mi2 is not None and mi2.title:
                 return mi2
    -    
    +
         for path, ext in zip(formats, extensions):
             with open(path, 'rb') as stream:
                 try:
    -                newmi = get_metadata(stream, stream_type=ext, 
    +                newmi = get_metadata(stream, stream_type=ext,
                                          use_libprs_metadata=True)
                     mi.smart_update(newmi)
                 except:
                     continue
                 if getattr(mi, 'application_id', None) is not None:
                     return mi
    -    
    +
         if not mi.title:
             mi.title = _('Unknown')
         if not mi.authors:
    @@ -64,20 +64,20 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
             stream_type = 'mobi'
         if stream_type in ('odt', 'ods', 'odp', 'odg', 'odf'):
             stream_type = 'odt'
    -        
    +
         opf = None
         if hasattr(stream, 'name'):
             c = os.path.splitext(stream.name)[0]+'.opf'
             if os.access(c, os.R_OK):
                 opf = opf_metadata(os.path.abspath(c))
    -        
    +
         if use_libprs_metadata and getattr(opf, 'application_id', None) is not None:
             return opf
    -    
    +
         mi = MetaInformation(None, None)
         if prefs['read_file_metadata']:
             mi = get_file_type_metadata(stream, stream_type)
    -        
    +
         name = os.path.basename(getattr(stream, 'name', ''))
         base = metadata_from_filename(name)
         if base.title == os.path.splitext(name)[0] and base.authors is None:
    @@ -98,17 +98,17 @@ def get_metadata(stream, stream_type='lrf', use_libprs_metadata=False):
         base.smart_update(mi)
         if opf is not None:
             base.smart_update(opf)
    -        
    +
         return base
     
     def set_metadata(stream, mi, stream_type='lrf'):
         if stream_type:
             stream_type = stream_type.lower()
         set_file_type_metadata(stream, mi, stream_type)
    -    
    -    
    +
    +
     def metadata_from_filename(name, pat=None):
    -    name = os.path.splitext(name)[0]
    +    name = name.rpartition('.')[0]
         mi = MetaInformation(None, None)
         if pat is None:
             pat = re.compile(prefs.get('filename_pattern'))
    @@ -161,7 +161,7 @@ def opf_metadata(opfpath):
                 mi = MetaInformation(opf)
                 if hasattr(opf, 'cover') and opf.cover:
                     cpath = os.path.join(os.path.dirname(opfpath), opf.cover)
    -                if os.access(cpath, os.R_OK):                     
    +                if os.access(cpath, os.R_OK):
                         fmt = cpath.rpartition('.')[-1]
                         data = open(cpath, 'rb').read()
                         mi.cover_data = (fmt, data)
    diff --git a/upload.py b/upload.py
    index b2fc81c8b6..6bc90aada2 100644
    --- a/upload.py
    +++ b/upload.py
    @@ -530,6 +530,7 @@ class build_windows(VMInstaller):
             self.run_windows_install_jammer(installer)
             return os.path.basename(installer)
     
    +    @classmethod
         def run_windows_install_jammer(self, installer):
             ibp = os.path.abspath('installer/windows')
             sys.path.insert(0, ibp)
    
    From 29c232c6ce699df6c8dea9342b447968b1958fe0 Mon Sep 17 00:00:00 2001
    From: Kovid Goyal 
    Date: Mon, 13 Apr 2009 09:09:07 -0700
    Subject: [PATCH 11/27] New recipe for Der Standard by Gerhard Aigner
    
    ---
     src/calibre/web/feeds/recipes/__init__.py     |  2 +-
     .../web/feeds/recipes/recipe_der_standard.py  | 42 +++++++++++++++++++
     2 files changed, 43 insertions(+), 1 deletion(-)
     create mode 100644 src/calibre/web/feeds/recipes/recipe_der_standard.py
    
    diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py
    index 191bf905ca..ef9f58b003 100644
    --- a/src/calibre/web/feeds/recipes/__init__.py
    +++ b/src/calibre/web/feeds/recipes/__init__.py
    @@ -39,7 +39,7 @@ recipe_modules = ['recipe_' + r for r in (
                'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
                'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet',
                'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en',
    -           'moneynews',
    +           'moneynews', 'der_standard',
               )]
     
     import re, imp, inspect, time, os
    diff --git a/src/calibre/web/feeds/recipes/recipe_der_standard.py b/src/calibre/web/feeds/recipes/recipe_der_standard.py
    new file mode 100644
    index 0000000000..eec4c4e74d
    --- /dev/null
    +++ b/src/calibre/web/feeds/recipes/recipe_der_standard.py
    @@ -0,0 +1,42 @@
    +
    +''' http://www.derstandard.at - Austrian Newspaper '''
    +import re
    +from calibre.web.feeds.news import BasicNewsRecipe
    +
    +class DerStandardRecipe(BasicNewsRecipe):
    +    title          = u'derStandard'
    +    __author__  = 'Gerhard Aigner'
    +
    +    oldest_article = 1
    +    max_articles_per_feed = 100
    +    feeds          = [(u'International', u'http://derstandard.at/?page=rss&ressort=internationalpolitik'),
    +        (u'Inland', u'http://derstandard.at/?page=rss&ressort=innenpolitik'),
    +        (u'Wirtschaft', u'http://derstandard.at/?page=rss&ressort=investor'),
    +        (u'Web', u'http://derstandard.at/?page=rss&ressort=webstandard'),
    +        (u'Sport', u'http://derstandard.at/?page=rss&ressort=sport'),
    +        (u'Panorama', u'http://derstandard.at/?page=rss&ressort=panorama'),
    +        (u'Etat', u'http://derstandard.at/?page=rss&ressort=etat'),
    +        (u'Kultur', u'http://derstandard.at/?page=rss&ressort=kultur'),
    +        (u'Wissenschaft', u'http://derstandard.at/?page=rss&ressort=wissenschaft'),
    +        (u'Gesundheit', u'http://derstandard.at/?page=rss&ressort=gesundheit'),
    +        (u'Bildung', u'http://derstandard.at/?page=rss&ressort=subildung')]
    +
    +    encoding = 'utf-8'
    +    language = _('German')
    +    recursions = 0
    +    remove_tags = [dict(name='div'), dict(name='a'), dict(name='link'), dict(name='meta'),
    +        dict(name='form',attrs={'name':'sitesearch'}), dict(name='hr')]
    +    preprocess_regexps = [
    +        (re.compile(r'\[[\d*]\]', re.DOTALL|re.IGNORECASE), lambda match: ''),
    +        (re.compile(r'bgcolor="#\w{3,6}"', re.DOTALL|re.IGNORECASE), lambda match: '')
    +    ]
    +
    +    def print_version(self, url):
    +        return url.replace('?id=', 'txt/?id=')
    +
    +    def get_article_url(self, article):
    +        '''if the article links to a index page (ressort) or a picture gallery
    +           (ansichtssache), don't add it'''
    +        if (article.link.count('ressort') > 0 or article.title.lower().count('ansichtssache') > 0):
    +            return None
    +        return article.link
    
    From bbcc9d4614ca6c1ecef1e2a9c9898c339c408950 Mon Sep 17 00:00:00 2001
    From: Kovid Goyal 
    Date: Mon, 13 Apr 2009 10:20:59 -0700
    Subject: [PATCH 12/27] New recipe for Die Presse by Gerhard Aigner
    
    ---
     installer/windows/calibre/calibre.mpi         | 49 ++-----------------
     src/calibre/web/feeds/recipes/__init__.py     |  2 +-
     .../web/feeds/recipes/recipe_diepresse.py     | 40 +++++++++++++++
     3 files changed, 46 insertions(+), 45 deletions(-)
     create mode 100644 src/calibre/web/feeds/recipes/recipe_diepresse.py
    
    diff --git a/installer/windows/calibre/calibre.mpi b/installer/windows/calibre/calibre.mpi
    index 8073c45f29..a519695367 100644
    --- a/installer/windows/calibre/calibre.mpi
    +++ b/installer/windows/calibre/calibre.mpi
    @@ -571,9 +571,6 @@ Condition 08195201-0797-932C-4B51-E5EF9D1D41BD -active Yes -parent 710F2507-2557
     Condition 2E18F4AE-F1BB-5C62-2900-73A576A49261 -active Yes -parent 710F2507-2557-652D-EA55-440D710EFDFA -title {String Is Condition} -component StringIsCondition -TreeObject::id 2E18F4AE-F1BB-5C62-2900-73A576A49261
     InstallComponent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -setup Install -type action -conditions 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -title {Message Box} -component MessageBox -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
     Condition 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5 -active Yes -parent 21B897C4-24BE-70D1-58EA-DE78EFA60719 -title {String Is Condition} -component StringIsCondition -TreeObject::id 76FA3CA2-1F09-75C5-C6CF-72719A8EC4A5
    -InstallComponent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -setup Install -type action -conditions {E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C A8856922-E6C1-160B-E55C-5C1806A89136} -title {Launch Application Checkbutton} -component AddWidget -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
    -Condition E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C -active Yes -parent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -title {File Exists Condition} -component FileExistsCondition -TreeObject::id E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C
    -Condition A8856922-E6C1-160B-E55C-5C1806A89136 -active Yes -parent 5D20DD8D-064A-9922-29E1-A7FABEF3666A -title {String Is Condition} -component StringIsCondition -TreeObject::id A8856922-E6C1-160B-E55C-5C1806A89136
     InstallComponent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -setup Install -type action -conditions {96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 FBA33088-C809-DD6B-D337-EADBF1CEE966} -title {Desktop Shortcut Checkbutton} -component AddWidget -command insert -active Yes -parent 8A7FD0C2-F053-8764-F204-4BAE71E05708
     Condition 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 96440B8B-C6D0-FCCA-6D3C-7ECE1C304CC0
     Condition FBA33088-C809-DD6B-D337-EADBF1CEE966 -active Yes -parent 940F7FED-7D20-7264-3BF9-ED78205A76B3 -title {String Is Condition} -component StringIsCondition -TreeObject::id FBA33088-C809-DD6B-D337-EADBF1CEE966
    @@ -630,7 +627,7 @@ Condition 03FA7EEF-F626-B69A-09C6-0AA7A54EE9E7 -active Yes -parent E32519F3-A540
     InstallComponent D86BBA5C-4903-33BA-59F8-4266A3D45896 -setup Install -type action -conditions {C4C0A903-CF2A-D25A-27AB-A64219FB7E70 5EC7056B-6F90-311E-2C6F-76E96164CFFD} -title {Install Quick Launch Shortcut} -component InstallWindowsShortcut -command insert -active Yes -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC
     Condition C4C0A903-CF2A-D25A-27AB-A64219FB7E70 -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {String Is Condition} -component StringIsCondition -TreeObject::id C4C0A903-CF2A-D25A-27AB-A64219FB7E70
     Condition 5EC7056B-6F90-311E-2C6F-76E96164CFFD -active Yes -parent D86BBA5C-4903-33BA-59F8-4266A3D45896 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 5EC7056B-6F90-311E-2C6F-76E96164CFFD
    -InstallComponent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -setup Install -type action -conditions {4E5FC4FE-5D37-B216-CFFE-E046A2D6321E E560F3A1-208D-2B4F-2C87-E08595F8E1CD 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475} -title {Launch Application} -component ExecuteExternalProgram -command insert -active Yes -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC
    +InstallComponent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -setup Install -type action -conditions {4E5FC4FE-5D37-B216-CFFE-E046A2D6321E E560F3A1-208D-2B4F-2C87-E08595F8E1CD 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475} -title {Launch Application} -component ExecuteExternalProgram -command insert -active No -parent 28BAE662-E103-4E3F-D298-C8FBA36361FC
     Condition 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id 4E5FC4FE-5D37-B216-CFFE-E046A2D6321E
     Condition E560F3A1-208D-2B4F-2C87-E08595F8E1CD -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {String Is Condition} -component StringIsCondition -TreeObject::id E560F3A1-208D-2B4F-2C87-E08595F8E1CD
     Condition 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475 -active Yes -parent 2A230259-3A6F-8669-8B8B-23C3E7C1BFC2 -title {File Exists Condition} -component FileExistsCondition -TreeObject::id 9C1E4BD9-066D-ABCE-28D0-9E194B9F8475
    @@ -802,6 +799,9 @@ CreateQuickLaunchShortcut
     28FDA3F4-B799-901F-8A27-AA04F0C022AB,Title,subst
     1
     
    +2A230259-3A6F-8669-8B8B-23C3E7C1BFC2,Active
    +No
    +
     2A230259-3A6F-8669-8B8B-23C3E7C1BFC2,Conditions
     {3 conditions}
     
    @@ -976,27 +976,6 @@ disabled
     5C66451D-6042-DBDE-0D8C-31156EE244AD,Widget
     {Back Button;Next Button}
     
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Background
    -white
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Conditions
    -{2 conditions}
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Text,subst
    -1
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Type
    -checkbutton
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,VirtualText
    -LaunchApplication
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,X
    -185
    -
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Y
    -130
    -
     5EC7056B-6F90-311E-2C6F-76E96164CFFD,CheckCondition
     {Before Action is Executed}
     
    @@ -1408,15 +1387,6 @@ disabled
     A75C97CC-01AC-C12A-D663-A54E3257F11B,Widget
     {Back Button;Next Button}
     
    -A8856922-E6C1-160B-E55C-5C1806A89136,CheckCondition
    -{Before Action is Executed}
    -
    -A8856922-E6C1-160B-E55C-5C1806A89136,Operator
    -false
    -
    -A8856922-E6C1-160B-E55C-5C1806A89136,String
    -<%InstallStopped%>
    -
     AAEC34E6-7F02-18F2-30BB-744738192A3B,Conditions
     {2 conditions}
     
    @@ -1730,12 +1700,6 @@ disabled
     E5CBB018-A89D-3145-CFF5-CFC3B62BEA97,Widget
     {NextButton; CancelButton}
     
    -E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C,CheckCondition
    -{Before Action is Executed}
    -
    -E5D227F7-E549-EFA9-1781-EFA6C5EEEC5C,Filename
    -<%ProgramExecutable%>
    -
     E611105F-DC85-9E20-4F7B-E63C54E5DF06,Message,subst
     1
     
    @@ -2340,9 +2304,6 @@ Please make sure that calibre is not running, as this will cause the install to
     48E8A9D6-B57E-C506-680D-898C65DD2A1B,Title
     <%InstallApplicationText%>
     
    -5D20DD8D-064A-9922-29E1-A7FABEF3666A,Text
    -<%LaunchApplicationText%>
    -
     64B8D0F3-4B11-DA22-D6E7-7248872D5FA7,Message
     <%UninstallStartupText%>
     
    @@ -2356,7 +2317,7 @@ Please make sure that calibre is not running, as this will cause the install to
     {<%AppName%> Installation complete}
     
     8A7FD0C2-F053-8764-F204-4BAE71E05708,Message
    -{Installation of <%AppName%> was successful. Click Finish to quit the installer.}
    +{Installation of <%AppName%> was successful. Click Finish to quit the installer. <%AppName%> can be launched from the start menu.}
     
     940F7FED-7D20-7264-3BF9-ED78205A76B3,Text
     <%CreateDesktopShortcutText%>
    diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py
    index ef9f58b003..c006501ca5 100644
    --- a/src/calibre/web/feeds/recipes/__init__.py
    +++ b/src/calibre/web/feeds/recipes/__init__.py
    @@ -39,7 +39,7 @@ recipe_modules = ['recipe_' + r for r in (
                'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
                'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet',
                'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en',
    -           'moneynews', 'der_standard',
    +           'moneynews', 'der_standard', 'diepresse',
               )]
     
     import re, imp, inspect, time, os
    diff --git a/src/calibre/web/feeds/recipes/recipe_diepresse.py b/src/calibre/web/feeds/recipes/recipe_diepresse.py
    new file mode 100644
    index 0000000000..c806575356
    --- /dev/null
    +++ b/src/calibre/web/feeds/recipes/recipe_diepresse.py
    @@ -0,0 +1,40 @@
    +import re
    +
    +from calibre.web.feeds.news import BasicNewsRecipe
    +
    +class DiePresseRecipe(BasicNewsRecipe):
    +    title          = u'diePresse'
    +    oldest_article = 1
    +    max_articles_per_feed = 100
    +    recursions = 0
    +    language = _('German')
    +    __author__ = 'Gerhard Aigner'
    +
    +    preprocess_regexps = [
    +	(re.compile(r'Textversion', re.DOTALL), lambda match: ''),
    +    ]
    +    remove_tags = [dict(name='hr'),
    +	dict(name='br'),
    +	dict(name='small'),
    +	dict(name='img'),
    +	dict(name='div', attrs={'class':'textnavi'}),
    +	dict(name='h1', attrs={'class':'titel'}),
    +	dict(name='a', attrs={'class':'print'}),
    +	dict(name='div', attrs={'class':'hline'})]
    +    feeds = [(u'Politik', u'http://diepresse.com/rss/Politik'),
    +	(u'Wirtschaft', u'http://diepresse.com/rss/Wirtschaft'),
    +	(u'Europa', u'http://diepresse.com/rss/EU'),
    +	(u'Panorama', u'http://diepresse.com/rss/Panorama'),
    +	(u'Sport', u'http://diepresse.com/rss/Sport'),
    +	(u'Kultur', u'http://diepresse.com/rss/Kultur'),
    +	(u'Leben', u'http://diepresse.com/rss/Leben'),
    +	(u'Tech', u'http://diepresse.com/rss/Tech'),
    +	(u'Science', u'http://diepresse.com/rss/Science'),
    +	(u'Bildung', u'http://diepresse.com/rss/Bildung'),
    +	(u'Gesundheit', u'http://diepresse.com/rss/Gesundheit'),
    +	(u'Recht', u'http://diepresse.com/rss/Recht'),
    +	(u'Spectrum', u'http://diepresse.com/rss/Spectrum'),
    +	(u'Meinung', u'http://diepresse.com/rss/Meinung')]
    +
    +    def print_version(self, url):
    +        return url.replace('home','text/home')
    
    From d8ed8c0c079ce5985b4a44160a03d23040231080 Mon Sep 17 00:00:00 2001
    From: Kovid Goyal 
    Date: Tue, 14 Apr 2009 12:07:10 -0700
    Subject: [PATCH 13/27] New recipe for NZZ Online by Darko Miletic
    
    ---
     src/calibre/gui2/images/news/nzz_ger.png      | Bin 0 -> 811 bytes
     src/calibre/web/feeds/recipes/__init__.py     |   2 +-
     .../web/feeds/recipes/recipe_nzz_ger.py       |  66 ++++++++++++++++++
     3 files changed, 67 insertions(+), 1 deletion(-)
     create mode 100644 src/calibre/gui2/images/news/nzz_ger.png
     create mode 100644 src/calibre/web/feeds/recipes/recipe_nzz_ger.py
    
    diff --git a/src/calibre/gui2/images/news/nzz_ger.png b/src/calibre/gui2/images/news/nzz_ger.png
    new file mode 100644
    index 0000000000000000000000000000000000000000..ba9591853f174d660a8e26fcd84f9262d2c9c882
    GIT binary patch
    literal 811
    zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b
    zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87??slT^vI!PNz;b^pJ5BY1`j%bo1=DW!(+Y
    zEO$!OIW}&%;1}%b+0`QQP$7Y%aYgSztGi1%gdDG&i(v5;-17O2pW*|qCT)&)rPULX
    zme0QK+ZF5YyR7oep348fpV$Al$*8mWax$VN>L@3V=8OOV*414KD;gX(hCJ62;C%C6
    zPa?<1#bkN1z$?`ip_}F{HB4e)yjuM;OaI9#o{aTgS^hWgoXnj6+YC2;@P?W?4sYoa0YnwLBGyJEKP@g@`py`_5u}iOKe9w1(IY(qg2&-1p
    z0-la*XF@X1nM;@oNGQG+Tf!<5mTBi55o^D6>%1_YEVZsHJR&P5hVMTkAi06>)H+oI
    zCw1LG@fo|$8MAyiF3e)Q5M`~lzbL0>o)haWcgsle
    zY4==8{B+i?myNUY?{I$j;LM^~jUHzm4)V002zB#6$WSPBd8be1)(Mv-+U~wLW=KgF
    zJE57=5S)sn{|H$znQMOBCe#o-HtxUn=c-*W!CnPxmTk2j9y+AdFLMB!t=B8VEpS?-{*&>{l5fEY^o)$5hck*sfi`2
    zx+y?{!N|bCQrEyl*U%)y(8$Wb#LB=-*TCG$z`)D7<{?Z&ZhlH;S|z3iQ!672D^nwg
    VhG}*6TY(xFJYD@<);T3K0RTHJQ}h4;
    
    literal 0
    HcmV?d00001
    
    diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py
    index c006501ca5..c6444ec48e 100644
    --- a/src/calibre/web/feeds/recipes/__init__.py
    +++ b/src/calibre/web/feeds/recipes/__init__.py
    @@ -39,7 +39,7 @@ recipe_modules = ['recipe_' + r for r in (
                'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
                'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet',
                'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en',
    -           'moneynews', 'der_standard', 'diepresse',
    +           'moneynews', 'der_standard', 'diepresse', 'nzz_ger',
               )]
     
     import re, imp, inspect, time, os
    diff --git a/src/calibre/web/feeds/recipes/recipe_nzz_ger.py b/src/calibre/web/feeds/recipes/recipe_nzz_ger.py
    new file mode 100644
    index 0000000000..cdd23064bb
    --- /dev/null
    +++ b/src/calibre/web/feeds/recipes/recipe_nzz_ger.py
    @@ -0,0 +1,66 @@
    +#!/usr/bin/env  python
    +
    +__license__   = 'GPL v3'
    +__copyright__ = '2009, Darko Miletic '
    +
    +'''
    +www.nzz.ch
    +'''
    +
    +from calibre.web.feeds.recipes import BasicNewsRecipe
    +
    +class Nzz(BasicNewsRecipe):
    +    title                 = 'NZZ Online'
    +    __author__            = 'Darko Miletic'
    +    description           = 'Laufend aktualisierte Nachrichten, Analysen und Hintergruende zu Politik, Wirtschaft, Kultur und Sport'
    +    publisher             = 'NZZ AG'
    +    category              = 'news, politics, nachrichten, Switzerland'
    +    oldest_article        = 2
    +    max_articles_per_feed = 100
    +    no_stylesheets        = True
    +    encoding              = 'utf-8'
    +    use_embedded_content  = False
    +    lang                  = 'de-CH'
    +    language              = _('German')
    +
    +    html2lrf_options = [
    +                          '--comment', description
    +                        , '--category', category
    +                        , '--publisher', publisher
    +                        ]
    +
    +    html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em} img {margin-top: 0em; margin-bottom: 0.4em}"'
    +
    +    keep_only_tags = [dict(name='div', attrs={'class':'article'})]
    +
    +    remove_tags = [
    +                     dict(name=['object','link','base','script'])
    +                    ,dict(name='div',attrs={'class':['more','teaser','advXertXoriXals','legal']})
    +                    ,dict(name='div',attrs={'id':['popup-src','readercomments','google-ad','advXertXoriXals']})
    +                  ]
    +
    +    feeds = [
    +               (u'Neuste Artikel', u'http://www.nzz.ch/feeds/recent/'                     )
    +              ,(u'International' , u'http://www.nzz.ch/nachrichten/international?rss=true')
    +              ,(u'Schweiz'       , u'http://www.nzz.ch/nachrichten/schweiz?rss=true')
    +              ,(u'Wirtschaft'    , u'http://www.nzz.ch/nachrichten/wirtschaft/aktuell?rss=true')
    +              ,(u'Finanzmaerkte' , u'http://www.nzz.ch/finanzen/nachrichten?rss=true')
    +              ,(u'Zuerich'       , u'http://www.nzz.ch/nachrichten/zuerich?rss=true')
    +              ,(u'Sport'         , u'http://www.nzz.ch/nachrichten/sport?rss=true')
    +              ,(u'Panorama'      , u'http://www.nzz.ch/nachrichten/panorama?rss=true')
    +              ,(u'Kultur'        , u'http://www.nzz.ch/nachrichten/kultur/aktuell?rss=true')
    +              ,(u'Wissenschaft'  , u'http://www.nzz.ch/nachrichten/wissenschaft?rss=true')
    +              ,(u'Medien'        , u'http://www.nzz.ch/nachrichten/medien?rss=true')
    +              ,(u'Reisen'        , u'http://www.nzz.ch/magazin/reisen?rss=true')
    +            ]
    +
    +    def preprocess_html(self, soup):
    +        soup.html['xml:lang'] = self.lang
    +        soup.html['lang']     = self.lang
    +        mtag = ''
    +        soup.head.insert(0,mtag)
    +        return soup
    +
    +    def print_version(self, url):
    +        return url + '?printview=true'
    +
    
    From ead9de400281af161d35f7ba58e88fbda6e3d82d Mon Sep 17 00:00:00 2001
    From: Kovid Goyal 
    Date: Tue, 14 Apr 2009 23:38:22 -0700
    Subject: [PATCH 14/27] Fix #2273 (Little bug in Calibra Server)
    
    ---
     src/calibre/ebooks/mobi/reader.py |  32 ++++-----
     src/calibre/library/server.py     | 113 +++++++++++++++++-------------
     2 files changed, 79 insertions(+), 66 deletions(-)
    
    diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
    index 65ff86173f..2033ff11f5 100644
    --- a/src/calibre/ebooks/mobi/reader.py
    +++ b/src/calibre/ebooks/mobi/reader.py
    @@ -157,35 +157,35 @@ class BookHeader(object):
     class MetadataHeader(BookHeader):
         def __init__(self, stream):
             self.stream = stream
    -        
    +
             self.ident = self.identity()
             self.num_sections = self.section_count()
    -        
    +
             if self.num_sections >= 2:
                 header = self.header()
    -            BookHeader.__init__(self, header, self.ident, None)
    +            BookHeader.__init__(self, header, self.ident)
             else:
                 self.exth = None
    -            
    +
         def identity(self):
             self.stream.seek(60)
             ident = self.stream.read(8).upper()
    -        
    +
             if ident not in ['BOOKMOBI', 'TEXTREAD']:
                 raise MobiError('Unknown book type: %s' % ident)
             return ident
    -            
    +
         def section_count(self):
             self.stream.seek(76)
             return struct.unpack('>H', self.stream.read(2))[0]
    -            
    +
         def section_offset(self, number):
             self.stream.seek(78+number*8)
             return struct.unpack('>LBBBB', self.stream.read(8))[0]
    -        
    +
         def header(self):
             section_headers = []
    -            
    +
             # First section with the metadata
             section_headers.append(self.section_offset(0))
             # Second section used to get the lengh of the first
    @@ -193,20 +193,20 @@ class MetadataHeader(BookHeader):
     
             end_off = section_headers[1]
             off = section_headers[0]
    -    
    +
             self.stream.seek(off)
             return self.stream.read(end_off - off)
     
         def section_data(self, number):
             start = self.section_offset(number)
    -        
    +
             if number == self.num_sections -1:
                 end = os.stat(self.stream.name).st_size
             else:
                 end = self.section_offset(number + 1)
    -            
    +
             self.stream.seek(start)
    -        
    +
             return self.stream.read(end - start)
     
     
    @@ -618,7 +618,7 @@ class MobiReader(object):
                 self.image_names.append(os.path.basename(path))
                 im.convert('RGB').save(open(path, 'wb'), format='JPEG')
     
    -def get_metadata(stream):    
    +def get_metadata(stream):
         mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')])
         try:
             mh = MetadataHeader(stream)
    @@ -632,7 +632,7 @@ def get_metadata(stream):
                     mr.extract_content(tdir)
                     if mr.embedded_mi is not None:
                         mi = mr.embedded_mi
    -            
    +
             if hasattr(mh.exth, 'cover_offset'):
                 cover_index = mh.first_image_index + mh.exth.cover_offset
                 data  = mh.section_data(int(cover_index))
    @@ -646,7 +646,7 @@ def get_metadata(stream):
         except:
             import traceback
             traceback.print_exc()
    -    
    +
         return mi
     
     
    diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py
    index 4ba6253819..8e9b6278d8 100644
    --- a/src/calibre/library/server.py
    +++ b/src/calibre/library/server.py
    @@ -30,31 +30,31 @@ build_time = datetime.strptime(build_time, '%d %m %Y %H%M%S')
     server_resources['jquery.js'] = jquery
     
     def expose(func):
    -    
    +
         def do(self, *args, **kwargs):
             dict.update(cherrypy.response.headers, {'Server':self.server_name})
             return func(self, *args, **kwargs)
    -    
    +
         return cherrypy.expose(do)
     
     log_access_file = os.path.join(config_dir, 'server_access_log.txt')
     log_error_file = os.path.join(config_dir, 'server_error_log.txt')
    -    
    +
     
     class LibraryServer(object):
    -    
    +
         server_name = __appname__ + '/' + __version__
     
         BOOK = textwrap.dedent('''\
    -        ${r[8] if r[8] else ''}
                 
             ''')
    -    
    +
         LIBRARY = MarkupTemplate(textwrap.dedent('''\
         
         
    @@ -72,7 +72,7 @@ class LibraryServer(object):
         
         
         '''))
    -    
    +
         STANZA_ENTRY=MarkupTemplate(textwrap.dedent('''\
         
             ${record[FM['title']]}
    @@ -87,7 +87,7 @@ class LibraryServer(object):
             
         
         '''))
    -    
    +
         STANZA = MarkupTemplate(textwrap.dedent('''\
         
         
    @@ -107,7 +107,7 @@ class LibraryServer(object):
         
         '''))
     
    -    
    +
         def __init__(self, db, opts, embedded=False, show_tracebacks=True):
             self.db = db
             for item in self.db:
    @@ -116,7 +116,7 @@ class LibraryServer(object):
             self.opts = opts
             self.max_cover_width, self.max_cover_height = \
                             map(int, self.opts.max_cover.split('x'))
    -        
    +
             cherrypy.config.update({
                                     'log.screen'             : opts.develop,
                                     'engine.autoreload_on'   : opts.develop,
    @@ -141,10 +141,10 @@ class LibraryServer(object):
                           'tools.digest_auth.realm' : (_('Password to access your calibre library. Username is ') + opts.username.strip()).encode('ascii', 'replace'),
                           'tools.digest_auth.users' : {opts.username.strip():opts.password.strip()},
                           }
    -            
    +
             self.is_running = False
             self.exception = None
    -        
    +
         def setup_loggers(self):
             access_file = log_access_file
             error_file  = log_error_file
    @@ -152,20 +152,20 @@ class LibraryServer(object):
     
             maxBytes = getattr(log, "rot_maxBytes", 10000000)
             backupCount = getattr(log, "rot_backupCount", 1000)
    -        
    +
             # Make a new RotatingFileHandler for the error log.
             h = RotatingFileHandler(error_file, 'a', maxBytes, backupCount)
             h.setLevel(logging.DEBUG)
             h.setFormatter(cherrypy._cplogging.logfmt)
             log.error_log.addHandler(h)
    -        
    +
             # Make a new RotatingFileHandler for the access log.
             h = RotatingFileHandler(access_file, 'a', maxBytes, backupCount)
             h.setLevel(logging.DEBUG)
             h.setFormatter(cherrypy._cplogging.logfmt)
             log.access_log.addHandler(h)
     
    -    
    +
         def start(self):
             self.is_running = False
             self.setup_loggers()
    @@ -173,7 +173,7 @@ class LibraryServer(object):
             try:
                 cherrypy.engine.start()
                 self.is_running = True
    -            publish_zeroconf('Books in calibre', '_stanza._tcp', 
    +            publish_zeroconf('Books in calibre', '_stanza._tcp',
                                  self.opts.port, {'path':'/stanza'})
                 cherrypy.engine.block()
             except Exception, e:
    @@ -181,10 +181,10 @@ class LibraryServer(object):
             finally:
                 self.is_running = False
                 stop_zeroconf()
    -        
    +
         def exit(self):
             cherrypy.engine.exit()
    -    
    +
         def get_cover(self, id, thumbnail=False):
             cover = self.db.cover(id, index_is_id=True, as_file=False)
             if cover is None:
    @@ -196,14 +196,14 @@ class LibraryServer(object):
             try:
                 if QApplication.instance() is None:
                     QApplication([])
    -            
    +
                 im = QImage()
                 im.loadFromData(cover)
                 if im.isNull():
                     raise cherrypy.HTTPError(404, 'No valid cover found')
                 width, height = im.width(), im.height()
    -            scaled, width, height = fit_image(width, height, 
    -                60 if thumbnail else self.max_cover_width, 
    +            scaled, width, height = fit_image(width, height,
    +                60 if thumbnail else self.max_cover_width,
                     80 if thumbnail else self.max_cover_height)
                 if not scaled:
                     return cover
    @@ -217,7 +217,7 @@ class LibraryServer(object):
                 import traceback
                 traceback.print_exc()
                 raise cherrypy.HTTPError(404, 'Failed to generate cover: %s'%err)
    -        
    +
         def get_format(self, id, format):
             format = format.upper()
             fmt = self.db.format(id, format, index_is_id=True, as_file=True, mode='rb')
    @@ -232,7 +232,7 @@ class LibraryServer(object):
                 updated = datetime.utcfromtimestamp(os.stat(path).st_mtime)
                 cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
             return fmt.read()
    -    
    +
         def sort(self, items, field, order):
             field = field.lower().strip()
             if field == 'author':
    @@ -243,10 +243,23 @@ class LibraryServer(object):
                 raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
             cmpf = cmp if field in ('rating', 'size', 'timestamp') else \
                     lambda x, y: cmp(x.lower() if x else '', y.lower() if y else '')
    -        field = FIELD_MAP[field]
    -        getter = operator.itemgetter(field)
    -        items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
    -    
    +        if field == 'series':
    +            items.sort(cmp=self.seriescmp, reverse=not order)
    +        else:
    +            field = FIELD_MAP[field]
    +            getter = operator.itemgetter(field)
    +            items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
    +
    +    def seriescmp(self, x, y):
    +        si = FIELD_MAP['series']
    +        try:
    +            ans = cmp(x[si].lower(), y[si].lower())
    +        except AttributeError: # Some entries may be None
    +            ans = cmp(x[si], y[si])
    +        if ans != 0: return ans
    +        return cmp(x[FIELD_MAP['series_index']], y[FIELD_MAP['series_index']])
    +
    +
         def last_modified(self, updated):
             lm = updated.strftime('day, %d month %Y %H:%M:%S GMT')
             day ={0:'Sun', 1:'Mon', 2:'Tue', 3:'Wed', 4:'Thu', 5:'Fri', 6:'Sat'}
    @@ -254,8 +267,8 @@ class LibraryServer(object):
             month = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul',
                      8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
             return lm.replace('month', month[updated.month])
    -        
    -        
    +
    +
         @expose
         def stanza(self):
             ' Feeds to read calibre books on a ipod with stanza.'
    @@ -264,7 +277,7 @@ class LibraryServer(object):
                 r = record[FIELD_MAP['formats']]
                 r = r.upper() if r else ''
                 if 'EPUB' in r or 'PDB' in r:
    -                authors = ' & '.join([i.replace('|', ',') for i in 
    +                authors = ' & '.join([i.replace('|', ',') for i in
                                           record[FIELD_MAP['authors']].split(',')])
                     extra = []
                     rating = record[FIELD_MAP['rating']]
    @@ -276,7 +289,7 @@ class LibraryServer(object):
                         extra.append('TAGS: %s
    '%', '.join(tags.split(','))) series = record[FIELD_MAP['series']] if series: - extra.append('SERIES: %s [%d]
    '%(series, + extra.append('SERIES: %s [%d]
    '%(series, record[FIELD_MAP['series_index']])) fmt = 'epub' if 'EPUB' in r else 'pdb' mimetype = guess_type('dummy.'+fmt)[0] @@ -288,24 +301,24 @@ class LibraryServer(object): mimetype=mimetype, fmt=fmt, ).render('xml').decode('utf8')) - + updated = self.db.last_modified() cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) cherrypy.response.headers['Content-Type'] = 'text/xml' - + return self.STANZA.generate(subtitle='', data=books, FM=FIELD_MAP, updated=updated, id='urn:calibre:main').render('xml') - + @expose - def library(self, start='0', num='50', sort=None, search=None, + def library(self, start='0', num='50', sort=None, search=None, _=None, order='ascending'): ''' Serves metadata from the calibre database as XML. - + :param sort: Sort results by ``sort``. Can be one of `title,author,rating`. :param search: Filter results by ``search`` query. See :class:`SearchQueryParser` for query syntax :param start,num: Return the slice `[start:start+num]` of the sorted and filtered results - :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching + :param _: Firefox seems to sometimes send this when using XMLHttpRequest with no caching ''' try: start = int(start) @@ -321,19 +334,19 @@ class LibraryServer(object): items = [r for r in iter(self.db) if r[0] in ids] if sort is not None: self.sort(items, sort, order) - + book, books = MarkupTemplate(self.BOOK), [] for record in items[start:start+num]: aus = record[2] if record[2] else _('Unknown') authors = '|'.join([i.replace('|', ',') for i in aus.split(',')]) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) updated = self.db.last_modified() - + cherrypy.response.headers['Content-Type'] = 'text/xml' cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) - return self.LIBRARY.generate(books=books, start=start, updated=updated, + return self.LIBRARY.generate(books=books, start=start, updated=updated, total=len(ids)).render('xml') - + @expose def index(self, **kwargs): 'The / URL' @@ -341,8 +354,8 @@ class LibraryServer(object): if stanza == 919: return self.static('index.html') return self.stanza() - - + + @expose def get(self, what, id): 'Serves files, covers, thumbnails from the calibre database' @@ -361,7 +374,7 @@ class LibraryServer(object): if what == 'cover': return self.get_cover(id) return self.get_format(id, what) - + @expose def static(self, name): 'Serves static content' @@ -392,11 +405,11 @@ def start_threaded_server(db, opts): server.thread.setDaemon(True) server.thread.start() return server - + def stop_threaded_server(server): server.exit() server.thread = None - + def option_parser(): return config().option_parser('%prog '+ _('[options]\n\nStart the calibre content server.')) From 9d8e8dd8b93013e84668feab5feb4399f06f4044 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 12:53:40 -0700 Subject: [PATCH 15/27] =?UTF-8?q?New=20recipe=20for=20Hessisch=20Nieders?= =?UTF-8?q?=C3=A4chsische=20Allgemeine=20by=20Oliver=20neissner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/calibre/gui2/images/news/hna.png | Bin 0 -> 827 bytes src/calibre/web/feeds/recipes/__init__.py | 2 +- src/calibre/web/feeds/recipes/recipe_hna.py | 40 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/calibre/gui2/images/news/hna.png create mode 100644 src/calibre/web/feeds/recipes/recipe_hna.py diff --git a/src/calibre/gui2/images/news/hna.png b/src/calibre/gui2/images/news/hna.png new file mode 100644 index 0000000000000000000000000000000000000000..f4e1135dd560ce57312e174c27a044c6295cf7dc GIT binary patch literal 827 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87?@H#T^vI!PA{G8>mBMS(>{Nu<@0;R_s^d3 zp0)i_Q%103;FgIw(VikAfe9D2UPWKo)-UkHNvUe0P6x*#ahD5=I+zw7nQ}to^V;7< z&*sd`EYy3nZPwWXJT{;I{5;G5ub7j8{arkx9D|ooHCH2pVu6|)i{y&1z-x)djAnBW zMaLX7ix>56@G+2k_<(nj@bS*__ey)pXK%RFS@%t(c*XP`6Bd`fGZ9I_;_g zW6Las@I%^L6}T%ey_HR|_fdMspUjrO$Sm~4UFn0*ick2@PfSQCP21VkmAqzBgukPR z>h$e-3-w};*;Mk_+Ro68-4U)|I9q$i+uPR|YUE{?6r4VNqE&TEQ$cp?iyMMZPB=fg zn*HMO-xq&>d+e7u>Qn0!Z^Q4j#&hAE_JHQC6F42*C!9b0rY0#OAVBBP9i}zs8z+3I zRb{lvvVP#Qm@Vj0i-SaSFqd%KjFP?^PGLtmk_-2>UkIEOHX(P}#qY-!iYw|aNIb}S z!fKt2sEt&7O0410*PQY}b{YpJFOX%?J!!w=!gp!O@SWCtf7utVeNvpRljL9B=2_nt zc3oln=I*`Ln|*w=?asB#dDUC_e|2WT*IyI5Q~A0V*{)w{u9Q|6VRhQo?%cJTd*TAL zSkBbXUU+9?XWK`O)%reXp9?o{GHRw`XBBDL=Y5u=UPb_R9T;)!`@R z?eaPwN;Q2~?mp1`d~>Jc`ICw1b^B6x-SgG_J3AufK#K;eVCIyS%2WJvSTzMVE{VQ* zF)(%Ky`G7O7xM+PrXDnF7FAj~qu@cxXPui9#FQ8vvp4(PRLc1L-P9-bC-X!1Z(rrw zq+EcBPqoA~q9nN}HL)aBHw8#A7#SE?>Kd5n8k&X}8d;edSQ(k>8kk!d7`%`AvjV0e jH$NpatrA0nff-1Hp&3L&kd+J*Py>UftDnm{r-UW|Y2a5F literal 0 HcmV?d00001 diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py index c6444ec48e..9e2ef1969d 100644 --- a/src/calibre/web/feeds/recipes/__init__.py +++ b/src/calibre/web/feeds/recipes/__init__.py @@ -39,7 +39,7 @@ recipe_modules = ['recipe_' + r for r in ( 'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs', 'krstarica', 'krstarica_en', 'tanjug', 'laprensa_ni', 'azstarnet', 'corriere_della_sera_it', 'corriere_della_sera_en', 'msdnmag_en', - 'moneynews', 'der_standard', 'diepresse', 'nzz_ger', + 'moneynews', 'der_standard', 'diepresse', 'nzz_ger', 'hna', )] import re, imp, inspect, time, os diff --git a/src/calibre/web/feeds/recipes/recipe_hna.py b/src/calibre/web/feeds/recipes/recipe_hna.py new file mode 100644 index 0000000000..40193336d1 --- /dev/null +++ b/src/calibre/web/feeds/recipes/recipe_hna.py @@ -0,0 +1,40 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' + +''' +Fetch Hessisch Niedersächsische Allgemeine. +''' + +from calibre.web.feeds.news import BasicNewsRecipe + + +class hnaDe(BasicNewsRecipe): + + title = 'HNA' + description = 'local news from Hessen/Germany' + __author__ = 'Oliver Niesner' + use_embedded_content = False + language = _('German') + use_embedded_content = False + timefmt = ' [%d %b %Y]' + max_articles_per_feed = 40 + no_stylesheets = True + encoding = 'iso-8859-1' + + remove_tags = [dict(id='topnav'), + dict(id='nav_main'), + dict(id='suchen'), + dict(id=''), + dict(name='span'), + dict(name='ul', attrs={'class':'linklist'}), + dict(name='a', attrs={'href':'#'}), + dict(name='p', attrs={'class':'breadcrumb'}), + dict(name='p', attrs={'class':'h5'})] + #remove_tags_after = [dict(name='div', attrs={'class':'rahmenbreaking'})] + remove_tags_after = [dict(name='a', attrs={'href':'#'})] + + feeds = [ ('hna_soehre', 'http://feeds2.feedburner.com/hna/soehre'), + ('hna_kassel', 'http://feeds2.feedburner.com/hna/kassel') ] + + + From 332dbf44441a775d6affb52f768fcb86fee17a04 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 13:00:01 -0700 Subject: [PATCH 16/27] Updated recipes for Linux devices and Toms Hardware (German) --- src/calibre/web/feeds/recipes/recipe_hna.py | 2 +- .../web/feeds/recipes/recipe_linuxdevices.py | 158 +++++++++--------- .../feeds/recipes/recipe_tomshardware_de.py | 35 ++-- 3 files changed, 97 insertions(+), 98 deletions(-) diff --git a/src/calibre/web/feeds/recipes/recipe_hna.py b/src/calibre/web/feeds/recipes/recipe_hna.py index 40193336d1..c4faec94ba 100644 --- a/src/calibre/web/feeds/recipes/recipe_hna.py +++ b/src/calibre/web/feeds/recipes/recipe_hna.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' ''' -Fetch Hessisch Niedersächsische Allgemeine. +Fetch Hessisch Niedersachsische Allgemeine. ''' from calibre.web.feeds.news import BasicNewsRecipe diff --git a/src/calibre/web/feeds/recipes/recipe_linuxdevices.py b/src/calibre/web/feeds/recipes/recipe_linuxdevices.py index 04db6b02d5..cd914e96ad 100644 --- a/src/calibre/web/feeds/recipes/recipe_linuxdevices.py +++ b/src/calibre/web/feeds/recipes/recipe_linuxdevices.py @@ -1,80 +1,78 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' - -''' -Fetch Linuxdevices. -''' - -from calibre.web.feeds.news import BasicNewsRecipe - - -class Sueddeutsche(BasicNewsRecipe): - - title = u'Linuxdevices' - description = 'News about Linux driven Hardware' - __author__ = 'Oliver Niesner' - use_embedded_content = False - timefmt = ' [%a, %d %b %Y]' - language = _('English') - max_articles_per_feed = 50 - no_stylesheets = True - encoding = 'latin1' - - remove_tags_after = [dict(id='nointelliTXT')] - filter_regexps = [r'ad\.doubleclick\.net'] - - - remove_tags = [dict(name='div', attrs={'class':'bannerSuperBanner'}), - dict(name='div', attrs={'class':'bannerSky'}), - dict(name='div', attrs={'class':'footerLinks'}), - dict(name='div', attrs={'class':'seitenanfang'}), - dict(name='td', attrs={'class':'mar5'}), - dict(name='td', attrs={'class':'mar5'}), - dict(name='table', attrs={'class':'pageAktiv'}), - dict(name='table', attrs={'class':'xartable'}), - dict(name='table', attrs={'class':'wpnavi'}), - dict(name='table', attrs={'class':'bgcontent absatz'}), - dict(name='table', attrs={'class':'footer'}), - dict(name='table', attrs={'class':'artikelBox'}), - dict(name='table', attrs={'class':'kommentare'}), - dict(name='table', attrs={'class':'pageBoxBot'}), - #dict(name='table', attrs={'with':'100%'}), - dict(name='td', attrs={'nowrap':'nowrap'}), - dict(name='td', attrs={'valign':'middle'}), - dict(name='td', attrs={'align':'left'}), - dict(name='td', attrs={'align':'center'}), - dict(name='td', attrs={'height':'5'}), - dict(name='div', attrs={'class':'artikelBox navigatorBox'}), - dict(name='div', attrs={'class':'similar-article-box'}), - dict(name='div', attrs={'class':'videoBigHack'}), - dict(name='td', attrs={'class':'artikelDruckenRight'}), - dict(name='td', attrs={'class':'width="200"'}), - dict(name='a', attrs={'href':'/news'}), - dict(name='a', attrs={'href':'/'}), - dict(name='a', attrs={'href':'/articles'}), - dict(name='a', attrs={'href':'/cgi-bin/survey/survey.cgi'}), - dict(name='a', attrs={'href':'/cgi-bin/board/UltraBoard.pl'}), - dict(name='iframe'), - dict(name='form'), - #dict(name='tr', attrs={'td':'Click here to learn'}), - dict(name='span', attrs={'class':'hidePrint'}), - dict(id='headerLBox'), - dict(id='nointelliTXT'), - dict(id='rechteSpalte'), - dict(id='newsticker-list-small'), - dict(id='ntop5'), - dict(id='ntop5send'), - dict(id='ntop5commented'), - dict(id='nnav-bgheader'), - dict(id='nnav-headerteaser'), - dict(id='nnav-head'), - dict(id='nnav-top'), - dict(id='nnav-logodiv'), - dict(id='nnav-logo'), - dict(id='nnav-oly'), - dict(id='readcomment')] - - - - feeds = [ (u'Linuxdevices', u'http://www.linuxdevices.com/backend/headlines.rss') ] - +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' + +''' +Fetch Linuxdevices. +''' + +from calibre.web.feeds.news import BasicNewsRecipe + + +class Sueddeutsche(BasicNewsRecipe): + + title = u'Linuxdevices' + description = 'News about Linux driven Hardware' + __author__ = 'Oliver Niesner' + use_embedded_content = False + timefmt = ' [%a %d %b %Y]' + max_articles_per_feed = 50 + no_stylesheets = True + html2epub_options = 'linearize_tables = True\nbase_font_size2=14' + encoding = 'latin1' + + + remove_tags_after = [dict(id='nointelliTXT')] + filter_regexps = [r'ad\.doubleclick\.net'] + + remove_tags = [dict(name='div', attrs={'class':'bannerSuperBanner'}), + dict(name='div', attrs={'class':'bannerSky'}), + dict(name='div', attrs={'class':'footerLinks'}), + dict(name='div', attrs={'class':'seitenanfang'}), + dict(name='td', attrs={'class':'mar5'}), + dict(name='td', attrs={'class':'mar5'}), + dict(name='table', attrs={'class':'pageAktiv'}), + dict(name='table', attrs={'class':'xartable'}), + dict(name='table', attrs={'class':'wpnavi'}), + dict(name='table', attrs={'class':'bgcontent absatz'}), + dict(name='table', attrs={'class':'footer'}), + dict(name='table', attrs={'class':'artikelBox'}), + dict(name='table', attrs={'class':'kommentare'}), + dict(name='table', attrs={'class':'pageBoxBot'}), + dict(name='td', attrs={'nowrap':'nowrap'}), + dict(name='td', attrs={'valign':'middle'}), + dict(name='td', attrs={'align':'left'}), + dict(name='td', attrs={'align':'center'}), + dict(name='td', attrs={'height':'5'}), + dict(name='div', attrs={'class':'artikelBox navigatorBox'}), + dict(name='div', attrs={'class':'similar-article-box'}), + dict(name='div', attrs={'class':'videoBigHack'}), + dict(name='td', attrs={'class':'artikelDruckenRight'}), + dict(name='td', attrs={'class':'width="200"'}), + dict(name='a', attrs={'href':'/news'}), + dict(name='a', attrs={'href':'/'}), + dict(name='a', attrs={'href':'/articles'}), + dict(name='a', attrs={'href':'/cgi-bin/survey/survey.cgi'}), + dict(name='a', attrs={'href':'/cgi-bin/board/UltraBoard.pl'}), + dict(name='iframe'), + dict(name='form'), + dict(name='span', attrs={'class':'hidePrint'}), + dict(id='headerLBox'), + dict(id='nointelliTXT'), + dict(id='rechteSpalte'), + dict(id='newsticker-list-small'), + dict(id='ntop5'), + dict(id='ntop5send'), + dict(id='ntop5commented'), + dict(id='nnav-bgheader'), + dict(id='nnav-headerteaser'), + dict(id='nnav-head'), + dict(id='nnav-top'), + dict(id='nnav-logodiv'), + dict(id='nnav-logo'), + dict(id='nnav-oly'), + dict(id='readcomment')] + + + + feeds = [ (u'Linuxdevices', u'http://www.linuxdevices.com/backend/headlines.rss') ] + diff --git a/src/calibre/web/feeds/recipes/recipe_tomshardware_de.py b/src/calibre/web/feeds/recipes/recipe_tomshardware_de.py index 52f1583408..7ba656e1d5 100644 --- a/src/calibre/web/feeds/recipes/recipe_tomshardware_de.py +++ b/src/calibre/web/feeds/recipes/recipe_tomshardware_de.py @@ -8,26 +8,19 @@ Fetch tomshardware. from calibre.web.feeds.news import BasicNewsRecipe -class TomsHardwareDe(BasicNewsRecipe): - - title = 'Tom\'s Hardware German' - description = 'Computer news in german' +class cdnet(BasicNewsRecipe): + + title = 'tomshardware' + description = 'computer news in german' __author__ = 'Oliver Niesner' use_embedded_content = False timefmt = ' [%d %b %Y]' max_articles_per_feed = 50 - language = _('German') no_stylesheets = True + language = _('German') encoding = 'utf-8' - #preprocess_regexps = \ -# [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in -# [ -# (r'<84>', lambda match: ''), -# (r'<93>', lambda match: ''), -# ] -# ] - + remove_tags = [dict(id='outside-advert'), dict(id='advertRightWhite'), dict(id='header-advert'), @@ -36,9 +29,15 @@ class TomsHardwareDe(BasicNewsRecipe): dict(id='header-top'), dict(id='header-tools'), dict(id='nbComment'), + dict(id='commentTools'), dict(id='internalSidebar'), dict(id='header-news-infos'), + dict(id='header-news-tools'), dict(id='breadcrumbs'), + dict(id='emailTools'), + dict(id='bookmarkTools'), + dict(id='printTools'), + dict(id='header-nextNews'), dict(id=''), dict(name='div', attrs={'class':'pyjama'}), dict(name='href', attrs={'class':'comment'}), @@ -47,8 +46,10 @@ class TomsHardwareDe(BasicNewsRecipe): dict(name='div', attrs={'class':'greyBox clearfix'}), dict(id='')] #remove_tags_before = [dict(id='header-news-title')] - remove_tags_after = [dict(name='div', attrs={'class':'news-elm'})] + remove_tags_after = [dict(name='div', attrs={'class':'btmGreyTables'})] #remove_tags_after = [dict(name='div', attrs={'class':'intelliTXT'})] - - feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ] - + + feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ] + + + From c2b79fe5d93fb1c127ba0ba2351fe6a0118b3c30 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 13:36:37 -0700 Subject: [PATCH 17/27] Allow single click fetching of covers in the Edit metadata dialog. Now calibre will automatically try to get the ISBN needed to fetch the cover based on title and author of the book. --- src/calibre/ebooks/metadata/lit.py | 4 +- src/calibre/gui2/__init__.py | 2 + src/calibre/gui2/dialogs/metadata_single.py | 79 +++++++++++++++------ src/calibre/gui2/dialogs/password.ui | 79 +++++++++++---------- 4 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/calibre/ebooks/metadata/lit.py b/src/calibre/ebooks/metadata/lit.py index 2129af76dd..e45016f303 100644 --- a/src/calibre/ebooks/metadata/lit.py +++ b/src/calibre/ebooks/metadata/lit.py @@ -25,7 +25,7 @@ def get_metadata(stream): for item in litfile.manifest.values(): if item.path in candidates: try: - covers.append((litfile.get_file('/data/'+item.internal), + covers.append((litfile.get_file('/data/'+item.internal), ctype)) except: pass @@ -33,7 +33,7 @@ def get_metadata(stream): covers.sort(cmp=lambda x, y:cmp(len(x[0]), len(y[0])), reverse=True) idx = 0 if len(covers) > 1: - if covers[1][1] == covers[1][0]+'-standard': + if covers[1][1] == covers[0][1]+'-standard': idx = 1 mi.cover_data = ('jpg', covers[idx][0]) return mi diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 1da5bb6851..b3a67d003e 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -69,6 +69,8 @@ def _config(): 'clicked')) c.add_opt('show_donate_button', default=True, help='Show donation button') + c.add_opt('asked_library_thing_password', default=False, + help='Asked library thing password at least once.') return ConfigProxy(c) config = _config() diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index a9d20905c6..e3d4b5b521 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -25,24 +25,47 @@ from calibre import islinux from calibre.ebooks.metadata.meta import get_metadata from calibre.utils.config import prefs from calibre.customize.ui import run_plugins_on_import +from calibre.gui2 import config as gui_conf class CoverFetcher(QThread): - def __init__(self, username, password, isbn, timeout): + def __init__(self, username, password, isbn, timeout, title, author): self.username = username self.password = password self.timeout = timeout self.isbn = isbn + self.title = title + self.needs_isbn = False + self.author = author QThread.__init__(self) self.exception = self.traceback = self.cover_data = None def run(self): try: + if not self.isbn: + from calibre.ebooks.metadata.fetch import search + if not self.title: + self.needs_isbn = True + return + au = self.author if self.author else None + key = prefs['isbndb_com_key'] + if not key: + key = None + results = search(title=self.title, author=au, + isbndb_key=key)[0] + results = sorted([x.isbn for x in results if x.isbn], + cmp=lambda x,y:cmp(len(x),len(y)), reverse=True) + if not results: + self.needs_isbn = True + return + self.isbn = results[0] + login(self.username, self.password, force=False) self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0] except Exception, e: self.exception = e self.traceback = traceback.format_exc() + print self.traceback @@ -64,6 +87,8 @@ class AuthorCompleter(QCompleter): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): + COVER_FETCH_TIMEOUT = 240 # seconds + def do_reset_cover(self, *args): pix = QPixmap(':/images/book.svg') self.cover.setPixmap(pix) @@ -345,36 +370,39 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): def lt_password_dialog(self): return PasswordDialog(self, 'LibraryThing account', - _('

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

    ')) + _('

    Enter your username and password for ' + 'LibraryThing.com. This is optional. It will ' + 'make fetching of covers faster and more reliable.
    If ' + 'you do not have an account, you can ' + 'register for ' + 'free.

    ')) def change_password(self): 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() - if not d.username() or not d.password(): - d.exec_() - if d.result() != PasswordDialog.Accepted: - return - self.fetch_cover_button.setEnabled(False) - self.setCursor(Qt.WaitCursor) - self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn, - self.timeout) - self.cover_fetcher.start() - self._hangcheck = QTimer(self) - self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck) - self.cf_start_time = time.time() - self.pi.start(_('Downloading cover...')) - self._hangcheck.start(100) - else: - error_dialog(self, _('Cannot fetch cover'), - _('You must specify the ISBN identifier for this book.')).exec_() + isbn = unicode(self.isbn.text()).strip() + d = self.lt_password_dialog() + if not gui_conf['asked_library_thing_password'] and \ + (not d.username() or not d.password()): + d.exec_() + gui_conf['asked_library_thing_password'] = True + self.fetch_cover_button.setEnabled(False) + self.setCursor(Qt.WaitCursor) + title, author = map(unicode, (self.title.text(), self.authors.text())) + self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn, + self.timeout, title, author) + self.cover_fetcher.start() + self._hangcheck = QTimer(self) + self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck) + self.cf_start_time = time.time() + self.pi.start(_('Downloading cover...')) + self._hangcheck.start(100) def hangcheck(self): - if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150): + if not self.cover_fetcher.isFinished() and \ + time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT: return self._hangcheck.stop() @@ -385,6 +413,11 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): _('Could not fetch cover.
    ')+ _('The download timed out.')).exec_() return + if self.cover_fetcher.needs_isbn: + error_dialog(self, _('Cannot fetch cover'), + _('Could not find cover for this book. Try ' + 'specifying the ISBN first.')).exec_() + return if self.cover_fetcher.exception is not None: err = self.cover_fetcher.exception error_dialog(self, _('Cannot fetch cover'), diff --git a/src/calibre/gui2/dialogs/password.ui b/src/calibre/gui2/dialogs/password.ui index 865c065a10..3fc982371e 100644 --- a/src/calibre/gui2/dialogs/password.ui +++ b/src/calibre/gui2/dialogs/password.ui @@ -1,7 +1,8 @@ - + + Dialog - - + + 0 0 @@ -9,66 +10,70 @@ 209 - + Password needed - - :/images/mimetypes/unknown.svg + + + :/images/mimetypes/unknown.svg:/images/mimetypes/unknown.svg - - - - + + + + TextLabel - + + true + + true - - - + + + &Username: - + gui_username - - + + - - - + + + &Password: - + gui_password - - - + + + QLineEdit::Password - - - + + + Qt::Horizontal - - QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - + + + &Show password @@ -76,7 +81,7 @@ - + @@ -85,11 +90,11 @@ Dialog accept() - + 248 254 - + 157 274 @@ -101,11 +106,11 @@ Dialog reject() - + 316 260 - + 286 274 From d8430be7c84815b6e2b2003088f0bb6cbbb3596c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 14:37:49 -0700 Subject: [PATCH 18/27] MOBI Output: Fix regression that was preventing the creation of pagebreaks between the contents of different HTML files in the input. --- src/calibre/ebooks/mobi/writer.py | 2 +- src/calibre/gui2/dialogs/metadata_single.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 9990fa9061..c860a9418e 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -218,7 +218,7 @@ class Serializer(object): for elem in item.data.find(XHTML('body')): self.serialize_elem(elem, item) #buffer.write('') - buffer.write('') + buffer.write('') def serialize_elem(self, elem, item, nsrmap=NSRMAP): buffer = self.buffer diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index e3d4b5b521..c48c7c3640 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -30,8 +30,8 @@ from calibre.gui2 import config as gui_conf class CoverFetcher(QThread): def __init__(self, username, password, isbn, timeout, title, author): - self.username = username - self.password = password + self.username = username.strip() if username else username + self.password = password.strip() if password else password self.timeout = timeout self.isbn = isbn self.title = title @@ -60,7 +60,8 @@ class CoverFetcher(QThread): return self.isbn = results[0] - login(self.username, self.password, force=False) + if self.username and self.password: + login(self.username, self.password, force=False) self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0] except Exception, e: self.exception = e From 35e8e347fea42005c861675d58b82e3559984066 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 14:38:45 -0700 Subject: [PATCH 19/27] Implement the --linearize-tables transform. --- src/calibre/customize/profiles.py | 2 +- src/calibre/ebooks/conversion/plumber.py | 29 +++++++++++++++++-- src/calibre/ebooks/mobi/input.py | 2 +- .../ebooks/oeb/transforms/linearize_tables.py | 21 ++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/calibre/ebooks/oeb/transforms/linearize_tables.py diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 8623a94ddd..c11529f025 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -143,7 +143,7 @@ class OutputProfile(Plugin): # ADE dies an agonizing, long drawn out death if HTML files have more # bytes than this. - flow_size = sys.maxint + flow_size = -1 # ADE runs screaming when it sees these characters remove_special_chars = re.compile(u'[\u200b\u00ad]') # ADE falls to the ground in a dead faint when it sees an diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index ab30e71ba1..119ae4d63e 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -94,7 +94,8 @@ OptionRecommendation(name='font_size_mapping', OptionRecommendation(name='line_height', recommended_value=None, level=OptionRecommendation.LOW, help=_('The line height in pts. Controls spacing between consecutive ' - 'lines of text. By default ??' + 'lines of text. By default no line height manipulation is ' + 'performed.' ) ), @@ -102,12 +103,25 @@ OptionRecommendation(name='linearize_tables', recommended_value=False, level=OptionRecommendation.LOW, help=_('Some badly designed documents use tables to control the ' 'layout of text on the page. When converted these documents ' - 'often have text that runs of the page and other artifacts. ' + 'often have text that runs off the page and other artifacts. ' 'This option will extract the content from the tables and ' 'present it in a linear fashion.' ) ), +OptionRecommendation(name='dont_split_on_page_breaks', + recommended_value=False, level=OptionRecommendation.LOW, + help=_('Turn off splitting at page breaks. Normally, input ' + 'files are automatically split at every page break into ' + 'two files. This gives an output ebook that can be ' + 'parsed faster and with less resources. However, ' + 'splitting is slow and if your source file contains a ' + 'very large number of page breaks, you should turn off ' + 'splitting on page breaks.' + ) + ), + + OptionRecommendation(name='read_metadata_from_opf', recommended_value=None, level=OptionRecommendation.LOW, short_switch='m', @@ -330,6 +344,17 @@ OptionRecommendation(name='language', untable=self.opts.linearize_tables) flattener(self.oeb, self.opts) + if self.opts.linearize_tables: + from calibre.ebooks.oeb.transforms.linearize_tables import LinearizeTables + LinearizeTables()(self.oeb, self.opts) + + from calibre.ebooks.oeb.transforms.split import Split + pbx = accelerators.get('pagebreaks', None) + split = Split(not self.opts.dont_split_on_page_breaks, + max_flow_size=self.opts.output_profile.flow_size, + page_breaks_xpath=pbx) + split(self.oeb, self.opts) + from calibre.ebooks.oeb.transforms.trimmanifest import ManifestTrimmer self.log.info('Cleaning up manifest...') diff --git a/src/calibre/ebooks/mobi/input.py b/src/calibre/ebooks/mobi/input.py index 2eb45c9161..97d94a0e33 100644 --- a/src/calibre/ebooks/mobi/input.py +++ b/src/calibre/ebooks/mobi/input.py @@ -29,5 +29,5 @@ class MOBIInput(InputFormatPlugin): with open(f, 'wb') as q: q.write(html.tostring(root, encoding='utf-8', method='xml', include_meta_content_type=False)) - accelerators['pagebreaks'] = {f: '//*[@class="mbp_pagebreak"]'} + accelerators['pagebreaks'] = '//h:div[@class="mbp_pagebreak"]' return mr.created_opf_path diff --git a/src/calibre/ebooks/oeb/transforms/linearize_tables.py b/src/calibre/ebooks/oeb/transforms/linearize_tables.py new file mode 100644 index 0000000000..a0c11f848c --- /dev/null +++ b/src/calibre/ebooks/oeb/transforms/linearize_tables.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from calibre.ebooks.oeb.base import OEB_DOCS, XPNSMAP + +class LinearizeTables(object): + + def linearize(self, root): + for x in root.xpath('//h:table|//h:td|//h:tr|//h:th', + namespaces=XPNSMAP): + x.tag = 'div' + + def __call__(self, oeb, context): + for x in oeb.manifest.items: + if x.media_type in OEB_DOCS: + self.linearize(x.data) From 0820e5d90a3ffd49dac58333d236d4b889bc865b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 17:14:04 -0700 Subject: [PATCH 20/27] Implement #2277 (Remove empty feeds when fetching news with recipes) --- src/calibre/web/feeds/news.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index a3642b0a33..2494951ccd 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -127,6 +127,12 @@ class BasicNewsRecipe(object, LoggingInterface): #: extra_css = None + #: If True empty feeds are removed from the output. + #: This option has no effect if parse_index is overriden in + #: the sub class. It is meant only for recipes that return a list + #: of feeds using :member:`feeds` or :method:`get_feeds`. + remove_empty_feeds = False + #: List of regular expressions that determines which links to follow #: If empty, it is ignored. For example:: #: @@ -994,6 +1000,11 @@ class BasicNewsRecipe(object, LoggingInterface): self.log_exception(msg) + remove = [f for f in parsed_feeds if len(f) == 0 and + self.remove_empty_feeds] + for f in remove: + parsed_feeds.remove(f) + return parsed_feeds @classmethod From 54b5a25fa8f323eb0ab7cd3ae126ed8832caf685 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 17:44:00 -0700 Subject: [PATCH 21/27] Fix #2283 (Updated recipe Vreme) --- src/calibre/web/feeds/recipes/recipe_vreme.py | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/calibre/web/feeds/recipes/recipe_vreme.py b/src/calibre/web/feeds/recipes/recipe_vreme.py index 697413f2f3..1df953cae3 100644 --- a/src/calibre/web/feeds/recipes/recipe_vreme.py +++ b/src/calibre/web/feeds/recipes/recipe_vreme.py @@ -11,20 +11,23 @@ from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe class Vreme(BasicNewsRecipe): - title = 'Vreme' - __author__ = 'Darko Miletic' - description = 'Politicki Nedeljnik Srbije' - publisher = 'Vreme d.o.o.' - category = 'news, politics, Serbia' - no_stylesheets = True - remove_javascript = True - needs_subscription = True - INDEX = 'http://www.vreme.com' - LOGIN = 'http://www.vreme.com/account/index.php' - remove_javascript = True - use_embedded_content = False - language = _('Serbian') - extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{text-align: justify; font-family: serif1, serif} .article_description{font-family: serif1, serif}' + title = 'Vreme' + __author__ = 'Darko Miletic' + description = 'Politicki Nedeljnik Srbije' + publisher = 'NP Vreme d.o.o.' + category = 'news, politics, Serbia' + delay = 1 + no_stylesheets = True + needs_subscription = True + INDEX = 'http://www.vreme.com' + LOGIN = 'http://www.vreme.com/account/login.php?url=%2F' + remove_javascript = True + use_embedded_content = False + encoding = 'utf-8' + language = _('Serbian') + lang = 'sr-Latn-RS' + direction = 'ltr' + extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{text-align: justify; font-family: serif1, serif} .article_description{font-family: serif1, serif}' html2lrf_options = [ '--comment' , description @@ -52,20 +55,11 @@ class Vreme(BasicNewsRecipe): articles = [] soup = self.index_to_soup(self.INDEX) - for item in soup.findAll('span', attrs={'class':'toc2'}): + for item in soup.findAll(['h3','h4']): description = '' title_prefix = '' - - descript_title_tag = item.findPreviousSibling('span', attrs={'class':'toc1'}) - if descript_title_tag: - title_prefix = self.tag_to_string(descript_title_tag) + ' ' - - descript_tag = item.findNextSibling('span', attrs={'class':'toc3'}) - if descript_tag: - description = self.tag_to_string(descript_tag) - feed_link = item.find('a') - if feed_link and feed_link.has_key('href'): + if feed_link and feed_link.has_key('href') and feed_link['href'].startswith('/cms/view.php'): url = self.INDEX + feed_link['href'] title = title_prefix + self.tag_to_string(feed_link) date = strftime(self.timefmt) @@ -93,14 +87,17 @@ class Vreme(BasicNewsRecipe): del item['face'] for item in soup.findAll(size=True): del item['size'] - mtag = '' - soup.head.insert(0,mtag) + soup.html['lang'] = self.lang + soup.html['dir' ] = self.direction + mtag = '' + mtag += '\n' + soup.head.insert(0,mtag) return soup def get_cover_url(self): cover_url = None soup = self.index_to_soup(self.INDEX) - cover_item = soup.find('img',attrs={'alt':'Naslovna strana broja'}) + cover_item = soup.find('div',attrs={'id':'najava'}) if cover_item: - cover_url = self.INDEX + cover_item['src'] + cover_url = self.INDEX + cover_item.img['src'] return cover_url From b3e528c706bfcc1a86c044fac4a20678281ba2ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 19:34:25 -0700 Subject: [PATCH 22/27] version 0.5.7 --- src/calibre/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 00276f6970..138a631b7c 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -2,7 +2,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = 'calibre' -__version__ = '0.5.6' +__version__ = '0.5.7' __author__ = "Kovid Goyal " ''' Various run time constants. From 10082403ce5786acc33b185365baf210dfce525c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 15 Apr 2009 19:35:15 -0700 Subject: [PATCH 23/27] IGN:Tag release From b10681b98e2c030b4eb0bfbaf4295f92fce5bff7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Apr 2009 08:55:21 -0700 Subject: [PATCH 24/27] IGN:Switch to my server since dev.mobileread.com is down --- src/calibre/trac/plugins/download.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py index e4a0fe36af..c6afeee485 100644 --- a/src/calibre/trac/plugins/download.py +++ b/src/calibre/trac/plugins/download.py @@ -68,7 +68,8 @@ else: DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads' - MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' + #MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' + MOBILEREAD = 'http://calibre.kovidgoyal.net/downloads/' class OS(dict): """Dictionary with a default value for unknown keys.""" @@ -196,7 +197,9 @@ else: LINUX_INSTALLER = textwrap.dedent(r''' import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat - MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' + #MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' + MOBILEREAD='http://calibre.kovidgoyal.net/downloads/' + class TerminalController: BOL = '' #: Move the cursor to the beginning of the line From b4fb12ed6704bd539c2c55a46a4f067cdddf84d1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Apr 2009 08:57:31 -0700 Subject: [PATCH 25/27] IGN:... --- .../trac/plugins/htdocs/images/binary_logo.png | Bin 0 -> 51099 bytes .../plugins/htdocs/images/foresight_logo.png | Bin 0 -> 36476 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/calibre/trac/plugins/htdocs/images/binary_logo.png create mode 100644 src/calibre/trac/plugins/htdocs/images/foresight_logo.png diff --git a/src/calibre/trac/plugins/htdocs/images/binary_logo.png b/src/calibre/trac/plugins/htdocs/images/binary_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4387cc8cfe8c1faa26731e198a79b0b581b4f346 GIT binary patch literal 51099 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 literal 0 HcmV?d00001 diff --git a/src/calibre/trac/plugins/htdocs/images/foresight_logo.png b/src/calibre/trac/plugins/htdocs/images/foresight_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b947eca1a38cd205572f19b1058761b72b3e783f GIT binary patch literal 36476 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 literal 0 HcmV?d00001 From 8b34a4ebca6091a1790ebbfdda3f95efae979ffb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Apr 2009 09:02:28 -0700 Subject: [PATCH 26/27] IGN:dev.mobileread.com is back --- src/calibre/trac/plugins/download.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/trac/plugins/download.py b/src/calibre/trac/plugins/download.py index c6afeee485..a6c9876f20 100644 --- a/src/calibre/trac/plugins/download.py +++ b/src/calibre/trac/plugins/download.py @@ -68,8 +68,8 @@ else: DOWNLOAD_DIR = '/var/www/calibre.kovidgoyal.net/htdocs/downloads' - #MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' - MOBILEREAD = 'http://calibre.kovidgoyal.net/downloads/' + MOBILEREAD = 'https://dev.mobileread.com/dist/kovid/calibre/' + #MOBILEREAD = 'http://calibre.kovidgoyal.net/downloads/' class OS(dict): """Dictionary with a default value for unknown keys.""" @@ -197,8 +197,8 @@ else: LINUX_INSTALLER = textwrap.dedent(r''' import sys, os, shutil, tarfile, subprocess, tempfile, urllib2, re, stat - #MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' - MOBILEREAD='http://calibre.kovidgoyal.net/downloads/' + MOBILEREAD='https://dev.mobileread.com/dist/kovid/calibre/' + #MOBILEREAD='http://calibre.kovidgoyal.net/downloads/' class TerminalController: From cf77ec2c4a70c914cd8628caf7805a493de59653 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 16 Apr 2009 10:25:55 -0700 Subject: [PATCH 27/27] Speed up device detection in windows and Fix #2287 (Calibre 0.5.7 No Longer Detects Kindle 2 When Plugged In) --- src/calibre/devices/prs505/driver.py | 2 +- src/calibre/devices/usbms/device.py | 2 +- src/calibre/gui2/device.py | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 6e21c60d1b..b32c7e702e 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -150,7 +150,7 @@ class PRS505(Device): time.sleep(6) drives = [] wmi = __import__('wmi', globals(), locals(), [], -1) - c = wmi.WMI() + c = wmi.WMI(find_classes=False) for drive in c.Win32_DiskDrive(): if self.__class__.is_device(str(drive.PNPDeviceID)): if drive.Partitions == 0: diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 5a1b5ef40d..5bcc384b00 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -186,7 +186,7 @@ class Device(_Device): time.sleep(6) drives = {} wmi = __import__('wmi', globals(), locals(), [], -1) - c = wmi.WMI() + c = wmi.WMI(find_classes=False) for drive in c.Win32_DiskDrive(): if self.windows_match_device(str(drive.PNPDeviceID), self.WINDOWS_MAIN_MEM): drives['main'] = self.windows_get_drive_prefix(drive) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 0ef4191b84..03a6220f87 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -11,6 +11,7 @@ from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \ Qt from calibre.devices import devices +from calibre.constants import iswindows from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.parallel import Job from calibre.devices.scanner import DeviceScanner @@ -69,7 +70,14 @@ class DeviceManager(Thread): if connected and not device[1]: try: dev = device[0]() - dev.open() + if iswindows: + import pythoncom + pythoncom.CoInitialize() + try: + dev.open() + finally: + if iswindows: + pythoncom.CoUninitialize() self.device = dev self.device_class = dev.__class__ self.connected_slot(True)