From 25a4310fb90ccd8639f89e99cdda2387a302e2f0 Mon Sep 17 00:00:00 2001 From: "ken@szboeye.com" <> Date: Tue, 3 May 2011 14:24:21 +0800 Subject: [PATCH 01/19] Add support for the Boeye Digital Reader . --- src/calibre/customize/builtins.py | 3 + src/calibre/customize/profiles.py | 89 ++++++++++++++++++++++++++- src/calibre/devices/boeye/__init__.py | 0 src/calibre/devices/boeye/driver.py | 57 +++++++++++++++++ 4 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 src/calibre/devices/boeye/__init__.py create mode 100644 src/calibre/devices/boeye/driver.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 776b04d5f6..c1da8391e0 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -613,6 +613,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \ from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.kobo.driver import KOBO from calibre.devices.bambook.driver import BAMBOOK +from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX from calibre.ebooks.epub.fix.unmanifested import Unmanifested @@ -743,6 +744,8 @@ plugins += [ EEEREADER, NEXTBOOK, ITUNES, + BOEYE_BEX, + BOEYE_BDX, USER_DEFINED, ] plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 5c29f1e79b..e771a36c2e 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -214,11 +214,51 @@ class NookInput(InputProfile): dpi = 167 fbase = 16 fsizes = [12, 12, 14, 16, 18, 20, 22, 24] + +class BoeyeG5Input(InputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G5' + short_name = 'boeyeg5' + description = _('This profile is intended for the Boeye G5.') + + # Screen size is a best guess + screen_size = (600, 800) + dpi = 200 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + +class BoeyeG6Input(InputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G6' + short_name = 'boeyeg6' + description = _('This profile is intended for the Boeye G6.') + + # Screen size is a best guess + screen_size = (600, 800) + dpi = 166.66 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + +class BoeyeG10Input(InputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G10' + short_name = 'boeyeg10' + description = _('This profile is intended for the Boeye G10.') + + # Screen size is a best guess + screen_size = (825, 1200) + dpi = 150 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + input_profiles = [InputProfile, SonyReaderInput, SonyReader300Input, SonyReader900Input, MSReaderInput, MobipocketInput, HanlinV3Input, HanlinV5Input, CybookG3Input, CybookOpusInput, KindleInput, IlliadInput, - IRexDR1000Input, IRexDR800Input, NookInput] + IRexDR1000Input, IRexDR800Input, NookInput, BoeyeG5Input, BoeyeG6Input, BoeyeG10Input] input_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) @@ -730,6 +770,50 @@ class BambookOutput(OutputProfile): dpi = 168.451 fbase = 12 fsizes = [10, 12, 14, 16] + +class BoeyeG5Output(OutputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G5' + short_name = 'boeyeg5' + description = _('This profile is intended for the Boeye Digital Reader G5.') + + # Screen size is a best guess + screen_size = (600, 800) + comic_screen_size = (600, 740) + dpi = 200 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + + +class BoeyeG6Output(OutputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G6' + short_name = 'boeyeg6' + description = _('This profile is intended for the Boeye Digital Reader G6.') + + # Screen size is a best guess + screen_size = (600, 800) + comic_screen_size = (600, 740) + dpi = 160 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + +class BoeyeG10Output(OutputProfile): + + author = 'Ken' + name = 'Boeye Digital Reader G10' + short_name = 'boeyeg10' + description = _('This profile is intended for the Boeye Digital Reader G10.') + + # Screen size is a best guess + screen_size = (825, 1200) + comic_screen_size = (824, 1140) + dpi = 150 + fbase = 16 + fsizes = [12, 14, 16, 18, 20, 22, 24] + output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output, @@ -737,6 +821,7 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, - BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge] + BambookOutput, NookColorOutput, BoeyeG5Output, BoeyeG6Output, BoeyeG10Output, + GenericEink, GenericEinkLarge] output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) diff --git a/src/calibre/devices/boeye/__init__.py b/src/calibre/devices/boeye/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/devices/boeye/driver.py b/src/calibre/devices/boeye/driver.py new file mode 100644 index 0000000000..fcde1653d1 --- /dev/null +++ b/src/calibre/devices/boeye/driver.py @@ -0,0 +1,57 @@ +__license__ = 'GPL v3' +__copyright__ = '2011, Ken ' +__docformat__ = 'restructuredtext en' + +''' +Device driver for BOEYE serial readers +''' + +import re +from calibre.devices.usbms.driver import USBMS + +class BOEYE_BEX(USBMS): + name = 'BOEYE BEX reader driver' + gui_name = 'BOEYE BEX' + description = _('Communicate with BOEYE BEX Serial eBook readers.') + author = 'szboeye' + supported_platforms = ['windows', 'osx', 'linux'] + + FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb'] + + VENDOR_ID = [0x0085] + PRODUCT_ID = [0x600] + + VENDOR_NAME = 'LINUX' + WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET' + OSX_MAIN_MEM = 'Linux File-Stor Gadget Media' + + MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BEX Storage Card' + + EBOOK_DIR_MAIN = 'Documents' + SUPPORTS_SUB_DIRS = True + +class BOEYE_BDX(USBMS): + name = 'BOEYE BDX reader driver' + gui_name = 'BOEYE BDX' + description = _('Communicate with BOEYE BDX serial eBook readers.') + author = 'szboeye' + supported_platforms = ['windows', 'osx', 'linux'] + + FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb'] + + VENDOR_ID = [0x0085] + PRODUCT_ID = [0x800] + + VENDOR_NAME = 'LINUX' + WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET' + WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' + + OSX_MAIN_MEM = 'Linux File-Stor Gadget Media' + OSX_CARD_A_MEM = 'Linux File-Stor Gadget Media' + + MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BDX Internal Memory' + STORAGE_CARD_VOLUME_LABEL = 'BOEYE BDX Storage Card' + + EBOOK_DIR_MAIN = 'Documents' + EBOOK_DIR_CARD_A = 'Documents' + SUPPORTS_SUB_DIRS = True From 731ff8eac4aef3bf27472f27f7d9d97c8d7d34cf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 3 May 2011 09:39:41 -0600 Subject: [PATCH 02/19] ... --- recipes/f_secure.recipe | 1 - 1 file changed, 1 deletion(-) diff --git a/recipes/f_secure.recipe b/recipes/f_secure.recipe index f276a4961a..5a03f01ac8 100644 --- a/recipes/f_secure.recipe +++ b/recipes/f_secure.recipe @@ -12,7 +12,6 @@ class AdvancedUserRecipe1301860159(BasicNewsRecipe): max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False - language = 'en_EN' remove_javascript = True keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})] remove_tags = [dict(name='a'),dict(name='hr')] From 72120ed403490d4a9938b67f476639d92f49fd13 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 3 May 2011 10:15:46 -0600 Subject: [PATCH 03/19] Ensure the Preferences menu in OSX has an action to launch the preferences --- src/calibre/gui2/actions/preferences.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py index 24d20b23f9..b65967e994 100644 --- a/src/calibre/gui2/actions/preferences.py +++ b/src/calibre/gui2/actions/preferences.py @@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt from calibre.gui2.actions import InterfaceAction from calibre.gui2.preferences.main import Preferences from calibre.gui2 import error_dialog -from calibre.constants import DEBUG +from calibre.constants import DEBUG, isosx class PreferencesAction(InterfaceAction): @@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction): def genesis(self): pm = QMenu() - pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config) + acname = _('Change calibre behavior') if isosx else _('Preferences') + pm.addAction(QIcon(I('config.png')), acname, self.do_config) pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'), self.gui.run_wizard) if not DEBUG: From 62892d7161d414f18b62bab766f15ad5651ce47f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 3 May 2011 14:10:35 -0600 Subject: [PATCH 04/19] Add Get Books action to main toolbar by default --- src/calibre/gui2/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 60d2a0a7dd..1dfe1d8d14 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -34,7 +34,7 @@ if isosx: ) gprefs.defaults['action-layout-toolbar'] = ( 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, - 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', + 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk', 'Connect Share', None, 'Remove Books', ) gprefs.defaults['action-layout-toolbar-device'] = ( @@ -48,7 +48,7 @@ else: gprefs.defaults['action-layout-menubar-device'] = () gprefs.defaults['action-layout-toolbar'] = ( 'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None, - 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk', + 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk', 'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences', ) gprefs.defaults['action-layout-toolbar-device'] = ( From b9973c45404f0457684bbbfa1daf0f143bd492a8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 3 May 2011 21:19:47 +0100 Subject: [PATCH 05/19] Add an 'all metadata' edit metadata variant --- src/calibre/gui2/metadata/single.py | 136 ++++++++++++++++++++++- src/calibre/gui2/preferences/behavior.py | 3 +- 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 63d4499966..e85a0adc13 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -13,7 +13,7 @@ from functools import partial from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, - QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu) + QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu, QLabel) from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data @@ -198,7 +198,7 @@ class MetadataSingleDialogBase(ResizableDialog): ans = self.custom_metadata_widgets for i in range(len(ans)-1): if before is not None and i == 0: - pass# Do something + pass if len(ans[i+1].widgets) == 2: sto(ans[i].widgets[-1], ans[i+1].widgets[1]) else: @@ -206,7 +206,7 @@ class MetadataSingleDialogBase(ResizableDialog): for c in range(2, len(ans[i].widgets), 2): sto(ans[i].widgets[c-1], ans[i].widgets[c+1]) if after is not None: - pass # Do something + pass # }}} def do_view_format(self, path, fmt): @@ -728,7 +728,135 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{ # }}} -editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1} +class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{ + + cc_two_column = False + one_line_comments_toolbar = True + + def do_layout(self): + self.central_widget.clear() + self.labels = [] + sto = QWidget.setTabOrder + + self.central_widget.tabBar().setVisible(False) + tab0 = QWidget(self) + self.central_widget.addTab(tab0, _("&Metadata")) + l = QGridLayout() + tab0.setLayout(l) + + # Basic metadata in col 0 + tl = QGridLayout() + gb = QGroupBox(_('Basic metadata'), tab0) + l.addWidget(gb, 0, 0, 1, 1) + gb.setLayout(tl) + + self.button_box.addButton(self.fetch_metadata_button, + QDialogButtonBox.ActionRole) + self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly) + self.config_metadata_button.setText(_('Configure metadata downloading')) + self.button_box.addButton(self.config_metadata_button, + QDialogButtonBox.ActionRole) + sto(self.button_box, self.title) + + def create_row(row, widget, tab_to, button=None, icon=None, span=1): + ql = BuddyLabel(widget) + tl.addWidget(ql, row, 1, 1, 1) + tl.addWidget(widget, row, 2, 1, 1) + if button is not None: + tl.addWidget(button, row, 3, span, 1) + if icon is not None: + button.setIcon(QIcon(I(icon))) + if tab_to is not None: + if button is not None: + sto(widget, button) + sto(button, tab_to) + else: + sto(widget, tab_to) + + tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1) + + create_row(0, self.title, self.title_sort, + button=self.deduce_title_sort_button, span=2, + icon='auto_author_sort.png') + create_row(1, self.title_sort, self.authors) + create_row(2, self.authors, self.author_sort, + button=self.deduce_author_sort_button, + span=2, icon='auto_author_sort.png') + create_row(3, self.author_sort, self.series) + create_row(4, self.series, self.series_index, + button=self.remove_unused_series_button, icon='trash.png') + create_row(5, self.series_index, self.tags) + create_row(6, self.tags, self.rating, button=self.tags_editor_button) + create_row(7, self.rating, self.pubdate) + create_row(8, self.pubdate, self.publisher, + button=self.pubdate.clear_button, icon='trash.png') + create_row(9, self.publisher, self.timestamp) + create_row(10, self.timestamp, self.identifiers, + button=self.timestamp.clear_button, icon='trash.png') + create_row(11, self.identifiers, self.comments, + button=self.clear_identifiers_button, icon='trash.png') + tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), + 12, 1, 1 ,1) + + # Custom metadata in col 1 + w = getattr(self, 'custom_metadata_widgets_parent', None) + if w is not None: + gb = QGroupBox(_('Custom metadata'), tab0) + gbl = QVBoxLayout() + gb.setLayout(gbl) + sr = QScrollArea(gb) + sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + sr.setWidgetResizable(True) + sr.setBackgroundRole(QPalette.Base) + sr.setFrameStyle(QFrame.NoFrame) + sr.setWidget(w) + gbl.addWidget(sr) + l.addWidget(gb, 0, 1, 1, 1) + sp = QSizePolicy() + sp.setVerticalStretch(10) + sp.setHorizontalPolicy(QSizePolicy.Fixed) + sp.setVerticalPolicy(QSizePolicy.Expanding) + gb.setSizePolicy(sp) + self.set_custom_metadata_tab_order() + + # comments span col 0 & 1 + w = QGroupBox(_('Comments'), tab0) + sp = QSizePolicy() + sp.setVerticalStretch(10) + sp.setHorizontalPolicy(QSizePolicy.Expanding) + sp.setVerticalPolicy(QSizePolicy.Expanding) + w.setSizePolicy(sp) + lb = QHBoxLayout() + w.setLayout(lb) + lb.addWidget(self.comments) + l.addWidget(w, 1, 0, 1, 2) + + # Cover & formats in col 3 + gb = QGroupBox(_('Cover'), tab0) + lb = QGridLayout() + gb.setLayout(lb) + lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter) + sto(self.clear_identifiers_button, self.cover.buttons[0]) + for i, b in enumerate(self.cover.buttons[:3]): + lb.addWidget(b, 1, i, 1, 1) + sto(b, self.cover.buttons[i+1]) + hl = QHBoxLayout() + for b in self.cover.buttons[3:]: + hl.addWidget(b) + sto(self.cover.buttons[-2], self.cover.buttons[-1]) + lb.addLayout(hl, 2, 0, 1, 3) + l.addWidget(gb, 0, 2, 1, 1) + l.addWidget(self.formats_manager, 1, 2, 1, 1) + sto(self.cover.buttons[-1], self.formats_manager) + + self.formats_manager.formats.setMaximumWidth(10000) + self.formats_manager.formats.setIconSize(QSize(32, 32)) + +# }}} + + +editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1, + 'alt2': MetadataSingleDialogAlt2} def edit_metadata(db, row_list, current_row, parent=None, view_slot=None, set_current_callback=None): diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py index e062ae2662..1247c54ec9 100644 --- a/src/calibre/gui2/preferences/behavior.py +++ b/src/calibre/gui2/preferences/behavior.py @@ -61,7 +61,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r('bools_are_tristate', db.prefs, restart_required=True) r = self.register - choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')] + choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1'), + (_('All on 1 tab'), 'alt2')] r('edit_metadata_single_layout', gprefs, choices=choices) def initialize(self): From 3ac69128e7899d8c24bb5c1cfbac1a91c9c26757 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 3 May 2011 16:04:59 -0600 Subject: [PATCH 06/19] Update FrazPC --- recipes/frazpc.recipe | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/recipes/frazpc.recipe b/recipes/frazpc.recipe index 56e45076ac..2d0d54d10c 100644 --- a/recipes/frazpc.recipe +++ b/recipes/frazpc.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = u'2010, Tomasz Dlugosz ' +__copyright__ = u'2010-2011, Tomasz Dlugosz ' ''' frazpc.pl ''' @@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe): use_embedded_content = False no_stylesheets = True - feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ] - - keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})] - - remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})] - - preprocess_regexps = [ - (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in - [(r'
(Skomentuj|Komentarz(e)?\([0-9]*\))  \|', lambda match: '')] + feeds = [ + (u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'), + (u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly') ] + keep_only_tags = [dict(name='div', attrs={'class':'article'})] + + remove_tags = [ + dict(name='div', attrs={'class':'title-wrapper'}), + dict(name='p', attrs={'class':'tags'}), + dict(name='p', attrs={'class':'article-links'}), + dict(name='div', attrs={'class':'comments_box'}) + ] + + preprocess_regexps = [(re.compile(r'\| Komentarze \([0-9]*\)'), lambda match: '')] + remove_attributes = [ 'width', 'height' ] From 0168ce58667ce362d62bbc4a1f524e5882f39184 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 3 May 2011 19:13:57 -0400 Subject: [PATCH 07/19] GUI: OS X fix, show books in library when device connected as menu item so user can go to their library. --- src/calibre/gui2/layout.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index b5cc0163ed..b3c9bd3a02 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{ receiver = partial(self._location_selected, name) ac.triggered.connect(receiver) self.tooltips[name] = tooltip + + m = QMenu(parent) + self._mem.append(m) + a = m.addAction(icon, tooltip) + a.triggered.connect(receiver) if name != 'library': - m = QMenu(parent) - self._mem.append(m) - a = m.addAction(icon, tooltip) - a.triggered.connect(receiver) self._mem.append(a) a = m.addAction(QIcon(I('eject.png')), _('Eject this device')) a.triggered.connect(self._eject_requested) - ac.setMenu(m) self._mem.append(a) else: ac.setToolTip(tooltip) + ac.setMenu(m) ac.calibre_name = name return ac @@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{ def set_switch_actions(self, quick_actions, rename_actions, delete_actions, switch_actions, choose_action): - self.switch_menu = QMenu() + self.switch_menu = self.library_action.menu() + if self.switch_menu: + self.switch_menu.addSeparator() + else: + self.switch_menu = QMenu() + self.switch_menu.addAction(choose_action) self.cs_menus = [] for t, acs in [(_('Quick switch'), quick_actions), @@ -85,7 +91,9 @@ class LocationManager(QObject): # {{{ self.switch_menu.addSeparator() for ac in switch_actions: self.switch_menu.addAction(ac) - self.library_action.setMenu(self.switch_menu) + + if self.switch_menu != self.library_action.menu(): + self.library_action.setMenu(self.switch_menu) def _location_selected(self, location, *args): if location != self.current_location and hasattr(self, From ce28d66991ebd825ce41ecedb68503be2c243267 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 3 May 2011 19:37:27 -0400 Subject: [PATCH 08/19] Store: Open external quick check in search dialog. --- src/calibre/gui2/store/search/search.py | 5 ++++- src/calibre/gui2/store/search/search.ui | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 5654df8ffc..07d4afca54 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -155,6 +155,7 @@ class SearchDialog(QDialog, Ui_Dialog): self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())] self.config['sort_col'] = self.results_view.model().sort_col self.config['sort_order'] = self.results_view.model().sort_order + self.config['open_external'] = self.open_external.isChecked() store_check = {} for n in self.store_plugins: @@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog): else: self.resize_columns() + self.open_external.setChecked(self.config.get('open_external', False)) + store_check = self.config.get('store_checked', None) if store_check: for n in store_check: @@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog): def open_store(self, index): result = self.results_view.model().get_result(index) - self.store_plugins[result.store_name].open(self, result.detail_item) + self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui index 0d39a70a29..7e8dd36284 100644 --- a/src/calibre/gui2/store/search/search.ui +++ b/src/calibre/gui2/store/search/search.ui @@ -70,7 +70,7 @@ 0 0 215 - 116 + 93 @@ -101,6 +101,16 @@ + + + + Open a selected book in the system's web browser + + + Open external + + + From bb0e6a60e749ad5fe9768fcee11a45b3d4ded7de Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 10:24:23 -0600 Subject: [PATCH 09/19] ... --- recipes/telepolis.recipe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/telepolis.recipe b/recipes/telepolis.recipe index 4ca57f8275..8109e3e39a 100644 --- a/recipes/telepolis.recipe +++ b/recipes/telepolis.recipe @@ -18,7 +18,7 @@ class TelepolisNews(BasicNewsRecipe): recursion = 0 no_stylesheets = True encoding = "utf-8" - language = 'de_AT' + language = 'de' use_embedded_content =False remove_empty_feeds = True From bfbd42dd6d0bc5494162cea64c981e46ab8ab8be Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 10:39:20 -0600 Subject: [PATCH 10/19] Fix USA Today --- recipes/usatoday.recipe | 397 ++-------------------------------------- 1 file changed, 20 insertions(+), 377 deletions(-) diff --git a/recipes/usatoday.recipe b/recipes/usatoday.recipe index bd47262563..a4899b7187 100644 --- a/recipes/usatoday.recipe +++ b/recipes/usatoday.recipe @@ -7,13 +7,11 @@ usatoday.com ''' from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag -import re class USAToday(BasicNewsRecipe): title = 'USA Today' - __author__ = 'GRiker' + __author__ = 'Kovid Goyal' oldest_article = 1 timefmt = '' max_articles_per_feed = 20 @@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe): margin-bottom: 0em; \ font-size: smaller;}\n \ .articleBody {text-align: left;}\n ' - conversion_options = { 'linearize_tables' : True } #simultaneous_downloads = 1 feeds = [ ('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'), @@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe): ('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'), ('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'), ] - keep_only_tags = [dict(attrs={'class':[ - 'byLine', - 'inside-copy', - 'inside-head', - 'inside-head2', - 'item', - 'item-block', - 'photo-container', - ]}), - dict(id=[ - 'applyMainStoryPhoto', - 'permalink', - ])] + keep_only_tags = [dict(attrs={'class':'story'})] + remove_tags = [ + dict(attrs={'class':[ + 'share', + 'reprints', + 'inline-h3', + 'info-extras', + 'ppy-outer', + 'ppy-caption', + 'comments', + 'jump', + 'pagetools', + 'post-attributes', + 'tags', + 'bottom-tools', + 'sponsoredlinks', + ]}), + dict(id=['pluck']), + ] - remove_tags = [dict(attrs={'class':[ - 'comments', - 'jump', - 'pagetools', - 'post-attributes', - 'tags', - ]}), - dict(id=[])] - - #feeds = [('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles')] - - def dump_hex(self, src, length=16): - ''' Diagnostic ''' - FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)]) - N=0; result='' - while src: - s,src = src[:length],src[length:] - hexa = ' '.join(["%02X"%ord(x) for x in s]) - s = s.translate(FILTER) - result += "%04X %-*s %s\n" % (N, length*3, hexa, s) - N+=length - print result - - def fixChars(self,string): - # Replace lsquo (\x91) - fixed = re.sub("\x91","‘",string) - - # Replace rsquo (\x92) - fixed = re.sub("\x92","’",fixed) - - # Replace ldquo (\x93) - fixed = re.sub("\x93","“",fixed) - - # Replace rdquo (\x94) - fixed = re.sub("\x94","”",fixed) - - # Replace ndash (\x96) - fixed = re.sub("\x96","–",fixed) - - # Replace mdash (\x97) - fixed = re.sub("\x97","—",fixed) - - return fixed def get_masthead_url(self): masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif' @@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe): masthead = None return masthead - def massageNCXText(self, description): - # Kindle TOC descriptions won't render certain characters - if description: - massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)) - # Replace '&' with '&' - massaged = re.sub("&","&", massaged) - return self.fixChars(massaged) - else: - return description - def parse_feeds(self, *args, **kwargs): - parsed_feeds = BasicNewsRecipe.parse_feeds(self, *args, **kwargs) - # Count articles for progress dialog - article_count = 0 - for feed in parsed_feeds: - article_count += len(feed) - self.log( "Queued %d articles" % article_count) - return parsed_feeds - - def preprocess_html(self, soup): - soup = self.strip_anchors(soup) - return soup - - def postprocess_html(self, soup, first_fetch): - - # Remove navLinks
- navLinks = soup.find(True,{'style':'padding-bottom:3px'}) - if navLinks: - navLinks.extract() - - # Remove
- gibberish = soup.find(True,{'style':'margin-bottom:10px'}) - if gibberish: - gibberish.extract() - - # Change to

- headline = soup.find(True, {'class':['inside-head','inside-head2']}) - if not headline: - headline = soup.find('h3') - if headline: - tag = Tag(soup, "h2") - tag['class'] = "headline" - tag.insert(0, headline.contents[0]) - headline.replaceWith(tag) - else: - print "unable to find headline:\n%s\n" % soup - - # Change byLine to byline, change commas to middot - # Kindle renders commas in byline as '&' - byline = soup.find(True, {'class':'byLine'}) - if byline: - byline['class'] = 'byline' - # Replace comma with middot - byline.contents[0].replaceWith(re.sub(","," ·", byline.renderContents())) - - jumpout_punc_list = [':','?'] - # Remove the inline jumpouts in
- paras = soup.findAll(True, {'class':'inside-copy'}) - for para in paras: - if re.match("[\w\W]+ ",para.renderContents()): - p = para.find('b') - for punc in jumpout_punc_list: - punc_offset = p.contents[0].find(punc) - if punc_offset == -1: - continue - if punc_offset > 1: - if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper(): - #print "extracting \n%s\n" % para.prettify() - para.extract() - - # Reset class for remaining - paras = soup.findAll(True, {'class':'inside-copy'}) - for para in paras: - para['class'] = 'articleBody' - - # Remove inline jumpouts in

- paras = soup.findAll(['p']) - for p in paras: - if hasattr(p,'contents') and len(p.contents): - for punc in jumpout_punc_list: - punc_offset = p.contents[0].find(punc) - if punc_offset == -1: - continue - if punc_offset > 2 and hasattr(p,'a') and len(p.contents): - #print "evaluating %s\n" % p.contents[0][:punc_offset+1] - if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper(): - #print "extracting \n%s\n" % p.prettify() - p.extract() - - # Capture the first img, insert after headline - imgs = soup.findAll('img') - print "postprocess_html(): %d images" % len(imgs) - if imgs: - divTag = Tag(soup, 'div') - divTag['class'] = 'image' - body = soup.find('body') - img = imgs[0] - #print "img: \n%s\n" % img.prettify() - - # Table for photo and credit - tableTag = Tag(soup,'table') - - # Photo - trimgTag = Tag(soup, 'tr') - tdimgTag = Tag(soup, 'td') - tdimgTag.insert(0,img) - trimgTag.insert(0,tdimgTag) - tableTag.insert(0,trimgTag) - - # Credit - trcreditTag = Tag(soup, 'tr') - - tdcreditTag = Tag(soup, 'td') - tdcreditTag['class'] = 'credit' - credit = soup.find('td',{'class':'photoCredit'}) - if credit: - tdcreditTag.insert(0,NavigableString(credit.renderContents())) - else: - credit = img['credit'] - if credit: - tdcreditTag.insert(0,NavigableString(credit)) - else: - tdcreditTag.insert(0,NavigableString('')) - - trcreditTag.insert(0,tdcreditTag) - tableTag.insert(1,trcreditTag) - dtc = 0 - divTag.insert(dtc,tableTag) - dtc += 1 - - if False: - # Add the caption in the table - tableCaptionTag = Tag(soup,'caption') - tableCaptionTag.insert(0,soup.find('td',{'class':'photoCredit'}).renderContents()) - tableTag.insert(1,tableCaptionTag) - divTag.insert(dtc,tableTag) - dtc += 1 - body.insert(1,divTag) - else: - # Add the caption below the table - #print "Looking for caption in this soup:\n%s" % img.prettify() - captionTag = Tag(soup,'p') - captionTag['class'] = 'caption' - if hasattr(img,'alt') and img['alt']: - captionTag.insert(0,NavigableString('

%s
' % img['alt'])) - divTag.insert(dtc, captionTag) - dtc += 1 - else: - try: - captionTag.insert(0,NavigableString('
%s
' % img['cutline'])) - divTag.insert(dtc, captionTag) - dtc += 1 - except: - pass - - hrTag = Tag(soup, 'hr') - divTag.insert(dtc, hrTag) - dtc += 1 - - # Delete
- restructure - tag = body.find(True) - while True: - insertLoc += 1 - try: - if hasattr(tag,'class') and tag['class'] == 'headline': - headline_found = True - tag.insert(insertLoc,divTag) - break - except: - pass - tag = tag.next - if not tag: - break - - # Yank out headline, img and caption - headline = body.find('h2','headline') - img = body.find('div','image') - caption = body.find('p''class') - - # body(0) is calibre_navbar - # body(1) is
- - btc = 1 - headline.extract() - body.insert(1, headline) - btc += 1 - if img: - img.extract() - body.insert(btc, img) - btc += 1 - if caption: - caption.extract() - body.insert(btc, caption) - btc += 1 - - if len(imgs) > 1: - if True: - [img.extract() for img in imgs[1:]] - else: - # Format the remaining images - # This doesn't work yet - for img in imgs[1:]: - print "img:\n%s\n" % img.prettify() - divTag = Tag(soup, 'div') - divTag['class'] = 'image' - - # Table for photo and credit - tableTag = Tag(soup,'table') - - # Photo - trimgTag = Tag(soup, 'tr') - tdimgTag = Tag(soup, 'td') - tdimgTag.insert(0,img) - trimgTag.insert(0,tdimgTag) - tableTag.insert(0,trimgTag) - - # Credit - trcreditTag = Tag(soup, 'tr') - - tdcreditTag = Tag(soup, 'td') - tdcreditTag['class'] = 'credit' - try: - tdcreditTag.insert(0,NavigableString(img['credit'])) - except: - tdcreditTag.insert(0,NavigableString('')) - trcreditTag.insert(0,tdcreditTag) - tableTag.insert(1,trcreditTag) - divTag.insert(0,tableTag) - soup.img.replaceWith(divTag) - - return soup - - def postprocess_book(self, oeb, opts, log) : - - def extract_byline(href) : - # '' : - return self.massageNCXText(self.tag_to_string(p,use_alt=False)) - else: - print "Didn't find
in this soup:\n%s" % soup.prettify() - return None - - # Method entry point here - # Single section toc looks different than multi-section tocs - if oeb.toc.depth() == 2 : - for article in oeb.toc : - if article.author is None : - article.author = extract_byline(article.href) - if article.description is None : - article.description = extract_description(article.href) - elif oeb.toc.depth() == 3 : - for section in oeb.toc : - for article in section : - article.author = extract_byline(article.href) - ''' - if article.author is None : - article.author = self.massageNCXText(extract_byline(article.href)) - else: - article.author = self.massageNCXText(article.author) - ''' - if article.description is None : - article.description = extract_description(article.href) - - def strip_anchors(self,soup): - paras = soup.findAll(True) - for para in paras: - aTags = para.findAll('a') - for a in aTags: - if a.img is None: - a.replaceWith(a.renderContents().decode('cp1252','replace')) - return soup From 1400f54107185d80ea880bc786de78910a382e48 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 11:02:56 -0600 Subject: [PATCH 11/19] EPUB metadata: When extracting covers from epub files handle invalid epubs that specify their content as a raster cover. Apparently PG produces these. --- src/calibre/ebooks/metadata/opf2.py | 4 +++- src/calibre/gui2/actions/choose_library.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 58c887bfdb..1d91236757 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -966,7 +966,9 @@ class OPF(object): # {{{ cover_id = covers[0].get('content') for item in self.itermanifest(): if item.get('id', None) == cover_id: - return item.get('href', None) + mt = item.get('media-type', '') + if 'xml' not in mt: + return item.get('href', None) @dynamic_property def cover(self): diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 4b262ad9dd..a663f288af 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -246,7 +246,7 @@ class ChooseLibraryAction(InterfaceAction): def delete_requested(self, name, location): loc = location.replace('/', os.sep) if not question_dialog(self.gui, _('Are you sure?'), '

'+ - _('All files from %s will be ' + _('All files from %s will be ' 'permanently deleted. Are you sure?') % loc, show_copy_button=False): return From fe90a1b04fa8fecd5abfacbc795ca8b526897b11 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 13:13:38 -0600 Subject: [PATCH 12/19] Implement #777001 (Add Preview ePub button to Tweak ePub dialog) --- src/calibre/gui2/dialogs/tweak_epub.py | 38 ++++++++++++++--- src/calibre/gui2/dialogs/tweak_epub.ui | 57 +++++++++++++++----------- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/calibre/gui2/dialogs/tweak_epub.py b/src/calibre/gui2/dialogs/tweak_epub.py index edc274c9b2..732d74b77d 100755 --- a/src/calibre/gui2/dialogs/tweak_epub.py +++ b/src/calibre/gui2/dialogs/tweak_epub.py @@ -7,16 +7,16 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os, shutil -from contextlib import closing from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED from PyQt4.Qt import QDialog from calibre.constants import isosx -from calibre.gui2 import open_local_file +from calibre.gui2 import open_local_file, error_dialog from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog from calibre.libunzip import extract as zipextract -from calibre.ptempfile import PersistentTemporaryDirectory +from calibre.ptempfile import (PersistentTemporaryDirectory, + PersistentTemporaryFile) class TweakEpub(QDialog, Ui_Dialog): ''' @@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog): self.cancel_button.clicked.connect(self.reject) self.explode_button.clicked.connect(self.explode) self.rebuild_button.clicked.connect(self.rebuild) + self.preview_button.clicked.connect(self.preview) # Position update dialog overlaying top left of app window parent_loc = parent.pos() self.move(parent_loc.x(),parent_loc.y()) + self.gui = parent + self._preview_files = [] + def cleanup(self): if isosx: try: @@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog): # Delete directory containing exploded ePub if self._exploded is not None: shutil.rmtree(self._exploded, ignore_errors=True) + for x in self._preview_files: + try: + os.remove(x) + except: + pass def display_exploded(self): ''' @@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog): self.rebuild_button.setEnabled(True) self.explode_button.setEnabled(False) - def rebuild(self, *args): - self._output = os.path.join(self._exploded, 'rebuilt.epub') - with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf: + def do_rebuild(self, src): + with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf: # Write mimetype zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED) # Write everything else @@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog): zfn = os.path.relpath(absfn, self._exploded).replace(os.sep, '/') zf.write(absfn, zfn) + + def preview(self): + if not self._exploded: + return error_dialog(self, _('Cannot preview'), + _('You must first explode the epub before previewing.'), + show=True) + + tf = PersistentTemporaryFile('.epub') + tf.close() + self._preview_files.append(tf.name) + + self.do_rebuild(tf.name) + + self.gui.iactions['View']._view_file(tf.name) + + def rebuild(self, *args): + self._output = os.path.join(self._exploded, 'rebuilt.epub') + self.do_rebuild(self._output) return QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/tweak_epub.ui b/src/calibre/gui2/dialogs/tweak_epub.ui index fc6f24675f..a59af4fde1 100644 --- a/src/calibre/gui2/dialogs/tweak_epub.ui +++ b/src/calibre/gui2/dialogs/tweak_epub.ui @@ -23,6 +23,16 @@ false + + + + <p>Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p> + + + true + + + @@ -37,23 +47,6 @@ - - - - false - - - Rebuild ePub from exploded contents - - - &Rebuild ePub - - - - :/images/exec.png:/images/exec.png - - - @@ -68,13 +61,31 @@ - - - - <p>Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p> + + + + false - - true + + Rebuild ePub from exploded contents + + + &Rebuild ePub + + + + :/images/exec.png:/images/exec.png + + + + + + + &Preview ePub + + + + :/images/view.png:/images/view.png From c84c5f297bcfcd30f99f503768acd86235f1da43 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 15:18:37 -0600 Subject: [PATCH 13/19] Add select all/none buttons to the metadata download prefs --- .../gui2/preferences/metadata_sources.py | 13 ++++++++++++- .../gui2/preferences/metadata_sources.ui | 18 ++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/preferences/metadata_sources.py b/src/calibre/gui2/preferences/metadata_sources.py index f487051d07..05ff23987d 100644 --- a/src/calibre/gui2/preferences/metadata_sources.py +++ b/src/calibre/gui2/preferences/metadata_sources.py @@ -190,7 +190,15 @@ class FieldsModel(QAbstractListModel): # {{{ return ans | Qt.ItemIsUserCheckable def restore_defaults(self): - self.overrides = dict([(f, self.state(f, True)) for f in self.fields]) + self.overrides = dict([(f, self.state(f, Qt.Checked)) for f in self.fields]) + self.reset() + + def select_all(self): + self.overrides = dict([(f, Qt.Checked) for f in self.fields]) + self.reset() + + def clear_all(self): + self.overrides = dict([(f, Qt.Unchecked) for f in self.fields]) self.reset() def setData(self, index, val, role): @@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.fields_view.setModel(self.fields_model) self.fields_model.dataChanged.connect(self.changed_signal) + self.select_all_button.clicked.connect(self.fields_model.select_all) + self.clear_all_button.clicked.connect(self.fields_model.clear_all) + def configure_plugin(self): for index in self.sources_view.selectionModel().selectedRows(): plugin = self.sources_model.data(index, Qt.UserRole) diff --git a/src/calibre/gui2/preferences/metadata_sources.ui b/src/calibre/gui2/preferences/metadata_sources.ui index e46069b036..ff161654dd 100644 --- a/src/calibre/gui2/preferences/metadata_sources.ui +++ b/src/calibre/gui2/preferences/metadata_sources.ui @@ -77,8 +77,8 @@ Downloaded metadata fields - - + + If you uncheck any fields, metadata for those fields will not be downloaded @@ -88,6 +88,20 @@ + + + + &Select all + + + + + + + &Clear all + + + From 45ec80f4bb580b88d6c957906f5201648c2b9edf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 15:49:34 -0600 Subject: [PATCH 14/19] For the lazy among us --- src/calibre/gui2/actions/choose_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index a663f288af..f6b19fc4aa 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -246,7 +246,7 @@ class ChooseLibraryAction(InterfaceAction): def delete_requested(self, name, location): loc = location.replace('/', os.sep) if not question_dialog(self.gui, _('Are you sure?'), '

'+ - _('All files from %s will be ' + _('All files from

%s

will be ' 'permanently deleted. Are you sure?') % loc, show_copy_button=False): return From a34ea87a940feaf743ade8eddb893ce353aef6be Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 4 May 2011 18:56:30 -0400 Subject: [PATCH 15/19] Fix Bug #763105: RTF output displays some unicode characters incorrectly. --- src/calibre/ebooks/rtf/rtfml.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py index 97fa175d1a..cd877c63c2 100644 --- a/src/calibre/ebooks/rtf/rtfml.py +++ b/src/calibre/ebooks/rtf/rtfml.py @@ -79,8 +79,7 @@ def txt2rtf(text): elif val <= 127: buf.write(x) else: - repl = ascii_text(x) - c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl)) + c = r'\u{0:d}?'.format(val) buf.write(c) return buf.getvalue() From e15ee70a1ded03bf4b84f5a30f8fce89aeefa56e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 18:56:07 -0600 Subject: [PATCH 16/19] ODT Input: Speed up conversion of ODT files that define huge amounts of redundant style information. Fixes #777468 (Conversion from ODT to EPUB extremely slow) --- src/calibre/ebooks/odt/input.py | 55 ++++++++++++++++++++++++++++++--- src/odf/odf2xhtml.py | 14 +++++++-- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py index 1184148e80..10553dac2b 100644 --- a/src/calibre/ebooks/odt/input.py +++ b/src/calibre/ebooks/odt/input.py @@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en' Convert an ODT file into a Open Ebook ''' import os + +from lxml import etree from odf.odf2xhtml import ODF2XHTML from calibre import CurrentDir, walk @@ -23,7 +25,48 @@ class Extract(ODF2XHTML): with open(name, 'wb') as f: f.write(data) - def __call__(self, stream, odir): + def filter_css(self, html, log): + root = etree.fromstring(html) + style = root.xpath('//*[local-name() = "style" and @type="text/css"]') + if style: + style = style[0] + css = style.text + if css: + style.text, sel_map = self.do_filter_css(css) + for x in root.xpath('//*[@class]'): + extra = [] + orig = x.get('class') + for cls in orig.split(): + extra.extend(sel_map.get(cls, [])) + if extra: + x.set('class', orig + ' ' + ' '.join(extra)) + html = etree.tostring(root, encoding='utf-8', + xml_declaration=True) + return html + + def do_filter_css(self, css): + from cssutils import parseString + from cssutils.css import CSSRule + sheet = parseString(css) + rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) + sel_map = {} + count = 0 + for r in rules: + # Check if we have only class selectors for this rule + nc = [x for x in r.selectorList if not + x.selectorText.startswith('.')] + if len(r.selectorList) > 1 and not nc: + replace_name = 'c_odt%d'%count + count += 1 + for sel in r.selectorList: + s = sel.selectorText[1:] + if s not in sel_map: + sel_map[s] = [] + sel_map[s].append(replace_name) + r.selectorText = '.'+replace_name + return sheet.cssText, sel_map + + def __call__(self, stream, odir, log): from calibre.utils.zipfile import ZipFile from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.opf2 import OPFCreator @@ -32,13 +75,17 @@ class Extract(ODF2XHTML): if not os.path.exists(odir): os.makedirs(odir) with CurrentDir(odir): - print 'Extracting ODT file...' + log('Extracting ODT file...') html = self.odf2xhtml(stream) # A blanket img specification like this causes problems - # with EPUB output as the contaiing element often has + # with EPUB output as the containing element often has # an absolute height and width set that is larger than # the available screen real estate html = html.replace('img { width: 100%; height: 100%; }', '') + try: + html = self.filter_css(html, log) + except: + log.exception('Failed to filter CSS, conversion may be slow') with open('index.xhtml', 'wb') as f: f.write(html.encode('utf-8')) zf = ZipFile(stream, 'r') @@ -67,7 +114,7 @@ class ODTInput(InputFormatPlugin): def convert(self, stream, options, file_ext, log, accelerators): - return Extract()(stream, '.') + return Extract()(stream, '.', log) def postprocess_book(self, oeb, opts, log): # Fix

constructs as the asinine epubchecker complains diff --git a/src/odf/odf2xhtml.py b/src/odf/odf2xhtml.py index 26da9d9905..a04aa48bf7 100644 --- a/src/odf/odf2xhtml.py +++ b/src/odf/odf2xhtml.py @@ -841,11 +841,19 @@ ol, ul { padding-left: 2em; } self.styledict[name] = styles # Write the styles to HTML self.writeout(self.default_styles) + # Changed by Kovid to not write out endless copies of the same style + css_styles = {} for name in self.stylestack: styles = self.styledict.get(name) - css2 = self.cs.convert_styles(styles) - self.writeout("%s {\n" % name) - for style, val in css2.items(): + css2 = tuple(self.cs.convert_styles(styles).iteritems()) + if css2 in css_styles: + css_styles[css2].append(name) + else: + css_styles[css2] = [name] + + for css2, names in css_styles.iteritems(): + self.writeout("%s {\n" % ', '.join(names)) + for style, val in css2: self.writeout("\t%s: %s;\n" % (style, val) ) self.writeout("}\n") From 07da7b239c377a856fbc43f16aa2f01042964e3d Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 4 May 2011 21:48:44 -0400 Subject: [PATCH 17/19] Fix Bug #775669: HTMLZ OPF sets cover, adds cover properly when updating metadata, reads cover form guide section of opf in archive. --- src/calibre/ebooks/htmlz/input.py | 25 +++++++++++++++++-- src/calibre/ebooks/htmlz/output.py | 25 ++++++++++++++++++- src/calibre/ebooks/metadata/extz.py | 24 ++++++++++-------- src/calibre/ebooks/oeb/transforms/metadata.py | 2 +- 4 files changed, 61 insertions(+), 15 deletions(-) diff --git a/src/calibre/ebooks/htmlz/input.py b/src/calibre/ebooks/htmlz/input.py index dcf2ed0ed3..d083fcc4ab 100644 --- a/src/calibre/ebooks/htmlz/input.py +++ b/src/calibre/ebooks/htmlz/input.py @@ -7,10 +7,12 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' import os +import posixpath -from calibre import walk +from calibre import guess_type, walk from calibre.customize.conversion import InputFormatPlugin from calibre.ebooks.chardet import xml_to_unicode +from calibre.ebooks.metadata.opf2 import OPF from calibre.utils.zipfile import ZipFile class HTMLZInput(InputFormatPlugin): @@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin): # Extract content from zip archive. zf = ZipFile(stream) - zf.extractall('.') + zf.extractall() for x in walk('.'): if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'): @@ -70,5 +72,24 @@ class HTMLZInput(InputFormatPlugin): from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata mi = get_file_type_metadata(stream, file_ext) meta_info_to_oeb_metadata(mi, oeb.metadata, log) + + # Get the cover path from the OPF. + cover_href = None + opf = None + for x in walk('.'): + if os.path.splitext(x)[1].lower() in ('.opf'): + opf = x + break + if opf: + opf = OPF(opf) + cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name)) + # Set the cover. + if cover_href: + cdata = None + with open(cover_href, 'rb') as cf: + cdata = cf.read() + id, href = oeb.manifest.generate('cover', cover_href) + oeb.manifest.add(id, href, guess_type(cover_href)[0], data=cdata) + oeb.guide.add('cover', 'Cover', href) return oeb diff --git a/src/calibre/ebooks/htmlz/output.py b/src/calibre/ebooks/htmlz/output.py index 6d2ad54a12..a1ef57af2c 100644 --- a/src/calibre/ebooks/htmlz/output.py +++ b/src/calibre/ebooks/htmlz/output.py @@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' import os +from cStringIO import StringIO from lxml import etree from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation +from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf from calibre.ptempfile import TemporaryDirectory from calibre.utils.zipfile import ZipFile @@ -79,10 +81,31 @@ class HTMLZOutput(OutputFormatPlugin): fname = os.path.join(tdir, 'images', images[item.href]) with open(fname, 'wb') as img: img.write(data) + + # Cover + cover_path = None + try: + cover_data = None + if oeb_book.metadata.cover: + term = oeb_book.metadata.cover[0].term + cover_data = oeb_book.guide[term].item.data + if cover_data: + from calibre.utils.magick.draw import save_cover_data_to + cover_path = os.path.join(tdir, 'cover.jpg') + with open(cover_path, 'w') as cf: + cf.write('') + save_cover_data_to(cover_data, cover_path) + except: + import traceback + traceback.print_exc() # Metadata with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf: - mdataf.write(etree.tostring(oeb_book.metadata.to_opf1())) + opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1()))) + mi = opf.to_book_metadata() + if cover_path: + mi.cover = 'cover.jpg' + mdataf.write(metadata_to_opf(mi)) htmlz = ZipFile(output_path, 'w') htmlz.add_dir(tdir) diff --git a/src/calibre/ebooks/metadata/extz.py b/src/calibre/ebooks/metadata/extz.py index 18069b2a6a..21c10278e1 100644 --- a/src/calibre/ebooks/metadata/extz.py +++ b/src/calibre/ebooks/metadata/extz.py @@ -13,7 +13,7 @@ import posixpath from cStringIO import StringIO from calibre.ebooks.metadata import MetaInformation -from calibre.ebooks.metadata.opf2 import OPF +from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.zipfile import ZipFile, safe_replace @@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True): opf = OPF(opf_stream) mi = opf.to_book_metadata() if extract_cover: - cover_name = opf.raster_cover - if cover_name: - mi.cover_data = ('jpg', zf.read(cover_name)) + cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name)) + if cover_href: + mi.cover_data = ('jpg', zf.read(cover_href)) except: return mi return mi @@ -59,17 +59,19 @@ def set_metadata(stream, mi): except: pass if new_cdata: - raster_cover = opf.raster_cover - if not raster_cover: - raster_cover = 'cover.jpg' - cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover) + cover = opf.cover + if not cover: + cover = 'cover.jpg' + cpath = posixpath.join(posixpath.dirname(opf_path), cover) new_cover = _write_new_cover(new_cdata, cpath) replacements[cpath] = open(new_cover.name, 'rb') + mi.cover = cover # Update the metadata. - opf.smart_update(mi, replace_metadata=True) - newopf = StringIO(opf.render()) - safe_replace(stream, opf_path, newopf, extra_replacements=replacements) + old_mi = opf.to_book_metadata() + old_mi.smart_update(mi) + newopf = StringIO(metadata_to_opf(old_mi)) + safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True) # Cleanup temporary files. try: diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py index 19c209b74d..f719ee3eb5 100644 --- a/src/calibre/ebooks/oeb/transforms/metadata.py +++ b/src/calibre/ebooks/oeb/transforms/metadata.py @@ -36,7 +36,7 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False): m.clear('description') m.add('description', mi.comments) elif override_input_metadata: - m.clear('description') + m.clear('description') if not mi.is_null('publisher'): m.clear('publisher') m.add('publisher', mi.publisher) From 6fbb996d2a30afd36da0e3566cc8e22e23a912ab Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 4 May 2011 21:50:08 -0400 Subject: [PATCH 18/19] HTMLZ: Keep existing non-metadata sections when updating. --- src/calibre/ebooks/metadata/extz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/extz.py b/src/calibre/ebooks/metadata/extz.py index 21c10278e1..1bda263015 100644 --- a/src/calibre/ebooks/metadata/extz.py +++ b/src/calibre/ebooks/metadata/extz.py @@ -70,7 +70,8 @@ def set_metadata(stream, mi): # Update the metadata. old_mi = opf.to_book_metadata() old_mi.smart_update(mi) - newopf = StringIO(metadata_to_opf(old_mi)) + opf.smart_update(metadata_to_opf(old_mi)) + newopf = StringIO(opf.render()) safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True) # Cleanup temporary files. From b461f5bc26030f3f8ce43d02facc3914ade45c5e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 4 May 2011 20:30:34 -0600 Subject: [PATCH 19/19] ... --- src/calibre/ebooks/odt/input.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py index 10553dac2b..e724acb981 100644 --- a/src/calibre/ebooks/odt/input.py +++ b/src/calibre/ebooks/odt/input.py @@ -56,6 +56,9 @@ class Extract(ODF2XHTML): nc = [x for x in r.selectorList if not x.selectorText.startswith('.')] if len(r.selectorList) > 1 and not nc: + # Replace all the class selectors with a single class selector + # This will be added to the class attribute of all elements + # that have one of these selectors. replace_name = 'c_odt%d'%count count += 1 for sel in r.selectorList: