mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
8127e5a4fe
@ -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
32
recipes/dvhn.recipe
Normal 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;}
|
||||||
|
'''
|
BIN
resources/images/connect_share_on.png
Normal file
BIN
resources/images/connect_share_on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -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 {{{
|
||||||
|
@ -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'
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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:
|
||||||
|
@ -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>'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user