From 3d30edcc83a335f79910a6284b0a45416dd9df30 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 13:48:32 -0700 Subject: [PATCH 01/17] Fix #8008 (QWebPage import error) --- src/calibre/gui2/comments_editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index d19c97e87b..ca93145915 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -11,9 +11,9 @@ from lxml import html from lxml.html import soupparser from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \ - QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, QUrl, \ + QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \ QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog -from PyQt4.QtWebKit import QWebView +from PyQt4.QtWebKit import QWebView, QWebPage from calibre.ebooks.chardet import xml_to_unicode from calibre import xml_replace_entities From fc5934436c7335f64ebe69b412081a2f764f5d5f Mon Sep 17 00:00:00 2001 From: Marcell van Geest Date: Tue, 21 Dec 2010 23:39:25 +0100 Subject: [PATCH 02/17] Added possibility to sort fetched metadata using the column headers. --- src/calibre/gui2/dialogs/fetch_metadata.py | 57 ++++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py index 3da0e67e3d..e6f5e6ae9a 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.py +++ b/src/calibre/gui2/dialogs/fetch_metadata.py @@ -16,6 +16,8 @@ from calibre.gui2 import error_dialog, NONE, info_dialog, config from calibre.gui2.widgets import ProgressIndicator from calibre import strftime, force_unicode from calibre.customize.ui import get_isbndb_key, set_isbndb_key +from calibre.utils.icu import sort_key +from calibre import force_unicode _hung_fetchers = set([]) @@ -72,25 +74,32 @@ class Matches(QAbstractTableModel): def summary(self, row): return self.matches[row].comments + def data_as_text(self, book, col): + if col == 0 and book.title is not None: + return book.title + elif col == 1: + return ', '.join(book.authors) + elif col == 2 and book.author_sort is not None: + return book.author_sort + elif col == 3 and book.publisher is not None: + return book.publisher + elif col == 4 and book.isbn is not None: + return book.isbn + elif col == 5 and hasattr(book.pubdate, 'timetuple'): + return strftime('%b %Y', book.pubdate.timetuple()) + elif col == 6 and book.has_cover: + return 'has_cover' + elif col == 7 and book.comments is not None: + return book.comments + else: + return '' + def data(self, index, role): row, col = index.row(), index.column() book = self.matches[row] if role == Qt.DisplayRole: - res = None - if col == 0: - res = book.title - elif col == 1: - res = ', '.join(book.authors) - elif col == 2: - res = book.author_sort - elif col == 3: - res = book.publisher - elif col == 4: - res = book.isbn - elif col == 5: - if hasattr(book.pubdate, 'timetuple'): - res = strftime('%b %Y', book.pubdate.timetuple()) - if not res: + res = self.data_as_text(book, col) + if not (col <= 5 and res): return NONE return QVariant(res) elif role == Qt.DecorationRole: @@ -100,6 +109,16 @@ class Matches(QAbstractTableModel): return self.yes_icon return NONE + def sort(self, col, order, reset=True): + if not self.matches: + return + descending = order == Qt.DescendingOrder + self.matches.sort(None, + lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))), + descending) + if reset: + self.reset() + class FetchMetadata(QDialog, Ui_FetchMetadata): HANG_TIME = 75 #seconds @@ -136,6 +155,11 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.connect(self.matches, SIGNAL('entered(QModelIndex)'), self.show_summary) self.matches.setMouseTracking(True) + # Enabling sorting and setting a sort column will not change the initial + # order of the results, as they are filled in later + self.matches.setSortingEnabled(True) + QObject.connect(self.matches.horizontalHeader(), SIGNAL('sectionClicked(int)'), self.show_sort_indicator) + self.matches.horizontalHeader().setSortIndicatorShown(False) self.fetch_metadata() self.opt_get_social_metadata.setChecked(config['get_social_metadata']) self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata']) @@ -243,3 +267,6 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): def chosen(self, index): self.matches.setCurrentIndex(index) self.accept() + + def show_sort_indicator(self): + self.matches.horizontalHeader().setSortIndicatorShown(True) From ef1fc13d570d3b9f7fe0767ced659403807c3f5e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 15:56:19 -0700 Subject: [PATCH 03/17] ... --- src/calibre/gui2/viewer/documentview.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index d5f881d4b4..683951400c 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -3,8 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' -''' -''' +# Imports {{{ import os, math, re, glob, sys from base64 import b64encode from functools import partial @@ -24,6 +23,8 @@ from calibre.constants import iswindows from calibre import prints, guess_type from calibre.gui2.viewer.keys import SHORTCUTS +# }}} + bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \ hyphenator = images = hyphen_pats = None @@ -33,6 +34,7 @@ def load_builtin_fonts(): QFontDatabase.addApplicationFont(f) return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono' +# Config {{{ def config(defaults=None): desc = _('Options to customize the ebook viewer') if defaults is None: @@ -137,8 +139,9 @@ class ConfigDialog(QDialog, Ui_Dialog): str(self.hyphenate_default_lang.itemData(idx).toString())) return QDialog.accept(self, *args) +# }}} -class Document(QWebPage): +class Document(QWebPage): # {{{ def set_font_settings(self): opts = config().parse() @@ -449,7 +452,9 @@ class Document(QWebPage): self.height+amount) self.setPreferredContentsSize(s) -class EntityDeclarationProcessor(object): +# }}} + +class EntityDeclarationProcessor(object): # {{{ def __init__(self, html): self.declared_entities = {} @@ -460,8 +465,9 @@ class EntityDeclarationProcessor(object): self.processed_html = html for key, val in self.declared_entities.iteritems(): self.processed_html = self.processed_html.replace('&%s;'%key, val) +# }}} -class DocumentView(QWebView): +class DocumentView(QWebView): # {{{ DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern) @@ -961,4 +967,5 @@ class DocumentView(QWebView): self.manager.scrolled(self.scroll_fraction) return ret +# }}} From e414c67486b700313b0c4b02c550505c7ea49a9d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 17:47:04 -0700 Subject: [PATCH 04/17] ... --- src/calibre/gui2/dialogs/metadata_single.ui | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index dfa8c45797..6d31342dcf 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -7,7 +7,7 @@ 0 0 994 - 726 + 716 @@ -44,7 +44,7 @@ 0 0 986 - 687 + 677 @@ -495,6 +495,22 @@ Using this button to create author sort will change author sort from red to gree + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 40 + + + + From 4a41f77eb32fa7f9ecd0273b763e2f4b48251d95 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 17:51:46 -0700 Subject: [PATCH 05/17] Preliminary code for touchscreen swipe based page flipping --- src/calibre/gui2/viewer/documentview.py | 25 ++++++++++ src/calibre/gui2/viewer/gestures.py | 61 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/calibre/gui2/viewer/gestures.py diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 683951400c..343d85e63e 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -18,6 +18,7 @@ from calibre.utils.config import Config, StringConfig from calibre.utils.localization import get_language from calibre.gui2.viewer.config_ui import Ui_Dialog from calibre.gui2.viewer.flip import SlideFlip +from calibre.gui2.viewer.gestures import Gestures from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig from calibre.constants import iswindows from calibre import prints, guess_type @@ -474,6 +475,7 @@ class DocumentView(QWebView): # {{{ def __init__(self, *args): QWebView.__init__(self, *args) self.flipper = SlideFlip(self) + self.gestures = Gestures() self.is_auto_repeat_event = False self.debug_javascript = False self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') @@ -959,6 +961,29 @@ class DocumentView(QWebView): # {{{ self.manager.viewport_resized(self.scroll_fraction) return ret + def event(self, ev): + typ = ev.type() + if typ == ev.TouchBegin: + try: + self.gestures.start_gesture('touch', ev) + except: + import traceback + traceback.print_exc() + elif typ == ev.TouchEnd: + try: + gesture = self.gestures.end_gesture('touch', ev, self.rect()) + except: + import traceback + traceback.print_exc() + if gesture is not None: + ev.accept() + if gesture == 'lineleft': + self.next_page() + elif gesture == 'lineright': + self.previous_page() + return True + return QWebView.event(self, ev) + def mouseReleaseEvent(self, ev): opos = self.document.ypos ret = QWebView.mouseReleaseEvent(self, ev) diff --git a/src/calibre/gui2/viewer/gestures.py b/src/calibre/gui2/viewer/gestures.py new file mode 100644 index 0000000000..86d2f842b9 --- /dev/null +++ b/src/calibre/gui2/viewer/gestures.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import time + +class Gestures(object): + + def __init__(self): + self.in_progress = {} + + def get_boundary_point(self, event): + t = time.time() + id_ = None + if hasattr(event, 'touchPoints'): + tps = list(event.touchPoints()) + tp = None + for t in tps: + if t.isPrimary(): + tp = t + break + if tp is None: + tp = tps[0] + gp, p = tp.screenPos(), tp.pos() + id_ = tp.id() + else: + gp, p = event.globalPos(), event.pos() + return (t, gp, p, id_) + + def start_gesture(self, typ, event): + self.in_progress[typ] = self.get_boundary_point(event) + + def is_in_progress(self, typ): + return typ in self.in_progress + + def end_gesture(self, typ, event, widget_rect): + if not self.is_in_progress(typ): + return + start = self.in_progress[typ] + end = self.get_boundary_point(event) + if start[3] != end[3]: + return + timespan = end[0] - start[0] + start_pos, end_pos = start[1], end[1] + xspan = end_pos.x() - start_pos.x() + yspan = end_pos.y() - start_pos.y() + + width = widget_rect.width() + + if timespan < 1.1 and abs(xspan) >= width/5. and \ + abs(yspan) < abs(xspan)/5.: + # Quick horizontal gesture + return 'line'+('left' if xspan < 0 else 'right') + + return None + + + From 6abc12cf18764e6c052ace6fedf7c9591c827352 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 18:56:36 -0700 Subject: [PATCH 06/17] Fix #7995 (fonta replacement through extra css stopped working) --- src/calibre/ebooks/oeb/transforms/filenames.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index 46f9fc5539..bad75b9a6f 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import posixpath -from urlparse import urldefrag +from urlparse import urldefrag, urlparse from lxml import etree import cssutils @@ -67,6 +67,10 @@ class RenameFiles(object): # {{{ def url_replacer(self, orig_url): url = urlnormalize(orig_url) + parts = urlparse(url) + if parts.scheme: + # Only rewrite local URLs + return orig_url path, frag = urldefrag(url) if self.renamed_items_map: orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item) From 523185f7a94fc5a6eef32716dc55d10c6f941de1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 19:16:10 -0700 Subject: [PATCH 07/17] Conversion pipeline: Fix broken link rewriting for inline CSS embedded in HTML --- src/calibre/ebooks/oeb/base.py | 51 ++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 0f364b8030..d1bd1cab78 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -11,12 +11,11 @@ import os, re, uuid, logging from mimetypes import types_map from collections import defaultdict from itertools import count -from urlparse import urldefrag, urlparse, urlunparse +from urlparse import urldefrag, urlparse, urlunparse, urljoin from urllib import unquote as urlunquote -from urlparse import urljoin from lxml import etree, html -from cssutils import CSSParser +from cssutils import CSSParser, parseString, parseStyle, replaceUrls from cssutils.css import CSSRule import calibre @@ -88,11 +87,11 @@ def XLINK(name): def CALIBRE(name): return '{%s}%s' % (CALIBRE_NS, name) -_css_url_re = re.compile(r'url\((.*?)\)', re.I) +_css_url_re = re.compile(r'url\s*\((.*?)\)', re.I) _css_import_re = re.compile(r'@import "(.*?)"') _archive_re = re.compile(r'[^ ]+') -def iterlinks(root): +def iterlinks(root, find_links_in_css=True): ''' Iterate over all links in a OEB Document. @@ -134,6 +133,8 @@ def iterlinks(root): yield (el, attr, attribs[attr], 0) + if not find_links_in_css: + continue if tag == XHTML('style') and el.text: for match in _css_url_re.finditer(el.text): yield (el, None, match.group(1), match.start(1)) @@ -180,7 +181,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): ''' if resolve_base_href: resolve_base_href(root) - for el, attrib, link, pos in iterlinks(root): + for el, attrib, link, pos in iterlinks(root, find_links_in_css=False): new_link = link_repl_func(link.strip()) if new_link == link: continue @@ -203,6 +204,44 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): new = cur[:pos] + new_link + cur[pos+len(link):] el.attrib[attrib] = new + def set_property(v): + if v.CSS_PRIMITIVE_VALUE == v.cssValueType and \ + v.CSS_URI == v.primitiveType: + v.setStringValue(v.CSS_URI, + link_repl_func(v.getStringValue())) + + for el in root.iter(): + try: + tag = el.tag + except UnicodeDecodeError: + continue + + if tag == XHTML('style') and el.text and \ + (_css_url_re.search(el.text) is not None or '@import' in + el.text): + stylesheet = parseString(el.text) + replaceUrls(stylesheet, link_repl_func) + el.text = '\n'+stylesheet.cssText + '\n' + + if 'style' in el.attrib: + text = el.attrib['style'] + if _css_url_re.search(text) is not None: + stext = parseStyle(text) + changed = False + for p in stext.getProperties(all=True): + v = p.cssValue + if v.CSS_VALUE_LIST == v.cssValueType: + for item in v: + changed = True + set_property(item) + elif v.CSS_PRIMITIVE_VALUE == v.cssValueType: + changed = True + set_property(v) + if changed: + el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r', + ' ') + + EPUB_MIME = types_map['.epub'] XHTML_MIME = types_map['.xhtml'] From be17dd8e4b9cc128af685e6d547add6aed18cbb0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Dec 2010 19:17:29 -0700 Subject: [PATCH 08/17] ... --- src/calibre/ebooks/oeb/base.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index d1bd1cab78..35d565606d 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -227,18 +227,14 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False): text = el.attrib['style'] if _css_url_re.search(text) is not None: stext = parseStyle(text) - changed = False for p in stext.getProperties(all=True): v = p.cssValue if v.CSS_VALUE_LIST == v.cssValueType: for item in v: - changed = True set_property(item) elif v.CSS_PRIMITIVE_VALUE == v.cssValueType: - changed = True set_property(v) - if changed: - el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r', + el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r', ' ') From d59a20fc2a2d9ec6e4d5abc17324631f845a047d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 10:39:14 -0700 Subject: [PATCH 09/17] Tag browser: When renaming items dont reset the library view and try not to scroll the Tag Browser itself --- src/calibre/gui2/tag_view.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 478f6b042f..3d43d49a75 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -695,8 +695,10 @@ class TagsModel(QAbstractItemModel): # {{{ def setData(self, index, value, role=Qt.EditRole): if not index.isValid(): return NONE - # set up to position at the category label - path = self.path_for_index(self.parent(index)) + # set up to reposition at the same item. We can do this except if + # working with the last item and that item is deleted, in which case + # we position at the parent label + path = index.model().path_for_index(index) val = unicode(value.toString()) if not val: error_dialog(self.tags_view, _('Item is blank'), @@ -947,18 +949,22 @@ class TagBrowserMixin(object): # {{{ for old_id in to_rename[text]: rename_func(old_id, new_name=unicode(text)) - # Clean up everything, as information could have changed for many books. - self.library_view.model().refresh() - self.tags_view.set_new_model() - self.tags_view.recount() - self.saved_search.clear() - self.search.clear() + # Clean up the library view + self.do_tag_item_renamed() + self.tags_view.set_new_model() # does a refresh for free def do_tag_item_renamed(self): # Clean up library view and search - self.library_view.model().refresh() - self.saved_search.clear() - self.search.clear() + # get information to redo the selection + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + m = self.library_view.model() + ids = [m.id(r) for r in rows] + + m.refresh(reset=False) + m.research() + self.library_view.select_rows(ids) + # refreshing the tags view happens at the emit()/call() site def do_author_sort_edit(self, parent, id): db = self.library_view.model().db From a722155d7638803838f8a2f90178afcf252d4063 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 10:52:36 -0700 Subject: [PATCH 10/17] oops --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 9cb9f7bbbc..c2588f57a8 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -809,7 +809,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.pubdate.setDate(QDate(dt.year, dt.month, dt.day)) summ = book.comments if summ: - prefix = self.comment.html + prefix = self.comments.html if prefix: prefix += '\n' self.comments.html = prefix + comments_to_html(summ) From bf661325db67fe0394e80e83dea8d1db51c43eba Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 11:01:48 -0700 Subject: [PATCH 11/17] ... --- resources/recipes/le_monde.recipe | 2 +- src/calibre/ebooks/oeb/base.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/recipes/le_monde.recipe b/resources/recipes/le_monde.recipe index 18be6ca711..c14b8eeeff 100644 --- a/resources/recipes/le_monde.recipe +++ b/resources/recipes/le_monde.recipe @@ -4,7 +4,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe class LeMonde(BasicNewsRecipe): title = 'Le Monde' __author__ = 'veezh' - description = 'Actualités' + description = u'Actualit\xe9s' oldest_article = 1 max_articles_per_feed = 100 no_stylesheets = True diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 35d565606d..c015868992 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -657,7 +657,10 @@ class Metadata(object): attrib[key] = prefixname(value, nsrmap) if namespace(self.term) == DC11_NS: elem = element(parent, self.term, attrib=attrib) - elem.text = self.value + try: + elem.text = self.value + except: + elem.text = repr(self.value) else: elem = element(parent, OPF('meta'), attrib=attrib) elem.attrib['name'] = prefixname(self.term, nsrmap) From cea5261e1207593889ef2be1165de11852b9b1bd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 11:05:12 -0700 Subject: [PATCH 12/17] NRC EPUB version by veezh --- resources/recipes/nrc-nl-epub.recipe | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 resources/recipes/nrc-nl-epub.recipe diff --git a/resources/recipes/nrc-nl-epub.recipe b/resources/recipes/nrc-nl-epub.recipe new file mode 100644 index 0000000000..da9b9195ce --- /dev/null +++ b/resources/recipes/nrc-nl-epub.recipe @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#Based on Lars Jacob's Taz Digiabo recipe + +__license__ = 'GPL v3' +__copyright__ = '2010, veezh' + +''' +www.nrc.nl +''' +import os, urllib2, zipfile +import time +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ptempfile import PersistentTemporaryFile + + +class NRCHandelsblad(BasicNewsRecipe): + + title = u'NRC Handelsblad' + description = u'De EPUB-versie van NRC' + language = 'nl' + lang = 'nl-NL' + + __author__ = 'veezh' + + conversion_options = { + 'no_default_epub_cover' : True + } + + def build_index(self): + today = time.strftime("%Y%m%d") + domain = "http://digitaleeditie.nrc.nl" + + url = domain + "/digitaleeditie/helekrant/epub/nrc_" + today + ".epub" +# print url + + try: + f = urllib2.urlopen(url) + except urllib2.HTTPError: + self.report_progress(0,_('Kan niet inloggen om editie te downloaden')) + raise ValueError('Krant van vandaag nog niet beschikbaar') + + tmp = PersistentTemporaryFile(suffix='.epub') + self.report_progress(0,_('downloading epub')) + tmp.write(f.read()) + tmp.close() + + zfile = zipfile.ZipFile(tmp.name, 'r') + self.report_progress(0,_('extracting epub')) + + zfile.extractall(self.output_dir) + + tmp.close() + index = os.path.join(self.output_dir, 'content.opf') + + self.report_progress(1,_('epub downloaded and extracted')) + + return index From 1a0cbaaf522d7046a507adeed84327c4b925fd4c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 11:09:00 -0700 Subject: [PATCH 13/17] Fix #8017 (WSJ Not Working) --- resources/recipes/wsj.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe index 88e07bcea3..4ce315200c 100644 --- a/resources/recipes/wsj.recipe +++ b/resources/recipes/wsj.recipe @@ -46,7 +46,7 @@ class WallStreetJournal(BasicNewsRecipe): br = BasicNewsRecipe.get_browser() if self.username is not None and self.password is not None: br.open('http://commerce.wsj.com/auth/login') - br.select_form(nr=0) + br.select_form(nr=1) br['user'] = self.username br['password'] = self.password res = br.submit() From f182953c354774f065499bdf95cb853894952f5c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 11:53:20 -0700 Subject: [PATCH 14/17] Add an entry to the menu for the calibre library to pick a random book. Fixes #8010 (random book) --- src/calibre/gui2/actions/choose_library.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index b1f0bd6b0e..6f4e883b1a 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -166,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction): self.choose_menu = QMenu(self.gui) self.qaction.setMenu(self.choose_menu) + if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None): self.choose_menu.addAction(self.action_choose) @@ -176,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction): self.delete_menu = QMenu(_('Delete library')) self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu) + ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png', + None, None), attr='action_pick_random') + ac.triggered.connect(self.pick_random) + self.choose_menu.addAction(ac) + self.rename_separator = self.choose_menu.addSeparator() self.switch_actions = [] @@ -213,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction): self.maintenance_menu.addAction(ac) self.choose_menu.addMenu(self.maintenance_menu) + def pick_random(self, *args): + import random + pick = random.randint(0, self.gui.library_view.model().rowCount(None)) + self.gui.library_view.set_current_row(pick) + self.gui.library_view.scroll_to_row(pick) + def library_name(self): db = self.gui.library_view.model().db path = db.library_path From c1e01e107b5e55c8b97a518c856ac23b208829d1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 12:57:24 -0700 Subject: [PATCH 15/17] OTA instruction for iBooks --- src/calibre/manual/faq.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 55451206b6..b05d4d2a5a 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -181,16 +181,17 @@ How do I use |app| with my iPad/iPhone/iTouch? Over the air ^^^^^^^^^^^^^^ -The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air. - -First perform the following steps in |app| +The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the calibre sontent server, which makes your collection available over the net. First perform the following steps in |app| * Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`) * Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->Page Setup` * Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button. * Turn on the Content Server in |app|'s preferences and leave |app| running. -Install the free Stanza reader app on your iPad/iPhone/iTouch using iTunes. +Now on your iPad/iPhone you have two choices, use either iBooks (version 1.2 and later) or Stanza (version 3.0 and later). Both are available free from the app store. + +Using Stanza +*************** Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following:: @@ -200,6 +201,18 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout. +Using iBooks +************** + +Start the Safari browser and type in the IP address and port of the computer running the calibre server, like this:: + + http://192.168.1.2:8080/ + +Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you have changed the port the |app| content server is running on, you will have to change ``8080`` as well to the new port. The local IP address is the IP address you computer is assigned on your home network. A quick Google search will tell you how to find out your local IP address. + +You wills ee a list of books in Safari, just click on the epub link for whichever book you want to read, Safari will then prompt you to open it with iBooks. + + With the USB cable ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From d93b5395ada27625924a3eb19db963cdc5ac173c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 15:37:19 -0700 Subject: [PATCH 16/17] Fix #7930 (BusinessWeek not fetching) --- resources/recipes/bwmagazine.recipe | 130 ++++++++++++++++++---------- 1 file changed, 85 insertions(+), 45 deletions(-) diff --git a/resources/recipes/bwmagazine.recipe b/resources/recipes/bwmagazine.recipe index 26dbc459d3..e3a4e3337a 100644 --- a/resources/recipes/bwmagazine.recipe +++ b/resources/recipes/bwmagazine.recipe @@ -1,64 +1,104 @@ - __license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' +__copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic ' ''' -http://www.businessweek.com/magazine/news/articles/business_news.htm +www.businessweek.com ''' -from calibre import strftime +import re from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import BeautifulSoup -class BWmagazine(BasicNewsRecipe): - title = 'BusinessWeek Magazine' - __author__ = 'Darko Miletic' - description = 'Stay up to date with BusinessWeek magazine articles. Read news on international business, personal finances & the economy in the BusinessWeek online magazine.' +class BusinessWeek(BasicNewsRecipe): + title = 'Business Week' + __author__ = 'Kovid Goyal and Darko Miletic' + description = 'Read the latest international business news & stock market news. Get updated company profiles, financial advice, global economy and technology news.' publisher = 'Bloomberg L.P.' - category = 'news, International Business News, current news in international business,international business articles, personal business, business week magazine, business week magazine articles, business week magazine online, business week online magazine' - oldest_article = 10 - max_articles_per_feed = 100 + category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news' + oldest_article = 7 + max_articles_per_feed = 200 no_stylesheets = True - encoding = 'utf-8' + encoding = 'utf8' use_embedded_content = False language = 'en' - INDEX = 'http://www.businessweek.com/magazine/news/articles/business_news.htm' + remove_empty_feeds = True + publication_type = 'magazine' cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg' - + masthead_url = 'http://assets.businessweek.com/images/bw-logo.png' + extra_css = """ + body{font-family: Helvetica,Arial,sans-serif } + img{margin-bottom: 0.4em; display:block} + .tagline{color: gray; font-style: italic} + .photoCredit{font-size: small; color: gray} + """ conversion_options = { - 'comment' : description - , 'tags' : category - , 'publisher' : publisher - , 'language' : language + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language } + remove_tags = [ + dict(attrs={'class':'inStory'}) + ,dict(name=['meta','link','iframe','base','embed','object','table','th','tr','td']) + ,dict(attrs={'id':['inset','videoDisplay']}) + ] + keep_only_tags = [dict(name='div', attrs={'id':['story-body','storyBody']})] + remove_attributes = ['lang'] + match_regexps = [r'http://www.businessweek.com/.*_page_[1-9].*'] - def parse_index(self): - articles = [] - soup = self.index_to_soup(self.INDEX) - ditem = soup.find('div',attrs={'id':'column2'}) - if ditem: - for item in ditem.findAll('h3'): - title_prefix = '' - description = '' - feed_link = item.find('a') - if feed_link and feed_link.has_key('href'): - url = 'http://www.businessweek.com/magazine/' + feed_link['href'].partition('../../')[2] - title = title_prefix + self.tag_to_string(feed_link) - date = strftime(self.timefmt) - articles.append({ - 'title' :title - ,'date' :date - ,'url' :url - ,'description':description - }) - return [(soup.head.title.string, articles)] - keep_only_tags = dict(name='div', attrs={'id':'storyBody'}) + feeds = [ + (u'Top Stories', u'http://www.businessweek.com/topStories/rss/topStories.rss'), + (u'Top News' , u'http://www.businessweek.com/rss/bwdaily.rss' ), + (u'Asia', u'http://www.businessweek.com/rss/asia.rss'), + (u'Autos', u'http://www.businessweek.com/rss/autos/index.rss'), + (u'Classic Cars', u'http://rss.businessweek.com/bw_rss/classiccars'), + (u'Hybrids', u'http://rss.businessweek.com/bw_rss/hybrids'), + (u'Europe', u'http://www.businessweek.com/rss/europe.rss'), + (u'Auto Reviews', u'http://rss.businessweek.com/bw_rss/autoreviews'), + (u'Innovation & Design', u'http://www.businessweek.com/rss/innovate.rss'), + (u'Architecture', u'http://www.businessweek.com/rss/architecture.rss'), + (u'Brand Equity', u'http://www.businessweek.com/rss/brandequity.rss'), + (u'Auto Design', u'http://www.businessweek.com/rss/carbuff.rss'), + (u'Game Room', u'http://rss.businessweek.com/bw_rss/gameroom'), + (u'Technology', u'http://www.businessweek.com/rss/technology.rss'), + (u'Investing', u'http://rss.businessweek.com/bw_rss/investor'), + (u'Small Business', u'http://www.businessweek.com/rss/smallbiz.rss'), + (u'Careers', u'http://rss.businessweek.com/bw_rss/careers'), + (u'B-Schools', u'http://www.businessweek.com/rss/bschools.rss'), + (u'Magazine Selections', u'http://www.businessweek.com/rss/magazine.rss'), + (u'CEO Guide to Tech', u'http://www.businessweek.com/rss/ceo_guide_tech.rss'), + ] + + def get_article_url(self, article): + url = article.get('guid', None) + if 'podcasts' in url: + return None + if 'surveys' in url: + return None + if 'images' in url: + return None + if 'feedroom' in url: + return None + if '/magazine/toc/' in url: + return None + rurl, sep, rest = url.rpartition('?') + if rurl: + return rurl + return rest def print_version(self, url): - rurl = url.rpartition('?')[0] - if rurl == '': - rurl = url - return rurl.replace('.com/magazine/','.com/print/magazine/') - - + if '/news/' in url or '/blog/ in url': + return url + rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/') + return rurl.replace('/investing/','/investor/') + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + for alink in soup.findAll('a'): + if alink.string is not None: + tstr = alink.string + alink.replaceWith(tstr) + return soup \ No newline at end of file From 6e4be52e9497517f3017f1e0c58cb90330993e4c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 22 Dec 2010 15:56:40 -0700 Subject: [PATCH 17/17] ... --- resources/recipes/bwmagazine.recipe | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/resources/recipes/bwmagazine.recipe b/resources/recipes/bwmagazine.recipe index e3a4e3337a..9a1f10a680 100644 --- a/resources/recipes/bwmagazine.recipe +++ b/resources/recipes/bwmagazine.recipe @@ -4,9 +4,7 @@ __copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic