Esempio n. 1
0
 def _exportImageToPNG(self, filename=None):
     if not filename:
         if not self._export_png_dialog:
             dialog = self._export_png_dialog = QFileDialog(self, "Export image to PNG", ".", "*.png")
             dialog.setDefaultSuffix("png")
             dialog.setFileMode(QFileDialog.AnyFile)
             dialog.setAcceptMode(QFileDialog.AcceptSave)
             dialog.setModal(True)
             QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self._exportImageToPNG)
         return self._export_png_dialog.exec_() == QDialog.Accepted
     busy = BusyIndicator()
     if isinstance(filename, QStringList):
         filename = filename[0]
     filename = str(filename)
     # make QPixmap
     nx, ny = self.image.imageDims()
     (l0, l1), (m0, m1) = self.image.getExtents()
     pixmap = QPixmap(nx, ny)
     painter = QPainter(pixmap)
     # use QwtPlot implementation of draw canvas, since we want to avoid caching
     xmap = QwtScaleMap()
     xmap.setPaintInterval(0, nx)
     xmap.setScaleInterval(l1, l0)
     ymap = QwtScaleMap()
     ymap.setPaintInterval(ny, 0)
     ymap.setScaleInterval(m0, m1)
     self.image.draw(painter, xmap, ymap, pixmap.rect())
     painter.end()
     # save to file
     try:
         pixmap.save(filename, "PNG")
     except Exception as exc:
         self.emit(SIGNAL("showErrorMessage"), "Error writing %s: %s" % (filename, str(exc)))
         return
     self.emit(SIGNAL("showMessage"), "Exported image to file %s" % filename)
class MetadataComparisonDialog(SizePersistedDialog, Ui_Dialog, Logger):
    BORDER_COLOR = "#FDFF99"
    BORDER_WIDTH = 5
    COVER_ICON_SIZE = 200
    MISMATCH_COLOR = QColor(0xFD, 0xFF, 0x99)

    marvin_device_status_changed = pyqtSignal(dict)

    def accept(self):
        self._log_location()
        super(MetadataComparisonDialog, self).accept()

    def close(self):
        self._log_location()
        super(MetadataComparisonDialog, self).close()

    def dispatch_button_click(self, button):
        '''
        BUTTON_ROLES = ['AcceptRole', 'RejectRole', 'DestructiveRole', 'ActionRole',
                        'HelpRole', 'YesRole', 'NoRole', 'ApplyRole', 'ResetRole']
        '''
        self._log_location()
        if self.bb.buttonRole(button) == QDialogButtonBox.AcceptRole:
            self._log("AcceptRole")
            self.accept()
        elif self.bb.buttonRole(button) == QDialogButtonBox.RejectRole:
            self.close()

    def esc(self, *args):
        self.close()

    def initialize(self, parent, book_id, cid, installed_book, enable_metadata_updates, marvin_db_path):
        '''
        __init__ is called on SizePersistedDialog()
        shared attributes of interest:
            .authors
            .author_sort
            .cover_hash
            .pubdate
            .publisher
            .rating
            .series
            .series_index
            .title
            .title_sort
            .comments
            .tags
            .uuid
        '''
        self.setupUi(self)
        self.book_id = book_id
        self.cid = cid
        self.connected_device = parent.opts.gui.device_manager.device
        self.installed_book = installed_book
        self.marvin_db_path = marvin_db_path
        self.opts = parent.opts
        self.parent = parent
        self.stored_command = None
        self.verbose = parent.verbose
        self.BORDER_LR = 4
        self.BORDER_TB = 8
        self.GREY_FG = '<font style="color:#A0A0A0">{0}</font>'
        self.YELLOW_BG = '<font style="background:#FDFF99">{0}</font>'

        self._log_location(installed_book.title)

        # Subscribe to Marvin driver change events
        self.connected_device.marvin_device_signals.reader_app_status_changed.connect(
            self.marvin_status_changed)

        #self._log("mismatches:\n%s" % repr(installed_book.metadata_mismatches))
        self.mismatches = installed_book.metadata_mismatches

        self._populate_title()
        self._populate_title_sort()
        self._populate_series()
        self._populate_authors()
        self._populate_author_sort()
        self._populate_uuid()
        self._populate_covers()
        self._populate_subjects()
        self._populate_publisher()
        self._populate_pubdate()
        self._populate_rating()
        self._populate_description()

        # ~~~~~~~~ Export to Marvin button ~~~~~~~~
        self.export_to_marvin_button.setIcon(QIcon(os.path.join(self.parent.opts.resources_path,
                                                   'icons',
                                                   'from_calibre.png')))
        self.export_to_marvin_button.clicked.connect(partial(self.store_command, 'export_metadata'))
        self.export_to_marvin_button.setEnabled(enable_metadata_updates)

        # ~~~~~~~~ Import from Marvin button ~~~~~~~~
        self.import_from_marvin_button.setIcon(QIcon(os.path.join(self.parent.opts.resources_path,
                                                     'icons',
                                                     'from_marvin.png')))
        self.import_from_marvin_button.clicked.connect(partial(self.store_command, 'import_metadata'))
        self.import_from_marvin_button.setEnabled(enable_metadata_updates)

        # If no calibre book, or no mismatches, adjust the display accordingly
        if not self.cid:
            #self._log("self.cid: %s" % repr(self.cid))
            #self._log("self.mismatches: %s" % repr(self.mismatches))
            self.calibre_gb.setVisible(False)
            self.import_from_marvin_button.setVisible(False)
            self.setWindowTitle(u'Marvin metadata')
        elif not self.mismatches:
            # Show both panels, but hide the transfer buttons
            self.export_to_marvin_button.setVisible(False)
            self.import_from_marvin_button.setVisible(False)
        else:
            self.setWindowTitle(u'Metadata Summary')

        if False:
            # Set the Marvin QGroupBox to Marvin red
            marvin_red = QColor()
            marvin_red.setRgb(189, 17, 20, alpha=255)
            palette = QPalette()
            palette.setColor(QPalette.Background, marvin_red)
            self.marvin_gb.setPalette(palette)

        # ~~~~~~~~ Add a Close or Cancel button ~~~~~~~~
        self.close_button = QPushButton(QIcon(I('window-close.png')), 'Close')
        if self.mismatches:
            self.close_button.setText('Cancel')
        self.bb.addButton(self.close_button, QDialogButtonBox.RejectRole)

        self.bb.clicked.connect(self.dispatch_button_click)

        # Restore position
        self.resize_dialog()

    def marvin_status_changed(self, cmd_dict):
        '''

        '''
        self.marvin_device_status_changed.emit(cmd_dict)
        command = cmd_dict['cmd']

        self._log_location(command)

        if command in ['disconnected', 'yanked']:
            self._log("closing dialog: %s" % command)
            self.close()

    def store_command(self, command):
        '''
        '''
        self._log_location(command)
        self.stored_command = command
        self.accept()

    def _populate_authors(self):
        if 'authors' in self.mismatches:
            cs_authors = ', '.join(self.mismatches['authors']['calibre'])
            self.calibre_authors.setText(self.YELLOW_BG.format(cs_authors))
            ms_authors = ', '.join(self.mismatches['authors']['Marvin'])
            self.marvin_authors.setText(self.YELLOW_BG.format(ms_authors))
        else:
            authors = ', '.join(self.installed_book.authors)
            self.calibre_authors.setText(authors)
            self.marvin_authors.setText(authors)

    def _populate_author_sort(self):
        if 'author_sort' in self.mismatches:
            cs_author_sort = self.mismatches['author_sort']['calibre']
            self.calibre_author_sort.setText(self.YELLOW_BG.format(cs_author_sort))
            ms_author_sort = self.mismatches['author_sort']['Marvin']
            self.marvin_author_sort.setText(self.YELLOW_BG.format(ms_author_sort))
        else:
            author_sort = self.installed_book.author_sort
            self.calibre_author_sort.setText(self.GREY_FG.format(author_sort))
            self.marvin_author_sort.setText(self.GREY_FG.format(author_sort))

    def _populate_covers(self):
        '''
        Display calibre cover for both unless mismatch
        '''
        def _fetch_marvin_cover(border_width=0):
            '''
            Retrieve LargeCoverJpg from cache
            '''
            #self._log_location('border_width: {0}'.format(border_width))
            con = sqlite3.connect(self.marvin_db_path)
            with con:
                con.row_factory = sqlite3.Row

                # Fetch Hash from mainDb
                cover_cur = con.cursor()
                cover_cur.execute('''SELECT
                                      Hash
                                     FROM Books
                                     WHERE ID = '{0}'
                                  '''.format(self.book_id))
                row = cover_cur.fetchone()

            book_hash = row[b'Hash']
            large_covers_subpath = self.connected_device._cover_subpath(size="large")
            cover_path = '/'.join([large_covers_subpath, '%s.jpg' % book_hash])
            stats = self.parent.ios.exists(cover_path)
            if stats:
                self._log("fetching large cover from cache")
                #self._log("cover size: {:,} bytes".format(int(stats['st_size'])))
                cover_bytes = self.parent.ios.read(cover_path, mode='rb')
                m_image = QImage()
                m_image.loadFromData(cover_bytes)

                if border_width:
                    # Construct a QPixmap with oversized yellow background
                    m_image = m_image.scaledToHeight(
                        self.COVER_ICON_SIZE - border_width * 2,
                        Qt.SmoothTransformation)

                    self.m_pixmap = QPixmap(
                        QSize(m_image.width() + border_width * 2,
                              m_image.height() + border_width * 2))

                    m_painter = QPainter(self.m_pixmap)
                    m_painter.setRenderHints(m_painter.Antialiasing)

                    m_painter.fillRect(self.m_pixmap.rect(), self.MISMATCH_COLOR)
                    m_painter.drawImage(border_width,
                                        border_width,
                                        m_image)
                else:
                    m_image = m_image.scaledToHeight(
                        self.COVER_ICON_SIZE,
                        Qt.SmoothTransformation)

                    self.m_pixmap = QPixmap(
                        QSize(m_image.width(),
                              m_image.height()))

                    m_painter = QPainter(self.m_pixmap)
                    m_painter.setRenderHints(m_painter.Antialiasing)

                    m_painter.drawImage(0, 0, m_image)

                self.marvin_cover.setPixmap(self.m_pixmap)
            else:
                # No cover available, use generic
                self._log("No cached cover, using generic")
                pixmap = QPixmap()
                pixmap.load(I('book.png'))
                pixmap = pixmap.scaled(self.COVER_ICON_SIZE,
                                       self.COVER_ICON_SIZE,
                                       aspectRatioMode=Qt.KeepAspectRatio,
                                       transformMode=Qt.SmoothTransformation)
                self.marvin_cover.setPixmap(pixmap)

        self.calibre_cover.setMaximumSize(QSize(self.COVER_ICON_SIZE, self.COVER_ICON_SIZE))
        self.calibre_cover.setText('')
        self.calibre_cover.setScaledContents(False)

        self.marvin_cover.setMaximumSize(QSize(self.COVER_ICON_SIZE, self.COVER_ICON_SIZE))
        self.marvin_cover.setText('')
        self.marvin_cover.setScaledContents(False)

        if self.cid:
            db = self.opts.gui.current_db
            if 'cover_hash' not in self.mismatches:
                mi = db.get_metadata(self.cid, index_is_id=True, get_cover=True, cover_as_data=True)

                c_image = QImage()
                if mi.has_cover:
                    c_image.loadFromData(mi.cover_data[1])
                    c_image = c_image.scaledToHeight(self.COVER_ICON_SIZE,
                                                     Qt.SmoothTransformation)
                    self.c_pixmap = QPixmap(QSize(c_image.width(),
                                                  c_image.height()))
                    c_painter = QPainter(self.c_pixmap)
                    c_painter.setRenderHints(c_painter.Antialiasing)
                    c_painter.drawImage(0, 0, c_image)
                else:
                    c_image.load(I('book.png'))
                    c_image = c_image.scaledToWidth(135,
                                                    Qt.SmoothTransformation)
                    # Construct a QPixmap with dialog background
                    self.c_pixmap = QPixmap(
                        QSize(c_image.width(),
                              c_image.height()))
                    c_painter = QPainter(self.c_pixmap)
                    c_painter.setRenderHints(c_painter.Antialiasing)
                    bgcolor = self.palette().color(QPalette.Background)
                    c_painter.fillRect(self.c_pixmap.rect(), bgcolor)
                    c_painter.drawImage(0, 0, c_image)

                # Set calibre cover
                self.calibre_cover.setPixmap(self.c_pixmap)

                if self.opts.prefs.get('development_mode', False):
                    # Show individual covers
                    _fetch_marvin_cover()
                else:
                    # Show calibre cover on both sides
                    self.marvin_cover.setPixmap(self.c_pixmap)

            else:
                # Covers don't match - render with border
                # Construct a QImage with the cover sized to fit inside border
                c_image = QImage()
                cdata = db.cover(self.cid, index_is_id=True)
                if cdata is None:
                    c_image.load(I('book.png'))
                    self.calibre_cover.setScaledContents(True)
                else:
                    c_image.loadFromData(cdata)

                c_image = c_image.scaledToHeight(
                    self.COVER_ICON_SIZE - self.BORDER_WIDTH * 2,
                    Qt.SmoothTransformation)

                # Construct a QPixmap with yellow background
                self.c_pixmap = QPixmap(
                    QSize(c_image.width() + self.BORDER_WIDTH * 2,
                          c_image.height() + self.BORDER_WIDTH * 2))
                c_painter = QPainter(self.c_pixmap)
                c_painter.setRenderHints(c_painter.Antialiasing)
                c_painter.fillRect(self.c_pixmap.rect(),self.MISMATCH_COLOR)
                c_painter.drawImage(self.BORDER_WIDTH, self.BORDER_WIDTH, c_image)
                self.calibre_cover.setPixmap(self.c_pixmap)

                # Render Marvin cover with small border if different covers,
                # large cover if no cover hash (loaded via OPDS)
                border_width = self.BORDER_WIDTH
                if self.mismatches['cover_hash']['Marvin'] is None:
                    border_width = self.BORDER_WIDTH * 3
                _fetch_marvin_cover(border_width=border_width)
        else:
            _fetch_marvin_cover()

    def _populate_description(self):
        # Set the bg color of the description text fields to the dialog bg color
        bgcolor = self.palette().color(QPalette.Background)
        palette = QPalette()
        palette.setColor(QPalette.Base, bgcolor)
        self.calibre_description.setPalette(palette)
        self.marvin_description.setPalette(palette)

        if 'comments' in self.mismatches:
            self.calibre_description_label.setText(self.YELLOW_BG.format("<b>Description</b>"))
            if self.mismatches['comments']['calibre']:
                self.calibre_description.setText(self.mismatches['comments']['calibre'])

            self.marvin_description_label.setText(self.YELLOW_BG.format("<b>Description</b>"))
            if self.mismatches['comments']['Marvin']:
                self.marvin_description.setText(self.mismatches['comments']['Marvin'])
        else:
            if self.installed_book.comments:
                self.calibre_description.setText(self.installed_book.comments)
                self.marvin_description.setText(self.installed_book.comments)

    def _populate_pubdate(self):
        if 'pubdate' in self.mismatches:
            if self.mismatches['pubdate']['calibre']:
                cs_pubdate = "<b>Published:</b> {0}".format(strftime("%d %B %Y", t=self.mismatches['pubdate']['calibre']))
            else:
                cs_pubdate = "<b>Published:</b> Date unknown"
            self.calibre_pubdate.setText(self.YELLOW_BG.format(cs_pubdate))

            if self.mismatches['pubdate']['Marvin']:
                ms_pubdate = "<b>Published:</b> {0}".format(strftime("%d %B %Y", t=self.mismatches['pubdate']['Marvin']))
            else:
                ms_pubdate = "<b>Published:</b> Date unknown"
            self.marvin_pubdate.setText(self.YELLOW_BG.format(ms_pubdate))
        elif self.installed_book.pubdate:
            pubdate = "<b>Published:</b> {0}".format(strftime("%d %B %Y", t=self.installed_book.pubdate))
            self.calibre_pubdate.setText(pubdate)
            self.marvin_pubdate.setText(pubdate)
        else:
            pubdate = "<b>Published:</b> Date unknown"
            self.calibre_pubdate.setText(pubdate)
            self.marvin_pubdate.setText(pubdate)

    def _populate_publisher(self):
        if 'publisher' in self.mismatches:
            csp = self.mismatches['publisher']['calibre']
            if not csp:
                cs_publisher = "<b>Publisher:</b> Unknown"
            else:
                cs_publisher = "<b>Publisher:</b> {0}".format(csp)
            self.calibre_publisher.setText(self.YELLOW_BG.format(cs_publisher))

            msp = self.mismatches['publisher']['Marvin']
            if not msp:
                ms_publisher = "<b>Publisher:</b> Unknown"
            else:
                ms_publisher = "<b>Publisher:</b> {0}".format(msp)
            self.marvin_publisher.setText(self.YELLOW_BG.format(ms_publisher))
        else:
            if not self.installed_book.publisher:
                publisher = "<b>Publisher:</b> Unknown"
            else:
                publisher = "<b>Publisher:</b> {0}".format(self.installed_book.publisher)
            self.calibre_publisher.setText(publisher)
            self.marvin_publisher.setText(publisher)

    def _populate_rating(self):

        def _construct_stars(rating):
            '''
            Marvin ratings colors:
            Yellow: 242,220,109 F2DC6D
            Gray: 240,240,240 E0E0E0
            '''
            EMPTY = '<span style="color:#CCC">{0}</span>'
            FULL = '<span style="color:#000">{0}</span>'
            ans = ''
            empty = 5 - rating
            for x in range(rating):
                ans += FULL.format(FULL_STAR)
            for x in range(empty):
                ans += EMPTY.format(EMPTY_STAR)
            return ans

        if self.installed_book.rating is not None:
            if 'rating' in self.mismatches:
                calibre_stars = _construct_stars(self.mismatches['rating']['calibre'])
                self.calibre_rating.setText(self.YELLOW_BG.format(calibre_stars))
                marvin_stars = _construct_stars(self.mismatches['rating']['Marvin'])
                self.marvin_rating.setText(self.YELLOW_BG.format(marvin_stars))
            else:
                self.calibre_rating.setText(_construct_stars(self.installed_book.rating))
                self.marvin_rating.setText(_construct_stars(self.installed_book.rating))
        else:
            self.calibre_rating.setVisible(False)
            self.marvin_rating.setVisible(False)

    def _populate_series(self):
        if 'series' in self.mismatches:
            cs_index = str(self.mismatches['series_index']['calibre'])
            if cs_index.endswith('.0'):
                cs_index = cs_index[:-2]
            cs = "%s (%s)" % (self.mismatches['series']['calibre'], cs_index)
            self.calibre_series.setText(self.YELLOW_BG.format(cs))
            ms_index = str(self.mismatches['series_index']['Marvin'])
            if ms_index.endswith('.0'):
                ms_index = ms_index[:-2]
            ms = "%s (%s)" % (self.mismatches['series']['Marvin'], ms_index)
            self.marvin_series.setText(self.YELLOW_BG.format(ms))
        elif self.installed_book.series:
            cs_index = str(self.installed_book.series_index)
            if cs_index.endswith('.0'):
                cs_index = cs_index[:-2]
            cs = "%s (%s)" % (self.installed_book.series, cs_index)
            self.calibre_series.setText(cs)
            self.marvin_series.setText(cs)
        else:
            self.calibre_series.setVisible(False)
            self.marvin_series.setVisible(False)

    def _populate_subjects(self):
        '''
        '''
        # Setting size policy allows us to match Subjects fields height
        sp = QSizePolicy()
        sp.setHorizontalStretch(True)
        sp.setVerticalStretch(False)
        sp.setHeightForWidth(False)
        self.calibre_subjects.setSizePolicy(sp)
        self.marvin_subjects.setSizePolicy(sp)

        if 'tags' in self.mismatches:
            cs = "<b>Subjects:</b> {0}".format(', '.join(self.mismatches['tags']['calibre']))
            self.calibre_subjects.setText(self.YELLOW_BG.format(cs))
            ms = "<b>Subjects:</b> {0}".format(', '.join(self.mismatches['tags']['Marvin']))
            self.marvin_subjects.setText(self.YELLOW_BG.format(ms))

            calibre_height = self.calibre_subjects.sizeHint().height()
            marvin_height = self.marvin_subjects.sizeHint().height()
            if calibre_height > marvin_height:
                self.marvin_subjects.setMinimumHeight(calibre_height)
                self.marvin_subjects.setMaximumHeight(calibre_height)
            elif marvin_height > calibre_height:
                self.calibre_subjects.setMinimumHeight(marvin_height)
                self.calibre_subjects.setMaximumHeight(marvin_height)
        else:
            #self._log(repr(self.installed_book.tags))
            cs = "<b>Subjects:</b> {0}".format(', '.join(self.installed_book.tags))
            #self._log("cs: %s" % repr(cs))
            self.calibre_subjects.setText(cs)
            self.marvin_subjects.setText(cs)

    def _populate_title(self):
        if 'title' in self.mismatches:
            ct = self.mismatches['title']['calibre']
            self.calibre_title.setText(self.YELLOW_BG.format(ct))
            mt = self.mismatches['title']['Marvin']
            self.marvin_title.setText(self.YELLOW_BG.format(mt))
        else:
            title = self.installed_book.title
            self.calibre_title.setText(title)
            self.marvin_title.setText(title)

    def _populate_title_sort(self):
        if 'title_sort' in self.mismatches:
            cts = self.mismatches['title_sort']['calibre']
            self.calibre_title_sort.setText(self.YELLOW_BG.format(cts))
            mts = self.mismatches['title_sort']['Marvin']
            self.marvin_title_sort.setText(self.YELLOW_BG.format(mts))
        else:
            title_sort = self.installed_book.title_sort
            self.calibre_title_sort.setText(self.GREY_FG.format(title_sort))
            self.marvin_title_sort.setText(self.GREY_FG.format(title_sort))

    def _populate_uuid(self):
        if 'uuid' in self.mismatches:
            if self.mismatches['uuid']['calibre']:
                self.calibre_uuid.setText(self.YELLOW_BG.format('uuid'))
            if self.mismatches['uuid']['Marvin']:
                self.marvin_uuid.setText(self.YELLOW_BG.format('uuid'))
            else:
                self.marvin_uuid.setText(self.YELLOW_BG.format('no uuid'))
        else:
            self.calibre_uuid.setVisible(False)
            self.marvin_uuid.setVisible(False)
Esempio n. 3
0
class MetadataComparisonDialog(SizePersistedDialog, Ui_Dialog, Logger):
    BORDER_COLOR = "#FDFF99"
    BORDER_WIDTH = 4
    COVER_ICON_SIZE = 200

    marvin_device_status_changed = pyqtSignal(str)

    def accept(self):
        self._log_location()
        super(MetadataComparisonDialog, self).accept()

    def close(self):
        self._log_location()
        super(MetadataComparisonDialog, self).close()

    def dispatch_button_click(self, button):
        '''
        BUTTON_ROLES = ['AcceptRole', 'RejectRole', 'DestructiveRole', 'ActionRole',
                        'HelpRole', 'YesRole', 'NoRole', 'ApplyRole', 'ResetRole']
        '''
        self._log_location()
        if self.bb.buttonRole(button) == QDialogButtonBox.AcceptRole:
            self._log("AcceptRole")
            self.accept()
        elif self.bb.buttonRole(button) == QDialogButtonBox.RejectRole:
            self.close()

    def esc(self, *args):
        self.close()

    def initialize(self, parent, book_id, cid, installed_book,
                   enable_metadata_updates, marvin_db_path):
        '''
        __init__ is called on SizePersistedDialog()
        shared attributes of interest:
            .authors
            .author_sort
            .cover_hash
            .pubdate
            .publisher
            .series
            .series_index
            .title
            .title_sort
            .comments
            .tags
            .uuid
        '''
        self.setupUi(self)
        self.book_id = book_id
        self.cid = cid
        self.connected_device = parent.opts.gui.device_manager.device
        self.installed_book = installed_book
        self.marvin_db_path = marvin_db_path
        self.opts = parent.opts
        self.parent = parent
        self.stored_command = None
        self.verbose = parent.verbose
        self.BORDER_LR = 4
        self.BORDER_TB = 8
        self.GREY_FG = '<font style="color:#A0A0A0">{0}</font>'
        self.YELLOW_BG = '<font style="background:#FDFF99">{0}</font>'

        self._log_location(installed_book.title)

        # Subscribe to Marvin driver change events
        self.connected_device.marvin_device_signals.reader_app_status_changed.connect(
            self.marvin_status_changed)

        #self._log("mismatches:\n%s" % repr(installed_book.metadata_mismatches))
        self.mismatches = installed_book.metadata_mismatches

        self._populate_title()
        self._populate_title_sort()
        self._populate_series()
        self._populate_authors()
        self._populate_author_sort()
        self._populate_uuid()
        self._populate_covers()
        self._populate_subjects()
        self._populate_publisher()
        self._populate_pubdate()
        self._populate_description()

        # ~~~~~~~~ Export to Marvin button ~~~~~~~~
        self.export_to_marvin_button.setIcon(
            QIcon(
                os.path.join(self.parent.opts.resources_path, 'icons',
                             'from_calibre.png')))
        self.export_to_marvin_button.clicked.connect(
            partial(self.store_command, 'export_metadata'))
        self.export_to_marvin_button.setEnabled(enable_metadata_updates)

        # ~~~~~~~~ Import from Marvin button ~~~~~~~~
        self.import_from_marvin_button.setIcon(
            QIcon(
                os.path.join(self.parent.opts.resources_path, 'icons',
                             'from_marvin.png')))
        self.import_from_marvin_button.clicked.connect(
            partial(self.store_command, 'import_metadata'))
        self.import_from_marvin_button.setEnabled(enable_metadata_updates)

        # If no calibre book, or no mismatches, adjust the display accordingly
        if not self.cid:
            #self._log("self.cid: %s" % repr(self.cid))
            #self._log("self.mismatches: %s" % repr(self.mismatches))
            self.calibre_gb.setVisible(False)
            self.import_from_marvin_button.setVisible(False)
            self.setWindowTitle(u'Marvin metadata')
        elif not self.mismatches:
            # Show both panels, but hide the transfer buttons
            self.export_to_marvin_button.setVisible(False)
            self.import_from_marvin_button.setVisible(False)
        else:
            self.setWindowTitle(u'Metadata Summary')

        if False:
            # Set the Marvin QGroupBox to Marvin red
            marvin_red = QColor()
            marvin_red.setRgb(189, 17, 20, alpha=255)
            palette = QPalette()
            palette.setColor(QPalette.Background, marvin_red)
            self.marvin_gb.setPalette(palette)

        # ~~~~~~~~ Add a Close or Cancel button ~~~~~~~~
        self.close_button = QPushButton(QIcon(I('window-close.png')), 'Close')
        if self.mismatches:
            self.close_button.setText('Cancel')
        self.bb.addButton(self.close_button, QDialogButtonBox.RejectRole)

        self.bb.clicked.connect(self.dispatch_button_click)

        # Restore position
        self.resize_dialog()

    def marvin_status_changed(self, command):
        '''

        '''
        self.marvin_device_status_changed.emit(command)

        self._log_location(command)

        if command in ['disconnected', 'yanked']:
            self._log("closing dialog: %s" % command)
            self.close()

    def store_command(self, command):
        '''
        '''
        self._log_location(command)
        self.stored_command = command
        self.accept()

    def _populate_authors(self):
        if 'authors' in self.mismatches:
            cs_authors = ', '.join(self.mismatches['authors']['calibre'])
            self.calibre_authors.setText(self.YELLOW_BG.format(cs_authors))
            ms_authors = ', '.join(self.mismatches['authors']['Marvin'])
            self.marvin_authors.setText(self.YELLOW_BG.format(ms_authors))
        else:
            authors = ', '.join(self.installed_book.authors)
            self.calibre_authors.setText(authors)
            self.marvin_authors.setText(authors)

    def _populate_author_sort(self):
        if 'author_sort' in self.mismatches:
            cs_author_sort = self.mismatches['author_sort']['calibre']
            self.calibre_author_sort.setText(
                self.YELLOW_BG.format(cs_author_sort))
            ms_author_sort = self.mismatches['author_sort']['Marvin']
            self.marvin_author_sort.setText(
                self.YELLOW_BG.format(ms_author_sort))
        else:
            author_sort = self.installed_book.author_sort
            self.calibre_author_sort.setText(self.GREY_FG.format(author_sort))
            self.marvin_author_sort.setText(self.GREY_FG.format(author_sort))

    def _populate_covers(self):
        '''
        Display calibre cover for both unless mismatch
        '''
        def _fetch_marvin_cover(with_border=False):
            '''
            Retrieve LargeCoverJpg from cache
            '''
            self._log_location()
            con = sqlite3.connect(self.marvin_db_path)
            with con:
                con.row_factory = sqlite3.Row

                # Fetch Hash from mainDb
                cover_cur = con.cursor()
                cover_cur.execute('''SELECT
                                      Hash
                                     FROM Books
                                     WHERE ID = '{0}'
                                  '''.format(self.book_id))
                row = cover_cur.fetchone()

            book_hash = row[b'Hash']
            large_covers_subpath = self.connected_device._cover_subpath(
                size="large")
            cover_path = '/'.join([large_covers_subpath, '%s.jpg' % book_hash])
            stats = self.parent.ios.exists(cover_path)
            if stats:
                self._log("fetching large cover from cache")
                #self._log("cover size: {:,} bytes".format(int(stats['st_size'])))
                cover_bytes = self.parent.ios.read(cover_path, mode='rb')
                m_image = QImage()
                m_image.loadFromData(cover_bytes)

                if with_border:
                    m_image = m_image.scaledToHeight(
                        self.COVER_ICON_SIZE - self.BORDER_WIDTH * 2,
                        Qt.SmoothTransformation)

                    # Construct a QPixmap with yellow background
                    self.m_pixmap = QPixmap(
                        QSize(m_image.width() + self.BORDER_WIDTH * 2,
                              m_image.height() + self.BORDER_WIDTH * 2))

                    m_painter = QPainter(self.m_pixmap)
                    m_painter.setRenderHints(m_painter.Antialiasing)

                    m_painter.fillRect(self.m_pixmap.rect(),
                                       QColor(0xFD, 0xFF, 0x99))
                    m_painter.drawImage(self.BORDER_WIDTH, self.BORDER_WIDTH,
                                        m_image)
                else:
                    m_image = m_image.scaledToHeight(self.COVER_ICON_SIZE,
                                                     Qt.SmoothTransformation)

                    self.m_pixmap = QPixmap(
                        QSize(m_image.width(), m_image.height()))

                    m_painter = QPainter(self.m_pixmap)
                    m_painter.setRenderHints(m_painter.Antialiasing)

                    m_painter.drawImage(0, 0, m_image)

                self.marvin_cover.setPixmap(self.m_pixmap)
            else:
                # No cover available, use generic
                self._log("No cached cover, using generic")
                pixmap = QPixmap()
                pixmap.load(I('book.png'))
                pixmap = pixmap.scaled(self.COVER_ICON_SIZE,
                                       self.COVER_ICON_SIZE,
                                       aspectRatioMode=Qt.KeepAspectRatio,
                                       transformMode=Qt.SmoothTransformation)
                self.marvin_cover.setPixmap(pixmap)

        self.calibre_cover.setMaximumSize(
            QSize(self.COVER_ICON_SIZE, self.COVER_ICON_SIZE))
        self.calibre_cover.setText('')
        self.calibre_cover.setScaledContents(False)

        self.marvin_cover.setMaximumSize(
            QSize(self.COVER_ICON_SIZE, self.COVER_ICON_SIZE))
        self.marvin_cover.setText('')
        self.marvin_cover.setScaledContents(False)

        if self.cid:
            db = self.opts.gui.current_db
            if 'cover_hash' not in self.mismatches:
                mi = db.get_metadata(self.cid,
                                     index_is_id=True,
                                     get_cover=True,
                                     cover_as_data=True)

                c_image = QImage()
                if mi.has_cover:
                    c_image.loadFromData(mi.cover_data[1])
                    c_image = c_image.scaledToHeight(self.COVER_ICON_SIZE,
                                                     Qt.SmoothTransformation)
                    self.c_pixmap = QPixmap(
                        QSize(c_image.width(), c_image.height()))
                    c_painter = QPainter(self.c_pixmap)
                    c_painter.setRenderHints(c_painter.Antialiasing)
                    c_painter.drawImage(0, 0, c_image)
                else:
                    c_image.load(I('book.png'))
                    c_image = c_image.scaledToWidth(135,
                                                    Qt.SmoothTransformation)
                    # Construct a QPixmap with dialog background
                    self.c_pixmap = QPixmap(
                        QSize(c_image.width(), c_image.height()))
                    c_painter = QPainter(self.c_pixmap)
                    c_painter.setRenderHints(c_painter.Antialiasing)
                    bgcolor = self.palette().color(QPalette.Background)
                    c_painter.fillRect(self.c_pixmap.rect(), bgcolor)
                    c_painter.drawImage(0, 0, c_image)

                # Set calibre cover
                self.calibre_cover.setPixmap(self.c_pixmap)

                if self.opts.prefs.get('development_mode', False):
                    # Show individual covers
                    _fetch_marvin_cover()
                else:
                    # Show calibre cover on both sides
                    self.marvin_cover.setPixmap(self.c_pixmap)

            else:
                # Covers don't match - render with border
                # Construct a QImage with the cover sized to fit inside border
                c_image = QImage()
                cdata = db.cover(self.cid, index_is_id=True)
                if cdata is None:
                    c_image.load(I('book.png'))
                else:
                    c_image.loadFromData(cdata)

                c_image = c_image.scaledToHeight(
                    self.COVER_ICON_SIZE - self.BORDER_WIDTH * 2,
                    Qt.SmoothTransformation)

                # Construct a QPixmap with yellow background
                self.c_pixmap = QPixmap(
                    QSize(c_image.width() + self.BORDER_WIDTH * 2,
                          c_image.height() + self.BORDER_WIDTH * 2))
                c_painter = QPainter(self.c_pixmap)
                c_painter.setRenderHints(c_painter.Antialiasing)
                c_painter.fillRect(self.c_pixmap.rect(),
                                   QColor(0xFD, 0xFF, 0x99))
                c_painter.drawImage(self.BORDER_WIDTH, self.BORDER_WIDTH,
                                    c_image)
                self.calibre_cover.setPixmap(self.c_pixmap)
                _fetch_marvin_cover(with_border=True)
        else:
            _fetch_marvin_cover()

    def _populate_description(self):
        # Set the bg color of the description text fields to the dialog bg color
        bgcolor = self.palette().color(QPalette.Background)
        palette = QPalette()
        palette.setColor(QPalette.Base, bgcolor)
        self.calibre_description.setPalette(palette)
        self.marvin_description.setPalette(palette)

        if 'comments' in self.mismatches:
            self.calibre_description_label.setText(
                self.YELLOW_BG.format("<b>Description</b>"))
            if self.mismatches['comments']['calibre']:
                self.calibre_description.setText(
                    self.mismatches['comments']['calibre'])

            self.marvin_description_label.setText(
                self.YELLOW_BG.format("<b>Description</b>"))
            if self.mismatches['comments']['Marvin']:
                self.marvin_description.setText(
                    self.mismatches['comments']['Marvin'])
        else:
            if self.installed_book.comments:
                self.calibre_description.setText(self.installed_book.comments)
                self.marvin_description.setText(self.installed_book.comments)

    def _populate_pubdate(self):
        if 'pubdate' in self.mismatches:
            if self.mismatches['pubdate']['calibre']:
                cs_pubdate = "<b>Published:</b> {0}".format(
                    strftime("%d %B %Y",
                             t=self.mismatches['pubdate']['calibre']))
            else:
                cs_pubdate = "<b>Published:</b> Date unknown"
            self.calibre_pubdate.setText(self.YELLOW_BG.format(cs_pubdate))

            if self.mismatches['pubdate']['Marvin']:
                ms_pubdate = "<b>Published:</b> {0}".format(
                    strftime("%d %B %Y",
                             t=self.mismatches['pubdate']['Marvin']))
            else:
                ms_pubdate = "<b>Published:</b> Date unknown"
            self.marvin_pubdate.setText(self.YELLOW_BG.format(ms_pubdate))
        elif self.installed_book.pubdate:
            pubdate = "<b>Published:</b> {0}".format(
                strftime("%d %B %Y", t=self.installed_book.pubdate))
            self.calibre_pubdate.setText(pubdate)
            self.marvin_pubdate.setText(pubdate)
        else:
            pubdate = "<b>Published:</b> Date unknown"
            self.calibre_pubdate.setText(pubdate)
            self.marvin_pubdate.setText(pubdate)

    def _populate_publisher(self):
        if 'publisher' in self.mismatches:
            csp = self.mismatches['publisher']['calibre']
            if not csp:
                cs_publisher = "<b>Publisher:</b> Unknown"
            else:
                cs_publisher = "<b>Publisher:</b> {0}".format(csp)
            self.calibre_publisher.setText(self.YELLOW_BG.format(cs_publisher))

            msp = self.mismatches['publisher']['Marvin']
            if not msp:
                ms_publisher = "<b>Publisher:</b> Unknown"
            else:
                ms_publisher = "<b>Publisher:</b> {0}".format(msp)
            self.marvin_publisher.setText(self.YELLOW_BG.format(ms_publisher))
        else:
            if not self.installed_book.publisher:
                publisher = "<b>Publisher:</b> Unknown"
            else:
                publisher = "<b>Publisher:</b> {0}".format(
                    self.installed_book.publisher)
            self.calibre_publisher.setText(publisher)
            self.marvin_publisher.setText(publisher)

    def _populate_series(self):
        if 'series' in self.mismatches:
            cs_index = str(self.mismatches['series_index']['calibre'])
            if cs_index.endswith('.0'):
                cs_index = cs_index[:-2]
            cs = "%s (%s)" % (self.mismatches['series']['calibre'], cs_index)
            self.calibre_series.setText(self.YELLOW_BG.format(cs))
            ms_index = str(self.mismatches['series_index']['Marvin'])
            if ms_index.endswith('.0'):
                ms_index = ms_index[:-2]
            ms = "%s (%s)" % (self.mismatches['series']['Marvin'], ms_index)
            self.marvin_series.setText(self.YELLOW_BG.format(ms))
        elif self.installed_book.series:
            cs_index = str(self.installed_book.series_index)
            if cs_index.endswith('.0'):
                cs_index = cs_index[:-2]
            cs = "%s (%s)" % (self.installed_book.series, cs_index)
            self.calibre_series.setText(cs)
            self.marvin_series.setText(cs)
        else:
            self.calibre_series.setVisible(False)
            self.marvin_series.setVisible(False)

    def _populate_subjects(self):
        '''
        '''
        # Setting size policy allows us to match Subjects fields height
        sp = QSizePolicy()
        sp.setHorizontalStretch(True)
        sp.setVerticalStretch(False)
        sp.setHeightForWidth(False)
        self.calibre_subjects.setSizePolicy(sp)
        self.marvin_subjects.setSizePolicy(sp)

        if 'tags' in self.mismatches:
            cs = "<b>Subjects:</b> {0}".format(', '.join(
                self.mismatches['tags']['calibre']))
            self.calibre_subjects.setText(self.YELLOW_BG.format(cs))
            ms = "<b>Subjects:</b> {0}".format(', '.join(
                self.mismatches['tags']['Marvin']))
            self.marvin_subjects.setText(self.YELLOW_BG.format(ms))

            calibre_height = self.calibre_subjects.sizeHint().height()
            marvin_height = self.marvin_subjects.sizeHint().height()
            if calibre_height > marvin_height:
                self.marvin_subjects.setMinimumHeight(calibre_height)
                self.marvin_subjects.setMaximumHeight(calibre_height)
            elif marvin_height > calibre_height:
                self.calibre_subjects.setMinimumHeight(marvin_height)
                self.calibre_subjects.setMaximumHeight(marvin_height)
        else:
            #self._log(repr(self.installed_book.tags))
            cs = "<b>Subjects:</b> {0}".format(', '.join(
                self.installed_book.tags))
            #self._log("cs: %s" % repr(cs))
            self.calibre_subjects.setText(cs)
            self.marvin_subjects.setText(cs)

    def _populate_title(self):
        if 'title' in self.mismatches:
            ct = self.mismatches['title']['calibre']
            self.calibre_title.setText(self.YELLOW_BG.format(ct))
            mt = self.mismatches['title']['Marvin']
            self.marvin_title.setText(self.YELLOW_BG.format(mt))
        else:
            title = self.installed_book.title
            self.calibre_title.setText(title)
            self.marvin_title.setText(title)

    def _populate_title_sort(self):
        if 'title_sort' in self.mismatches:
            cts = self.mismatches['title_sort']['calibre']
            self.calibre_title_sort.setText(self.YELLOW_BG.format(cts))
            mts = self.mismatches['title_sort']['Marvin']
            self.marvin_title_sort.setText(self.YELLOW_BG.format(mts))
        else:
            title_sort = self.installed_book.title_sort
            self.calibre_title_sort.setText(self.GREY_FG.format(title_sort))
            self.marvin_title_sort.setText(self.GREY_FG.format(title_sort))

    def _populate_uuid(self):
        if 'uuid' in self.mismatches:
            if self.mismatches['uuid']['calibre']:
                self.calibre_uuid.setText(self.YELLOW_BG.format('uuid'))
            if self.mismatches['uuid']['Marvin']:
                self.marvin_uuid.setText(self.YELLOW_BG.format('uuid'))
            else:
                self.marvin_uuid.setText(self.YELLOW_BG.format('no uuid'))
        else:
            self.calibre_uuid.setVisible(False)
            self.marvin_uuid.setVisible(False)