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
41af71af27
@ -19,6 +19,66 @@
|
|||||||
# new recipes:
|
# new recipes:
|
||||||
# - title:
|
# - title:
|
||||||
|
|
||||||
|
- version: 0.8.7
|
||||||
|
date: 2011-06-24
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Connect to iTunes: You now need to tell iTunes to keep its own copy of every ebook. Do this in iTunes by going to Preferences->Advanced and setting the 'Copy files to iTunes Media folder when adding to library' option. To learn about why this is necessary, see: http://www.mobileread.com/forums/showthread.php?t=140260"
|
||||||
|
type: major
|
||||||
|
|
||||||
|
- title: "Add a couple of date related functions to the calibre template langauge to get 'todays' date and create text based on the value of a date type field"
|
||||||
|
|
||||||
|
- title: "Improved reading of metadata from FB2 files, with support for reading isbns, tags, published date, etc."
|
||||||
|
|
||||||
|
- title: "Driver for the Imagine IMEB5"
|
||||||
|
tickets: [800642]
|
||||||
|
|
||||||
|
- title: "Show the currently used network proxies in Preferences->Miscellaneous"
|
||||||
|
|
||||||
|
- title: "Kobo Touch driver: Show Favorites as a device collection. Various other minor fixes."
|
||||||
|
|
||||||
|
- title: "Content server now sends the Content-Disposition header when sending ebook files."
|
||||||
|
|
||||||
|
- title: "Allow search and replace on comments custom columns."
|
||||||
|
|
||||||
|
- title: "Add a new action 'Quick View' to show the books in your library by the author/tags/series/etc. of the currently selected book, in a separate window. You can add it to your toolbar or right click menu by going to Preferences->Toolbars."
|
||||||
|
|
||||||
|
- title: "Get Books: Add libri.de as a book source. Fix a bug that caused some books downloads to fail. Fixes to the Legimi and beam-ebooks.de stores"
|
||||||
|
tickets: [799367]
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "Fix a memory leak that could result in the leaking of several MB of memory with large libraries"
|
||||||
|
tickets: [800952]
|
||||||
|
|
||||||
|
- title: "Fix the read metadata from format button in the edit metadata dialog using the wrong timezone when setting published date"
|
||||||
|
tickets: [799777]
|
||||||
|
|
||||||
|
- title: "Generating catalog: Fix occassional file in use errors when generating catalogs on windows"
|
||||||
|
|
||||||
|
- title: "Fix clicking on News in Tag Browser not working in non English locales."
|
||||||
|
tickets: [799471]
|
||||||
|
|
||||||
|
- title: "HTML Input: Fix a regression in 0.8.6 that caused CSS stylesheets to be ignored"
|
||||||
|
tickets: [799171]
|
||||||
|
|
||||||
|
- title: "Fix a regression that caused restore database to stop working on some windows sytems"
|
||||||
|
|
||||||
|
- title: "EPUB Output: Convert <br> tags with text in them into <divs> as ADE cannot handle them."
|
||||||
|
tickets: [794427]
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Le Temps
|
||||||
|
- Perfil
|
||||||
|
- Financial Times UK
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "Daytona Beach Journal"
|
||||||
|
author: BRGriff
|
||||||
|
|
||||||
|
- title: "El club del ebook and Frontline"
|
||||||
|
author: Darko Miletic
|
||||||
|
|
||||||
|
|
||||||
- version: 0.8.6
|
- version: 0.8.6
|
||||||
date: 2011-06-17
|
date: 2011-06-17
|
||||||
|
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
ft.com
|
www.ft.com/uk-edition
|
||||||
'''
|
'''
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class FinancialTimes(BasicNewsRecipe):
|
class FinancialTimes(BasicNewsRecipe):
|
||||||
title = u'Financial Times - UK printed edition'
|
title = 'Financial Times - UK printed edition'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Financial world news'
|
description = "The Financial Times (FT) is one of the world's leading business news and information organisations, recognised internationally for its authority, integrity and accuracy."
|
||||||
|
publisher = 'The Financial Times Ltd.'
|
||||||
|
category = 'news, finances, politics, UK, World'
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
language = 'en_GB'
|
language = 'en_GB'
|
||||||
max_articles_per_feed = 250
|
max_articles_per_feed = 250
|
||||||
@ -17,14 +19,24 @@ class FinancialTimes(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
simultaneous_downloads= 1
|
publication_type = 'newspaper'
|
||||||
delay = 1
|
cover_url = strftime('http://specials.ft.com/vtf_pdf/%d%m%y_FRONT1_LON.pdf')
|
||||||
|
masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg'
|
||||||
LOGIN = 'https://registration.ft.com/registration/barrier/login'
|
LOGIN = 'https://registration.ft.com/registration/barrier/login'
|
||||||
INDEX = 'http://www.ft.com/uk-edition'
|
INDEX = 'http://www.ft.com/uk-edition'
|
||||||
PREFIX = 'http://www.ft.com'
|
PREFIX = 'http://www.ft.com'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
, 'linearize_tables' : True
|
||||||
|
}
|
||||||
|
|
||||||
def get_browser(self):
|
def get_browser(self):
|
||||||
br = BasicNewsRecipe.get_browser()
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
br.open(self.INDEX)
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
br.open(self.LOGIN)
|
br.open(self.LOGIN)
|
||||||
br.select_form(name='loginForm')
|
br.select_form(name='loginForm')
|
||||||
@ -33,29 +45,34 @@ class FinancialTimes(BasicNewsRecipe):
|
|||||||
br.submit()
|
br.submit()
|
||||||
return br
|
return br
|
||||||
|
|
||||||
keep_only_tags = [ dict(name='div', attrs={'id':'cont'}) ]
|
keep_only_tags = [dict(name='div', attrs={'class':['fullstory fullstoryHeader','fullstory fullstoryBody','ft-story-header','ft-story-body','index-detail']})]
|
||||||
remove_tags_after = dict(name='p', attrs={'class':'copyright'})
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'id':'floating-con'})
|
dict(name='div', attrs={'id':'floating-con'})
|
||||||
,dict(name=['meta','iframe','base','object','embed','link'])
|
,dict(name=['meta','iframe','base','object','embed','link'])
|
||||||
|
,dict(attrs={'class':['storyTools','story-package','screen-copy','story-package separator','expandable-image']})
|
||||||
]
|
]
|
||||||
remove_attributes = ['width','height','lang']
|
remove_attributes = ['width','height','lang']
|
||||||
|
|
||||||
extra_css = """
|
extra_css = """
|
||||||
body{font-family:Arial,Helvetica,sans-serif;}
|
body{font-family: Georgia,Times,"Times New Roman",serif}
|
||||||
h2{font-size:large;}
|
h2{font-size:large}
|
||||||
.ft-story-header{font-size:xx-small;}
|
.ft-story-header{font-size: x-small}
|
||||||
.ft-story-body{font-size:small;}
|
|
||||||
a{color:#003399;}
|
|
||||||
.container{font-size:x-small;}
|
.container{font-size:x-small;}
|
||||||
h3{font-size:x-small;color:#003399;}
|
h3{font-size:x-small;color:#003399;}
|
||||||
.copyright{font-size: x-small}
|
.copyright{font-size: x-small}
|
||||||
|
img{margin-top: 0.8em; display: block}
|
||||||
|
.lastUpdated{font-family: Arial,Helvetica,sans-serif; font-size: x-small}
|
||||||
|
.byline,.ft-story-body,.ft-story-header{font-family: Arial,Helvetica,sans-serif}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_artlinks(self, elem):
|
def get_artlinks(self, elem):
|
||||||
articles = []
|
articles = []
|
||||||
for item in elem.findAll('a',href=True):
|
for item in elem.findAll('a',href=True):
|
||||||
url = self.PREFIX + item['href']
|
rawlink = item['href']
|
||||||
|
if rawlink.startswith('http://'):
|
||||||
|
url = rawlink
|
||||||
|
else:
|
||||||
|
url = self.PREFIX + rawlink
|
||||||
title = self.tag_to_string(item)
|
title = self.tag_to_string(item)
|
||||||
date = strftime(self.timefmt)
|
date = strftime(self.timefmt)
|
||||||
articles.append({
|
articles.append({
|
||||||
@ -86,5 +103,28 @@ class FinancialTimes(BasicNewsRecipe):
|
|||||||
return feeds
|
return feeds
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
return self.adeify_images(soup)
|
items = ['promo-box','promo-title',
|
||||||
|
'promo-headline','promo-image',
|
||||||
|
'promo-intro','promo-link','subhead']
|
||||||
|
for item in items:
|
||||||
|
for it in soup.findAll(item):
|
||||||
|
it.name = 'div'
|
||||||
|
it.attrs = []
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
limg = item.find('img')
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
if limg:
|
||||||
|
item.name = 'div'
|
||||||
|
item.attrs = []
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
||||||
|
BIN
recipes/icons/financial_times_uk.png
Normal file
BIN
recipes/icons/financial_times_uk.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = u'calibre'
|
__appname__ = u'calibre'
|
||||||
numeric_version = (0, 8, 6)
|
numeric_version = (0, 8, 7)
|
||||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
|
@ -1378,6 +1378,14 @@ class StoreNextoStore(StoreBase):
|
|||||||
formats = ['EPUB', 'PDF']
|
formats = ['EPUB', 'PDF']
|
||||||
affiliate = True
|
affiliate = True
|
||||||
|
|
||||||
|
class StoreOpenBooksStore(StoreBase):
|
||||||
|
name = 'Open Books'
|
||||||
|
description = u'Comprehensive listing of DRM free ebooks from a variety of sources provided by users of calibre.'
|
||||||
|
actual_plugin = 'calibre.gui2.store.open_books_plugin:OpenBooksStore'
|
||||||
|
|
||||||
|
drm_free_only = True
|
||||||
|
headquarters = 'US'
|
||||||
|
|
||||||
class StoreOpenLibraryStore(StoreBase):
|
class StoreOpenLibraryStore(StoreBase):
|
||||||
name = 'Open Library'
|
name = 'Open Library'
|
||||||
description = u'One web page for every book ever published. The goal is to be a true online library. Over 20 million records from a variety of large catalogs as well as single contributions, with more on the way.'
|
description = u'One web page for every book ever published. The goal is to be a true online library. Over 20 million records from a variety of large catalogs as well as single contributions, with more on the way.'
|
||||||
@ -1504,6 +1512,7 @@ plugins += [
|
|||||||
StoreManyBooksStore,
|
StoreManyBooksStore,
|
||||||
StoreMobileReadStore,
|
StoreMobileReadStore,
|
||||||
StoreNextoStore,
|
StoreNextoStore,
|
||||||
|
StoreOpenBooksStore,
|
||||||
StoreOpenLibraryStore,
|
StoreOpenLibraryStore,
|
||||||
StoreOReillyStore,
|
StoreOReillyStore,
|
||||||
StorePragmaticBookshelfStore,
|
StorePragmaticBookshelfStore,
|
||||||
|
@ -45,8 +45,11 @@ class ANDROID(USBMS):
|
|||||||
0xfce : { 0xd12e : [0x0100]},
|
0xfce : { 0xd12e : [0x0100]},
|
||||||
|
|
||||||
# Google
|
# Google
|
||||||
0x18d1 : { 0x4e11 : [0x0100, 0x226, 0x227], 0x4e12: [0x0100, 0x226,
|
0x18d1 : {
|
||||||
0x227], 0x4e21: [0x0100, 0x226, 0x227], 0xb058: [0x0222]},
|
0x4e11 : [0x0100, 0x226, 0x227],
|
||||||
|
0x4e12: [0x0100, 0x226, 0x227],
|
||||||
|
0x4e21: [0x0100, 0x226, 0x227],
|
||||||
|
0xb058: [0x0222, 0x226, 0x227]},
|
||||||
|
|
||||||
# Samsung
|
# Samsung
|
||||||
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
|
0x04e8 : { 0x681d : [0x0222, 0x0223, 0x0224, 0x0400],
|
||||||
@ -107,7 +110,7 @@ class ANDROID(USBMS):
|
|||||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
||||||
'GENERIC-', 'ZTE']
|
'GENERIC-', 'ZTE', 'MID']
|
||||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||||
@ -116,7 +119,7 @@ class ANDROID(USBMS):
|
|||||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
||||||
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
||||||
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
|
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
|
||||||
'MB525']
|
'MB525', 'ANDROID2.3']
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||||
|
@ -1211,7 +1211,8 @@ class ITUNES(DriverBase):
|
|||||||
'''
|
'''
|
||||||
windows assumes pythoncom wrapper
|
windows assumes pythoncom wrapper
|
||||||
'''
|
'''
|
||||||
self.log.info(" ITUNES._add_library_book()")
|
if DEBUG:
|
||||||
|
self.log.info(" ITUNES._add_library_book()")
|
||||||
if isosx:
|
if isosx:
|
||||||
added = self.iTunes.add(appscript.mactypes.File(file))
|
added = self.iTunes.add(appscript.mactypes.File(file))
|
||||||
|
|
||||||
@ -1335,7 +1336,8 @@ class ITUNES(DriverBase):
|
|||||||
assumes pythoncom wrapper for db_added
|
assumes pythoncom wrapper for db_added
|
||||||
as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation
|
as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation
|
||||||
'''
|
'''
|
||||||
self.log.info(" ITUNES._cover_to_thumb()")
|
if DEBUG:
|
||||||
|
self.log.info(" ITUNES._cover_to_thumb()")
|
||||||
|
|
||||||
thumb = None
|
thumb = None
|
||||||
if metadata.cover:
|
if metadata.cover:
|
||||||
@ -2489,7 +2491,8 @@ class ITUNES(DriverBase):
|
|||||||
'''
|
'''
|
||||||
Windows assumes pythoncom wrapper
|
Windows assumes pythoncom wrapper
|
||||||
'''
|
'''
|
||||||
self.log.info(" ITUNES._remove_from_device()")
|
if DEBUG:
|
||||||
|
self.log.info(" ITUNES._remove_from_device()")
|
||||||
if isosx:
|
if isosx:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
|
self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
|
||||||
@ -2616,7 +2619,8 @@ class ITUNES(DriverBase):
|
|||||||
def _update_epub_metadata(self, fpath, metadata):
|
def _update_epub_metadata(self, fpath, metadata):
|
||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
self.log.info(" ITUNES._update_epub_metadata()")
|
if DEBUG:
|
||||||
|
self.log.info(" ITUNES._update_epub_metadata()")
|
||||||
|
|
||||||
# Fetch plugboard updates
|
# Fetch plugboard updates
|
||||||
metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub')
|
metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub')
|
||||||
@ -2983,7 +2987,8 @@ class ITUNES(DriverBase):
|
|||||||
self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
|
self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
|
||||||
newmi.tags if book.tags != newmi.tags else ''))
|
newmi.tags if book.tags != newmi.tags else ''))
|
||||||
else:
|
else:
|
||||||
self.log(" matching plugboard not found")
|
if DEBUG:
|
||||||
|
self.log(" matching plugboard not found")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
newmi = book
|
newmi = book
|
||||||
|
@ -351,7 +351,9 @@ class ComicInput(InputFormatPlugin):
|
|||||||
comics = []
|
comics = []
|
||||||
with CurrentDir(tdir):
|
with CurrentDir(tdir):
|
||||||
if not os.path.exists('comics.txt'):
|
if not os.path.exists('comics.txt'):
|
||||||
raise ValueError('%s is not a valid comic collection'
|
raise ValueError((
|
||||||
|
'%s is not a valid comic collection'
|
||||||
|
' no comics.txt was found in the file')
|
||||||
%stream.name)
|
%stream.name)
|
||||||
raw = open('comics.txt', 'rb').read()
|
raw = open('comics.txt', 'rb').read()
|
||||||
if raw.startswith(codecs.BOM_UTF16_BE):
|
if raw.startswith(codecs.BOM_UTF16_BE):
|
||||||
|
@ -59,6 +59,8 @@ class CompositeProgressReporter(object):
|
|||||||
(self.global_max - self.global_min)
|
(self.global_max - self.global_min)
|
||||||
self.global_reporter(global_frac, msg)
|
self.global_reporter(global_frac, msg)
|
||||||
|
|
||||||
|
ARCHIVE_FMTS = ('zip', 'rar', 'oebzip')
|
||||||
|
|
||||||
class Plumber(object):
|
class Plumber(object):
|
||||||
'''
|
'''
|
||||||
The `Plumber` manages the conversion pipeline. An UI should call the methods
|
The `Plumber` manages the conversion pipeline. An UI should call the methods
|
||||||
@ -594,7 +596,7 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
raise ValueError('Input file must have an extension')
|
raise ValueError('Input file must have an extension')
|
||||||
input_fmt = input_fmt[1:].lower()
|
input_fmt = input_fmt[1:].lower()
|
||||||
self.archive_input_tdir = None
|
self.archive_input_tdir = None
|
||||||
if input_fmt in ('zip', 'rar', 'oebzip'):
|
if input_fmt in ARCHIVE_FMTS:
|
||||||
self.log('Processing archive...')
|
self.log('Processing archive...')
|
||||||
tdir = PersistentTemporaryDirectory('_plumber_archive')
|
tdir = PersistentTemporaryDirectory('_plumber_archive')
|
||||||
self.input, input_fmt = self.unarchive(self.input, tdir)
|
self.input, input_fmt = self.unarchive(self.input, tdir)
|
||||||
|
@ -248,10 +248,11 @@ def error_dialog(parent, title, msg, det_msg='', show=False,
|
|||||||
return d.exec_()
|
return d.exec_()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False):
|
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False,
|
||||||
|
default_yes=True):
|
||||||
from calibre.gui2.dialogs.message_box import MessageBox
|
from calibre.gui2.dialogs.message_box import MessageBox
|
||||||
d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent,
|
d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent,
|
||||||
show_copy_button=show_copy_button)
|
show_copy_button=show_copy_button, default_yes=default_yes)
|
||||||
return d.exec_() == d.Accepted
|
return d.exec_() == d.Accepted
|
||||||
|
|
||||||
def info_dialog(parent, title, msg, det_msg='', show=False,
|
def info_dialog(parent, title, msg, det_msg='', show=False,
|
||||||
|
@ -252,11 +252,12 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
|
|
||||||
def delete_requested(self, name, location):
|
def delete_requested(self, name, location):
|
||||||
loc = location.replace('/', os.sep)
|
loc = location.replace('/', os.sep)
|
||||||
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
|
if not question_dialog(self.gui, _('Are you sure?'),
|
||||||
|
_('<h1 style="color:red">WARNING</h1>')+
|
||||||
_('<b style="color: red">All files</b> (not just ebooks) '
|
_('<b style="color: red">All files</b> (not just ebooks) '
|
||||||
'from <br><br><b>%s</b><br><br> will be '
|
'from <br><br><b>%s</b><br><br> will be '
|
||||||
'<b>permanently deleted</b>. Are you sure?') % loc,
|
'<b>permanently deleted</b>. Are you sure?') % loc,
|
||||||
show_copy_button=False):
|
show_copy_button=False, default_yes=False):
|
||||||
return
|
return
|
||||||
exists = self.gui.library_view.model().db.exists_at(loc)
|
exists = self.gui.library_view.model().db.exists_at(loc)
|
||||||
if exists:
|
if exists:
|
||||||
|
@ -139,7 +139,12 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
try:
|
try:
|
||||||
self.open_book(fpath)
|
self.open_book(fpath)
|
||||||
finally:
|
finally:
|
||||||
os.remove(fpath)
|
try:
|
||||||
|
os.remove(fpath)
|
||||||
|
except:
|
||||||
|
# Fails on windows if the input plugin for this format keeps the file open
|
||||||
|
# Happens for LIT files
|
||||||
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def open_book(self, pathtoebook):
|
def open_book(self, pathtoebook):
|
||||||
@ -148,7 +153,8 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
|
|||||||
text = [u'']
|
text = [u'']
|
||||||
preprocessor = HTMLPreProcessor(None, False)
|
preprocessor = HTMLPreProcessor(None, False)
|
||||||
for path in self.iterator.spine:
|
for path in self.iterator.spine:
|
||||||
html = open(path, 'rb').read().decode('utf-8', 'replace')
|
with open(path, 'rb') as f:
|
||||||
|
html = f.read().decode('utf-8', 'replace')
|
||||||
html = preprocessor(html, get_preprocess_html=True)
|
html = preprocessor(html, get_preprocess_html=True)
|
||||||
text.append(html)
|
text.append(html)
|
||||||
self.preview.setPlainText('\n---\n'.join(text))
|
self.preview.setPlainText('\n---\n'.join(text))
|
||||||
|
@ -11,8 +11,8 @@ import sys, cPickle, shutil, importlib
|
|||||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
||||||
|
|
||||||
from calibre.gui2 import ResizableDialog, NONE
|
from calibre.gui2 import ResizableDialog, NONE
|
||||||
from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics, \
|
from calibre.ebooks.conversion.config import (GuiRecommendations, save_specifics,
|
||||||
load_specifics
|
load_specifics)
|
||||||
from calibre.gui2.convert.single_ui import Ui_Dialog
|
from calibre.gui2.convert.single_ui import Ui_Dialog
|
||||||
from calibre.gui2.convert.metadata import MetadataWidget
|
from calibre.gui2.convert.metadata import MetadataWidget
|
||||||
from calibre.gui2.convert.look_and_feel import LookAndFeelWidget
|
from calibre.gui2.convert.look_and_feel import LookAndFeelWidget
|
||||||
@ -24,7 +24,8 @@ from calibre.gui2.convert.toc import TOCWidget
|
|||||||
from calibre.gui2.convert.debug import DebugWidget
|
from calibre.gui2.convert.debug import DebugWidget
|
||||||
|
|
||||||
|
|
||||||
from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats
|
from calibre.ebooks.conversion.plumber import (Plumber,
|
||||||
|
supported_input_formats, ARCHIVE_FMTS)
|
||||||
from calibre.ebooks.conversion.config import delete_specifics
|
from calibre.ebooks.conversion.config import delete_specifics
|
||||||
from calibre.customize.ui import available_output_formats
|
from calibre.customize.ui import available_output_formats
|
||||||
from calibre.customize.conversion import OptionRecommendation
|
from calibre.customize.conversion import OptionRecommendation
|
||||||
@ -158,7 +159,10 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
output_path = 'dummy.'+output_format
|
output_path = 'dummy.'+output_format
|
||||||
log = Log()
|
log = Log()
|
||||||
log.outputs = []
|
log.outputs = []
|
||||||
self.plumber = Plumber('dummy.'+input_format, output_path, log)
|
input_file = 'dummy.'+input_format
|
||||||
|
if input_format in ARCHIVE_FMTS:
|
||||||
|
input_file = 'dummy.html'
|
||||||
|
self.plumber = Plumber(input_file, output_path, log)
|
||||||
|
|
||||||
def widget_factory(cls):
|
def widget_factory(cls):
|
||||||
return cls(self.stack, self.plumber.get_option_by_name,
|
return cls(self.stack, self.plumber.get_option_by_name,
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<item row="4" column="0" colspan="4">
|
<item row="4" column="0" colspan="4">
|
||||||
<widget class="QRadioButton" name="existing_library">
|
<widget class="QRadioButton" name="existing_library">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &existing library at the new location</string>
|
<string>Use the previously &existing library at the new location</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="checked">
|
<property name="checked">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -23,7 +23,7 @@ class MessageBox(QDialog, Ui_Dialog): # {{{
|
|||||||
det_msg='',
|
det_msg='',
|
||||||
q_icon=None,
|
q_icon=None,
|
||||||
show_copy_button=True,
|
show_copy_button=True,
|
||||||
parent=None):
|
parent=None, default_yes=True):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
if q_icon is None:
|
if q_icon is None:
|
||||||
icon = {
|
icon = {
|
||||||
@ -65,7 +65,9 @@ class MessageBox(QDialog, Ui_Dialog): # {{{
|
|||||||
self.is_question = type_ == self.QUESTION
|
self.is_question = type_ == self.QUESTION
|
||||||
if self.is_question:
|
if self.is_question:
|
||||||
self.bb.setStandardButtons(self.bb.Yes|self.bb.No)
|
self.bb.setStandardButtons(self.bb.Yes|self.bb.No)
|
||||||
self.bb.button(self.bb.Yes).setDefault(True)
|
self.bb.button(self.bb.Yes if default_yes else self.bb.No
|
||||||
|
).setDefault(True)
|
||||||
|
self.default_yes = default_yes
|
||||||
else:
|
else:
|
||||||
self.bb.button(self.bb.Ok).setDefault(True)
|
self.bb.button(self.bb.Ok).setDefault(True)
|
||||||
|
|
||||||
@ -101,7 +103,8 @@ class MessageBox(QDialog, Ui_Dialog): # {{{
|
|||||||
ret = QDialog.showEvent(self, ev)
|
ret = QDialog.showEvent(self, ev)
|
||||||
if self.is_question:
|
if self.is_question:
|
||||||
try:
|
try:
|
||||||
self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason)
|
self.bb.button(self.bb.Yes if self.default_yes else self.bb.No
|
||||||
|
).setFocus(Qt.OtherFocusReason)
|
||||||
except:
|
except:
|
||||||
pass# Buttons were changed
|
pass# Buttons were changed
|
||||||
else:
|
else:
|
||||||
|
@ -173,9 +173,20 @@ class TitleSortEdit(TitleEdit):
|
|||||||
|
|
||||||
def auto_generate(self, *args):
|
def auto_generate(self, *args):
|
||||||
self.current_val = title_sort(self.title_edit.current_val)
|
self.current_val = title_sort(self.title_edit.current_val)
|
||||||
self.title_edit.textChanged.disconnect()
|
|
||||||
self.textChanged.disconnect()
|
def break_cycles(self):
|
||||||
self.autogen_button.clicked.disconnect()
|
try:
|
||||||
|
self.title_edit.textChanged.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.textChanged.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.autogen_button.clicked.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -280,7 +291,10 @@ class AuthorsEdit(MultiCompleteComboBox):
|
|||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.db = self.dialog = None
|
self.db = self.dialog = None
|
||||||
self.manage_authors_signal.triggered.disconnect()
|
try:
|
||||||
|
self.manage_authors_signal.triggered.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
class AuthorSortEdit(EnLineEdit):
|
class AuthorSortEdit(EnLineEdit):
|
||||||
|
|
||||||
@ -387,11 +401,26 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.db = None
|
self.db = None
|
||||||
self.authors_edit.editTextChanged.disconnect()
|
try:
|
||||||
self.textChanged.disconnect()
|
self.authors_edit.editTextChanged.disconnect()
|
||||||
self.autogen_button.clicked.disconnect()
|
except:
|
||||||
self.copy_a_to_as_action.triggered.disconnect()
|
pass
|
||||||
self.copy_as_to_a_action.triggered.disconnect()
|
try:
|
||||||
|
self.textChanged.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.autogen_button.clicked.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.copy_a_to_as_action.triggered.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.copy_as_to_a_action.triggered.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
self.authors_edit = None
|
self.authors_edit = None
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
@ -519,9 +548,18 @@ class SeriesIndexEdit(QDoubleSpinBox):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.series_edit.currentIndexChanged.disconnect()
|
try:
|
||||||
self.series_edit.editTextChanged.disconnect()
|
self.series_edit.currentIndexChanged.disconnect()
|
||||||
self.series_edit.lineEdit().editingFinished.disconnect()
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.series_edit.editTextChanged.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.series_edit.lineEdit().editingFinished.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
self.db = self.series_edit = self.dialog = None
|
self.db = self.series_edit = self.dialog = None
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
@ -898,7 +936,10 @@ class Cover(ImageView): # {{{
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self.cover_changed.disconnect()
|
try:
|
||||||
|
self.cover_changed.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
self.dialog = self._cdata = self.current_val = self.original_val = None
|
self.dialog = self._cdata = self.current_val = self.original_val = None
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -133,7 +133,7 @@ class Matches(QAbstractItemModel):
|
|||||||
return QVariant('<p>%s</p>' % result.description)
|
return QVariant('<p>%s</p>' % result.description)
|
||||||
elif col == 2:
|
elif col == 2:
|
||||||
if result.drm_free_only:
|
if result.drm_free_only:
|
||||||
return QVariant('<p>' + _('This store only distributes ebooks with DRM.') + '</p>')
|
return QVariant('<p>' + _('This store only distributes ebooks without DRM.') + '</p>')
|
||||||
else:
|
else:
|
||||||
return QVariant('<p>' + _('This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis.') + '</p>')
|
return QVariant('<p>' + _('This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis.') + '</p>')
|
||||||
elif col == 3:
|
elif col == 3:
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
This is a list of stores that objected, declined
|
This is a list of stores that objected, declined
|
||||||
or asked not to be included in the store integration.
|
or asked not to be included in the store integration.
|
||||||
|
|
||||||
* Borders (http://www.borders.com/)
|
* Borders (http://www.borders.com/).
|
||||||
* Indigo (http://www.chapters.indigo.ca/)
|
* Indigo (http://www.chapters.indigo.ca/).
|
||||||
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
|
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
|
||||||
No reply with two attempts over 2 weeks
|
|
||||||
* WH Smith (http://www.whsmith.co.uk/)
|
|
||||||
Refused to permit signing up for the affiliate program
|
|
75
src/calibre/gui2/store/open_books_plugin.py
Normal file
75
src/calibre/gui2/store/open_books_plugin.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from PyQt4.Qt import QUrl
|
||||||
|
|
||||||
|
from calibre import browser, url_slash_cleaner
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
from calibre.gui2.store import StorePlugin
|
||||||
|
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||||
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
|
||||||
|
class OpenBooksStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
|
url = 'http://drmfree.calibre-ebook.com/'
|
||||||
|
|
||||||
|
if external or self.config.get('open_external', False):
|
||||||
|
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||||
|
else:
|
||||||
|
d = WebStoreDialog(self.gui, self.url, parent, detail_item)
|
||||||
|
d.setWindowTitle(self.name)
|
||||||
|
d.set_tags(self.config.get('tags', ''))
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def search(self, query, max_results=10, timeout=60):
|
||||||
|
url = 'http://drmfree.calibre-ebook.com/search/?q=' + urllib.quote_plus(query)
|
||||||
|
|
||||||
|
br = browser()
|
||||||
|
|
||||||
|
counter = max_results
|
||||||
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
|
doc = html.fromstring(f.read())
|
||||||
|
for data in doc.xpath('//ul[@id="object_list"]//li'):
|
||||||
|
if counter <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
id = ''.join(data.xpath('.//div[@class="links"]/a[1]/@href'))
|
||||||
|
id = id.strip()
|
||||||
|
if not id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cover_url = ''.join(data.xpath('.//div[@class="cover"]/img/@src'))
|
||||||
|
|
||||||
|
price = ''.join(data.xpath('.//div[@class="price"]/text()'))
|
||||||
|
a, b, price = price.partition('Price:')
|
||||||
|
price = price.strip()
|
||||||
|
if not price:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = ''.join(data.xpath('.//div/strong/text()'))
|
||||||
|
author = ''.join(data.xpath('.//div[@class="author"]//text()'))
|
||||||
|
author = author.partition('by')[-1]
|
||||||
|
|
||||||
|
counter -= 1
|
||||||
|
|
||||||
|
s = SearchResult()
|
||||||
|
s.cover_url = cover_url
|
||||||
|
s.title = title.strip()
|
||||||
|
s.author = author.strip()
|
||||||
|
s.price = price.strip()
|
||||||
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
|
yield s
|
@ -1493,7 +1493,6 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.tags_view.tag_item_renamed.emit()
|
self.tags_view.tag_item_renamed.emit()
|
||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
self.rename_item_in_all_user_categories(name, key, val)
|
self.rename_item_in_all_user_categories(name, key, val)
|
||||||
self.refresh_required.emit()
|
|
||||||
self.show_item_at_path(path)
|
self.show_item_at_path(path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ class UpdateNotification(QDialog):
|
|||||||
'See the <a href="http://calibre-ebook.com/whats-new'
|
'See the <a href="http://calibre-ebook.com/whats-new'
|
||||||
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
|
'">new features</a>.') + '<p>'+_('Update <b>only</b> if one of the '
|
||||||
'new features or bug fixes is important to you. '
|
'new features or bug fixes is important to you. '
|
||||||
'If the current version works well for you, do not update.'))%(
|
'If the current version works well for you, there is no need to update.'))%(
|
||||||
__appname__, calibre_version))
|
__appname__, calibre_version))
|
||||||
self.label.setOpenExternalLinks(True)
|
self.label.setOpenExternalLinks(True)
|
||||||
self.label.setWordWrap(True)
|
self.label.setWordWrap(True)
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
165
src/calibre/utils/ipc/proxy.py
Normal file
165
src/calibre/utils/ipc/proxy.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
|
print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, cPickle, struct
|
||||||
|
from threading import Thread
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
from multiprocessing.connection import arbitrary_address, Listener
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from calibre import as_unicode, prints
|
||||||
|
from calibre.constants import iswindows, DEBUG
|
||||||
|
|
||||||
|
def _encode(msg):
|
||||||
|
raw = cPickle.dumps(msg, -1)
|
||||||
|
size = len(raw)
|
||||||
|
header = struct.pack('!Q', size)
|
||||||
|
return header + raw
|
||||||
|
|
||||||
|
def _decode(raw):
|
||||||
|
sz = struct.calcsize('!Q')
|
||||||
|
if len(raw) < sz:
|
||||||
|
return 'invalid', None
|
||||||
|
header, = struct.unpack('!Q', raw[:sz])
|
||||||
|
if len(raw) != sz + header or header == 0:
|
||||||
|
return 'invalid', None
|
||||||
|
return cPickle.loads(raw[sz:])
|
||||||
|
|
||||||
|
|
||||||
|
class Writer(Thread):
|
||||||
|
|
||||||
|
TIMEOUT = 60 #seconds
|
||||||
|
|
||||||
|
def __init__(self, conn):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.dataq, self.resultq = Queue(), Queue()
|
||||||
|
self.conn = conn
|
||||||
|
self.start()
|
||||||
|
self.data_written = False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.dataq.put(None)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, raw_data):
|
||||||
|
self.dataq.put(raw_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ex = self.resultq.get(True, self.TIMEOUT)
|
||||||
|
except Empty:
|
||||||
|
raise IOError('Writing to socket timed out')
|
||||||
|
else:
|
||||||
|
if ex is not None:
|
||||||
|
raise IOError('Writing to socket failed with error: %s' % ex)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
x = self.dataq.get()
|
||||||
|
if x is None:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
self.data_written = True
|
||||||
|
self.conn.send_bytes(x)
|
||||||
|
except Exception as e:
|
||||||
|
self.resultq.put(as_unicode(e))
|
||||||
|
else:
|
||||||
|
self.resultq.put(None)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
class Server(Thread):
|
||||||
|
|
||||||
|
def __init__(self, dispatcher):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
|
||||||
|
self.auth_key = os.urandom(32)
|
||||||
|
self.address = arbitrary_address('AF_PIPE' if iswindows else 'AF_UNIX')
|
||||||
|
if iswindows and self.address[1] == ':':
|
||||||
|
self.address = self.address[2:]
|
||||||
|
self.listener = Listener(address=self.address,
|
||||||
|
authkey=self.auth_key, backlog=4)
|
||||||
|
|
||||||
|
self.keep_going = True
|
||||||
|
self.dispatcher = dispatcher
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connection_information(self):
|
||||||
|
if not self.is_alive():
|
||||||
|
self.start()
|
||||||
|
return (self.address, self.auth_key)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.keep_going = False
|
||||||
|
try:
|
||||||
|
self.listener.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.keep_going:
|
||||||
|
try:
|
||||||
|
conn = self.listener.accept()
|
||||||
|
self.handle_client(conn)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handle_client(self, conn):
|
||||||
|
t = Thread(target=partial(self._handle_client, conn))
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _handle_client(self, conn):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
func_name, args, kwargs = conn.recv()
|
||||||
|
except EOFError:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.call_func(func_name, args, kwargs, conn)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
prints('Proxy function: %s with args: %r and'
|
||||||
|
' kwargs: %r failed')
|
||||||
|
if DEBUG:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
break
|
||||||
|
|
||||||
|
def call_func(self, func_name, args, kwargs, conn):
|
||||||
|
with Writer(conn) as f:
|
||||||
|
try:
|
||||||
|
self.dispatcher(f, func_name, args, kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
if not f.data_written:
|
||||||
|
import traceback
|
||||||
|
# Try to tell the client process what error happened
|
||||||
|
try:
|
||||||
|
conn.send_bytes(_encode(('failed', (unicode(e),
|
||||||
|
as_unicode(traceback.format_exc())))))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ def serialize_builtin_recipes():
|
|||||||
return serialize_collection(recipe_mapping)
|
return serialize_collection(recipe_mapping)
|
||||||
|
|
||||||
def get_builtin_recipe_collection():
|
def get_builtin_recipe_collection():
|
||||||
return etree.parse(P('builtin_recipes.xml')).getroot()
|
return etree.parse(P('builtin_recipes.xml', allow_user_override=False)).getroot()
|
||||||
|
|
||||||
def get_custom_recipe_collection(*args):
|
def get_custom_recipe_collection(*args):
|
||||||
from calibre.web.feeds.recipes import compile_recipe, \
|
from calibre.web.feeds.recipes import compile_recipe, \
|
||||||
@ -179,7 +179,7 @@ def download_builtin_recipe(urn):
|
|||||||
return br.open_novisit('http://status.calibre-ebook.com/recipe/'+urn).read()
|
return br.open_novisit('http://status.calibre-ebook.com/recipe/'+urn).read()
|
||||||
|
|
||||||
def get_builtin_recipe(urn):
|
def get_builtin_recipe(urn):
|
||||||
with zipfile.ZipFile(P('builtin_recipes.zip'), 'r') as zf:
|
with zipfile.ZipFile(P('builtin_recipes.zip', allow_user_override=False), 'r') as zf:
|
||||||
return zf.read(urn+'.recipe')
|
return zf.read(urn+'.recipe')
|
||||||
|
|
||||||
def get_builtin_recipe_by_title(title, log=None, download_recipe=False):
|
def get_builtin_recipe_by_title(title, log=None, download_recipe=False):
|
||||||
|
@ -140,7 +140,8 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
|||||||
self.builtin_recipe_collection = get_builtin_recipe_collection()
|
self.builtin_recipe_collection = get_builtin_recipe_collection()
|
||||||
self.scheduler_config = SchedulerConfig()
|
self.scheduler_config = SchedulerConfig()
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(P('builtin_recipes.zip'), 'r') as zf:
|
with zipfile.ZipFile(P('builtin_recipes.zip',
|
||||||
|
allow_user_override=False), 'r') as zf:
|
||||||
self.favicons = dict([(x.filename, x) for x in zf.infolist() if
|
self.favicons = dict([(x.filename, x) for x in zf.infolist() if
|
||||||
x.filename.endswith('.png')])
|
x.filename.endswith('.png')])
|
||||||
except:
|
except:
|
||||||
@ -181,7 +182,7 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
|
|||||||
|
|
||||||
def do_refresh(self, restrict_to_urns=set([])):
|
def do_refresh(self, restrict_to_urns=set([])):
|
||||||
self.custom_recipe_collection = get_custom_recipe_collection()
|
self.custom_recipe_collection = get_custom_recipe_collection()
|
||||||
zf = P('builtin_recipes.zip')
|
zf = P('builtin_recipes.zip', allow_user_override=False)
|
||||||
|
|
||||||
def factory(cls, parent, *args):
|
def factory(cls, parent, *args):
|
||||||
args = list(args)
|
args = list(args)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user