Merge from trunk

This commit is contained in:
Charles Haley 2011-04-10 13:57:09 +01:00
commit 8127e5a4fe
17 changed files with 666 additions and 81 deletions

View File

@ -3,8 +3,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>' __copyright__ = '2010, Constantin Hofstetter <consti at consti.de>, Steffen Siebert <calibre at steffensiebert.de>'
__version__ = '0.97' __version__ = '0.98' # 2011-04-10
''' http://brandeins.de - Wirtschaftsmagazin ''' ''' http://brandeins.de - Wirtschaftsmagazin '''
import re import re
import string import string
@ -14,8 +13,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BrandEins(BasicNewsRecipe): class BrandEins(BasicNewsRecipe):
title = u'brand eins' title = u'brand eins'
__author__ = 'Constantin Hofstetter' __author__ = 'Constantin Hofstetter; Steffen Siebert'
description = u'Wirtschaftsmagazin' description = u'Wirtschaftsmagazin: Gets the last full issue on default. Set a integer value for the username-field to get older issues: 1 -> the newest (but not complete) issue, 2 -> the last complete issue (default), 3 -> the issue before 2 etc.'
publisher ='brandeins.de' publisher ='brandeins.de'
category = 'politics, business, wirtschaft, Germany' category = 'politics, business, wirtschaft, Germany'
use_embedded_content = False use_embedded_content = False

32
recipes/dvhn.recipe Normal file
View File

@ -0,0 +1,32 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1302341394(BasicNewsRecipe):
title = u'DvhN'
oldest_article = 1
max_articles_per_feed = 200
__author__ = 'Reijndert'
no_stylesheets = True
cover_url = 'http://www.dvhn.nl/template/Dagblad_v2.0/gfx/logo_DvhN.gif'
language = 'nl'
country = 'NL'
version = 1
publisher = u'Dagblad van het Noorden'
category = u'Nieuws'
description = u'Nieuws uit Noord Nederland'
keep_only_tags = [dict(name='div', attrs={'id':'fullPicture'})
,dict(name='div', attrs={'id':'articleText'})
]
remove_tags = [
dict(name=['object','link','iframe','base'])
,dict(name='span',attrs={'class':'copyright'})
]
feeds = [(u'Drenthe', u'http://www.dvhn.nl/nieuws/drenthe/index.jsp?service=rss'), (u'Groningen', u'http://www.dvhn.nl/nieuws/groningen/index.jsp?service=rss'), (u'Nederland', u'http://www.dvhn.nl/nieuws/nederland/index.jsp?service=rss'), (u'Wereld', u'http://www.dvhn.nl/nieuws/wereld/index.jsp?service=rss'), (u'Economie', u'http://www.dvhn.nl/nieuws/economie/index.jsp?service=rss'), (u'Sport', u'http://www.dvhn.nl/nieuws/sport/index.jsp?service=rss'), (u'Cultuur', u'http://www.dvhn.nl/nieuws/kunst/index.jsp?service=rss'), (u'24 Uur', u'http://www.dvhn.nl/nieuws/24uurdvhn/index.jsp?service=rss&selectiontype=last24hours')]
extra_css = '''
body {font-family: verdana, arial, helvetica, geneva, sans-serif;}
'''

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -453,12 +453,15 @@ def epub_fixers():
# Metadata sources2 {{{ # Metadata sources2 {{{
def metadata_plugins(capabilities): def metadata_plugins(capabilities):
capabilities = frozenset(capabilities) capabilities = frozenset(capabilities)
for plugin in _initialized_plugins: for plugin in all_metadata_plugins():
if isinstance(plugin, Source) and \ if plugin.capabilities.intersection(capabilities) and \
plugin.capabilities.intersection(capabilities) and \
not is_disabled(plugin): not is_disabled(plugin):
yield plugin yield plugin
def all_metadata_plugins():
for plugin in _initialized_plugins:
if isinstance(plugin, Source):
yield plugin
# }}} # }}}
# Initialize plugins {{{ # Initialize plugins {{{

View File

@ -26,9 +26,9 @@ class EDGE(USBMS):
PRODUCT_ID = [0x0c02] PRODUCT_ID = [0x0c02]
BCD = [0x0223] BCD = [0x0223]
VENDOR_NAME = 'ANDROID' VENDOR_NAME = ['ANDROID', 'LINUX']
WINDOWS_MAIN_MEM = '__FILE-STOR_GADG' WINDOWS_MAIN_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
WINDOWS_CARD_A_MEM = '__FILE-STOR_GADG' WINDOWS_CARD_A_MEM = ['__FILE-STOR_GADG', 'FILE-CD_GADGET']
MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'Edge Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card' STORAGE_CARD_VOLUME_LABEL = 'Edge Storage Card'

View File

@ -279,7 +279,7 @@ class Worker(Thread): # Get details {{{
class Amazon(Source): class Amazon(Source):
name = 'Amazon Store' name = 'Amazon Web'
description = _('Downloads metadata from Amazon') description = _('Downloads metadata from Amazon')
capabilities = frozenset(['identify', 'cover']) capabilities = frozenset(['identify', 'cover'])
@ -295,6 +295,14 @@ class Amazon(Source):
'uk' : _('UK'), 'uk' : _('UK'),
} }
def get_book_url(self, identifiers): # {{{
asin = identifiers.get('amazon', None)
if asin is None:
asin = identifiers.get('asin', None)
if asin:
return 'http://amzn.com/%s'%asin
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
domain = self.prefs.get('domain', 'com') domain = self.prefs.get('domain', 'com')

View File

@ -78,8 +78,8 @@ class InternalMetadataCompareKeyGen(object):
exact_title = 1 if title and \ exact_title = 1 if title and \
cleanup_title(title) == cleanup_title(mi.title) else 2 cleanup_title(title) == cleanup_title(mi.title) else 2
has_cover = 2 if source_plugin.get_cached_cover_url(mi.identifiers)\ has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or
is None else 1 source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1
self.base = (isbn, has_cover, all_fields, exact_title) self.base = (isbn, has_cover, all_fields, exact_title)
self.comments_len = len(mi.comments.strip() if mi.comments else '') self.comments_len = len(mi.comments.strip() if mi.comments else '')
@ -157,6 +157,12 @@ class Source(Plugin):
#: correctly first #: correctly first
supports_gzip_transfer_encoding = False supports_gzip_transfer_encoding = False
#: Cached cover URLs can sometimes be unreliable (i.e. the download could
#: fail or the returned image could be bogus. If that is the case set this to
#: False
cached_cover_url_is_reliable = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs) Plugin.__init__(self, *args, **kwargs)
self._isbn_to_identifier_cache = {} self._isbn_to_identifier_cache = {}
@ -301,6 +307,13 @@ class Source(Plugin):
# Metadata API {{{ # Metadata API {{{
def get_book_url(self, identifiers):
'''
Return the URL for the book identified by identifiers at this source.
If no URL is found, return None.
'''
return None
def get_cached_cover_url(self, identifiers): def get_cached_cover_url(self, identifiers):
''' '''
Return cached cover URL for the book identified by Return cached cover URL for the book identified by

View File

@ -7,7 +7,7 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import time import time, hashlib
from urllib import urlencode from urllib import urlencode
from functools import partial from functools import partial
from Queue import Queue, Empty from Queue import Queue, Empty
@ -133,7 +133,7 @@ def to_metadata(browser, log, entry_, timeout): # {{{
default = utcnow().replace(day=15) default = utcnow().replace(day=15)
mi.pubdate = parse_date(pubdate, assume_utc=True, default=default) mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)
except: except:
log.exception('Failed to parse pubdate') log.error('Failed to parse pubdate %r'%pubdate)
# Ratings # Ratings
for x in rating(extra): for x in rating(extra):
@ -164,9 +164,18 @@ class GoogleBooks(Source):
'comments', 'publisher', 'identifier:isbn', 'rating', 'comments', 'publisher', 'identifier:isbn', 'rating',
'identifier:google']) # language currently disabled 'identifier:google']) # language currently disabled
supports_gzip_transfer_encoding = True supports_gzip_transfer_encoding = True
cached_cover_url_is_reliable = False
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1' GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1'
DUMMY_IMAGE_MD5 = frozenset(['0de4383ebad0adad5eeb8975cd796657'])
def get_book_url(self, identifiers): # {{{
goog = identifiers.get('google', None)
if goog is not None:
return 'http://books.google.com/books?id=%s'%goog
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{ def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
BASE_URL = 'http://books.google.com/books/feeds/volumes?' BASE_URL = 'http://books.google.com/books/feeds/volumes?'
isbn = check_isbn(identifiers.get('isbn', None)) isbn = check_isbn(identifiers.get('isbn', None))
@ -229,7 +238,11 @@ class GoogleBooks(Source):
log('Downloading cover from:', cached_url) log('Downloading cover from:', cached_url)
try: try:
cdata = br.open_novisit(cached_url, timeout=timeout).read() cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put((self, cdata)) if cdata:
if hashlib.md5(cdata).hexdigest() in self.DUMMY_IMAGE_MD5:
log.warning('Google returned a dummy image, ignoring')
else:
result_queue.put((self, cdata))
except: except:
log.exception('Failed to download cover from:', cached_url) log.exception('Failed to download cover from:', cached_url)

View File

@ -14,7 +14,7 @@ from threading import Thread
from io import BytesIO from io import BytesIO
from operator import attrgetter from operator import attrgetter
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins, all_metadata_plugins
from calibre.ebooks.metadata.sources.base import create_log, msprefs from calibre.ebooks.metadata.sources.base import create_log, msprefs
from calibre.ebooks.metadata.xisbn import xisbn from calibre.ebooks.metadata.xisbn import xisbn
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
@ -338,8 +338,9 @@ def identify(log, abort, # {{{
for i, result in enumerate(presults): for i, result in enumerate(presults):
result.relevance_in_source = i result.relevance_in_source = i
result.has_cached_cover_url = \ result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
plugin.get_cached_cover_url(result.identifiers) is not None and plugin.get_cached_cover_url(result.identifiers) is not
None)
result.identify_plugin = plugin result.identify_plugin = plugin
log('The identify phase took %.2f seconds'%(time.time() - start_time)) log('The identify phase took %.2f seconds'%(time.time() - start_time))
@ -366,6 +367,22 @@ def identify(log, abort, # {{{
return results return results
# }}} # }}}
def urls_from_identifiers(identifiers): # {{{
ans = []
for plugin in all_metadata_plugins():
try:
url = plugin.get_book_url(identifiers)
if url is not None:
ans.append((plugin.name, url))
except:
pass
isbn = identifiers.get('isbn', None)
if isbn:
ans.append(('ISBN',
'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn))
return ans
# }}}
if __name__ == '__main__': # tests {{{ if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e # To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/identify.py # src/calibre/ebooks/metadata/sources/identify.py

View File

@ -165,6 +165,10 @@ class ConnectShareAction(InterfaceAction):
def content_server_state_changed(self, running): def content_server_state_changed(self, running):
self.share_conn_menu.server_state_changed(running) self.share_conn_menu.server_state_changed(running)
if running:
self.qaction.setIcon(QIcon(I('connect_share_on.png')))
else:
self.qaction.setIcon(QIcon(I('connect_share.png')))
def toggle_content_server(self): def toggle_content_server(self):
if self.gui.content_server is None: if self.gui.content_server is None:

View File

@ -308,22 +308,47 @@ class MenuBar(QMenuBar): # {{{
ac.setMenu(m) ac.setMenu(m)
return ac return ac
# }}} # }}}
class ToolBar(QToolBar): # {{{ class BaseToolBar(QToolBar): # {{{
def __init__(self, donate, location_manager, child_bar, parent): def __init__(self, parent):
QToolBar.__init__(self, parent) QToolBar.__init__(self, parent)
self.gui = parent
self.child_bar = child_bar
self.setContextMenuPolicy(Qt.PreventContextMenu) self.setContextMenuPolicy(Qt.PreventContextMenu)
self.setMovable(False) self.setMovable(False)
self.setFloatable(False) self.setFloatable(False)
self.setOrientation(Qt.Horizontal) self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setStyleSheet('QToolButton:checked { font-weight: bold }') self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.preferred_width = self.sizeHint().width()
def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev)
style = self.get_text_style()
self.setToolButtonStyle(style)
def get_text_style(self):
style = Qt.ToolButtonTextUnderIcon
s = gprefs['toolbar_icon_size']
if s != 'off':
p = gprefs['toolbar_text']
if p == 'never':
style = Qt.ToolButtonIconOnly
elif p == 'auto' and self.preferred_width > self.width()+35:
style = Qt.ToolButtonIconOnly
return style
def contextMenuEvent(self, *args):
pass
# }}}
class ToolBar(BaseToolBar): # {{{
def __init__(self, donate, location_manager, child_bar, parent):
BaseToolBar.__init__(self, parent)
self.gui = parent
self.child_bar = child_bar
self.donate_button = donate self.donate_button = donate
self.apply_settings() self.apply_settings()
@ -333,7 +358,6 @@ class ToolBar(QToolBar): # {{{
donate.setCursor(Qt.PointingHandCursor) donate.setCursor(Qt.PointingHandCursor)
self.added_actions = [] self.added_actions = []
self.build_bar() self.build_bar()
self.preferred_width = self.sizeHint().width()
self.setAcceptDrops(True) self.setAcceptDrops(True)
def apply_settings(self): def apply_settings(self):
@ -348,9 +372,6 @@ class ToolBar(QToolBar): # {{{
self.child_bar.setToolButtonStyle(style) self.child_bar.setToolButtonStyle(style)
self.donate_button.set_normal_icon_size(sz, sz) self.donate_button.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args):
pass
def build_bar(self): def build_bar(self):
self.showing_donate = False self.showing_donate = False
showing_device = self.location_manager.has_device showing_device = self.location_manager.has_device
@ -394,6 +415,8 @@ class ToolBar(QToolBar): # {{{
bar.addAction(action.qaction) bar.addAction(action.qaction)
self.added_actions.append(action.qaction) self.added_actions.append(action.qaction)
self.setup_tool_button(bar, action.qaction, action.popup_type) self.setup_tool_button(bar, action.qaction, action.popup_type)
self.preferred_width = self.sizeHint().width()
self.child_bar.preferred_width = self.child_bar.sizeHint().width()
def setup_tool_button(self, bar, ac, menu_mode=None): def setup_tool_button(self, bar, ac, menu_mode=None):
ch = bar.widgetForAction(ac) ch = bar.widgetForAction(ac)
@ -405,21 +428,6 @@ class ToolBar(QToolBar): # {{{
ch.setPopupMode(menu_mode) ch.setPopupMode(menu_mode)
return ch return ch
def resizeEvent(self, ev):
QToolBar.resizeEvent(self, ev)
style = Qt.ToolButtonTextUnderIcon
s = gprefs['toolbar_icon_size']
if s != 'off':
p = gprefs['toolbar_text']
if p == 'never':
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35 and \
not gprefs['action-layout-toolbar-child']:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
def database_changed(self, db): def database_changed(self, db):
pass pass
@ -497,7 +505,7 @@ class MainWindowMixin(object): # {{{
self.iactions['Fetch News'].init_scheduler(db) self.iactions['Fetch News'].init_scheduler(db)
self.search_bar = SearchBar(self) self.search_bar = SearchBar(self)
self.child_bar = QToolBar(self) self.child_bar = BaseToolBar(self)
self.tool_bar = ToolBar(self.donate_button, self.tool_bar = ToolBar(self.donate_button,
self.location_manager, self.child_bar, self) self.location_manager, self.child_bar, self)
self.addToolBar(Qt.TopToolBarArea, self.tool_bar) self.addToolBar(Qt.TopToolBarArea, self.tool_bar)

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import textwrap, re, os import textwrap, re, os
from PyQt4.Qt import (Qt, QDateEdit, QDate, from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal,
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
QPushButton, QSpinBox, QLineEdit, QSizePolicy) QPushButton, QSpinBox, QLineEdit, QSizePolicy)
@ -315,7 +315,7 @@ class SeriesEdit(MultiCompleteComboBox):
if not val: if not val:
val = '' val = ''
self.setEditText(val.strip()) self.setEditText(val.strip())
self.setCursorPosition(0) self.lineEdit().setCursorPosition(0)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
@ -613,6 +613,8 @@ class FormatsManager(QWidget): # {{{
class Cover(ImageView): # {{{ class Cover(ImageView): # {{{
download_cover = pyqtSignal()
def __init__(self, parent): def __init__(self, parent):
ImageView.__init__(self, parent) ImageView.__init__(self, parent)
self.dialog = parent self.dialog = parent
@ -703,9 +705,6 @@ class Cover(ImageView): # {{{
cdata = im.export('png') cdata = im.export('png')
self.current_val = cdata self.current_val = cdata
def download_cover(self, *args):
pass # TODO: Implement this
def generate_cover(self, *args): def generate_cover(self, *args):
from calibre.ebooks import calibre_cover from calibre.ebooks import calibre_cover
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
@ -862,6 +861,7 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
if not val: if not val:
val = [] val = []
self.setText(', '.join([x.strip() for x in val])) self.setText(', '.join([x.strip() for x in val]))
self.setCursorPosition(0)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
@ -928,6 +928,7 @@ class IdentifiersEdit(QLineEdit): # {{{
val = {} val = {}
txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()]) txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()])
self.setText(txt.strip()) self.setText(txt.strip())
self.setCursorPosition(0)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
@ -977,7 +978,7 @@ class PublisherEdit(MultiCompleteComboBox): # {{{
if not val: if not val:
val = '' val = ''
self.setEditText(val.strip()) self.setEditText(val.strip())
self.setCursorPosition(0) self.lineEdit().setCursorPosition(0)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)

View File

@ -16,11 +16,12 @@ from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QSizePolicy, QPalette, QFrame, QSize, QKeySequence) QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit, from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit,
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit, AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit,
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit,
BuddyLabel, DateEdit, PubdateEdit) BuddyLabel, DateEdit, PubdateEdit)
from calibre.gui2.metadata.single_download import FullFetch
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
@ -132,6 +133,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.formats_manager.cover_from_format_button.clicked.connect( self.formats_manager.cover_from_format_button.clicked.connect(
self.cover_from_format) self.cover_from_format)
self.cover = Cover(self) self.cover = Cover(self)
self.cover.download_cover.connect(self.download_cover)
self.basic_metadata_widgets.append(self.cover) self.basic_metadata_widgets.append(self.cover)
self.comments = CommentsEdit(self, self.one_line_comments_toolbar) self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
@ -158,7 +160,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
self.fetch_metadata_button = QPushButton( self.fetch_metadata_button = QPushButton(
_('&Fetch metadata from server'), self) _('&Download metadata'), self)
self.fetch_metadata_button.clicked.connect(self.fetch_metadata) self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
font = self.fmb_font = QFont() font = self.fmb_font = QFont()
font.setBold(True) font.setBold(True)
@ -303,7 +305,26 @@ class MetadataSingleDialogBase(ResizableDialog):
self.comments.current_val = mi.comments self.comments.current_val = mi.comments
def fetch_metadata(self, *args): def fetch_metadata(self, *args):
pass # TODO: fetch metadata d = FullFetch(self.cover.pixmap(), self)
ret = d.start(title=self.title.current_val, authors=self.authors.current_val,
identifiers=self.identifiers.current_val)
if ret == d.Accepted:
mi = d.book
if mi is not None:
self.update_from_mi(mi)
if d.cover_pixmap is not None:
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
def download_cover(self, *args):
from calibre.gui2.metadata.single_download import CoverFetch
d = CoverFetch(self.cover.pixmap(), self)
ret = d.start(self.title.current_val, self.authors.current_val,
self.identifiers.current_val)
if ret == d.Accepted:
if d.cover_pixmap is not None:
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
# }}} # }}}
def apply_changes(self): def apply_changes(self):

View File

@ -7,23 +7,31 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
DEBUG_DIALOG = False
# Imports {{{
from threading import Thread, Event from threading import Thread, Event
from operator import attrgetter from operator import attrgetter
from Queue import Queue, Empty
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize) QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView,
QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.utils.logging import GUILog as Log from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify from calibre.ebooks.metadata.sources.identify import (identify,
urls_from_identifiers)
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE from calibre.gui2 import error_dialog, NONE
from calibre.utils.date import utcnow, fromordinal, format_date from calibre.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre import force_unicode
# }}}
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
@ -36,7 +44,10 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
return doc return doc
def sizeHint(self, option, index): def sizeHint(self, option, index):
ans = self.to_doc(index).size().toSize() doc = self.to_doc(index)
ans = doc.size().toSize()
if ans.width() > 150:
ans.setWidth(160)
ans.setHeight(ans.height()+10) ans.setHeight(ans.height()+10)
return ans return ans
@ -52,6 +63,65 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore() painter.restore()
# }}} # }}}
class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal()
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.angle = 0
self.timer = QTimer(self)
self.timer.timeout.connect(self.frame_changed)
self.color = parent.palette().color(QPalette.WindowText)
self.spinner_width = 64
def frame_changed(self, *args):
self.angle = (self.angle+30)%360
self.needs_redraw.emit()
def start_animation(self):
self.angle = 0
self.timer.start(200)
def stop_animation(self):
self.timer.stop()
def draw_spinner(self, painter, rect):
width = rect.width()
outer_radius = (width-1)*0.5
inner_radius = (width-1)*0.5*0.38
capsule_height = outer_radius - inner_radius
capsule_width = int(capsule_height * (0.23 if width > 32 else 0.35))
capsule_radius = capsule_width//2
painter.save()
painter.setRenderHint(painter.Antialiasing)
for i in xrange(12):
color = QColor(self.color)
color.setAlphaF(1.0 - (i/12.0))
painter.setPen(Qt.NoPen)
painter.setBrush(color)
painter.save()
painter.translate(rect.center())
painter.rotate(self.angle - i*30.0)
painter.drawRoundedRect(-capsule_width*0.5,
-(inner_radius+capsule_height), capsule_width,
capsule_height, capsule_radius, capsule_radius)
painter.restore()
painter.restore()
def paint(self, painter, option, index):
QStyledItemDelegate.paint(self, painter, option, index)
if self.timer.isActive() and index.data(Qt.UserRole).toBool():
rect = QRect(0, 0, self.spinner_width, self.spinner_width)
rect.moveCenter(option.rect.center())
self.draw_spinner(painter, rect)
# }}}
class ResultsModel(QAbstractTableModel): # {{{ class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = ( COLUMNS = (
@ -110,6 +180,13 @@ class ResultsModel(QAbstractTableModel): # {{{
return self.yes_icon return self.yes_icon
elif role == Qt.UserRole: elif role == Qt.UserRole:
return book return book
elif role == Qt.ToolTipRole and col == 3:
return QVariant(
_('The has cover indication is not fully\n'
'reliable. Sometimes results marked as not\n'
'having a cover will find a cover in the download\n'
'cover stage, and vice versa.'))
return NONE return NONE
def sort(self, col, order=Qt.AscendingOrder): def sort(self, col, order=Qt.AscendingOrder):
@ -119,7 +196,7 @@ class ResultsModel(QAbstractTableModel): # {{{
elif col == 1: elif col == 1:
key = attrgetter('title') key = attrgetter('title')
elif col == 2: elif col == 2:
key = attrgetter('authors') key = attrgetter('pubdate')
elif col == 3: elif col == 3:
key = attrgetter('has_cached_cover_url') key = attrgetter('has_cached_cover_url')
elif key == 4: elif key == 4:
@ -170,6 +247,11 @@ class ResultsView(QTableView): # {{{
if not book.is_null('rating'): if not book.is_null('rating'):
parts.append('<div>%s</div>'%('\u2605'*int(book.rating))) parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
parts.append('</center>') parts.append('</center>')
if book.identifiers:
urls = urls_from_identifiers(book.identifiers)
ids = ['<a href="%s">%s</a>'%(url, name) for name, url in urls]
if ids:
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
if book.tags: if book.tags:
parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags)) parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags))
if book.comments: if book.comments:
@ -201,6 +283,14 @@ class Comments(QWebView): # {{{
self.page().setPalette(palette) self.page().setPalette(palette)
self.setAttribute(Qt.WA_OpaquePaintEvent, False) self.setAttribute(Qt.WA_OpaquePaintEvent, False)
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
self.linkClicked.connect(self.link_clicked)
def link_clicked(self, url):
from calibre.gui2 import open_url
if unicode(url.toString()).startswith('http://'):
open_url(url)
def turnoff_scrollbar(self, *args): def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
@ -268,7 +358,7 @@ class IdentifyWorker(Thread): # {{{
def run(self): def run(self):
try: try:
if True: if DEBUG_DIALOG:
self.results = self.sample_results() self.results = self.sample_results()
else: else:
self.results = identify(self.log, self.abort, title=self.title, self.results = identify(self.log, self.abort, title=self.title,
@ -277,7 +367,7 @@ class IdentifyWorker(Thread): # {{{
result.gui_rank = i result.gui_rank = i
except: except:
import traceback import traceback
self.error = traceback.format_exc() self.error = force_unicode(traceback.format_exc())
# }}} # }}}
class IdentifyWidget(QWidget): # {{{ class IdentifyWidget(QWidget): # {{{
@ -318,7 +408,7 @@ class IdentifyWidget(QWidget): # {{{
self.query.setWordWrap(True) self.query.setWordWrap(True)
l.addWidget(self.query, 2, 0, 1, 2) l.addWidget(self.query, 2, 0, 1, 2)
self.comments_view.show_data('<h2>'+_('Downloading')+ self.comments_view.show_data('<h2>'+_('Please wait')+
'<br><span id="dots">.</span></h2>'+ '<br><span id="dots">.</span></h2>'+
''' '''
<script type="text/javascript"> <script type="text/javascript">
@ -345,7 +435,7 @@ class IdentifyWidget(QWidget): # {{{
if authors: if authors:
parts.append('authors:'+authors_to_string(authors)) parts.append('authors:'+authors_to_string(authors))
if identifiers: if identifiers:
x = ', '.join('%s:%s'%(k, v) for k, v in identifiers) x = ', '.join('%s:%s'%(k, v) for k, v in identifiers.iteritems())
parts.append(x) parts.append(x)
self.query.setText(_('Query: ')+'; '.join(parts)) self.query.setText(_('Query: ')+'; '.join(parts))
self.log(unicode(self.query.text())) self.log(unicode(self.query.text()))
@ -398,23 +488,323 @@ class IdentifyWidget(QWidget): # {{{
self.abort.set() self.abort.set()
# }}} # }}}
class CoverWidget(QWidget): # {{{ class CoverWorker(Thread): # {{{
def __init__(self, log, parent=None): def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self)
self.daemon = True
self.log, self.abort = log, abort
self.title, self.authors, self.identifiers = (title, authors,
identifiers)
self.rq = Queue()
self.error = None
def fake_run(self):
images = ['donate.png', 'config.png', 'column.png', 'eject.png', ]
import time
time.sleep(2)
for pl, im in zip(metadata_plugins(['cover']), images):
self.rq.put((pl, 1, 1, 'png', I(im, data=True)))
def run(self):
try:
if DEBUG_DIALOG:
self.fake_run()
else:
from calibre.ebooks.metadata.sources.covers import run_download
run_download(self.log, self.rq, self.abort, title=self.title,
authors=self.authors, identifiers=self.identifiers)
except:
import traceback
self.error = force_unicode(traceback.format_exc())
# }}}
class CoversModel(QAbstractListModel): # {{{
def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent)
if current_cover is None:
current_cover = QPixmap(I('default_cover.png'))
self.blank = QPixmap(I('blank.png')).scaled(150, 200)
self.covers = [self.get_item(_('Current cover'), current_cover)]
self.plugin_map = {}
for i, plugin in enumerate(metadata_plugins(['cover'])):
self.covers.append((plugin.name+'\n'+_('Searching...'),
QVariant(self.blank), None, True))
self.plugin_map[plugin] = i+1
def get_item(self, src, pmap, waiting=False):
sz = '%dx%d'%(pmap.width(), pmap.height())
text = QVariant(src + '\n' + sz)
scaled = pmap.scaled(150, 200, Qt.IgnoreAspectRatio,
Qt.SmoothTransformation)
return (text, QVariant(scaled), pmap, waiting)
def rowCount(self, parent=None):
return len(self.covers)
def data(self, index, role):
try:
text, pmap, cover, waiting = self.covers[index.row()]
except:
return NONE
if role == Qt.DecorationRole:
return pmap
if role == Qt.DisplayRole:
return text
if role == Qt.UserRole:
return waiting
return NONE
def plugin_for_index(self, index):
row = index.row() if hasattr(index, 'row') else index
for k, v in self.plugin_map.iteritems():
if v == row:
return k
def cover_keygen(self, x):
pmap = x[2]
if pmap is None:
return 1
return pmap.width()*pmap.height()
def clear_failed(self):
good = []
pmap = {}
dcovers = sorted(self.covers[1:], key=self.cover_keygen, reverse=True)
for i, x in enumerate(self.covers[0:1] + dcovers):
if not x[-1]:
good.append(x)
if i > 0:
plugin = self.plugin_for_index(i)
pmap[plugin] = len(good) - 1
self.covers = good
self.plugin_map = pmap
self.reset()
def index_for_plugin(self, plugin):
idx = self.plugin_map.get(plugin, 0)
return self.index(idx)
def update_result(self, plugin, width, height, data):
try:
idx = self.plugin_map[plugin]
except:
return
pmap = QPixmap()
pmap.loadFromData(data)
if pmap.isNull():
return
self.covers[idx] = self.get_item(plugin.name, pmap, waiting=False)
self.dataChanged.emit(self.index(idx), self.index(idx))
def cover_pixmap(self, index):
row = index.row()
if row > 0 and row < len(self.covers):
pmap = self.covers[row][2]
if pmap is not None and not pmap.isNull():
return pmap
# }}}
class CoversView(QListView): # {{{
chosen = pyqtSignal()
def __init__(self, current_cover, parent=None):
QListView.__init__(self, parent)
self.m = CoversModel(current_cover, self)
self.setModel(self.m)
self.setFlow(self.LeftToRight)
self.setWrapping(True)
self.setResizeMode(self.Adjust)
self.setGridSize(QSize(190, 260))
self.setIconSize(QSize(150, 200))
self.setSelectionMode(self.SingleSelection)
self.setViewMode(self.IconMode)
self.delegate = CoverDelegate(self)
self.setItemDelegate(self.delegate)
self.delegate.needs_redraw.connect(self.viewport().update,
type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
def select(self, num):
current = self.model().index(num)
sm = self.selectionModel()
sm.select(current, sm.SelectCurrent)
def start(self):
self.select(0)
self.delegate.start_animation()
def clear_failed(self):
plugin = self.m.plugin_for_index(self.currentIndex())
self.m.clear_failed()
self.select(self.m.index_for_plugin(plugin).row())
# }}}
class CoversWidget(QWidget): # {{{
chosen = pyqtSignal()
finished = pyqtSignal()
def __init__(self, log, current_cover, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.log = log self.log = log
self.abort = Event()
self.l = l = QGridLayout()
self.setLayout(l)
self.msg = QLabel()
self.msg.setWordWrap(True)
l.addWidget(self.msg, 0, 0)
self.covers_view = CoversView(current_cover, self)
self.covers_view.chosen.connect(self.chosen)
l.addWidget(self.covers_view, 1, 0)
self.continue_processing = True
def start(self, book, current_cover, title, authors): def start(self, book, current_cover, title, authors):
self.book, self.current_cover = book, current_cover self.book, self.current_cover = book, current_cover
self.title, self.authors = title, authors self.title, self.authors = title, authors
self.log('\n\nStarting cover download for:', book.title) self.log('Starting cover download for:', book.title)
self.log('Query:', title, authors, self.book.identifiers)
self.msg.setText('<p>'+_('Downloading covers for <b>%s</b>, please wait...')%book.title)
self.covers_view.start()
self.worker = CoverWorker(self.log, self.abort, self.title,
self.authors, book.identifiers)
self.worker.start()
QTimer.singleShot(50, self.check)
self.covers_view.setFocus(Qt.OtherFocusReason)
def check(self):
if self.worker.is_alive() and not self.abort.is_set():
QTimer.singleShot(50, self.check)
try:
self.process_result(self.worker.rq.get_nowait())
except Empty:
pass
else:
self.process_results()
def process_results(self):
while self.continue_processing:
try:
self.process_result(self.worker.rq.get_nowait())
except Empty:
break
self.covers_view.clear_failed()
if self.worker.error is not None:
error_dialog(self, _('Download failed'),
_('Failed to download any covers, click'
' "Show details" for details.'),
det_msg=self.worker.error, show=True)
num = self.covers_view.model().rowCount()
if num < 2:
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
else:
txt = _('Found <b>%d</b> covers of %s. Pick the one you like'
' best.')%(num-1, self.title)
self.msg.setText(txt)
self.finished.emit()
def process_result(self, result):
if not self.continue_processing:
return
plugin, width, height, fmt, data = result
self.covers_view.model().update_result(plugin, width, height, data)
def cleanup(self):
self.covers_view.delegate.stop_animation()
self.continue_processing = False
def cancel(self):
self.continue_processing = False
self.abort.set()
def cover_pixmap(self):
idx = None
for i in self.covers_view.selectionModel().selectedIndexes():
if i.isValid():
idx = i
break
if idx is None:
idx = self.covers_view.currentIndex()
return self.covers_view.model().cover_pixmap(idx)
# }}}
class LogViewer(QDialog): # {{{
def __init__(self, log, parent=None):
QDialog.__init__(self, parent)
self.log = log
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tb = QTextBrowser(self)
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Close)
l.addWidget(self.bb)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.clicked.connect(self.copy_to_clipboard)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.bb.rejected.connect(self.reject)
self.bb.accepted.connect(self.accept)
self.setWindowTitle(_('Download log'))
self.setWindowIcon(QIcon(I('debug.png')))
self.resize(QSize(800, 400))
self.keep_updating = True
self.last_html = None
self.finished.connect(self.stop)
QTimer.singleShot(100, self.update_log)
self.show()
def copy_to_clipboard(self):
QApplication.clipboard().setText(''.join(self.log.plain_text))
def stop(self, *args):
self.keep_updating = False
def update_log(self):
if not self.keep_updating:
return
html = self.log.html
if html != self.last_html:
self.last_html = html
self.tb.setHtml('<pre style="font-family:monospace">%s</pre>'%html)
QTimer.singleShot(1000, self.update_log)
# }}} # }}}
class FullFetch(QDialog): # {{{ class FullFetch(QDialog): # {{{
def __init__(self, log, current_cover=None, parent=None): def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.log, self.current_cover = log, current_cover self.current_cover = current_cover
self.log = Log()
self.book = self.cover_pixmap = None
self.setWindowTitle(_('Downloading metadata...')) self.setWindowTitle(_('Downloading metadata...'))
self.setWindowIcon(QIcon(I('metadata.png'))) self.setWindowIcon(QIcon(I('metadata.png')))
@ -430,28 +820,39 @@ class FullFetch(QDialog): # {{{
self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole) self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole)
self.next_button.setDefault(True) self.next_button.setDefault(True)
self.next_button.setEnabled(False) self.next_button.setEnabled(False)
self.next_button.setIcon(QIcon(I('ok.png')))
self.next_button.clicked.connect(self.next_clicked) self.next_button.clicked.connect(self.next_clicked)
self.ok_button = self.bb.button(self.bb.Ok) self.ok_button = self.bb.button(self.bb.Ok)
self.ok_button.setVisible(False)
self.ok_button.clicked.connect(self.ok_clicked) self.ok_button.clicked.connect(self.ok_clicked)
self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
self.log_button.clicked.connect(self.view_log)
self.log_button.setIcon(QIcon(I('debug.png')))
self.ok_button.setVisible(False)
self.identify_widget = IdentifyWidget(log, self) self.identify_widget = IdentifyWidget(self.log, self)
self.identify_widget.rejected.connect(self.reject) self.identify_widget.rejected.connect(self.reject)
self.identify_widget.results_found.connect(self.identify_results_found) self.identify_widget.results_found.connect(self.identify_results_found)
self.identify_widget.book_selected.connect(self.book_selected) self.identify_widget.book_selected.connect(self.book_selected)
self.stack.addWidget(self.identify_widget) self.stack.addWidget(self.identify_widget)
self.cover_widget = CoverWidget(self.log, parent=self) self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
self.stack.addWidget(self.cover_widget) self.covers_widget.chosen.connect(self.ok_clicked)
self.stack.addWidget(self.covers_widget)
self.resize(850, 500) self.resize(850, 550)
self.finished.connect(self.cleanup)
def view_log(self):
self._lv = LogViewer(self.log, self)
def book_selected(self, book): def book_selected(self, book):
self.next_button.setVisible(False) self.next_button.setVisible(False)
self.ok_button.setVisible(True) self.ok_button.setVisible(True)
self.book = book self.book = book
self.stack.setCurrentIndex(1) self.stack.setCurrentIndex(1)
self.cover_widget.start(book, self.current_cover, self.log('\n\n')
self.covers_widget.start(book, self.current_cover,
self.title, self.authors) self.title, self.authors)
def accept(self): def accept(self):
@ -460,8 +861,12 @@ class FullFetch(QDialog): # {{{
def reject(self): def reject(self):
self.identify_widget.cancel() self.identify_widget.cancel()
self.covers_widget.cancel()
return QDialog.reject(self) return QDialog.reject(self)
def cleanup(self):
self.covers_widget.cleanup()
def identify_results_found(self): def identify_results_found(self):
self.next_button.setEnabled(True) self.next_button.setEnabled(True)
@ -469,17 +874,79 @@ class FullFetch(QDialog): # {{{
self.identify_widget.get_result() self.identify_widget.get_result()
def ok_clicked(self, *args): def ok_clicked(self, *args):
pass self.cover_pixmap = self.covers_widget.cover_pixmap()
if DEBUG_DIALOG:
if self.cover_pixmap is not None:
self.w = QLabel()
self.w.setPixmap(self.cover_pixmap)
self.stack.addWidget(self.w)
self.stack.setCurrentIndex(2)
else:
QDialog.accept(self)
def start(self, title=None, authors=None, identifiers={}): def start(self, title=None, authors=None, identifiers={}):
self.title, self.authors = title, authors self.title, self.authors = title, authors
self.identify_widget.start(title=title, authors=authors, self.identify_widget.start(title=title, authors=authors,
identifiers=identifiers) identifiers=identifiers)
self.exec_() return self.exec_()
# }}}
class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)
self.current_cover = current_cover
self.log = Log()
self.cover_pixmap = None
self.setWindowTitle(_('Downloading cover...'))
self.setWindowIcon(QIcon(I('book.png')))
self.l = l = QVBoxLayout()
self.setLayout(l)
self.covers_widget = CoversWidget(self.log, self.current_cover, parent=self)
self.covers_widget.chosen.connect(self.accept)
l.addWidget(self.covers_widget)
self.resize(850, 550)
self.finished.connect(self.cleanup)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb)
self.log_button = self.bb.addButton(_('View log'), self.bb.ActionRole)
self.log_button.clicked.connect(self.view_log)
self.log_button.setIcon(QIcon(I('debug.png')))
self.bb.rejected.connect(self.reject)
self.bb.accepted.connect(self.accept)
def cleanup(self):
self.covers_widget.cleanup()
def reject(self):
self.covers_widget.cancel()
return QDialog.reject(self)
def accept(self, *args):
self.cover_pixmap = self.covers_widget.cover_pixmap()
QDialog.accept(self)
def start(self, title, authors, identifiers):
book = Metadata(title, authors)
book.identifiers = identifiers
self.covers_widget.start(book, self.current_cover,
title, authors)
return self.exec_()
def view_log(self):
self._lv = LogViewer(self.log, self)
# }}} # }}}
if __name__ == '__main__': if __name__ == '__main__':
#DEBUG_DIALOG = True
app = QApplication([]) app = QApplication([])
d = FullFetch(Log()) d = FullFetch()
d.start(title='great gatsby', authors=['Fitzgerald']) d.start(title='great gatsby', authors=['fitzgerald'])

View File

@ -51,7 +51,7 @@
<item row="2" column="2"> <item row="2" column="2">
<widget class="QCheckBox" name="opt_bools_are_tristate"> <widget class="QCheckBox" name="opt_bools_are_tristate">
<property name="text"> <property name="text">
<string>Custom Yes/No columns have three values (Requires restart):</string> <string>Yes/No columns have three values (Requires restart)</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown. <string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.

View File

@ -786,8 +786,7 @@ def write_tweaks(raw):
tweaks = read_tweaks() tweaks = read_tweaks()
test_eight_code = tweaks.get('test_eight_code', False) test_eight_code = tweaks.get('test_eight_code', False)
# test_eight_code notes # test_eight_code notes
# Change documentation of bool columns are tristate to indicate that it can be # Change Amazon plugin name to just Amazon
# overridden on a per library basis via Preferences->Custom columns
def migrate(): def migrate():
if hasattr(os, 'geteuid') and os.geteuid() == 0: if hasattr(os, 'geteuid') and os.geteuid() == 0:

View File

@ -66,7 +66,7 @@ class HTMLStream(Stream):
color = { color = {
DEBUG: '<span style="color:green">', DEBUG: '<span style="color:green">',
INFO:'<span>', INFO:'<span>',
WARN: '<span style="color:yellow">', WARN: '<span style="color:blue">',
ERROR: '<span style="color:red">' ERROR: '<span style="color:red">'
} }
normal = '</span>' normal = '</span>'