示例#1
0
 def collection_finished(self):
     self.do_one = self.do_one_write
     ensure_unique_components(self.collected_data)
     self.ids_to_write = iter(self.collected_data)
     self.pd.title = _('Copying files and writing metadata...'
                       ) if self.opts.update_metadata else _(
                           'Copying files...')
     self.pd.max = len(self.collected_data)
     self.pd.value = 0
     if self.opts.update_metadata:
         all_fmts = {
             fmt
             for data in self.collected_data.itervalues() for fmt in data[2]
         }
         plugboards_cache = {
             fmt: find_plugboard(plugboard_save_to_disk_value, fmt,
                                 self.plugboards)
             for fmt in all_fmts
         }
         self.pool = Pool(
             name='SaveToDisk') if self.pool is None else self.pool
         try:
             self.pool.set_common_data(plugboards_cache)
         except Failure as err:
             error_dialog(
                 self.pd,
                 _('Critical failure'),
                 _('Could not save books to disk, click "Show details" for more information'
                   ),
                 det_msg=unicode(err.failure_message) + '\n' +
                 unicode(err.details),
                 show=True)
             self.pd.canceled = True
     self.do_one_signal.emit()
示例#2
0
文件: add.py 项目: wynick27/calibre
 def monitor_scan(self):
     self.scan_thread.join(0.05)
     if self.scan_thread.is_alive():
         self.do_one_signal.emit()
         return
     if self.scan_error is not None:
         error_dialog(self.pd, _('Cannot add books'), _(
             'Failed to add any books, click "Show details" for more information.'),
                      det_msg=self.scan_error, show=True)
         self.break_cycles()
         return
     if not self.file_groups:
         error_dialog(self.pd, _('Could not add'), _(
             'No ebook files were found in %s') % self.source, show=True)
         self.break_cycles()
         return
     self.pd.max = len(self.file_groups)
     self.pd.title = _('Reading metadata and adding to library (%d books)...') % self.pd.max
     self.pd.msg = ''
     self.pd.value = 0
     self.pool = Pool(name='AddBooks') if self.pool is None else self.pool
     if self.db is not None:
         if self.add_formats_to_existing:
             self.find_identical_books_data = self.db.data_for_find_identical_books()
         else:
             try:
                 self.pool.set_common_data(self.db.data_for_has_book())
             except Failure as err:
                 error_dialog(self.pd, _('Cannot add books'), _(
                 'Failed to add any books, click "Show details" for more information.'),
                 det_msg=as_unicode(err.failure_message) + '\n' + as_unicode(err.details), show=True)
                 self.pd.canceled = True
     self.groups_to_add = iter(self.file_groups)
     self.do_one = self.do_one_group
     self.do_one_signal.emit()
示例#3
0
文件: add.py 项目: GRiker/calibre
 def monitor_scan(self):
     self.scan_thread.join(0.05)
     if self.scan_thread.is_alive():
         self.do_one_signal.emit()
         return
     if self.scan_error is not None:
         error_dialog(self.pd, _('Cannot add books'), _(
             'Failed to add any books, click "Show details" for more information.'),
                      det_msg=self.scan_error, show=True)
         self.break_cycles()
         return
     if not self.file_groups:
         error_dialog(self.pd, _('Could not add'), _(
             'No ebook files were found in %s') % self.source, show=True)
         self.break_cycles()
         return
     self.pd.max = len(self.file_groups)
     self.pd.title = _('Reading metadata and adding to library (%d books)...') % self.pd.max
     self.pd.msg = ''
     self.pd.value = 0
     self.pool = Pool(name='AddBooks') if self.pool is None else self.pool
     if self.db is not None:
         if self.add_formats_to_existing:
             self.find_identical_books_data = self.db.data_for_find_identical_books()
         else:
             try:
                 self.pool.set_common_data(self.db.data_for_has_book())
             except Failure as err:
                 error_dialog(self.pd, _('Cannot add books'), _(
                 'Failed to add any books, click "Show details" for more information.'),
                 det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
                 self.pd.canceled = True
     self.groups_to_add = iter(self.file_groups)
     self.do_one = self.do_one_group
     self.do_one_signal.emit()
示例#4
0
文件: save.py 项目: JimmXinu/calibre
 def collection_finished(self):
     self.do_one = self.do_one_write
     ensure_unique_components(self.collected_data)
     self.ids_to_write = iter(self.collected_data)
     self.pd.title = _('Copying files and writing metadata...') if self.opts.update_metadata else _(
         'Copying files...')
     self.pd.max = len(self.collected_data)
     self.pd.value = 0
     if self.opts.update_metadata:
         all_fmts = {fmt for data in itervalues(self.collected_data) for fmt in data[2]}
         plugboards_cache = {fmt:find_plugboard(plugboard_save_to_disk_value, fmt, self.plugboards) for fmt in all_fmts}
         self.pool = Pool(name='SaveToDisk') if self.pool is None else self.pool
         try:
             self.pool.set_common_data(plugboards_cache)
         except Failure as err:
             error_dialog(self.pd, _('Critical failure'), _(
                 'Could not save books to disk, click "Show details" for more information'),
                 det_msg=unicode_type(err.failure_message) + '\n' + unicode_type(err.details), show=True)
             self.pd.canceled = True
     self.do_one_signal.emit()
示例#5
0
文件: ui.py 项目: sportsbite/calibre
 def create_spare_pool(self, *args):
     if self._spare_pool is None:
         num = min(detect_ncpus(), int(config['worker_limit'] / 2.0))
         self._spare_pool = Pool(max_workers=num, name='GUIPool')
示例#6
0
文件: ui.py 项目: sportsbite/calibre
class Main(
        MainWindow,
        MainWindowMixin,
        DeviceMixin,
        EmailMixin,  # {{{
        TagBrowserMixin,
        CoverFlowMixin,
        LibraryViewMixin,
        SearchBoxMixin,
        SavedSearchBoxMixin,
        SearchRestrictionMixin,
        LayoutMixin,
        UpdateMixin,
        EbookDownloadMixin):

    'The main GUI'

    proceed_requested = pyqtSignal(object, object)
    book_converted = pyqtSignal(object, object)
    shutting_down = False

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self,
                            opts,
                            parent=parent,
                            disable_automatic_gc=True)
        self.setWindowIcon(QApplication.instance().windowIcon())
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                                       type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self._spare_pool = None
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.check_messages_timer.timeout.connect(
            self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                                     _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.init_main_window_mixin(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.init_layout_mixin(self)
        DeviceMixin.init_device_mixin(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = None
        if config['systray_icon']:
            self.system_tray_icon = factory(
                app_id='com.calibre-ebook.gui').create_system_tray_icon(
                    parent=self, title='calibre')
        if self.system_tray_icon is not None:
            self.system_tray_icon.setIcon(
                QIcon(I('lt.png', allow_user_override=False)))
            if not (iswindows or isosx):
                self.system_tray_icon.setIcon(
                    QIcon.fromTheme('calibre-gui',
                                    self.system_tray_icon.icon()))
            self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip())
            self.system_tray_icon.setVisible(True)
            self.jobs_button.tray_tooltip_updated.connect(
                self.system_tray_icon.setToolTip)
        elif config['systray_icon']:
            prints(
                'Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec'
            )
        self.system_tray_menu = QMenu(self)
        self.toggle_to_tray_action = self.system_tray_menu.addAction(
            QIcon(I('page.png')), '')
        self.toggle_to_tray_action.triggered.connect(
            self.system_tray_icon_activated)
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.clicked.connect(self.donate_action.trigger)
        self.donate_button.setToolTip(self.donate_action.text().replace(
            '&', ''))
        self.donate_button.setIcon(self.donate_action.icon())
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
            QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre',
                                        _('Quit calibre'),
                                        default_keys=('Ctrl+Q', ),
                                        action=self.quit_action)
        if self.system_tray_icon is not None:
            self.system_tray_icon.setContextMenu(self.system_tray_menu)
            self.system_tray_icon.activated.connect(
                self.system_tray_icon_activated)
        self.quit_action.triggered[bool].connect(self.quit)
        self.donate_action.triggered[bool].connect(self.donate)
        self.minimize_action = QAction(_('Minimize the calibre window'), self)
        self.addAction(self.minimize_action)
        self.keyboard.register_shortcut('minimize calibre',
                                        self.minimize_action.text(),
                                        default_keys=(),
                                        action=self.minimize_action)
        self.minimize_action.triggered.connect(self.showMinimized)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                                        _('Clear the current search'),
                                        default_keys=('Esc', ),
                                        action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                                        _('Focus the book list'),
                                        default_keys=('Shift+Esc', ),
                                        action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                                        _('Clear the virtual library'),
                                        default_keys=('Ctrl+Esc', ),
                                        action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                                        _('Clear the additional restriction'),
                                        default_keys=('Alt+Esc', ),
                                        action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(
            self.clear_additional_restriction)

        # ###################### Start spare job server ########################
        QTimer.singleShot(1000, self.create_spare_pool)

        # ###################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(
            self.device_manager.umount_device)
        self.location_manager.configure_device.connect(
            self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(
            self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        # ################### Update notification ###################
        UpdateMixin.init_update_mixin(self, opts)

        # ###################### Search boxes ########################
        SearchRestrictionMixin.init_search_restriction_mixin(self)
        SavedSearchBoxMixin.init_saved_seach_box_mixin(self)

        # ###################### Library view ########################
        LibraryViewMixin.init_library_view_mixin(self, db)
        SearchBoxMixin.init_search_box_mixin(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ) and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
            self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            try:
                add_quick_start_guide(self.library_view)
            except:
                import traceback
                traceback.print_exc()
        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(
            self.bars_manager.database_changed, type=Qt.QueuedConnection)

        # ########################## Tags Browser ##############################
        TagBrowserMixin.init_tag_browser_mixin(self, db)

        # ######################## Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        # ########################## Cover Flow ################################

        CoverFlowMixin.init_cover_flow_mixin(self)

        self._calculated_available_height = min(max_available_height() - 15,
                                                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.set_current_library_information(current_library_name(),
                                             db.library_id, db.field_metadata)

        register_keyboard_shortcuts()
        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        QApplication.instance().shutdown_signal_received.connect(self.quit)
        if show_gui and self.gui_debug is not None:
            QTimer.singleShot(10, self.show_gui_debug_msg)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)
        QTimer.singleShot(100, self.update_toggle_to_tray_action)

    def show_gui_debug_msg(self):
        info_dialog(self,
                    _('Debug mode'),
                    '<p>' +
                    _('You have started calibre in debug mode. After you '
                      'quit calibre, the debug log will be available in '
                      'the file: %s<p>The '
                      'log will be displayed automatically.') % self.gui_debug,
                    show=True)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(
                    self,
                    _('Problem starting the wireless device'),
                    _('The wireless device driver had problems starting. '
                      'It said "%s"') % message,
                    show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
            self.library_view.model().db,
            server_config().parse())
        self.content_server.state_callback = Dispatcher(
            self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self,
                     _('Failed to start Content Server'),
                     _('Could not start the content server. Error:\n\n%s') %
                     msg,
                     show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width() - 150)

    def create_spare_pool(self, *args):
        if self._spare_pool is None:
            num = min(detect_ncpus(), int(config['worker_limit'] / 2.0))
            self._spare_pool = Pool(max_workers=num, name='GUIPool')

    def spare_pool(self):
        ans, self._spare_pool = self._spare_pool, None
        QTimer.singleShot(1000, self.create_spare_pool)
        return ans

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r=False):
        if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False):
            if self.isVisible():
                if self.isMinimized():
                    self.showNormal()
                else:
                    self.hide_windows()
            else:
                self.show_windows()
                if self.isMinimized():
                    self.showNormal()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self,
                              title,
                              msg,
                              det_msg='',
                              show_copy_button=False,
                              ans_when_user_unavailable=True,
                              skip_dialog_name=None,
                              skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self,
                               title,
                               msg,
                               det_msg=det_msg,
                               show_copy_button=show_copy_button,
                               skip_dialog_name=skip_dialog_name,
                               skip_dialog_skipped_value=skipped_value)

    def update_toggle_to_tray_action(self, *args):
        if hasattr(self, 'toggle_to_tray_action'):
            self.toggle_to_tray_action.setText(
                _('Hide main window') if self.isVisible(
                ) else _('Show main window'))

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)
        self.update_toggle_to_tray_action()

    def show_windows(self, *args):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)
        self.update_toggle_to_tray_action()

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' %
                       msg)
                if DEBUG:
                    error_dialog(
                        self,
                        'Invalid message',
                        'Received an invalid message from other calibre instance.'
                        ' Do you have multiple versions of calibre installed?',
                        det_msg='Invalid msg: %r' % msg,
                        show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [
                    os.path.abspath(p) for p in argv[1:]
                    if not os.path.isdir(p) and os.access(p, os.R_OK)
                ]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized
                                | Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        elif msg.startswith('bookedited:'):
            parts = msg.split(':')[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id,
                                       fmt,
                                       allow_cache=False,
                                       update_db=True)
                    db.update_last_modified((book_id, ))
                    m.refresh_ids((book_id, ))
            except Exception:
                import traceback
                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model(
        ).db, self.card_b_view.model().db

    def library_moved(self,
                      newloc,
                      copy_structure=False,
                      call_close=True,
                      allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback
            repair = question_dialog(
                self,
                _('Corrupted database'),
                _('The library database at %s appears to be corrupted. Do '
                  'you want calibre to try and rebuild it automatically? '
                  'The rebuild may not be completely successful.') %
                force_unicode(newloc, filesystem_encoding),
                det_msg=traceback.format_exc())
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        # self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('')  # reset restriction to null
        self.saved_searches_changed(
            recount=False)  # reload the search restrictions combo box
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.set_current_library_information(current_library_name(),
                                             db.library_id, db.field_metadata)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [
            x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x
        ]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
            __appname__, self.iactions['Choose Library'].library_name(),
            restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x + '_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
            self.vl_tabs.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            self.vl_tabs.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self,
                      job,
                      dialog_title=_('Conversion Error'),
                      retry_func=None):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(
                        self,
                        _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s') % title
                msg += '<p>' + _('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self,
                                     _('Conversion Failed'),
                                     msg,
                                     det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.
                              find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                                     '<p>%s</p>' % msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:'
                    )[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {
                        'info': info_dialog,
                        'warn': warning_dialog,
                        'error': error_dialog
                    }.get(payload['level'], error_dialog)
                    d = d(self,
                          payload['title'],
                          '<p>%s</p>' % payload['msg'],
                          det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                                             _('<b>Failed</b>') + ': ' +
                                             unicode(job.description),
                                             det_msg=job.details,
                                             retry_func=retry_func)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self,
             checked=True,
             restart=False,
             debug_on_restart=False,
             confirm_quit=True):
        if self.shutting_down:
            return
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('https://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False

        if self.proceed_question.questions:
            msg = _(
                'There are library updates waiting. Are you sure you want to quit?'
            )
            if not question_dialog(self, _('Library Updates Waiting'), msg):
                return False

        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.shutting_down = True
        self.show_shutdown_message()

        from calibre.customize.ui import has_library_closed_plugins
        if has_library_closed_plugins():
            self.show_shutdown_message(
                _('Running database shutdown plugins. This could take a few seconds...'
                  ))

        self.grid_view.shutdown()
        db = None
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.new_api.set_pref('field_metadata',
                                db.field_metadata.all_metadata())
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        if hasattr(self, 'update_checker'):
            self.update_checker.shutdown()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        # Do not report any errors that happen after the shutdown
        # We cannot restore the original excepthook as that causes PyQt to
        # call abort() on unhandled exceptions
        import traceback

        def eh(t, v, tb):
            try:
                traceback.print_exception(t, v, tb, file=sys.stderr)
            except:
                pass

        sys.excepthook = eh

        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        if db is not None:
            db.close()

        try:
            try:
                if self.content_server is not None:
                    # If the content server has any sockets being closed then
                    # this can take quite a long time (minutes). Tell the user that it is
                    # happening.
                    self.show_shutdown_message(
                        _('Shutting down the content server. This could take a while ...'
                          ))
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        self.hide_windows()
        if self._spare_pool is not None:
            self._spare_pool.shutdown()
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        if self.shutting_down:
            return
        self.write_settings()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible(
        ):
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(
                    self,
                    'calibre',
                    'calibre ' +
                    _('will keep running in the system tray. To close it, '
                      'choose <b>Quit</b> in the context menu of the '
                      'system tray.'),
                    show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
示例#7
0
文件: ui.py 项目: KyoYang/calibre
 def create_spare_pool(self, *args):
     if self._spare_pool is None:
         num = min(detect_ncpus(), int(config['worker_limit']/2.0))
         self._spare_pool = Pool(max_workers=num, name='GUIPool')
示例#8
0
文件: ui.py 项目: KyoYang/calibre
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin,  # {{{
        TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
        SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
        EbookDownloadMixin
        ):

    'The main GUI'

    proceed_requested = pyqtSignal(object, object)
    book_converted = pyqtSignal(object, object)

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self._spare_pool = None
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.check_messages_timer.timeout.connect(self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.init_main_window_mixin(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.init_layout_mixin(self)
        DeviceMixin.init_device_mixin(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = None
        if config['systray_icon']:
            self.system_tray_icon = factory(app_id='com.calibre-ebook.gui').create_system_tray_icon(parent=self, title='calibre')
        if self.system_tray_icon is not None:
            self.system_tray_icon.setIcon(QIcon(I('lt.png')))
            self.system_tray_icon.setToolTip(self.jobs_button.tray_tooltip())
            self.system_tray_icon.setVisible(True)
            self.jobs_button.tray_tooltip_updated.connect(self.system_tray_icon.setToolTip)
        elif config['systray_icon']:
            prints('Failed to create system tray icon, your desktop environment probably does not support the StatusNotifier spec')
        self.system_tray_menu = QMenu(self)
        self.toggle_to_tray_action = self.system_tray_menu.addAction(QIcon(I('page.png')), '')
        self.toggle_to_tray_action.triggered.connect(self.system_tray_icon_activated)
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.setDefaultAction(self.donate_action)
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
                QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre', _('Quit calibre'),
                default_keys=('Ctrl+Q',), action=self.quit_action)
        if self.system_tray_icon is not None:
            self.system_tray_icon.setContextMenu(self.system_tray_menu)
            self.system_tray_icon.activated.connect(self.system_tray_icon_activated)
        self.quit_action.triggered[bool].connect(self.quit)
        self.donate_action.triggered[bool].connect(self.donate)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                _('Clear the current search'), default_keys=('Esc',),
                action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                _('Focus the book list'), default_keys=('Shift+Esc',),
                action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                _('Clear the virtual library'), default_keys=('Ctrl+Esc',),
                action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                _('Clear the additional restriction'), default_keys=('Alt+Esc',),
                action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(self.clear_additional_restriction)

        # ###################### Start spare job server ########################
        QTimer.singleShot(1000, self.create_spare_pool)

        # ###################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(self.device_manager.umount_device)
        self.location_manager.configure_device.connect(self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        # ################### Update notification ###################
        UpdateMixin.init_update_mixin(self, opts)

        # ###################### Search boxes ########################
        SearchRestrictionMixin.init_search_restirction_mixin(self)
        SavedSearchBoxMixin.init_saved_seach_box_mixin(self)

        # ###################### Library view ########################
        LibraryViewMixin.init_library_view_mixin(self, db)
        SearchBoxMixin.init_search_box_mixin(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon is not None and self.system_tray_icon.isVisible() and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
                self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            try:
                add_quick_start_guide(self.library_view)
            except:
                import traceback
                traceback.print_exc()
        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(self.bars_manager.database_changed,
                type=Qt.QueuedConnection)

        # ########################## Tags Browser ##############################
        TagBrowserMixin.init_tag_browser_mixin(self, db)

        # ######################## Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        # ########################## Cover Flow ################################

        CoverFlowMixin.init_cover_flow_mixin(self)

        self._calculated_available_height = min(max_available_height()-15,
                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.set_current_library_information(current_library_name(), db.library_id,
                                             db.field_metadata)

        register_keyboard_shortcuts()
        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        if show_gui and self.gui_debug is not None:
            info_dialog(self, _('Debug mode'), '<p>' +
                    _('You have started calibre in debug mode. After you '
                        'quit calibre, the debug log will be available in '
                        'the file: %s<p>The '
                        'log will be displayed automatically.')%self.gui_debug, show=True)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)
        QTimer.singleShot(100, self.update_toggle_to_tray_action)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(self, _('Problem starting the wireless device'),
                             _('The wireless device driver had problems starting. '
                               'It said "%s"')%message, show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
                self.library_view.model().db, server_config().parse())
        self.content_server.state_callback = Dispatcher(
                self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self, _('Failed to start Content Server'),
                _('Could not start the content server. Error:\n\n%s')%msg,
                show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width()-150)

    def create_spare_pool(self, *args):
        if self._spare_pool is None:
            num = min(detect_ncpus(), int(config['worker_limit']/2.0))
            self._spare_pool = Pool(max_workers=num, name='GUIPool')

    def spare_pool(self):
        ans, self._spare_pool = self._spare_pool, None
        QTimer.singleShot(1000, self.create_spare_pool)
        return ans

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r=False):
        if r in (QSystemTrayIcon.Trigger, QSystemTrayIcon.MiddleClick, False):
            if self.isVisible():
                if self.isMinimized():
                    self.showNormal()
                else:
                    self.hide_windows()
            else:
                self.show_windows()
                if self.isMinimized():
                    self.showNormal()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self, title, msg, det_msg='',
            show_copy_button=False, ans_when_user_unavailable=True,
            skip_dialog_name=None, skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self, title, msg, det_msg=det_msg,
                show_copy_button=show_copy_button,
                skip_dialog_name=skip_dialog_name,
                skip_dialog_skipped_value=skipped_value)

    def update_toggle_to_tray_action(self, *args):
        if hasattr(self, 'toggle_to_tray_action'):
            self.toggle_to_tray_action.setText(
                _('Hide main window') if self.isVisible() else _('Show main window'))

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)
        self.update_toggle_to_tray_action()

    def show_windows(self, *args):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)
        self.update_toggle_to_tray_action()

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' % msg)
                if DEBUG:
                    error_dialog(self, 'Invalid message',
                                 'Received an invalid message from other calibre instance.'
                                 ' Do you have multiple versions of calibre installed?',
                                 det_msg='Invalid msg: %r' % msg, show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [os.path.abspath(p) for p in argv[1:] if not os.path.isdir(p) and os.access(p, os.R_OK)]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized|Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        elif msg.startswith('bookedited:'):
            parts = msg.split(':')[1:]
            try:
                book_id, fmt, library_id = parts[:3]
                book_id = int(book_id)
                m = self.library_view.model()
                db = m.db.new_api
                if m.db.library_id == library_id and db.has_id(book_id):
                    db.format_metadata(book_id, fmt, allow_cache=False, update_db=True)
                    db.update_last_modified((book_id,))
                    m.refresh_ids((book_id,))
            except Exception:
                import traceback
                traceback.print_exc()
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db

    def library_moved(self, newloc, copy_structure=False, call_close=True,
            allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs

            from calibre.utils.formatter_functions import unload_user_template_functions
            unload_user_template_functions(olddb.library_id)
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback
            repair = question_dialog(self, _('Corrupted database'),
                    _('The library database at %s appears to be corrupted. Do '
                    'you want calibre to try and rebuild it automatically? '
                    'The rebuild may not be completely successful.')
                    % force_unicode(newloc, filesystem_encoding),
                    det_msg=traceback.format_exc()
                    )
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        # self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('')  # reset restriction to null
        self.saved_searches_changed(recount=False)  # reload the search restrictions combo box
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.set_current_library_information(current_library_name(), db.library_id,
                                             db.field_metadata)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
                __appname__, self.iactions['Choose Library'].library_name(), restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x+'_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
            self.vl_tabs.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            self.vl_tabs.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self, job, dialog_title=_('Conversion Error'), retry_func=None):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(self, _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s')%title
                msg += '<p>'+_('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self, _('Conversion Failed'), msg,
                            det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                        '<p>%s</p>'%msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:')[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {'info':info_dialog, 'warn':warning_dialog,
                            'error':error_dialog}.get(payload['level'],
                                    error_dialog)
                    d = d(self, payload['title'],
                            '<p>%s</p>'%payload['msg'],
                            det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                    _('<b>Failed</b>')+': '+unicode(job.description),
                    det_msg=job.details, retry_func=retry_func)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self, checked=True, restart=False, debug_on_restart=False,
            confirm_quit=True):
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('http://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False
        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.grid_view.shutdown()
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.new_api.set_pref('field_metadata', db.field_metadata.all_metadata())
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        if hasattr(self, 'update_checker'):
            self.update_checker.shutdown()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.hide_windows()
        try:
            try:
                if self.content_server is not None:
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        if self._spare_pool is not None:
            self._spare_pool.shutdown()
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        self.hide_windows()
        # Do not report any errors that happen after the shutdown
        sys.excepthook = sys.__excepthook__
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        self.write_settings()
        if self.system_tray_icon is not None and self.system_tray_icon.isVisible():
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(self, 'calibre', 'calibre '+
                        _('will keep running in the system tray. To close it, '
                        'choose <b>Quit</b> in the context menu of the '
                        'system tray.'), show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
示例#9
0
文件: add.py 项目: GRiker/calibre
class Adder(QObject):

    do_one_signal = pyqtSignal()

    def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None, pool=None, list_of_archives=False):
        if not validate_source(source, parent):
            return
        QObject.__init__(self, parent)
        self.single_book_per_directory = single_book_per_directory
        self.ignore_opf = False
        self.list_of_archives = list_of_archives
        self.callback = callback
        self.add_formats_to_existing = prefs['add_formats_to_existing']
        self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
        self.pool = pool
        self.pd = ProgressDialog(_('Adding books...'), _('Scanning for files...'), min=0, max=0, parent=parent, icon='add_book.png')
        self.db = getattr(db, 'new_api', None)
        if self.db is not None:
            self.dbref = weakref.ref(db)
        self.source = source
        self.tdir = PersistentTemporaryDirectory('_add_books')
        self.scan_error = None
        self.file_groups = OrderedDict()
        self.abort_scan = False
        self.duplicates = []
        self.report = []
        self.items = []
        self.added_book_ids = set()
        self.merged_books = set()
        self.added_duplicate_info = set()
        self.pd.show()

        self.scan_thread = Thread(target=self.scan, name='ScanBooks')
        self.scan_thread.daemon = True
        self.scan_thread.start()
        self.do_one = self.monitor_scan
        self.do_one_signal.emit()
        if DEBUG:
            self.start_time = time.time()

    def break_cycles(self):
        self.abort_scan = True
        self.pd.close()
        self.pd.deleteLater()
        if self.pool is not None:
            self.pool.shutdown()
        if not self.items:
            shutil.rmtree(self.tdir, ignore_errors=True)
        self.setParent(None)
        self.find_identical_books_data = self.merged_books = self.added_duplicate_info = self.pool = self.items = self.duplicates = self.pd = self.db = self.dbref = self.tdir = self.file_groups = self.scan_thread = None  # noqa
        self.deleteLater()

    def tick(self):
        if self.pd.canceled:
            try:
                if callable(self.callback):
                    self.callback(self)
            finally:
                self.break_cycles()
            return
        self.do_one()

    # Filesystem scan {{{

    def scan(self):

        def find_files(root):
            for dirpath, dirnames, filenames in os.walk(root):
                for files in find_books_in_directory(dirpath, self.single_book_per_directory):
                    if self.abort_scan:
                        return
                    self.file_groups[len(self.file_groups)] = files

        def extract(source):
            tdir = tempfile.mkdtemp(suffix='_archive', dir=self.tdir)
            if source.lower().endswith('.zip'):
                from calibre.utils.zipfile import ZipFile
                try:
                    with ZipFile(source) as zf:
                        zf.extractall(tdir)
                except Exception:
                    prints('Corrupt ZIP file, trying to use local headers')
                    from calibre.utils.localunzip import extractall
                    extractall(source, tdir)
            elif source.lower().endswith('.rar'):
                from calibre.utils.unrar import extract
                extract(source, tdir)
            return tdir

        try:
            if isinstance(self.source, basestring):
                find_files(self.source)
                self.ignore_opf = True
            else:
                unreadable_files = []
                for path in self.source:
                    if self.abort_scan:
                        return
                    if os.access(path, os.R_OK):
                        if self.list_of_archives:
                            find_files(extract(path))
                            self.ignore_opf = True
                        else:
                            self.file_groups[len(self.file_groups)] = [path]
                    else:
                        unreadable_files.append(path)
                if unreadable_files:
                    if not self.file_groups:
                        self.scan_error = _('You do not have permission to read the selected file(s).') + '\n'
                        self.scan_error += '\n'.join(unreadable_files)
                    else:
                        a = self.report.append
                        for f in unreadable_files:
                            a(_('Could not add %s as you do not have permission to read the file' % f))
                            a('')
        except Exception:
            self.scan_error = traceback.format_exc()

    def monitor_scan(self):
        self.scan_thread.join(0.05)
        if self.scan_thread.is_alive():
            self.do_one_signal.emit()
            return
        if self.scan_error is not None:
            error_dialog(self.pd, _('Cannot add books'), _(
                'Failed to add any books, click "Show details" for more information.'),
                         det_msg=self.scan_error, show=True)
            self.break_cycles()
            return
        if not self.file_groups:
            error_dialog(self.pd, _('Could not add'), _(
                'No ebook files were found in %s') % self.source, show=True)
            self.break_cycles()
            return
        self.pd.max = len(self.file_groups)
        self.pd.title = _('Reading metadata and adding to library (%d books)...') % self.pd.max
        self.pd.msg = ''
        self.pd.value = 0
        self.pool = Pool(name='AddBooks') if self.pool is None else self.pool
        if self.db is not None:
            if self.add_formats_to_existing:
                self.find_identical_books_data = self.db.data_for_find_identical_books()
            else:
                try:
                    self.pool.set_common_data(self.db.data_for_has_book())
                except Failure as err:
                    error_dialog(self.pd, _('Cannot add books'), _(
                    'Failed to add any books, click "Show details" for more information.'),
                    det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
                    self.pd.canceled = True
        self.groups_to_add = iter(self.file_groups)
        self.do_one = self.do_one_group
        self.do_one_signal.emit()
    # }}}

    def do_one_group(self):
        try:
            group_id = next(self.groups_to_add)
        except StopIteration:
            self.do_one = self.monitor_pool
            self.do_one_signal.emit()
            return
        try:
            self.pool(group_id, 'calibre.ebooks.metadata.worker', 'read_metadata',
                      self.file_groups[group_id], group_id, self.tdir)
        except Failure as err:
            error_dialog(self.pd, _('Cannot add books'), _(
            'Failed to add any books, click "Show details" for more information.'),
            det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
            self.pd.canceled = True
        self.do_one_signal.emit()

    def monitor_pool(self):
        try:
            worker_result = self.pool.results.get(True, 0.05)
            self.pool.results.task_done()
        except Empty:
            try:
                self.pool.wait_for_tasks(timeout=0.01)
            except RuntimeError:
                pass  # Tasks still remaining
            except Failure as err:
                error_dialog(self.pd, _('Cannot add books'), _(
                'Failed to add some books, click "Show details" for more information.'),
                det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
                self.pd.canceled = True
            else:
                # All tasks completed
                try:
                    join_with_timeout(self.pool.results, 0.01)
                except RuntimeError:
                    pass  # There are results remaining
                else:
                    # No results left
                    self.process_duplicates()
                    return
        else:
            group_id = worker_result.id
            if worker_result.is_terminal_failure:
                error_dialog(self.pd, _('Critical failure'), _(
                    'The read metadata worker process crashed while processing'
                    ' some files. Adding of books is aborted. Click "Show details"'
                    ' to see which files caused the problem.'), show=True,
                    det_msg='\n'.join(self.file_groups[group_id]))
                self.pd.canceled = True
            else:
                try:
                    self.process_result(group_id, worker_result.result)
                except Exception:
                    self.report_metadata_failure(group_id, traceback.format_exc())
                self.pd.value += 1

        self.do_one_signal.emit()

    def report_metadata_failure(self, group_id, details):
        a = self.report.append
        paths = self.file_groups[group_id]
        a(''), a('-' * 70)
        a(_('Failed to read metadata from the file(s):'))
        [a('\t' + f) for f in paths]
        a(_('With error:')), a(details)
        mi = Metadata(_('Unknown'))
        mi.read_metadata_failed = False
        return mi

    def process_result(self, group_id, result):
        if result.err:
            mi = self.report_metadata_failure(group_id, result.traceback)
            paths = self.file_groups[group_id]
            has_cover = False
            duplicate_info = set() if self.add_formats_to_existing else False
        else:
            paths, opf, has_cover, duplicate_info = result.value
            try:
                mi = OPF(BytesIO(opf), basedir=self.tdir, populate_spine=False, try_to_guess_cover=False).to_book_metadata()
                mi.read_metadata_failed = False
            except Exception:
                mi = self.report_metadata_failure(group_id, traceback.format_exc())

        if mi.is_null('title'):
            for path in paths:
                mi.title = os.path.splitext(os.path.basename(path))[0]
                break
        if mi.application_id == '__calibre_dummy__':
            mi.application_id = None

        self.pd.msg = mi.title

        cover_path = os.path.join(self.tdir, '%s.cdata' % group_id) if has_cover else None

        if self.db is None:
            if paths:
                self.items.append((mi, cover_path, paths))
            return

        if self.add_formats_to_existing:
            identical_book_ids = find_identical_books(mi, self.find_identical_books_data)
            if identical_book_ids:
                try:
                    self.merge_books(mi, cover_path, paths, identical_book_ids)
                except Exception:
                    a = self.report.append
                    a(''), a('-' * 70)
                    a(_('Failed to merge the book: ') + mi.title)
                    [a('\t' + f) for f in paths]
                    a(_('With error:')), a(traceback.format_exc())
            else:
                self.add_book(mi, cover_path, paths)
        else:
            if duplicate_info or icu_lower(mi.title or _('Unknown')) in self.added_duplicate_info:
                self.duplicates.append((mi, cover_path, paths))
            else:
                self.add_book(mi, cover_path, paths)

    def merge_books(self, mi, cover_path, paths, identical_book_ids):
        self.merged_books.add((mi.title, ' & '.join(mi.authors)))
        seen_fmts = set()
        replace = gprefs['automerge'] == 'overwrite'
        cover_removed = False
        for identical_book_id in identical_book_ids:
            ib_fmts = {fmt.upper() for fmt in self.db.formats(identical_book_id)}
            seen_fmts |= ib_fmts
            self.add_formats(identical_book_id, paths, mi, replace=replace)
        if gprefs['automerge'] == 'new record':
            incoming_fmts = {path.rpartition(os.extsep)[-1].upper() for path in paths}
            if incoming_fmts.intersection(seen_fmts):
                # There was at least one duplicate format so create a new
                # record and put the incoming formats into it We should
                # arguably put only the duplicate formats, but no real harm is
                # done by having all formats
                self.add_book(mi, cover_path, paths)
                cover_removed = True
        if not cover_removed and cover_path:
            try:
                os.remove(cover_path)
            except Exception:
                pass

    def add_book(self, mi, cover_path, paths):
        if DEBUG:
            st = time.time()
        try:
            cdata = None
            if cover_path:
                with open(cover_path, 'rb') as f:
                    cdata = f.read()
                try:
                    os.remove(cover_path)
                except Exception:
                    pass
            book_id = self.dbref().create_book_entry(mi, cover=cdata)
            self.added_book_ids.add(book_id)
        except Exception:
            a = self.report.append
            a(''), a('-' * 70)
            a(_('Failed to add the book: ') + mi.title)
            [a('\t' + f) for f in paths]
            a(_('With error:')), a(traceback.format_exc())
            return
        self.add_formats(book_id, paths, mi)
        try:
            if self.add_formats_to_existing:
                self.db.update_data_for_find_identical_books(book_id, self.find_identical_books_data)
            else:
                self.added_duplicate_info.add(icu_lower(mi.title or _('Unknown')))
        except Exception:
            # Ignore this exception since all it means is that duplicate
            # detection/automerge will fail for this book.
            traceback.print_exc()
        if DEBUG:
            prints('Added', mi.title, 'to db in: %.1f' % (time.time() - st))

    def add_formats(self, book_id, paths, mi, replace=True):
        fmap = {p.rpartition(os.path.extsep)[-1].lower():p for p in paths}
        for fmt, path in fmap.iteritems():
            # The onimport plugins have already been run by the read metadata
            # worker
            if self.ignore_opf and fmt.lower() == 'opf':
                continue
            try:
                if self.db.add_format(book_id, fmt, path, run_hooks=False, replace=replace):
                    run_plugins_on_postimport(self.dbref(), book_id, fmt)
            except Exception:
                a = self.report.append
                a(''), a('-' * 70)
                a(_('Failed to add the file {0} to the book: {1}').format(path, mi.title))
                a(_('With error:')), a(traceback.format_exc())

    def process_duplicates(self):
        if self.duplicates:
            d = DuplicatesQuestion(self.dbref(), self.duplicates, self.pd)
            duplicates = tuple(d.duplicates)
            d.deleteLater()
            if duplicates:
                self.do_one = self.process_duplicate
                self.duplicates_to_process = iter(duplicates)
                self.pd.title = _('Adding duplicates')
                self.pd.msg = ''
                self.pd.max, self.pd.value = len(duplicates), 0
                self.do_one_signal.emit()
                return
        self.finish()

    def process_duplicate(self):
        try:
            mi, cover_path, paths = next(self.duplicates_to_process)
        except StopIteration:
            self.finish()
            return
        self.pd.value += 1
        self.pd.msg = mi.title
        self.add_book(mi, cover_path, paths)
        self.do_one_signal.emit()

    def finish(self):
        if DEBUG:
            prints('Added %s books in %.1f seconds' % (len(self.added_book_ids or self.items), time.time() - self.start_time))
        if self.report:
            added_some = self.items or self.added_book_ids
            d = warning_dialog if added_some else error_dialog
            msg = _('There were problems adding some files, click "Show details" for more information') if added_some else _(
                'Failed to add any books, click "Show details" for more information')
            d(self.pd, _('Errors while adding'), msg, det_msg='\n'.join(self.report), show=True)

        if gprefs['manual_add_auto_convert'] and self.added_book_ids and self.parent() is not None:
            self.parent().iactions['Convert Books'].auto_convert_auto_add(
                self.added_book_ids)

        try:
            if callable(self.callback):
                self.callback(self)
        finally:
            self.break_cycles()

    @property
    def number_of_books_added(self):
        return len(self.added_book_ids)
示例#10
0
class Saver(QObject):

    do_one_signal = pyqtSignal()

    def __init__(self, book_ids, db, opts, root, parent=None, pool=None):
        QObject.__init__(self, parent)
        self.db = db.new_api
        self.plugboards = self.db.pref('plugboards', {})
        self.template_functions = self.db.pref('user_template_functions', [])
        load_user_template_functions('', self.template_functions)
        self.collected_data = {}
        self.errors = defaultdict(list)
        self._book_id_data = {}
        self.all_book_ids = frozenset(book_ids)
        self.pd = ProgressDialog(_('Saving %d books...') %
                                 len(self.all_book_ids),
                                 _('Collecting metadata...'),
                                 min=0,
                                 max=0,
                                 parent=parent,
                                 icon='save.png')
        self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
        self.do_one = self.do_one_collect
        self.ids_to_collect = iter(self.all_book_ids)
        self.tdir = PersistentTemporaryDirectory('_save_to_disk')
        self.pool = pool

        self.pd.show()
        self.root, self.opts, self.path_length = sanitize_args(root, opts)
        self.do_one_signal.emit()
        if DEBUG:
            self.start_time = time.time()

    def tick(self):
        if self.pd.canceled:
            self.pd.close()
            self.pd.deleteLater()
            self.break_cycles()
            return
        self.do_one()

    def break_cycles(self):
        shutil.rmtree(self.tdir, ignore_errors=True)
        if self.pool is not None:
            self.pool.shutdown()
        self.setParent(None)
        self.jobs = self.pool = self.plugboards = self.template_functions = self.collected_data = self.all_book_ids = self.pd = self.db = None  # noqa
        self.deleteLater()

    def book_id_data(self, book_id):
        ans = self._book_id_data.get(book_id)
        if ans is None:
            try:
                ans = BookId(self.db.field_for('title', book_id),
                             self.db.field_for('authors', book_id))
            except Exception:
                ans = BookId((_('Unknown') + ' (%d)' % book_id),
                             (_('Unknown'), ))
            self._book_id_data[book_id] = ans
        return ans

    def do_one_collect(self):
        try:
            book_id = next(self.ids_to_collect)
        except StopIteration:
            self.collection_finished()
            return
        try:
            self.collect_data(book_id)
        except Exception:
            self.errors[book_id].append(('critical', traceback.format_exc()))
        self.do_one_signal.emit()

    def collect_data(self, book_id):
        mi = self.db.get_metadata(book_id)
        self._book_id_data[book_id] = BookId(mi.title, mi.authors)
        components = get_path_components(self.opts, mi, book_id,
                                         self.path_length)
        self.collected_data[book_id] = (mi, components, {
            fmt.lower()
            for fmt in self.db.formats(book_id)
        })

    def collection_finished(self):
        self.do_one = self.do_one_write
        ensure_unique_components(self.collected_data)
        self.ids_to_write = iter(self.collected_data)
        self.pd.title = _('Copying files and writing metadata...'
                          ) if self.opts.update_metadata else _(
                              'Copying files...')
        self.pd.max = len(self.collected_data)
        self.pd.value = 0
        if self.opts.update_metadata:
            all_fmts = {
                fmt
                for data in self.collected_data.itervalues() for fmt in data[2]
            }
            plugboards_cache = {
                fmt: find_plugboard(plugboard_save_to_disk_value, fmt,
                                    self.plugboards)
                for fmt in all_fmts
            }
            self.pool = Pool(
                name='SaveToDisk') if self.pool is None else self.pool
            try:
                self.pool.set_common_data(plugboards_cache)
            except Failure as err:
                error_dialog(
                    self.pd,
                    _('Critical failure'),
                    _('Could not save books to disk, click "Show details" for more information'
                      ),
                    det_msg=unicode(err.failure_message) + '\n' +
                    unicode(err.details),
                    show=True)
                self.pd.canceled = True
        self.do_one_signal.emit()

    def do_one_write(self):
        try:
            book_id = next(self.ids_to_write)
        except StopIteration:
            self.writing_finished()
            return
        if not self.opts.update_metadata:
            self.pd.msg = self.book_id_data(book_id).title
            self.pd.value += 1
        try:
            self.write_book(book_id, *self.collected_data[book_id])
        except Exception:
            self.errors[book_id].append(('critical', traceback.format_exc()))
        self.consume_results()
        self.do_one_signal.emit()

    def consume_results(self):
        if self.pool is not None:
            while True:
                try:
                    worker_result = self.pool.results.get_nowait()
                except Empty:
                    break
                book_id = worker_result.id
                if worker_result.is_terminal_failure:
                    error_dialog(
                        self.pd,
                        _('Critical failure'),
                        _('The update metadata worker process crashed while processing'
                          ' the book %s. Saving is aborted.') %
                        self.book_id_data(book_id).title,
                        show=True)
                    self.pd.canceled = True
                    return
                result = worker_result.result
                self.pd.value += 1
                self.pd.msg = self.book_id_data(book_id).title
                if result.err is not None:
                    self.errors[book_id].append(
                        ('metadata', (None,
                                      result.err + '\n' + result.traceback)))
                if result.value:
                    for fmt, tb in result.value:
                        self.errors[book_id].append(('metadata', (fmt, tb)))

    def write_book(self, book_id, mi, components, fmts):
        base_path = os.path.join(self.root, *components)
        base_dir = os.path.dirname(base_path)
        if self.opts.formats and self.opts.formats != 'all':
            asked_formats = {
                x.lower().strip()
                for x in self.opts.formats.split(',')
            }
            fmts = asked_formats.intersection(fmts)
            if not fmts:
                self.errors[book_id].append(
                    ('critical', _('Requested formats not available')))
                return

        if not fmts and not self.opts.write_opf and not self.opts.save_cover:
            return

        # On windows python incorrectly raises an access denied exception
        # when trying to create the root of a drive, like C:\
        if os.path.dirname(base_dir) != base_dir:
            try:
                os.makedirs(base_dir)
            except EnvironmentError as err:
                if err.errno != errno.EEXIST:
                    raise

        if self.opts.update_metadata:
            d = {}
            d['last_modified'] = mi.last_modified.isoformat()

        cdata = self.db.cover(book_id)
        mi.cover, mi.cover_data = None, (None, None)

        if cdata:
            fname = None
            if self.opts.save_cover:
                fname = base_path + os.extsep + 'jpg'
                mi.cover = os.path.basename(fname)
            elif self.opts.update_metadata:
                fname = os.path.join(self.tdir, '%d.jpg' % book_id)

            if fname:
                with lopen(fname, 'wb') as f:
                    f.write(cdata)
                if self.opts.update_metadata:
                    d['cover'] = fname

        fname = None
        if self.opts.write_opf:
            fname = base_path + os.extsep + 'opf'
        elif self.opts.update_metadata:
            fname = os.path.join(self.tdir, '%d.opf' % book_id)
        if fname:
            opf = metadata_to_opf(mi)
            with lopen(fname, 'wb') as f:
                f.write(opf)
            if self.opts.update_metadata:
                d['opf'] = fname
        mi.cover, mi.cover_data = None, (None, None)
        if self.opts.update_metadata:
            d['fmts'] = []
        for fmt in fmts:
            try:
                fmtpath = self.write_fmt(book_id, fmt, base_path)
                if fmtpath and self.opts.update_metadata and can_set_metadata(
                        fmt):
                    d['fmts'].append(fmtpath)
            except Exception:
                self.errors[book_id].append(
                    ('fmt', (fmt, traceback.format_exc())))
        if self.opts.update_metadata:
            if d['fmts']:
                try:
                    self.pool(book_id, 'calibre.library.save_to_disk',
                              'update_serialized_metadata', d)
                except Failure as err:
                    error_dialog(
                        self.pd,
                        _('Critical failure'),
                        _('Could not save books to disk, click "Show details" for more information'
                          ),
                        det_msg=unicode(err.failure_message) + '\n' +
                        unicode(err.details),
                        show=True)
                    self.pd.canceled = True
            else:
                self.pd.value += 1
                self.pd.msg = self.book_id_data(book_id).title

    def write_fmt(self, book_id, fmt, base_path):
        fmtpath = base_path + os.extsep + fmt
        written = False
        with lopen(fmtpath, 'w+b') as f:
            try:
                self.db.copy_format_to(book_id, fmt, f)
                written = True
            except NoSuchFormat:
                self.errors[book_id].append(
                    ('fmt', (fmt,
                             _('No %s format file present') % fmt.upper())))
        if not written:
            os.remove(fmtpath)
        if written:
            return fmtpath

    def writing_finished(self):
        if not self.opts.update_metadata:
            self.updating_metadata_finished()
        else:
            self.do_one = self.do_one_update
            self.do_one_signal.emit()

    def do_one_update(self):
        self.consume_results()
        try:
            self.pool.wait_for_tasks(0.1)
        except Failure as err:
            error_dialog(
                self.pd,
                _('Critical failure'),
                _('Could not save books to disk, click "Show details" for more information'
                  ),
                det_msg=unicode(err.failure_message) + '\n' +
                unicode(err.details),
                show=True)
            self.pd.canceled = True
        except RuntimeError:
            pass  # tasks not completed
        else:
            self.consume_results()
            return self.updating_metadata_finished()
        self.do_one_signal.emit()

    def updating_metadata_finished(self):
        if DEBUG:
            prints('Saved %d books in %.1f seconds' %
                   (len(self.all_book_ids), time.time() - self.start_time))
        self.pd.close()
        self.pd.deleteLater()
        self.report()
        self.break_cycles()
        if gprefs['show_files_after_save']:
            open_local_file(self.root)

    def format_report(self):
        report = []
        a = report.append

        def indent(text):
            text = force_unicode(text)
            return '\xa0\xa0\xa0\xa0' + '\n\xa0\xa0\xa0\xa0'.join(
                text.splitlines())

        for book_id, errors in self.errors.iteritems():
            types = {t for t, data in errors}
            title, authors = self.book_id_data(
                book_id).title, authors_to_string(
                    self.book_id_data(book_id).authors[:1])
            if report:
                a('\n' + ('_' * 70) + '\n')
            if 'critical' in types:
                a(
                    _('Failed to save: {0} by {1} to disk, with error:').
                    format(title, authors))
                for t, tb in errors:
                    if t == 'critical':
                        a(indent(tb))
            else:
                errs = defaultdict(list)
                for t, data in errors:
                    errs[t].append(data)
                for fmt, tb in errs['fmt']:
                    a(
                        _('Failed to save the {2} format of: {0} by {1} to disk, with error:'
                          ).format(title, authors, fmt.upper()))
                    a(indent(tb)), a('')
                for fmt, tb in errs['metadata']:
                    if fmt:
                        a(
                            _('Failed to update the metadata in the {2} format of: {0} by {1}, with error:'
                              ).format(title, authors, fmt.upper()))
                    else:
                        a(
                            _('Failed to update the metadata in all formats of: {0} by {1}, with error:'
                              ).format(title, authors))
                    a(indent(tb)), a('')
        return '\n'.join(report)

    def report(self):
        if not self.errors:
            return
        err_types = {
            e[0]
            for errors in self.errors.itervalues() for e in errors
        }
        if err_types == {'metadata'}:
            msg = _(
                'Failed to update metadata in some books, click "Show details" for more information'
            )
            d = warning_dialog
        elif len(self.errors) == len(self.all_book_ids):
            msg = _(
                'Failed to save any books to disk, click "Show details" for more information'
            )
            d = error_dialog
        else:
            msg = _(
                'Failed to save some books to disk, click "Show details" for more information'
            )
            d = warning_dialog
        d(self.parent(),
          _('Error while saving'),
          msg,
          det_msg=self.format_report(),
          show=True)
示例#11
0
class Adder(QObject):

    do_one_signal = pyqtSignal()

    def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None, pool=None, list_of_archives=False):
        if not validate_source(source, parent):
            return
        QObject.__init__(self, parent)
        self.author_map_rules = None
        if gprefs.get('author_map_on_add_rules'):
            from calibre.ebooks.metadata.author_mapper import compile_rules as acr
            self.author_map_rules = acr(gprefs['author_map_on_add_rules'])
        self.single_book_per_directory = single_book_per_directory
        self.ignore_opf = False
        self.list_of_archives = list_of_archives
        self.callback = callback
        self.add_formats_to_existing = prefs['add_formats_to_existing']
        self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
        self.pool = pool
        self.pd = ProgressDialog(_('Adding books...'), _('Scanning for files...'), min=0, max=0, parent=parent, icon='add_book.png')
        self.db = getattr(db, 'new_api', None)
        if self.db is not None:
            self.dbref = weakref.ref(db)
        self.source = source
        self.tdir = PersistentTemporaryDirectory('_add_books')
        self.scan_error = None
        self.file_groups = OrderedDict()
        self.abort_scan = False
        self.duplicates = []
        self.report = []
        self.items = []
        self.added_book_ids = set()
        self.merged_books = set()
        self.added_duplicate_info = set()
        self.pd.show()

        self.scan_thread = Thread(target=self.scan, name='ScanBooks')
        self.scan_thread.daemon = True
        self.scan_thread.start()
        self.do_one = self.monitor_scan
        self.do_one_signal.emit()
        if DEBUG:
            self.start_time = time.time()

    def break_cycles(self):
        self.abort_scan = True
        self.pd.close()
        self.pd.deleteLater()
        if self.pool is not None:
            self.pool.shutdown()
        if not self.items:
            shutil.rmtree(self.tdir, ignore_errors=True)
        self.setParent(None)
        self.find_identical_books_data = self.merged_books = self.added_duplicate_info = self.pool = self.items = self.duplicates = self.pd = self.db = self.dbref = self.tdir = self.file_groups = self.scan_thread = None  # noqa
        self.deleteLater()

    def tick(self):
        if self.pd.canceled:
            try:
                if callable(self.callback):
                    self.callback(self)
            finally:
                self.break_cycles()
            return
        self.do_one()

    # Filesystem scan {{{

    def scan(self):

        try:
            compiled_rules = tuple(map(compile_rule, gprefs.get('add_filter_rules', ())))
        except Exception:
            compiled_rules = ()
            import traceback
            traceback.print_exc()

        if iswindows or isosx:
            def find_files(root):
                for dirpath, dirnames, filenames in os.walk(root):
                    for files in find_books_in_directory(dirpath, self.single_book_per_directory, compiled_rules=compiled_rules):
                        if self.abort_scan:
                            return
                        self.file_groups[len(self.file_groups)] = files
        else:
            def find_files(root):
                if isinstance(root, type(u'')):
                    root = root.encode(filesystem_encoding)
                for dirpath, dirnames, filenames in os.walk(root):
                    try:
                        dirpath = dirpath.decode(filesystem_encoding)
                    except UnicodeDecodeError:
                        prints('Ignoring non-decodable directory:', dirpath)
                        continue
                    for files in find_books_in_directory(dirpath, self.single_book_per_directory, compiled_rules=compiled_rules):
                        if self.abort_scan:
                            return
                        self.file_groups[len(self.file_groups)] = files

        def extract(source):
            tdir = tempfile.mkdtemp(suffix='_archive', dir=self.tdir)
            if source.lower().endswith('.zip'):
                from calibre.utils.zipfile import ZipFile
                try:
                    with ZipFile(source) as zf:
                        zf.extractall(tdir)
                except Exception:
                    prints('Corrupt ZIP file, trying to use local headers')
                    from calibre.utils.localunzip import extractall
                    extractall(source, tdir)
            elif source.lower().endswith('.rar'):
                from calibre.utils.unrar import extract
                extract(source, tdir)
            return tdir

        try:
            if isinstance(self.source, basestring):
                find_files(self.source)
                self.ignore_opf = True
            else:
                unreadable_files = []
                for path in self.source:
                    if self.abort_scan:
                        return
                    if os.access(path, os.R_OK):
                        if self.list_of_archives:
                            find_files(extract(path))
                            self.ignore_opf = True
                        else:
                            self.file_groups[len(self.file_groups)] = [path]
                    else:
                        unreadable_files.append(path)
                if unreadable_files:
                    if not self.file_groups:
                        m = ngettext('You do not have permission to read the selected file.',
                                     'You do not have permission to read the selected files.', len(unreadable_files))
                        self.scan_error = m + '\n' + '\n'.join(unreadable_files)
                    else:
                        a = self.report.append
                        for f in unreadable_files:
                            a(_('Could not add %s as you do not have permission to read the file' % f))
                            a('')
        except Exception:
            self.scan_error = traceback.format_exc()

    def monitor_scan(self):
        self.scan_thread.join(0.05)
        if self.scan_thread.is_alive():
            self.do_one_signal.emit()
            return
        if self.scan_error is not None:
            error_dialog(self.pd, _('Cannot add books'), _(
                'Failed to add any books, click "Show details" for more information.'),
                         det_msg=self.scan_error, show=True)
            self.break_cycles()
            return
        if not self.file_groups:
            error_dialog(self.pd, _('Could not add'), _(
                'No e-book files were found in %s') % self.source, show=True)
            self.break_cycles()
            return
        self.pd.max = len(self.file_groups)
        self.pd.title = ngettext(
            'Reading metadata and adding to library (one book)...',
            'Reading metadata and adding to library ({} books)...',
            self.pd.max).format(self.pd.max)
        self.pd.msg = ''
        self.pd.value = 0
        self.pool = Pool(name='AddBooks') if self.pool is None else self.pool
        if self.db is not None:
            if self.add_formats_to_existing:
                self.find_identical_books_data = self.db.data_for_find_identical_books()
            else:
                try:
                    self.pool.set_common_data(self.db.data_for_has_book())
                except Failure as err:
                    error_dialog(self.pd, _('Cannot add books'), _(
                    'Failed to add any books, click "Show details" for more information.'),
                    det_msg=as_unicode(err.failure_message) + '\n' + as_unicode(err.details), show=True)
                    self.pd.canceled = True
        self.groups_to_add = iter(self.file_groups)
        self.do_one = self.do_one_group
        self.do_one_signal.emit()
    # }}}

    def do_one_group(self):
        try:
            group_id = next(self.groups_to_add)
        except StopIteration:
            self.do_one = self.monitor_pool
            self.do_one_signal.emit()
            return
        try:
            self.pool(group_id, 'calibre.ebooks.metadata.worker', 'read_metadata',
                      self.file_groups[group_id], group_id, self.tdir)
        except Failure as err:
            error_dialog(self.pd, _('Cannot add books'), _(
            'Failed to add any books, click "Show details" for more information.'),
            det_msg=as_unicode(err.failure_message) + '\n' + as_unicode(err.details), show=True)
            self.pd.canceled = True
        self.do_one_signal.emit()

    def monitor_pool(self):
        try:
            worker_result = self.pool.results.get(True, 0.05)
            self.pool.results.task_done()
        except Empty:
            try:
                self.pool.wait_for_tasks(timeout=0.01)
            except RuntimeError:
                pass  # Tasks still remaining
            except Failure as err:
                error_dialog(self.pd, _('Cannot add books'), _(
                'Failed to add some books, click "Show details" for more information.'),
                det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
                self.pd.canceled = True
            else:
                # All tasks completed
                try:
                    join_with_timeout(self.pool.results, 0.01)
                except RuntimeError:
                    pass  # There are results remaining
                else:
                    # No results left
                    self.process_duplicates()
                    return
        else:
            group_id = worker_result.id
            if worker_result.is_terminal_failure:
                error_dialog(self.pd, _('Critical failure'), _(
                    'The read metadata worker process crashed while processing'
                    ' some files. Adding of books is aborted. Click "Show details"'
                    ' to see which files caused the problem.'), show=True,
                    det_msg='\n'.join(self.file_groups[group_id]))
                self.pd.canceled = True
            else:
                try:
                    self.process_result(group_id, worker_result.result)
                except Exception:
                    self.report_metadata_failure(group_id, traceback.format_exc())
                self.pd.value += 1

        self.do_one_signal.emit()

    def report_metadata_failure(self, group_id, details):
        a = self.report.append
        paths = self.file_groups[group_id]
        a(''), a('-' * 70)
        m = ngettext('Failed to read metadata from the file:', 'Failed to read metadata from the files:', len(paths))
        a(m)
        [a('\t' + f) for f in paths]
        a(_('With error:')), a(details)
        mi = Metadata(_('Unknown'))
        mi.read_metadata_failed = False
        return mi

    def process_result(self, group_id, result):
        if result.err:
            mi = self.report_metadata_failure(group_id, result.traceback)
            paths = self.file_groups[group_id]
            has_cover = False
            duplicate_info = set() if self.add_formats_to_existing else False
        else:
            paths, opf, has_cover, duplicate_info = result.value
            try:
                mi = OPF(BytesIO(opf), basedir=self.tdir, populate_spine=False, try_to_guess_cover=False).to_book_metadata()
                mi.read_metadata_failed = False
            except Exception:
                mi = self.report_metadata_failure(group_id, traceback.format_exc())

        if mi.is_null('title'):
            for path in paths:
                mi.title = os.path.splitext(os.path.basename(path))[0]
                break
        if mi.application_id == '__calibre_dummy__':
            mi.application_id = None
        if gprefs.get('tag_map_on_add_rules'):
            from calibre.ebooks.metadata.tag_mapper import map_tags
            mi.tags = map_tags(mi.tags, gprefs['tag_map_on_add_rules'])
        if self.author_map_rules:
            from calibre.ebooks.metadata.author_mapper import map_authors
            new_authors = map_authors(mi.authors, self.author_map_rules)
            if new_authors != mi.authors:
                mi.authors = new_authors
                if self.db is None:
                    mi.author_sort = authors_to_sort_string(mi.authors)
                else:
                    mi.author_sort = self.db.author_sort_from_authors(mi.authors)

        self.pd.msg = mi.title

        cover_path = os.path.join(self.tdir, '%s.cdata' % group_id) if has_cover else None

        if self.db is None:
            if paths:
                self.items.append((mi, cover_path, paths))
            return

        if self.add_formats_to_existing:
            identical_book_ids = find_identical_books(mi, self.find_identical_books_data)
            if identical_book_ids:
                try:
                    self.merge_books(mi, cover_path, paths, identical_book_ids)
                except Exception:
                    a = self.report.append
                    a(''), a('-' * 70)
                    a(_('Failed to merge the book: ') + mi.title)
                    [a('\t' + f) for f in paths]
                    a(_('With error:')), a(traceback.format_exc())
            else:
                self.add_book(mi, cover_path, paths)
        else:
            if duplicate_info or icu_lower(mi.title or _('Unknown')) in self.added_duplicate_info:
                self.duplicates.append((mi, cover_path, paths))
            else:
                self.add_book(mi, cover_path, paths)

    def merge_books(self, mi, cover_path, paths, identical_book_ids):
        self.merged_books.add((mi.title, ' & '.join(mi.authors)))
        seen_fmts = set()
        replace = gprefs['automerge'] == 'overwrite'
        cover_removed = False
        for identical_book_id in identical_book_ids:
            ib_fmts = {fmt.upper() for fmt in self.db.formats(identical_book_id)}
            seen_fmts |= ib_fmts
            self.add_formats(identical_book_id, paths, mi, replace=replace)
        if gprefs['automerge'] == 'new record':
            incoming_fmts = {path.rpartition(os.extsep)[-1].upper() for path in paths}
            if incoming_fmts.intersection(seen_fmts):
                # There was at least one duplicate format so create a new
                # record and put the incoming formats into it We should
                # arguably put only the duplicate formats, but no real harm is
                # done by having all formats
                self.add_book(mi, cover_path, paths)
                cover_removed = True
        if not cover_removed and cover_path:
            try:
                os.remove(cover_path)
            except Exception:
                pass

    def add_book(self, mi, cover_path, paths):
        if DEBUG:
            st = time.time()
        try:
            cdata = None
            if cover_path:
                with open(cover_path, 'rb') as f:
                    cdata = f.read()
                try:
                    os.remove(cover_path)
                except Exception:
                    pass
            book_id = self.dbref().create_book_entry(mi, cover=cdata)
            self.added_book_ids.add(book_id)
        except Exception:
            a = self.report.append
            a(''), a('-' * 70)
            a(_('Failed to add the book: ') + mi.title)
            [a('\t' + f) for f in paths]
            a(_('With error:')), a(traceback.format_exc())
            return
        self.add_formats(book_id, paths, mi, is_an_add=True)
        try:
            if self.add_formats_to_existing:
                self.db.update_data_for_find_identical_books(book_id, self.find_identical_books_data)
            else:
                self.added_duplicate_info.add(icu_lower(mi.title or _('Unknown')))
        except Exception:
            # Ignore this exception since all it means is that duplicate
            # detection/automerge will fail for this book.
            traceback.print_exc()
        if DEBUG:
            prints('Added', mi.title, 'to db in: %.1f' % (time.time() - st))

    def add_formats(self, book_id, paths, mi, replace=True, is_an_add=False):
        fmap = {p.rpartition(os.path.extsep)[-1].lower():p for p in paths}
        fmt_map = {}
        for fmt, path in fmap.iteritems():
            # The onimport plugins have already been run by the read metadata
            # worker
            if self.ignore_opf and fmt.lower() == 'opf':
                continue
            try:
                if self.db.add_format(book_id, fmt, path, run_hooks=False, replace=replace):
                    run_plugins_on_postimport(self.dbref(), book_id, fmt)
                    fmt_map[fmt.lower()] = path
            except Exception:
                a = self.report.append
                a(''), a('-' * 70)
                a(_('Failed to add the file {0} to the book: {1}').format(path, mi.title))
                a(_('With error:')), a(traceback.format_exc())
        if is_an_add:
            run_plugins_on_postadd(self.dbref(), book_id, fmt_map)

    def process_duplicates(self):
        if self.duplicates:
            d = DuplicatesQuestion(self.dbref(), self.duplicates, self.pd)
            duplicates = tuple(d.duplicates)
            d.deleteLater()
            if duplicates:
                self.do_one = self.process_duplicate
                self.duplicates_to_process = iter(duplicates)
                self.pd.title = _('Adding duplicates')
                self.pd.msg = ''
                self.pd.max, self.pd.value = len(duplicates), 0
                self.do_one_signal.emit()
                return
        self.finish()

    def process_duplicate(self):
        try:
            mi, cover_path, paths = next(self.duplicates_to_process)
        except StopIteration:
            self.finish()
            return
        self.pd.value += 1
        self.pd.msg = mi.title
        self.add_book(mi, cover_path, paths)
        self.do_one_signal.emit()

    def finish(self):
        if DEBUG:
            prints('Added %s books in %.1f seconds' % (len(self.added_book_ids or self.items), time.time() - self.start_time))
        if self.report:
            added_some = self.items or self.added_book_ids
            d = warning_dialog if added_some else error_dialog
            msg = _('There were problems adding some files, click "Show details" for more information') if added_some else _(
                'Failed to add any books, click "Show details" for more information')
            d(self.pd, _('Errors while adding'), msg, det_msg='\n'.join(self.report), show=True)

        if gprefs['manual_add_auto_convert'] and self.added_book_ids and self.parent() is not None:
            self.parent().iactions['Convert Books'].auto_convert_auto_add(
                self.added_book_ids)

        try:
            if callable(self.callback):
                self.callback(self)
        finally:
            self.break_cycles()

    @property
    def number_of_books_added(self):
        return len(self.added_book_ids)
示例#12
0
文件: save.py 项目: JimmXinu/calibre
class Saver(QObject):

    do_one_signal = pyqtSignal()

    def __init__(self, book_ids, db, opts, root, parent=None, pool=None):
        QObject.__init__(self, parent)
        self.db = db.new_api
        self.plugboards = self.db.pref('plugboards', {})
        self.template_functions = self.db.pref('user_template_functions', [])
        load_user_template_functions('', self.template_functions)
        self.collected_data = {}
        self.errors = defaultdict(list)
        self._book_id_data = {}
        self.all_book_ids = frozenset(book_ids)
        self.pd = ProgressDialog(_('Saving %d books...') % len(self.all_book_ids), _('Collecting metadata...'), min=0, max=0, parent=parent, icon='save.png')
        self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
        self.do_one = self.do_one_collect
        self.ids_to_collect = iter(self.all_book_ids)
        self.tdir = PersistentTemporaryDirectory('_save_to_disk')
        self.pool = pool

        self.pd.show()
        self.root, self.opts, self.path_length = sanitize_args(root, opts)
        self.do_one_signal.emit()
        if DEBUG:
            self.start_time = time.time()

    def tick(self):
        if self.pd.canceled:
            self.pd.close()
            self.pd.deleteLater()
            self.break_cycles()
            return
        self.do_one()

    def break_cycles(self):
        shutil.rmtree(self.tdir, ignore_errors=True)
        if self.pool is not None:
            self.pool.shutdown()
        self.setParent(None)
        self.jobs = self.pool = self.plugboards = self.template_functions = self.collected_data = self.all_book_ids = self.pd = self.db = None  # noqa
        self.deleteLater()

    def book_id_data(self, book_id):
        ans = self._book_id_data.get(book_id)
        if ans is None:
            try:
                ans = BookId(self.db.field_for('title', book_id), self.db.field_for('authors', book_id))
            except Exception:
                ans = BookId((_('Unknown') + ' (%d)' % book_id), (_('Unknown'),))
            self._book_id_data[book_id] = ans
        return ans

    def do_one_collect(self):
        try:
            book_id = next(self.ids_to_collect)
        except StopIteration:
            self.collection_finished()
            return
        try:
            self.collect_data(book_id)
        except Exception:
            self.errors[book_id].append(('critical', traceback.format_exc()))
        self.do_one_signal.emit()

    def collect_data(self, book_id):
        mi = self.db.get_metadata(book_id)
        self._book_id_data[book_id] = BookId(mi.title, mi.authors)
        components = get_path_components(self.opts, mi, book_id, self.path_length)
        self.collected_data[book_id] = (mi, components, {fmt.lower() for fmt in self.db.formats(book_id)})

    def collection_finished(self):
        self.do_one = self.do_one_write
        ensure_unique_components(self.collected_data)
        self.ids_to_write = iter(self.collected_data)
        self.pd.title = _('Copying files and writing metadata...') if self.opts.update_metadata else _(
            'Copying files...')
        self.pd.max = len(self.collected_data)
        self.pd.value = 0
        if self.opts.update_metadata:
            all_fmts = {fmt for data in itervalues(self.collected_data) for fmt in data[2]}
            plugboards_cache = {fmt:find_plugboard(plugboard_save_to_disk_value, fmt, self.plugboards) for fmt in all_fmts}
            self.pool = Pool(name='SaveToDisk') if self.pool is None else self.pool
            try:
                self.pool.set_common_data(plugboards_cache)
            except Failure as err:
                error_dialog(self.pd, _('Critical failure'), _(
                    'Could not save books to disk, click "Show details" for more information'),
                    det_msg=unicode_type(err.failure_message) + '\n' + unicode_type(err.details), show=True)
                self.pd.canceled = True
        self.do_one_signal.emit()

    def do_one_write(self):
        try:
            book_id = next(self.ids_to_write)
        except StopIteration:
            self.writing_finished()
            return
        if not self.opts.update_metadata:
            self.pd.msg = self.book_id_data(book_id).title
            self.pd.value += 1
        try:
            self.write_book(book_id, *self.collected_data[book_id])
        except Exception:
            self.errors[book_id].append(('critical', traceback.format_exc()))
        self.consume_results()
        self.do_one_signal.emit()

    def consume_results(self):
        if self.pool is not None:
            while True:
                try:
                    worker_result = self.pool.results.get_nowait()
                except Empty:
                    break
                book_id = worker_result.id
                if worker_result.is_terminal_failure:
                    error_dialog(self.pd, _('Critical failure'), _(
                        'The update metadata worker process crashed while processing'
                        ' the book %s. Saving is aborted.') % self.book_id_data(book_id).title, show=True)
                    self.pd.canceled = True
                    return
                result = worker_result.result
                self.pd.value += 1
                self.pd.msg = self.book_id_data(book_id).title
                if result.err is not None:
                    self.errors[book_id].append(('metadata', (None, result.err + '\n' + result.traceback)))
                if result.value:
                    for fmt, tb in result.value:
                        self.errors[book_id].append(('metadata', (fmt, tb)))

    def write_book(self, book_id, mi, components, fmts):
        base_path = os.path.join(self.root, *components)
        base_dir = os.path.dirname(base_path)
        if self.opts.formats and self.opts.formats != 'all':
            asked_formats = {x.lower().strip() for x in self.opts.formats.split(',')}
            fmts = asked_formats.intersection(fmts)
            if not fmts:
                self.errors[book_id].append(('critical', _('Requested formats not available')))
                return

        if not fmts and not self.opts.write_opf and not self.opts.save_cover:
            return

        # On windows python incorrectly raises an access denied exception
        # when trying to create the root of a drive, like C:\
        if os.path.dirname(base_dir) != base_dir:
            try:
                os.makedirs(base_dir)
            except EnvironmentError as err:
                if err.errno != errno.EEXIST:
                    raise

        if self.opts.update_metadata:
            d = {}
            d['last_modified'] = mi.last_modified.isoformat()

        cdata = self.db.cover(book_id)
        mi.cover, mi.cover_data = None, (None, None)

        if cdata:
            fname = None
            if self.opts.save_cover:
                fname = base_path + os.extsep + 'jpg'
                mi.cover = os.path.basename(fname)
            elif self.opts.update_metadata:
                fname = os.path.join(self.tdir, '%d.jpg' % book_id)

            if fname:
                with lopen(fname, 'wb') as f:
                    f.write(cdata)
                if self.opts.update_metadata:
                    d['cover'] = fname

        fname = None
        if self.opts.write_opf:
            fname = base_path + os.extsep + 'opf'
        elif self.opts.update_metadata:
            fname = os.path.join(self.tdir, '%d.opf' % book_id)
        if fname:
            opf = metadata_to_opf(mi)
            with lopen(fname, 'wb') as f:
                f.write(opf)
            if self.opts.update_metadata:
                d['opf'] = fname
        mi.cover, mi.cover_data = None, (None, None)
        if self.opts.update_metadata:
            d['fmts'] = []
        for fmt in fmts:
            try:
                fmtpath = self.write_fmt(book_id, fmt, base_path)
                if fmtpath and self.opts.update_metadata and can_set_metadata(fmt):
                    d['fmts'].append(fmtpath)
            except Exception:
                self.errors[book_id].append(('fmt', (fmt, traceback.format_exc())))
        if self.opts.update_metadata:
            if d['fmts']:
                try:
                    self.pool(book_id, 'calibre.library.save_to_disk', 'update_serialized_metadata', d)
                except Failure as err:
                    error_dialog(self.pd, _('Critical failure'), _(
                        'Could not save books to disk, click "Show details" for more information'),
                        det_msg=unicode_type(err.failure_message) + '\n' + unicode_type(err.details), show=True)
                    self.pd.canceled = True
            else:
                self.pd.value += 1
                self.pd.msg = self.book_id_data(book_id).title

    def write_fmt(self, book_id, fmt, base_path):
        fmtpath = base_path + os.extsep + fmt
        written = False
        with lopen(fmtpath, 'w+b') as f:
            try:
                self.db.copy_format_to(book_id, fmt, f)
                written = True
            except NoSuchFormat:
                self.errors[book_id].append(('fmt', (fmt, _('No %s format file present') % fmt.upper())))
        if not written:
            os.remove(fmtpath)
        if written:
            return fmtpath

    def writing_finished(self):
        if not self.opts.update_metadata:
            self.updating_metadata_finished()
        else:
            self.do_one = self.do_one_update
            self.do_one_signal.emit()

    def do_one_update(self):
        self.consume_results()
        try:
            self.pool.wait_for_tasks(0.1)
        except Failure as err:
            error_dialog(self.pd, _('Critical failure'), _(
                'Could not save books to disk, click "Show details" for more information'),
                det_msg=unicode_type(err.failure_message) + '\n' + unicode_type(err.details), show=True)
            self.pd.canceled = True
        except RuntimeError:
            pass  # tasks not completed
        else:
            self.consume_results()
            return self.updating_metadata_finished()
        self.do_one_signal.emit()

    def updating_metadata_finished(self):
        if DEBUG:
            prints('Saved %d books in %.1f seconds' % (len(self.all_book_ids), time.time() - self.start_time))
        self.pd.close()
        self.pd.deleteLater()
        self.report()
        self.break_cycles()
        if gprefs['show_files_after_save']:
            open_local_file(self.root)

    def format_report(self):
        report = []
        a = report.append

        def indent(text):
            text = force_unicode(text)
            return '\xa0\xa0\xa0\xa0' + '\n\xa0\xa0\xa0\xa0'.join(text.splitlines())

        for book_id, errors in iteritems(self.errors):
            types = {t for t, data in errors}
            title, authors = self.book_id_data(book_id).title, authors_to_string(self.book_id_data(book_id).authors[:1])
            if report:
                a('\n' + ('_'*70) + '\n')
            if 'critical' in types:
                a(_('Failed to save: {0} by {1} to disk, with error:').format(title, authors))
                for t, tb in errors:
                    if t == 'critical':
                        a(indent(tb))
            else:
                errs = defaultdict(list)
                for t, data in errors:
                    errs[t].append(data)
                for fmt, tb in errs['fmt']:
                    a(_('Failed to save the {2} format of: {0} by {1} to disk, with error:').format(title, authors, fmt.upper()))
                    a(indent(tb)), a('')
                for fmt, tb in errs['metadata']:
                    if fmt:
                        a(_('Failed to update the metadata in the {2} format of: {0} by {1}, with error:').format(title, authors, fmt.upper()))
                    else:
                        a(_('Failed to update the metadata in all formats of: {0} by {1}, with error:').format(title, authors))
                    a(indent(tb)), a('')
        return '\n'.join(report)

    def report(self):
        if not self.errors:
            return
        err_types = {e[0] for errors in itervalues(self.errors) for e in errors}
        if err_types == {'metadata'}:
            msg = _('Failed to update metadata in some books, click "Show details" for more information')
            d = warning_dialog
        elif len(self.errors) == len(self.all_book_ids):
            msg = _('Failed to save any books to disk, click "Show details" for more information')
            d = error_dialog
        else:
            msg = _('Failed to save some books to disk, click "Show details" for more information')
            d = warning_dialog
        d(self.parent(), _('Error while saving'), msg, det_msg=self.format_report(), show=True)