mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
8520ab2cf3
116
Changelog.yaml
116
Changelog.yaml
@ -19,6 +19,122 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
- version: 0.7.56
|
||||
date: 2011-04-17
|
||||
|
||||
new features:
|
||||
- title: "This is primarily a bug fix release that fixes a bug in 0.7.55 that caused calibre to rescan the files on the device every time the device is connected. If you updated to 0.7.55 it is highly recommended you update to 0.7.56"
|
||||
|
||||
- title: "Device driver for Coby Kyros"
|
||||
|
||||
- title: "Remove the quick access to search options from next to the search bar, as we now have a separate search highlights toggle button"
|
||||
|
||||
- title: "MOBI Output: Ensure that MOBI files always have 8KB worth of null bytes at the end of record 0. This appears to be necessary for Amazon to be able to add DRM to calibre generated MOBI files sent to their publishing service."
|
||||
|
||||
- title: "Add a tool to inspect MOBI files. To use: calibre-debug -m file.mobi"
|
||||
|
||||
bug fixes:
|
||||
- title: "Fixed regression taht caused calibre to rescan files on the device on every reconnect"
|
||||
|
||||
- title: "Fix donate button causing the toolbar to be too large on OS X"
|
||||
|
||||
- title: "MOBI Input: Fix detection of Table of Contents for MOBI files that have a page break between the location designated as the Table of Contents and the actual table of contents."
|
||||
tickets: [763504]
|
||||
|
||||
- title: "Comic Input: Fix handling of some CBZ files that have wrongly encoded non ASCII filenames on windows."
|
||||
tickets: [763280]
|
||||
|
||||
- title: "PML Input: Fix multi-line chapter title causing a spurious page break"
|
||||
tickets: [763238]
|
||||
|
||||
- title: "EPUB Input: Speed up processing of files with very large manifest/spines"
|
||||
|
||||
- title: "Fix regression that broke cover:False searches in 0.7.55"
|
||||
|
||||
improved recipes:
|
||||
- Suedduetsche Zeitung
|
||||
- Irish Times
|
||||
- Big Oven
|
||||
- NSPM
|
||||
|
||||
|
||||
- version: 0.7.55
|
||||
date: 2011-04-15
|
||||
|
||||
new features:
|
||||
- title: "Add a menu bar. Useful if you use a lot of plugins and are running out of space in your toolbars. By default the menu bar is hidden (except on OS X). You can add actions to it via Preferences->Toolbars. As soon as you add actions, it will become visible."
|
||||
|
||||
- title: "OS X: Make the main calibre window look a little more 'native' on OS X"
|
||||
|
||||
- title: "Show recently viewed books in the View button's drop down menu"
|
||||
|
||||
- title: "Add a button next to the search bar to toggle easily between highlight and restrict search modes"
|
||||
|
||||
- title: "Allow the use of arbitrary searches as search restrictions, rather than just saved searches. Do this by using the special entry '*Current Search' in the Search Restriction dropdown."
|
||||
|
||||
- title: "The Connect/share icon now changes color to indicate that the content server is running"
|
||||
tickets: [755444]
|
||||
|
||||
- title: "Device drivers for Viewpad 7, Motorola Xoom and Asus Eee Note"
|
||||
|
||||
- title: "Add tags like composite custom column."
|
||||
tickets: [759663]
|
||||
|
||||
- title: "Add a new date format code 'iso'. Permits formatting dates to see the complete time (via Preferences->Tweaks)"
|
||||
|
||||
- title: "Allow the use of data from the size column in the template language"
|
||||
tickets: [759645]
|
||||
|
||||
- title: "Support reading/writing covers to txtz/htmlz files"
|
||||
|
||||
- title: "Speedup for large library sorting when using composite custom columns"
|
||||
|
||||
- title: "Move the boolean columns are tristate tweak to Preferences->Behavior"
|
||||
|
||||
bug fixes:
|
||||
- title: "Fix a regression in 0.7.54 that broke reading covers/metadata from cbz files."
|
||||
tickets: [756892]
|
||||
|
||||
- title: "Fix tweak names and help not translatable"
|
||||
tickets: [756736]
|
||||
|
||||
- title: "When the size of a book is less that 0.1MB but not zero, display the size as <0.1 instead of 0.0."
|
||||
tickets: [755768]
|
||||
|
||||
- title: "HTMLZ input: Fix handling of HTML files encoded in an encoding other than UTF-8"
|
||||
|
||||
- title: "EPUB Input: Fix EPUB files with empty Adobe PAGE templates causing conversion to abort."
|
||||
tickets: [760390]
|
||||
|
||||
- title: "Fix CHM input plugin not closing opened input file"
|
||||
tickets: [760589]
|
||||
|
||||
- title: "MOBI Output: Make super/subscripts use a slightly smaller font when rendered on a Kindle. Also allow the use of vertical-align:top/bottom in the CSS to specify a super/subscript."
|
||||
tickets: [758667]
|
||||
|
||||
- title: "LRF Input: Detect and workaround LRF files that have deeply nested spans, instead of crashing."
|
||||
tickets: [759680]
|
||||
|
||||
- title: "MOBI Output: Fix bug that would cause conversion to unneccessarily abort when malformed hyperlinks are present in the input document."
|
||||
tickets: [759313]
|
||||
|
||||
- title: "Make true and false searches work correctly for numeric fields."
|
||||
|
||||
- title: "MOBI Output: The Ignore margins setting no longer ignores blockquotes, only margins set via CSS on other elements."
|
||||
tickets: [758675]
|
||||
|
||||
- title: "Fix regression that caused clicking auto send to also change the email address in Preferences->Email"
|
||||
|
||||
improved recipes:
|
||||
- Wall Street Journal
|
||||
- Weblogs SL
|
||||
- Tabu.ro
|
||||
- Vecernje Novosti
|
||||
|
||||
new recipes:
|
||||
- title: Hallo Assen and Dvhn
|
||||
author: Reijendert
|
||||
|
||||
|
||||
- version: 0.7.54
|
||||
date: 2011-04-08
|
||||
|
@ -23,7 +23,7 @@ class BigOven(BasicNewsRecipe):
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
if self.username is not None and self.password is not None:
|
||||
@ -36,29 +36,38 @@ class BigOven(BasicNewsRecipe):
|
||||
|
||||
remove_attributes = ['style', 'font']
|
||||
|
||||
remove_tags = [dict(name='div', attrs={'class':['ppy-caption']})
|
||||
,dict(name='div', attrs={'id':['float_corner']})
|
||||
]
|
||||
def get_article_url(self, article):
|
||||
url = article.get('feedburner_origlink',article.get('link', None))
|
||||
front, middle, end = url.partition('comhttp//www.bigoven.com')
|
||||
url = front + 'com' + end
|
||||
return url
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':['nosidebar_main']})]
|
||||
|
||||
remove_tags_after = [dict(name='div', attrs={'class':['display-field']})]
|
||||
|
||||
remove_tags = [dict(name='ul', attrs={'class':['tabs']})]
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'Want detailed nutrition information?', re.DOTALL), lambda match: ''),
|
||||
(re.compile('\(You could win \$100 in our ', re.DOTALL), lambda match: ''),
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for tag in soup.findAll(name='a', attrs={'class':['deflink']}):
|
||||
tag.replaceWith(tag.string)
|
||||
for tag in soup.findAll(name='a', text=re.compile(r'.*View Metric.*', re.DOTALL)):
|
||||
tag.parent.parent.extract()
|
||||
for tag in soup.findAll(name='a', text=re.compile(r'.*Add my own photo.*', re.DOTALL)):
|
||||
tag.parent.parent.extract()
|
||||
for tag in soup.findAll(name='div', attrs={'class':['container']}):
|
||||
if tag.find(name='h1'):
|
||||
continue
|
||||
if tag.find(name='h2', text=re.compile(r'.*Ingredients.*', re.DOTALL)):
|
||||
print 'tag found Ingred h2'
|
||||
continue
|
||||
if tag.find(name='h2', text=re.compile(r'Preparation.*', re.DOTALL)):
|
||||
print 'tag found Prep h2'
|
||||
continue
|
||||
tag.extract()
|
||||
tag.parent.parent.extract()
|
||||
for tag in soup.findAll(text=re.compile(r'.*Try BigOven Pro for Free.*', re.DOTALL)):
|
||||
tag.extract()
|
||||
for tag in soup.findAll(text=re.compile(r'.*Add my photo of this recipe.*', re.DOTALL)):
|
||||
tag.parent.extract()
|
||||
for tag in soup.findAll(name='a', text=re.compile(r'.*photo contest.*', re.DOTALL)):
|
||||
tag.parent.extract()
|
||||
for tag in soup.findAll(name='a', text='Remove ads'):
|
||||
tag.parent.parent.extract()
|
||||
for tag in soup.findAll(name='ol', attrs={'class':['recipe-tags']}):
|
||||
tag.parent.extract()
|
||||
return soup
|
||||
|
||||
feeds = [(u'4 & 5 Star Rated Recipes', u'http://feeds.feedburner.com/Bigovencom-RecipeRaves?format=xml')]
|
||||
|
||||
feeds = [(u'Recent Raves', u'http://www.bigoven.com/rss/recentraves'),
|
||||
(u'Recipe Of The Day', u'http://feeds.feedburner.com/bigovencom-RecipeOfTheDay')]
|
||||
|
||||
|
@ -3,8 +3,7 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__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 '''
|
||||
import re
|
||||
import string
|
||||
@ -14,8 +13,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
class BrandEins(BasicNewsRecipe):
|
||||
|
||||
title = u'brand eins'
|
||||
__author__ = 'Constantin Hofstetter'
|
||||
description = u'Wirtschaftsmagazin'
|
||||
__author__ = 'Constantin Hofstetter; Steffen Siebert'
|
||||
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'
|
||||
category = 'politics, business, wirtschaft, Germany'
|
||||
use_embedded_content = False
|
||||
|
36
recipes/hallo_assen.recipe
Normal file
36
recipes/hallo_assen.recipe
Normal file
@ -0,0 +1,36 @@
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1302341394(BasicNewsRecipe):
|
||||
title = u'Hallo Assen'
|
||||
oldest_article = 180
|
||||
max_articles_per_feed = 100
|
||||
|
||||
__author__ = 'Reijndert'
|
||||
no_stylesheets = True
|
||||
cover_url = 'http://www.halloassen.nl/multimedia/halloassen/archive/00002/HalloAssen_2518a.gif'
|
||||
language = 'nl'
|
||||
country = 'NL'
|
||||
version = 1
|
||||
category = u'Nieuws'
|
||||
timefmt = ' %Y-%m-%d (%a)'
|
||||
|
||||
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'photoFrame'})
|
||||
,dict(name='div', attrs={'class':'textContent'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div',attrs={'id':'articleLinks'})
|
||||
,dict(name='div',attrs={'class':'categories clearfix'})
|
||||
,dict(name='div',attrs={'id':'rating'})
|
||||
,dict(name='div',attrs={'id':'comments'})
|
||||
]
|
||||
|
||||
feeds = [(u'Ons Nieuws', u'http://feeds.feedburner.com/halloassen/onsnieuws'), (u'Politie', u'http://www.halloassen.nl/rss/?c=37'), (u'Rechtbank', u'http://www.halloassen.nl/rss/?c=39'), (u'Justitie', u'http://www.halloassen.nl/rss/?c=36'), (u'Evenementen', u'http://www.halloassen.nl/rss/?c=34'), (u'Cultuur', u'http://www.halloassen.nl/rss/?c=32'), (u'Politiek', u'http://www.halloassen.nl/rss/?c=38'), (u'Economie', u'http://www.halloassen.nl/rss/?c=33')]
|
||||
|
||||
|
||||
extra_css = '''
|
||||
body {font-family: verdana, arial, helvetica, geneva, sans-serif;}
|
||||
'''
|
||||
|
@ -18,7 +18,6 @@ class IrishTimes(BasicNewsRecipe):
|
||||
oldest_article = 1.0
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
simultaneous_downloads= 5
|
||||
|
||||
r = re.compile('.*(?P<url>http:\/\/(www.irishtimes.com)|(rss.feedsportal.com\/c)\/.*\.html?).*')
|
||||
remove_tags = [dict(name='div', attrs={'class':'footer'})]
|
||||
@ -26,17 +25,17 @@ class IrishTimes(BasicNewsRecipe):
|
||||
|
||||
feeds = [
|
||||
('Frontpage', 'http://www.irishtimes.com/feeds/rss/newspaper/index.rss'),
|
||||
('Ireland', 'http://www.irishtimes.com/feeds/rss/newspaper/ireland.rss'),
|
||||
('World', 'http://www.irishtimes.com/feeds/rss/newspaper/world.rss'),
|
||||
('Finance', 'http://www.irishtimes.com/feeds/rss/newspaper/finance.rss'),
|
||||
('Features', 'http://www.irishtimes.com/feeds/rss/newspaper/features.rss'),
|
||||
('Sport', 'http://www.irishtimes.com/feeds/rss/newspaper/sport.rss'),
|
||||
('Opinion', 'http://www.irishtimes.com/feeds/rss/newspaper/opinion.rss'),
|
||||
('Letters', 'http://www.irishtimes.com/feeds/rss/newspaper/letters.rss'),
|
||||
('Ireland', 'http://rss.feedsportal.com/c/851/f/10845/index.rss'),
|
||||
('World', 'http://rss.feedsportal.com/c/851/f/10846/index.rss'),
|
||||
('Finance', 'http://rss.feedsportal.com/c/851/f/10847/index.rss'),
|
||||
('Features', 'http://rss.feedsportal.com/c/851/f/10848/index.rss'),
|
||||
('Sport', 'http://rss.feedsportal.com/c/851/f/10849/index.rss'),
|
||||
('Opinion', 'http://rss.feedsportal.com/c/851/f/10850/index.rss'),
|
||||
('Letters', 'http://rss.feedsportal.com/c/851/f/10851/index.rss'),
|
||||
('Magazine', 'http://www.irishtimes.com/feeds/rss/newspaper/magazine.rss'),
|
||||
('Health', 'http://www.irishtimes.com/feeds/rss/newspaper/health.rss'),
|
||||
('Education & Parenting', 'http://www.irishtimes.com/feeds/rss/newspaper/education.rss'),
|
||||
('Motors', 'http://www.irishtimes.com/feeds/rss/newspaper/motors.rss'),
|
||||
('Health', 'http://rss.feedsportal.com/c/851/f/10852/index.rss'),
|
||||
('Education & Parenting', 'http://rss.feedsportal.com/c/851/f/10853/index.rss'),
|
||||
('Motors', 'http://rss.feedsportal.com/c/851/f/10854/index.rss'),
|
||||
('An Teanga Bheo', 'http://www.irishtimes.com/feeds/rss/newspaper/anteangabheo.rss'),
|
||||
('Commercial Property', 'http://www.irishtimes.com/feeds/rss/newspaper/commercialproperty.rss'),
|
||||
('Science Today', 'http://www.irishtimes.com/feeds/rss/newspaper/sciencetoday.rss'),
|
||||
@ -57,5 +56,3 @@ class IrishTimes(BasicNewsRecipe):
|
||||
|
||||
def get_article_url(self, article):
|
||||
return article.link
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
novosti.rs
|
||||
'''
|
||||
@ -21,34 +21,71 @@ class Novosti(BasicNewsRecipe):
|
||||
encoding = 'utf-8'
|
||||
language = 'sr'
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://www.novosti.rs/images/basic/logo-print.png'
|
||||
extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
|
||||
.author{font-size: small}
|
||||
.articleLead{font-size: large; font-weight: bold}
|
||||
img{display: block; margin-bottom: 1em; margin-top: 1em}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'pretty_print' : True
|
||||
}
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
|
||||
keep_only_tags = [dict(attrs={'class':['articleTitle','author','articleLead','articleBody']})]
|
||||
remove_tags = [dict(name=['embed','object','iframe','base','link','meta'])]
|
||||
feeds = [(u'Vesti', u'http://www.novosti.rs/rss/rss-vesti')]
|
||||
keep_only_tags = [dict(attrs={'class':['articleTitle','articleInfo','articleLead','singlePhoto fl','articleBody']})]
|
||||
remove_tags = [
|
||||
dict(name=['embed','object','iframe','base','link','meta'])
|
||||
,dict(name='a', attrs={'class':'loadComments topCommentsLink'})
|
||||
]
|
||||
remove_attributes = ['lang','xmlns:fb']
|
||||
|
||||
feeds = [
|
||||
(u'Politika' , u'http://www.novosti.rs/rss/2-Sve%20vesti')
|
||||
,(u'Drustvo' , u'http://www.novosti.rs/rss/1-Sve%20vesti')
|
||||
,(u'Ekonomija' , u'http://www.novosti.rs/rss/3-Sve%20vesti')
|
||||
,(u'Hronika' , u'http://www.novosti.rs/rss/4-Sve%20vesti')
|
||||
,(u'Dosije' , u'http://www.novosti.rs/rss/5-Sve%20vesti')
|
||||
,(u'Reportaze' , u'http://www.novosti.rs/rss/6-Sve%20vesti')
|
||||
,(u'Tehnologije' , u'http://www.novosti.rs/rss/35-Sve%20vesti')
|
||||
,(u'Zanimljivosti', u'http://www.novosti.rs/rss/26-Sve%20vesti')
|
||||
,(u'Auto' , u'http://www.novosti.rs/rss/50-Sve%20vesti')
|
||||
,(u'Sport' , u'http://www.novosti.rs/rss/11|47|12|14|13-Sve%20vesti')
|
||||
,(u'Svet' , u'http://www.novosti.rs/rss/7-Sve%20vesti')
|
||||
,(u'Region' , u'http://www.novosti.rs/rss/8-Sve%20vesti')
|
||||
,(u'Dijaspora' , u'http://www.novosti.rs/rss/9-Sve%20vesti')
|
||||
,(u'Spektakl' , u'http://www.novosti.rs/rss/10-Sve%20vesti')
|
||||
,(u'Kultura' , u'http://www.novosti.rs/rss/31-Sve%20vesti')
|
||||
,(u'Srbija' , u'http://www.novosti.rs/rss/15-Sve%20vesti')
|
||||
,(u'Beograd' , u'http://www.novosti.rs/rss/16-Sve%20vesti')
|
||||
,(u'Zivot+' , u'http://www.novosti.rs/rss/24|33|34|25|20|18|32|19-Sve%20vesti')
|
||||
,(u'Turizam' , u'http://www.novosti.rs/rss/36-Sve%20vesti')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('span', attrs={'class':'author'}):
|
||||
item.name='p'
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
nspm.rs
|
||||
'''
|
||||
|
||||
import re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import NavigableString
|
||||
from calibre.ebooks.BeautifulSoup import NavigableString, Tag
|
||||
|
||||
class Nspm(BasicNewsRecipe):
|
||||
title = 'Nova srpska politicka misao'
|
||||
@ -21,7 +21,6 @@ class Nspm(BasicNewsRecipe):
|
||||
INDEX = 'http://www.nspm.rs/?alphabet=l'
|
||||
encoding = 'utf-8'
|
||||
language = 'sr'
|
||||
delay = 2
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'magazine'
|
||||
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
|
||||
@ -29,27 +28,21 @@ class Nspm(BasicNewsRecipe):
|
||||
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||
body{font-family: "Times New Roman", serif1, serif}
|
||||
.article_description{font-family: Arial, sans1, sans-serif}
|
||||
img{margin-top:0.5em; margin-bottom: 0.7em}
|
||||
img{margin-top:0.5em; margin-bottom: 0.7em; display: block}
|
||||
.author{color: #990000; font-weight: bold}
|
||||
.author,.createdate{font-size: 0.9em} """
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'linearize_tables' : True
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'pretty_print' : True
|
||||
}
|
||||
|
||||
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
|
||||
remove_tags = [
|
||||
dict(name=['link','object','embed','script','meta','base','iframe'])
|
||||
,dict(attrs={'class':'buttonheading'})
|
||||
]
|
||||
remove_tags_before = dict(attrs={'class':'contentheading'})
|
||||
remove_tags_after = dict(attrs={'class':'article_separator'})
|
||||
remove_attributes = ['width','height']
|
||||
remove_tags = [dict(name=['link','script','meta','base','img'])]
|
||||
remove_attributes = ['width','height','lang','xmlns:fb','xmlns:og','vspace','hspace','type','start','size']
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
@ -57,21 +50,67 @@ class Nspm(BasicNewsRecipe):
|
||||
return br
|
||||
|
||||
feeds = [
|
||||
(u'Rubrike' , u'http://www.nspm.rs/rubrike/feed/rss.html')
|
||||
,(u'Debate' , u'http://www.nspm.rs/debate/feed/rss.html')
|
||||
,(u'Reci i misli' , u'http://www.nspm.rs/reci-i-misli/feed/rss.html')
|
||||
(u'Rubrike' , u'http://www.nspm.rs/rubrike/feed/rss.html' )
|
||||
,(u'Debate' , u'http://www.nspm.rs/debate/feed/rss.html' )
|
||||
,(u'Reci i misli' , u'http://www.nspm.rs/reci-i-misli/feed/rss.html' )
|
||||
,(u'Samo smeh srbina spasava', u'http://www.nspm.rs/samo-smeh-srbina-spasava/feed/rss.html')
|
||||
,(u'Polemike' , u'http://www.nspm.rs/polemike/feed/rss.html')
|
||||
,(u'Prikazi' , u'http://www.nspm.rs/prikazi/feed/rss.html')
|
||||
,(u'Prenosimo' , u'http://www.nspm.rs/prenosimo/feed/rss.html')
|
||||
,(u'Hronika' , u'http://www.nspm.rs/tabela/hronika/feed/rss.html')
|
||||
,(u'Polemike' , u'http://www.nspm.rs/polemike/feed/rss.html' )
|
||||
,(u'Prikazi' , u'http://www.nspm.rs/prikazi/feed/rss.html' )
|
||||
,(u'Prenosimo' , u'http://www.nspm.rs/prenosimo/feed/rss.html' )
|
||||
,(u'Hronika' , u'http://www.nspm.rs/tabela/hronika/feed/rss.html' )
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.body.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.body.findAll('h1'):
|
||||
nh = NavigableString(item.a.string)
|
||||
item.a.extract()
|
||||
item.insert(0,nh)
|
||||
return self.adeify_images(soup)
|
||||
atitle = soup.body.find('a',attrs={'class':'contentpagetitle'})
|
||||
if atitle:
|
||||
cleanTitle = Tag(soup,'h1',[('class','contentpagetitle')])
|
||||
cnt = NavigableString(self.tag_to_string(atitle))
|
||||
cleanTitle.append(cnt)
|
||||
|
||||
author = soup.body.find('span',attrs={'class':'author'})
|
||||
if author:
|
||||
author.extract()
|
||||
author.name = 'div'
|
||||
|
||||
crdate = soup.body.find('td',attrs={'class':'createdate'})
|
||||
if crdate:
|
||||
cleanCrdate = Tag(soup,'div',[('class','createdate')])
|
||||
cnt = NavigableString(self.tag_to_string(crdate))
|
||||
cleanCrdate.append(cnt)
|
||||
|
||||
#get the dependant element
|
||||
artText = Tag(soup,'div',[('class','text')])
|
||||
textHolderp = crdate.parent
|
||||
textHolder = textHolderp.nextSibling
|
||||
while textHolder and (not isinstance(textHolder,Tag) or (textHolder.name <> textHolderp.name)):
|
||||
textHolder = textHolder.nextSibling
|
||||
if textHolder.td:
|
||||
artText = textHolder.td
|
||||
artText.name = 'div'
|
||||
artText.attrs = []
|
||||
artText['class'] = 'text'
|
||||
artText.extract()
|
||||
|
||||
soup.body.contents=[]
|
||||
|
||||
soup.body.append(cleanTitle)
|
||||
soup.body.append(author)
|
||||
soup.body.append(cleanCrdate)
|
||||
soup.body.append(artText)
|
||||
|
||||
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
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
@ -19,11 +18,11 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
encoding = 'cp1252'
|
||||
needs_subscription = True
|
||||
remove_empty_feeds = True
|
||||
delay = 2
|
||||
delay = 1
|
||||
PREFIX = 'http://www.sueddeutsche.de'
|
||||
INDEX = PREFIX + '/app/epaper/textversion/'
|
||||
use_embedded_content = False
|
||||
masthead_url = 'http://pix.sueddeutsche.de/img/layout/header/logo.gif'
|
||||
masthead_url = 'http://pix.sueddeutsche.de/img/layout/header/SZ_solo288x31.gif'
|
||||
language = 'de'
|
||||
publication_type = 'newspaper'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} '
|
||||
@ -36,7 +35,7 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
, 'linearize_tables' : True
|
||||
}
|
||||
|
||||
remove_attributes = ['height','width']
|
||||
remove_attributes = ['height','width','style']
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
@ -50,24 +49,37 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
|
||||
remove_tags =[
|
||||
dict(attrs={'class':'hidePrint'})
|
||||
,dict(name=['link','object','embed','base','iframe'])
|
||||
,dict(name=['link','object','embed','base','iframe','br'])
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':'artikelBox'})]
|
||||
remove_tags_before = dict(attrs={'class':'artikelTitel'})
|
||||
remove_tags_after = dict(attrs={'class':'author'})
|
||||
|
||||
feeds = [
|
||||
(u'Politik' , INDEX + 'Politik/' )
|
||||
,(u'Seite drei' , INDEX + 'Seite+drei/' )
|
||||
,(u'Meinungsseite', INDEX + 'Meinungsseite/')
|
||||
,(u'Wissen' , INDEX + 'Wissen/' )
|
||||
,(u'Panorama' , INDEX + 'Panorama/' )
|
||||
,(u'Feuilleton' , INDEX + 'Feuilleton/' )
|
||||
,(u'Medien' , INDEX + 'Medien/' )
|
||||
,(u'Wirtschaft' , INDEX + 'Wirtschaft/' )
|
||||
,(u'Sport' , INDEX + 'Sport/' )
|
||||
,(u'Bayern' , INDEX + 'Bayern/' )
|
||||
,(u'Muenchen' , INDEX + 'M%FCnchen/' )
|
||||
(u'Politik' , INDEX + 'Politik/' )
|
||||
,(u'Seite drei' , INDEX + 'Seite+drei/' )
|
||||
,(u'Meinungsseite' , INDEX + 'Meinungsseite/')
|
||||
,(u'Wissen' , INDEX + 'Wissen/' )
|
||||
,(u'Panorama' , INDEX + 'Panorama/' )
|
||||
,(u'Feuilleton' , INDEX + 'Feuilleton/' )
|
||||
,(u'Medien' , INDEX + 'Medien/' )
|
||||
,(u'Wirtschaft' , INDEX + 'Wirtschaft/' )
|
||||
,(u'Sport' , INDEX + 'Sport/' )
|
||||
,(u'Bayern' , INDEX + 'Bayern/' )
|
||||
,(u'Muenchen' , INDEX + 'M%FCnchen/' )
|
||||
,(u'Muenchen City' , INDEX + 'M%FCnchen+City/' )
|
||||
,(u'Jetzt.de' , INDEX + 'Jetzt.de/' )
|
||||
,(u'Reise' , INDEX + 'Reise/' )
|
||||
,(u'SZ Extra' , INDEX + 'SZ+Extra/' )
|
||||
,(u'Wochenende' , INDEX + 'SZ+am+Wochenende/' )
|
||||
,(u'Stellen-Markt' , INDEX + 'Stellen-Markt/')
|
||||
,(u'Motormarkt' , INDEX + 'Motormarkt/')
|
||||
,(u'Immobilien-Markt', INDEX + 'Immobilien-Markt/')
|
||||
,(u'Thema' , INDEX + 'Thema/' )
|
||||
,(u'Forum' , INDEX + 'Forum/' )
|
||||
,(u'Leute' , INDEX + 'Leute/' )
|
||||
,(u'Jugend' , INDEX + 'Jugend/' )
|
||||
,(u'Beilage' , INDEX + 'Beilage/' )
|
||||
]
|
||||
|
||||
def parse_index(self):
|
||||
|
@ -24,30 +24,29 @@ class TabuRo(BasicNewsRecipe):
|
||||
cover_url = 'http://www.tabu.ro/img/tabu-logo2.png'
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'id':'Article'}),
|
||||
]
|
||||
dict(name='h2', attrs={'class':'articol_titlu'}),
|
||||
dict(name='div', attrs={'class':'poza_articol_featured'}),
|
||||
dict(name='div', attrs={'class':'articol_text'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['advertisementArticle']}),
|
||||
dict(name='div', attrs={'class':'voting_number'}),
|
||||
dict(name='div', attrs={'id':'number_votes'}),
|
||||
dict(name='div', attrs={'id':'rating_one'}),
|
||||
dict(name='div', attrs={'class':'float: right;'})
|
||||
dict(name='div', attrs={'class':'asemanatoare'})
|
||||
]
|
||||
|
||||
remove_tags_after = [
|
||||
dict(name='div', attrs={'id':'comments'}),
|
||||
]
|
||||
dict(name='div', attrs={'class':'asemanatoare'})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'Feeds', u'http://www.tabu.ro/rss_all.xml')
|
||||
(u'Feeds', u'http://www.tabu.ro/feed/')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
|
@ -3,7 +3,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '4 February 2011, desUBIKado'
|
||||
__author__ = 'desUBIKado'
|
||||
__version__ = 'v0.05'
|
||||
__date__ = '9, February 2011'
|
||||
__date__ = '13, April 2011'
|
||||
'''
|
||||
http://www.weblogssl.com/
|
||||
'''
|
||||
@ -19,7 +19,7 @@ class weblogssl(BasicNewsRecipe):
|
||||
category = 'Gadgets, Tech news, Product reviews, mobiles, science, cinema, entertainment, culture, tv, food, recipes, life style, motor, F1, sports, economy'
|
||||
language = 'es'
|
||||
timefmt = '[%a, %d %b, %Y]'
|
||||
oldest_article = 1.5
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 100
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
@ -28,50 +28,52 @@ class weblogssl(BasicNewsRecipe):
|
||||
no_stylesheets = True
|
||||
|
||||
# Si no se quiere recuperar todos los blogs se puede suprimir la descarga del que se desee poniendo
|
||||
# un caracter # por delante, es decir, # (u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
|
||||
# haría que no se descargase Applesfera. OJO: El último feed no debe llevar la coma al final
|
||||
# un caracter # por delante, es decir, # ,(u'Applesfera', u'http://feeds.weblogssl.com/applesfera')
|
||||
# haría que no se descargase Applesfera.
|
||||
|
||||
feeds = [
|
||||
(u'Xataka', u'http://feeds.weblogssl.com/xataka2'),
|
||||
(u'Xataka M\xf3vil', u'http://feeds.weblogssl.com/xatakamovil'),
|
||||
(u'Xataka Android', u'http://feeds.weblogssl.com/xatakandroid'),
|
||||
(u'Xataka Foto', u'http://feeds.weblogssl.com/xatakafoto'),
|
||||
(u'Xataka ON', u'http://feeds.weblogssl.com/xatakaon'),
|
||||
(u'Xataka Ciencia', u'http://feeds.weblogssl.com/xatakaciencia'),
|
||||
(u'Genbeta', u'http://feeds.weblogssl.com/genbeta'),
|
||||
(u'Applesfera', u'http://feeds.weblogssl.com/applesfera'),
|
||||
(u'Vida Extra', u'http://feeds.weblogssl.com/vidaextra'),
|
||||
(u'Naci\xf3n Red', u'http://feeds.weblogssl.com/nacionred'),
|
||||
(u'Blog de Cine', u'http://feeds.weblogssl.com/blogdecine'),
|
||||
(u'Vaya tele', u'http://feeds.weblogssl.com/vayatele2'),
|
||||
(u'Hipers\xf3nica', u'http://feeds.weblogssl.com/hipersonica'),
|
||||
(u'Diario del viajero', u'http://feeds.weblogssl.com/diariodelviajero'),
|
||||
(u'Papel en blanco', u'http://feeds.weblogssl.com/papelenblanco'),
|
||||
(u'Pop rosa', u'http://feeds.weblogssl.com/poprosa'),
|
||||
(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom'),
|
||||
(u'Fandemia', u'http://feeds.weblogssl.com/fandemia'),
|
||||
(u'Noctamina', u'http://feeds.weblogssl.com/noctamina'),
|
||||
(u'Tendencias', u'http://feeds.weblogssl.com/trendencias'),
|
||||
(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas'),
|
||||
(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar'),
|
||||
(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion'),
|
||||
(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera'),
|
||||
(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia'),
|
||||
(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica'),
|
||||
(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg'),
|
||||
(u'Arrebatadora', u'http://feeds.weblogssl.com/arrebatadora'),
|
||||
(u'Mensencia', u'http://feeds.weblogssl.com/mensencia'),
|
||||
(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas'),
|
||||
(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion'),
|
||||
(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1'),
|
||||
(u'Motorpasi\xf3n Moto', u'http://feeds.weblogssl.com/motorpasionmoto'),
|
||||
(u'Notas de futbol', u'http://feeds.weblogssl.com/notasdefutbol'),
|
||||
(u'Fuera de l\xedmites', u'http://feeds.weblogssl.com/fueradelimites'),
|
||||
(u'Salir a ganar', u'http://feeds.weblogssl.com/saliraganar'),
|
||||
(u'El blog salm\xf3n', u'http://feeds.weblogssl.com/elblogsalmon2'),
|
||||
(u'Pymes y aut\xf3nomos', u'http://feeds.weblogssl.com/pymesyautonomos'),
|
||||
(u'Tecnolog\xeda Pyme', u'http://feeds.weblogssl.com/tecnologiapyme'),
|
||||
(u'Ahorro diario', u'http://feeds.weblogssl.com/ahorrodiario')
|
||||
(u'Xataka', u'http://feeds.weblogssl.com/xataka2')
|
||||
,(u'Xataka M\xf3vil', u'http://feeds.weblogssl.com/xatakamovil')
|
||||
,(u'Xataka Android', u'http://feeds.weblogssl.com/xatakandroid')
|
||||
,(u'Xataka Foto', u'http://feeds.weblogssl.com/xatakafoto')
|
||||
,(u'Xataka ON', u'http://feeds.weblogssl.com/xatakaon')
|
||||
,(u'Xataka Ciencia', u'http://feeds.weblogssl.com/xatakaciencia')
|
||||
,(u'Genbeta', u'http://feeds.weblogssl.com/genbeta')
|
||||
,(u'Genbeta Dev', u'http://feeds.weblogssl.com/genbetadev')
|
||||
,(u'Applesfera', u'http://feeds.weblogssl.com/applesfera')
|
||||
,(u'Vida Extra', u'http://feeds.weblogssl.com/vidaextra')
|
||||
,(u'Naci\xf3n Red', u'http://feeds.weblogssl.com/nacionred')
|
||||
,(u'Blog de Cine', u'http://feeds.weblogssl.com/blogdecine')
|
||||
,(u'Vaya tele', u'http://feeds.weblogssl.com/vayatele2')
|
||||
,(u'Hipers\xf3nica', u'http://feeds.weblogssl.com/hipersonica')
|
||||
,(u'Diario del viajero', u'http://feeds.weblogssl.com/diariodelviajero')
|
||||
,(u'Papel en blanco', u'http://feeds.weblogssl.com/papelenblanco')
|
||||
,(u'Pop rosa', u'http://feeds.weblogssl.com/poprosa')
|
||||
,(u'Zona FandoM', u'http://feeds.weblogssl.com/zonafandom')
|
||||
,(u'Fandemia', u'http://feeds.weblogssl.com/fandemia')
|
||||
,(u'Noctamina', u'http://feeds.weblogssl.com/noctamina')
|
||||
,(u'Tendencias', u'http://feeds.weblogssl.com/trendencias')
|
||||
,(u'Beb\xe9s y m\xe1s', u'http://feeds.weblogssl.com/bebesymas')
|
||||
,(u'Directo al paladar', u'http://feeds.weblogssl.com/directoalpaladar')
|
||||
,(u'Compradicci\xf3n', u'http://feeds.weblogssl.com/compradiccion')
|
||||
,(u'Decoesfera', u'http://feeds.weblogssl.com/decoesfera')
|
||||
,(u'Embelezzia', u'http://feeds.weblogssl.com/embelezzia')
|
||||
,(u'Vit\xf3nica', u'http://feeds.weblogssl.com/vitonica')
|
||||
,(u'Ambiente G', u'http://feeds.weblogssl.com/ambienteg')
|
||||
,(u'Arrebatadora', u'http://feeds.weblogssl.com/arrebatadora')
|
||||
,(u'Mensencia', u'http://feeds.weblogssl.com/mensencia')
|
||||
,(u'Peques y m\xe1s', u'http://feeds.weblogssl.com/pequesymas')
|
||||
,(u'Motorpasi\xf3n', u'http://feeds.weblogssl.com/motorpasion')
|
||||
,(u'Motorpasi\xf3n F1', u'http://feeds.weblogssl.com/motorpasionf1')
|
||||
,(u'Motorpasi\xf3n Moto', u'http://feeds.weblogssl.com/motorpasionmoto')
|
||||
,(u'Motorpasi\xf3n Futuro', u'http://feeds.weblogssl.com/motorpasionfuturo')
|
||||
,(u'Notas de futbol', u'http://feeds.weblogssl.com/notasdefutbol')
|
||||
,(u'Fuera de l\xedmites', u'http://feeds.weblogssl.com/fueradelimites')
|
||||
,(u'Salir a ganar', u'http://feeds.weblogssl.com/saliraganar')
|
||||
,(u'El blog salm\xf3n', u'http://feeds.weblogssl.com/elblogsalmon2')
|
||||
,(u'Pymes y aut\xf3nomos', u'http://feeds.weblogssl.com/pymesyautonomos')
|
||||
,(u'Tecnolog\xeda Pyme', u'http://feeds.weblogssl.com/tecnologiapyme')
|
||||
,(u'Ahorro diario', u'http://feeds.weblogssl.com/ahorrodiario')
|
||||
]
|
||||
|
||||
|
||||
@ -102,3 +104,4 @@ class weblogssl(BasicNewsRecipe):
|
||||
video_yt['src'] = fuente3 + '/0.jpg'
|
||||
|
||||
return soup
|
||||
|
||||
|
@ -81,6 +81,11 @@ class WallStreetJournal(BasicNewsRecipe):
|
||||
feeds.append((title, articles))
|
||||
return feeds
|
||||
|
||||
def abs_wsj_url(self, href):
|
||||
if not href.startswith('http'):
|
||||
href = 'http://online.wsj.com' + href
|
||||
return href
|
||||
|
||||
def parse_index(self):
|
||||
soup = self.wsj_get_index()
|
||||
|
||||
@ -99,14 +104,14 @@ class WallStreetJournal(BasicNewsRecipe):
|
||||
pageone = a['href'].endswith('pageone')
|
||||
if pageone:
|
||||
title = 'Front Section'
|
||||
url = 'http://online.wsj.com' + a['href']
|
||||
url = self.abs_wsj_url(a['href'])
|
||||
feeds = self.wsj_add_feed(feeds,title,url)
|
||||
title = "What's News"
|
||||
url = url.replace('pageone','whatsnews')
|
||||
feeds = self.wsj_add_feed(feeds,title,url)
|
||||
else:
|
||||
title = self.tag_to_string(a)
|
||||
url = 'http://online.wsj.com' + a['href']
|
||||
url = self.abs_wsj_url(a['href'])
|
||||
feeds = self.wsj_add_feed(feeds,title,url)
|
||||
return feeds
|
||||
|
||||
@ -163,7 +168,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
||||
title = self.tag_to_string(a).strip() + ' [%s]'%meta
|
||||
else:
|
||||
title = self.tag_to_string(a).strip()
|
||||
url = 'http://online.wsj.com'+a['href']
|
||||
url = self.abs_wsj_url(a['href'])
|
||||
desc = ''
|
||||
for p in container.findAll('p'):
|
||||
desc = self.tag_to_string(p)
|
||||
|
BIN
resources/images/highlight_only_off.png
Normal file
BIN
resources/images/highlight_only_off.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 810 B |
BIN
resources/images/highlight_only_on.png
Normal file
BIN
resources/images/highlight_only_on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 396 B |
BIN
resources/images/store.png
Normal file
BIN
resources/images/store.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -170,8 +170,8 @@ from setup import __appname__, __version__ as version
|
||||
# there.
|
||||
pot_header = '''\
|
||||
# Translation template file..
|
||||
# Copyright (C) 2007 Kovid Goyal
|
||||
# Kovid Goyal <kovid@kovidgoyal.net>, 2007.
|
||||
# Copyright (C) %(year)s Kovid Goyal
|
||||
# Kovid Goyal <kovid@kovidgoyal.net>, %(year)s.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
@ -185,7 +185,7 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\\n"
|
||||
"Generated-By: pygettext.py %%(version)s\\n"
|
||||
|
||||
'''%dict(appname=__appname__, version=version)
|
||||
'''%dict(appname=__appname__, version=version, year=time.strftime('%Y'))
|
||||
|
||||
|
||||
def usage(code, msg=''):
|
||||
|
@ -26,6 +26,38 @@ class POT(Command):
|
||||
ans.append(os.path.abspath(os.path.join(root, name)))
|
||||
return ans
|
||||
|
||||
def get_tweaks_docs(self):
|
||||
path = self.a(self.j(self.SRC, '..', 'resources', 'default_tweaks.py'))
|
||||
with open(path, 'rb') as f:
|
||||
raw = f.read().decode('utf-8')
|
||||
msgs = []
|
||||
lines = list(raw.splitlines())
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('#:'):
|
||||
msgs.append((i, line[2:].strip()))
|
||||
j = i
|
||||
block = []
|
||||
while True:
|
||||
j += 1
|
||||
line = lines[j]
|
||||
if not line.startswith('#'):
|
||||
break
|
||||
block.append(line[1:].strip())
|
||||
if block:
|
||||
msgs.append((i+1, '\n'.join(block)))
|
||||
|
||||
ans = []
|
||||
for lineno, msg in msgs:
|
||||
ans.append('#: %s:%d'%(path, lineno))
|
||||
slash = unichr(92)
|
||||
msg = msg.replace(slash, slash*2).replace('"', r'\"').replace('\n',
|
||||
r'\n').replace('\r', r'\r').replace('\t', r'\t')
|
||||
ans.append('msgid "%s"'%msg)
|
||||
ans.append('msgstr ""')
|
||||
ans.append('')
|
||||
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
def run(self, opts):
|
||||
files = self.source_files()
|
||||
@ -35,10 +67,10 @@ class POT(Command):
|
||||
atexit.register(shutil.rmtree, tempdir)
|
||||
pygettext(buf, ['-k', '__', '-p', tempdir]+files)
|
||||
src = buf.getvalue()
|
||||
src += '\n\n' + self.get_tweaks_docs()
|
||||
pot = os.path.join(self.PATH, __appname__+'.pot')
|
||||
f = open(pot, 'wb')
|
||||
f.write(src)
|
||||
f.close()
|
||||
with open(pot, 'wb') as f:
|
||||
f.write(src)
|
||||
self.info('Translations template:', os.path.abspath(pot))
|
||||
return pot
|
||||
|
||||
|
@ -5,7 +5,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import uuid, sys, os, re, logging, time, random, \
|
||||
__builtin__, warnings, multiprocessing
|
||||
from contextlib import closing
|
||||
from urllib import getproxies
|
||||
from urllib2 import unquote as urllib2_unquote
|
||||
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
|
||||
from htmlentitydefs import name2codepoint
|
||||
from math import floor
|
||||
@ -290,6 +292,9 @@ def get_parsed_proxy(typ='http', debug=True):
|
||||
prints('Using http proxy', str(ans))
|
||||
return ans
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
USER_AGENT_MOBILE = 'Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016'
|
||||
|
||||
def random_user_agent():
|
||||
choices = [
|
||||
'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
|
||||
@ -305,7 +310,6 @@ def random_user_agent():
|
||||
#return choices[-1]
|
||||
return choices[random.randint(0, len(choices)-1)]
|
||||
|
||||
|
||||
def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
'''
|
||||
Create a mechanize browser for web scraping. The browser handles cookies,
|
||||
@ -319,8 +323,7 @@ def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
|
||||
opener.set_handle_robots(False)
|
||||
if user_agent is None:
|
||||
user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
|
||||
'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
user_agent = USER_AGENT_MOBILE if mobile_browser else USER_AGENT
|
||||
opener.addheaders = [('User-agent', user_agent)]
|
||||
http_proxy = get_proxies().get('http', None)
|
||||
if http_proxy:
|
||||
@ -537,7 +540,49 @@ def as_unicode(obj, enc=preferred_encoding):
|
||||
obj = repr(obj)
|
||||
return force_unicode(obj, enc=enc)
|
||||
|
||||
def url_slash_cleaner(url):
|
||||
'''
|
||||
Removes redundant /'s from url's.
|
||||
'''
|
||||
return re.sub(r'(?<!:)/{2,}', '/', url)
|
||||
|
||||
def get_download_filename(url, cookie_file=None):
|
||||
'''
|
||||
Get a local filename for a URL using the content disposition header
|
||||
'''
|
||||
filename = ''
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
from mechanize import MozillaCookieJar
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
|
||||
try:
|
||||
with closing(br.open(url)) as r:
|
||||
disposition = r.info().get('Content-disposition', '')
|
||||
for p in disposition.split(';'):
|
||||
if 'filename' in p:
|
||||
if '*=' in disposition:
|
||||
parts = disposition.split('*=')[-1]
|
||||
filename = parts.split('\'')[-1]
|
||||
else:
|
||||
filename = disposition.split('=')[-1]
|
||||
if filename[0] in ('\'', '"'):
|
||||
filename = filename[1:]
|
||||
if filename[-1] in ('\'', '"'):
|
||||
filename = filename[:-1]
|
||||
filename = urllib2_unquote(filename)
|
||||
break
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if not filename:
|
||||
filename = r.geturl().split('/')[-1]
|
||||
|
||||
return filename
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.54'
|
||||
__version__ = '0.7.56'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re, importlib
|
||||
|
@ -602,3 +602,35 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class StoreBase(Plugin): # {{{
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
This method must return the actual interface action plugin object.
|
||||
'''
|
||||
mod, cls = self.actual_plugin.split(':')
|
||||
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
|
||||
return self.actual_plugin_object
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.customization_help(gui)
|
||||
raise NotImplementedError()
|
||||
|
||||
def config_widget(self):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.config_widget()
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.save_settings(config_widget)
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
||||
|
@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import textwrap, os, glob, functools, re
|
||||
from calibre import guess_type
|
||||
from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
|
||||
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase
|
||||
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase, StoreBase
|
||||
from calibre.constants import numeric_version
|
||||
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
|
||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
@ -173,7 +173,7 @@ class ComicMetadataReader(MetadataReaderPlugin):
|
||||
stream.seek(pos)
|
||||
if id_ == b'Rar':
|
||||
ftype = 'cbr'
|
||||
elif id.startswith(b'PK'):
|
||||
elif id_.startswith(b'PK'):
|
||||
ftype = 'cbz'
|
||||
if ftype == 'cbr':
|
||||
from calibre.libunrar import extract_first_alphabetically as extract_first
|
||||
@ -625,8 +625,9 @@ if test_eight_code:
|
||||
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
||||
from calibre.ebooks.metadata.sources.amazon import Amazon
|
||||
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
||||
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
|
||||
|
||||
plugins += [GoogleBooks, Amazon, OpenLibrary]
|
||||
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB]
|
||||
|
||||
# }}}
|
||||
else:
|
||||
@ -853,6 +854,11 @@ class ActionNextMatch(InterfaceActionBase):
|
||||
name = 'Next Match'
|
||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||
|
||||
class ActionStore(InterfaceActionBase):
|
||||
name = 'Store'
|
||||
author = 'John Schember'
|
||||
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||
|
||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||
@ -861,6 +867,9 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
||||
|
||||
if test_eight_code:
|
||||
plugins += [ActionStore]
|
||||
|
||||
# }}}
|
||||
|
||||
# Preferences Plugins {{{
|
||||
@ -1038,6 +1047,17 @@ class Server(PreferencesPlugin):
|
||||
'give you access to your calibre library from anywhere, '
|
||||
'on any device, over the internet')
|
||||
|
||||
class MetadataSources(PreferencesPlugin):
|
||||
name = 'Metadata download'
|
||||
icon = I('metadata.png')
|
||||
gui_name = _('Metadata download')
|
||||
category = 'Sharing'
|
||||
gui_category = _('Sharing')
|
||||
category_order = 4
|
||||
name_order = 3
|
||||
config_widget = 'calibre.gui2.preferences.metadata_sources'
|
||||
description = _('Control how calibre downloads ebook metadata from the net')
|
||||
|
||||
class Plugins(PreferencesPlugin):
|
||||
name = 'Plugins'
|
||||
icon = I('plugins.png')
|
||||
@ -1076,6 +1096,86 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||
CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard,
|
||||
Email, Server, Plugins, Tweaks, Misc, TemplateFunctions]
|
||||
|
||||
if test_eight_code:
|
||||
plugins.append(MetadataSources)
|
||||
|
||||
#}}}
|
||||
|
||||
# Store plugins {{{
|
||||
class StoreAmazonKindleStore(StoreBase):
|
||||
name = 'Amazon Kindle'
|
||||
description = _('Kindle books from Amazon')
|
||||
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
|
||||
|
||||
class StoreBaenWebScriptionStore(StoreBase):
|
||||
name = 'Baen WebScription'
|
||||
description = _('Ebooks for readers.')
|
||||
actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore'
|
||||
|
||||
class StoreBNStore(StoreBase):
|
||||
name = 'Barnes and Noble'
|
||||
description = _('Books, Textbooks, eBooks, Toys, Games and More.')
|
||||
actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore'
|
||||
|
||||
class StoreBeWriteStore(StoreBase):
|
||||
name = 'BeWrite Books'
|
||||
description = _('Publishers of fine books.')
|
||||
actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore'
|
||||
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = _('World Famous eBook Store.')
|
||||
actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore'
|
||||
|
||||
class StoreEbookscomStore(StoreBase):
|
||||
name = 'eBooks.com'
|
||||
description = _('The digital bookstore.')
|
||||
actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore'
|
||||
|
||||
class StoreEHarlequinStoretore(StoreBase):
|
||||
name = 'eHarlequin'
|
||||
description = _('entertain, enrich, inspire.')
|
||||
actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore'
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = _('Read anywhere.')
|
||||
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
|
||||
|
||||
class StoreGutenbergStore(StoreBase):
|
||||
name = 'Project Gutenberg'
|
||||
description = _('The first producer of free ebooks.')
|
||||
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
|
||||
|
||||
class StoreKoboStore(StoreBase):
|
||||
name = 'Kobo'
|
||||
description = _('eReading: anytime. anyplace.')
|
||||
actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore'
|
||||
|
||||
class StoreManyBooksStore(StoreBase):
|
||||
name = 'ManyBooks'
|
||||
description = _('The best ebooks at the best price: free!')
|
||||
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
|
||||
|
||||
class StoreMobileReadStore(StoreBase):
|
||||
name = 'MobileRead'
|
||||
description = _('Ebooks handcrafted with the utmost care')
|
||||
actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore'
|
||||
|
||||
class StoreOpenLibraryStore(StoreBase):
|
||||
name = 'Open Library'
|
||||
description = _('One web page for every book.')
|
||||
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
|
||||
|
||||
class StoreSmashwordsStore(StoreBase):
|
||||
name = 'Smashwords'
|
||||
description = _('Your ebook. Your way.')
|
||||
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
|
||||
|
||||
plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore,
|
||||
StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore,
|
||||
StoreEHarlequinStoretore,
|
||||
StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore,
|
||||
StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore]
|
||||
|
||||
# }}}
|
||||
|
@ -344,6 +344,7 @@ class iPadOutput(OutputProfile):
|
||||
border-spacing:1px;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
page-break-inside:avoid;
|
||||
width: 90%;
|
||||
-webkit-border-radius:4px;
|
||||
}
|
||||
|
@ -7,7 +7,8 @@ import os, shutil, traceback, functools, sys
|
||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||
InterfaceActionBase as InterfaceAction,
|
||||
PreferencesPlugin, platform, InvalidPlugin)
|
||||
PreferencesPlugin, platform, InvalidPlugin,
|
||||
StoreBase as Store)
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.zipplugin import loader
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
@ -75,6 +76,17 @@ def enable_plugin(plugin_or_name):
|
||||
ep.add(x)
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
def restore_plugin_state_to_default(plugin_or_name):
|
||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
||||
dp = config['disabled_plugins']
|
||||
if x in dp:
|
||||
dp.remove(x)
|
||||
config['disabled_plugins'] = dp
|
||||
ep = config['enabled_plugins']
|
||||
if x in ep:
|
||||
ep.remove(x)
|
||||
config['enabled_plugins'] = ep
|
||||
|
||||
default_disabled_plugins = set([
|
||||
'Douban Books', 'Douban.com covers', 'Nicebooks', 'Nicebooks covers',
|
||||
'Kent District Library'
|
||||
@ -233,6 +245,17 @@ def preferences_plugins():
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Store Plugins # {{{
|
||||
|
||||
def store_plugins():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Store):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Metadata read/write {{{
|
||||
_metadata_readers = {}
|
||||
_metadata_writers = {}
|
||||
|
@ -51,6 +51,8 @@ Run an embedded python interpreter.
|
||||
'with sqlite3 works.')
|
||||
parser.add_option('-p', '--py-console', help='Run python console',
|
||||
default=False, action='store_true')
|
||||
parser.add_option('-m', '--inspect-mobi',
|
||||
help='Inspect the MOBI file at the specified path', default=None)
|
||||
|
||||
return parser
|
||||
|
||||
@ -227,6 +229,9 @@ def main(args=sys.argv):
|
||||
if len(args) > 1 and os.access(args[-1], os.R_OK):
|
||||
sql_dump = args[-1]
|
||||
reinit_db(opts.reinitialize_db, sql_dump=sql_dump)
|
||||
elif opts.inspect_mobi is not None:
|
||||
from calibre.ebooks.mobi.debug import inspect_mobi
|
||||
inspect_mobi(opts.inspect_mobi)
|
||||
else:
|
||||
from calibre import ipython
|
||||
ipython()
|
||||
|
@ -37,7 +37,7 @@ class ANDROID(USBMS):
|
||||
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
|
||||
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
|
||||
0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216],
|
||||
0x7086 : [0x0226],
|
||||
0x7086 : [0x0226], 0x70a8: [0x9999],
|
||||
},
|
||||
|
||||
# Sony Ericsson
|
||||
@ -54,6 +54,9 @@ class ANDROID(USBMS):
|
||||
0x6877 : [0x0400],
|
||||
},
|
||||
|
||||
# Viewsonic
|
||||
0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], },
|
||||
|
||||
# Acer
|
||||
0x502 : { 0x3203 : [0x0100]},
|
||||
|
||||
@ -96,7 +99,8 @@ class ANDROID(USBMS):
|
||||
|
||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA']
|
||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA',
|
||||
'GENERIC-']
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||
@ -104,7 +108,7 @@ class ANDROID(USBMS):
|
||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
|
||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
||||
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
||||
'MB860']
|
||||
'MB860', 'MULTI-CARD', 'MID7015A']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7']
|
||||
|
@ -349,7 +349,7 @@ class ITUNES(DriverBase):
|
||||
break
|
||||
break
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(j+1/task_count, _('Updating device metadata listing...'))
|
||||
self.report_progress((j+1)/task_count, _('Updating device metadata listing...'))
|
||||
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(1.0, _('Updating device metadata listing...'))
|
||||
@ -428,7 +428,7 @@ class ITUNES(DriverBase):
|
||||
}
|
||||
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
|
||||
self.report_progress((i+1)/book_count, _('%d of %d') % (i+1, book_count))
|
||||
self._purge_orphans(library_books, cached_books)
|
||||
|
||||
elif iswindows:
|
||||
@ -466,7 +466,7 @@ class ITUNES(DriverBase):
|
||||
}
|
||||
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/book_count,
|
||||
self.report_progress((i+1)/book_count,
|
||||
_('%d of %d') % (i+1, book_count))
|
||||
self._purge_orphans(library_books, cached_books)
|
||||
|
||||
@ -916,6 +916,8 @@ class ITUNES(DriverBase):
|
||||
"""
|
||||
if DEBUG:
|
||||
self.log.info("ITUNES.reset()")
|
||||
if report_progress:
|
||||
self.set_progress_reporter(report_progress)
|
||||
|
||||
def set_progress_reporter(self, report_progress):
|
||||
'''
|
||||
@ -924,6 +926,9 @@ class ITUNES(DriverBase):
|
||||
If it is called with -1 that means that the
|
||||
task does not have any progress information
|
||||
'''
|
||||
if DEBUG:
|
||||
self.log.info("ITUNES.set_progress_reporter()")
|
||||
|
||||
self.report_progress = report_progress
|
||||
|
||||
def set_plugboards(self, plugboards, pb_func):
|
||||
@ -1041,7 +1046,7 @@ class ITUNES(DriverBase):
|
||||
|
||||
# Report progress
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
|
||||
self.report_progress((i+1)/file_count, _('%d of %d') % (i+1, file_count))
|
||||
|
||||
elif iswindows:
|
||||
try:
|
||||
@ -1081,7 +1086,7 @@ class ITUNES(DriverBase):
|
||||
|
||||
# Report progress
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
|
||||
self.report_progress((i+1)/file_count, _('%d of %d') % (i+1, file_count))
|
||||
finally:
|
||||
pythoncom.CoUninitialize()
|
||||
|
||||
@ -3065,7 +3070,7 @@ class ITUNES_ASYNC(ITUNES):
|
||||
}
|
||||
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
|
||||
self.report_progress((i+1)/book_count, _('%d of %d') % (i+1, book_count))
|
||||
|
||||
elif iswindows:
|
||||
try:
|
||||
@ -3104,7 +3109,7 @@ class ITUNES_ASYNC(ITUNES):
|
||||
}
|
||||
|
||||
if self.report_progress is not None:
|
||||
self.report_progress(i+1/book_count,
|
||||
self.report_progress((i+1)/book_count,
|
||||
_('%d of %d') % (i+1, book_count))
|
||||
|
||||
finally:
|
||||
|
@ -244,7 +244,7 @@ class EEEREADER(USBMS):
|
||||
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
|
||||
|
||||
VENDOR_ID = [0x0b05]
|
||||
PRODUCT_ID = [0x178f]
|
||||
PRODUCT_ID = [0x178f, 0x17a1]
|
||||
BCD = [0x0319]
|
||||
|
||||
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Book'
|
||||
|
@ -203,6 +203,8 @@ class CollectionsBookList(BookList):
|
||||
val = [orig_val]
|
||||
elif fm['datatype'] == 'text' and fm['is_multiple']:
|
||||
val = orig_val
|
||||
elif fm['datatype'] == 'composite' and fm['is_multiple']:
|
||||
val = [v.strip() for v in val.split(fm['is_multiple'])]
|
||||
else:
|
||||
val = [val]
|
||||
|
||||
|
@ -26,7 +26,7 @@ class ParserError(ValueError):
|
||||
pass
|
||||
|
||||
BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'txtz', 'text', 'htm', 'xhtm',
|
||||
'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
|
||||
'html', 'htmlz', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc',
|
||||
'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip',
|
||||
'rb', 'imp', 'odt', 'chm', 'tpz', 'azw1', 'pml', 'pmlz', 'mbp', 'tan', 'snb']
|
||||
|
||||
|
@ -51,6 +51,10 @@ class CHMInput(InputFormatPlugin):
|
||||
mainpath = os.path.join(tdir, mainname)
|
||||
|
||||
metadata = get_metadata_from_reader(self._chm_reader)
|
||||
self._chm_reader.CloseCHM()
|
||||
#print tdir
|
||||
#from calibre import ipython
|
||||
#ipython()
|
||||
|
||||
odi = options.debug_pipeline
|
||||
options.debug_pipeline = None
|
||||
|
@ -147,7 +147,8 @@ class CHMReader(CHMFile):
|
||||
if self.hhc_path == '.hhc' and self.hhc_path not in files:
|
||||
from calibre import walk
|
||||
for x in walk(output_dir):
|
||||
if os.path.basename(x).lower() in ('index.htm', 'index.html'):
|
||||
if os.path.basename(x).lower() in ('index.htm', 'index.html',
|
||||
'contents.htm', 'contents.html'):
|
||||
self.hhc_path = os.path.relpath(x, output_dir)
|
||||
break
|
||||
|
||||
|
@ -12,6 +12,7 @@ from Queue import Empty
|
||||
|
||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||
from calibre import extract, CurrentDir, prints
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file):
|
||||
Un-archive the comic file.
|
||||
'''
|
||||
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
|
||||
if not isinstance(tdir, unicode):
|
||||
# Needed in case the zip file has wrongly encoded unicode file/dir
|
||||
# names
|
||||
tdir = tdir.decode(filesystem_encoding)
|
||||
extract(path_to_comic_file, tdir)
|
||||
return tdir
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
#define BUFFER 6000
|
||||
|
||||
#define MIN(x, y) ( ((x) < (y)) ? (x) : (y) )
|
||||
#define MAX(x, y) ( ((x) > (y)) ? (x) : (y) )
|
||||
|
||||
typedef unsigned short int Byte;
|
||||
typedef struct {
|
||||
@ -53,7 +54,7 @@ cpalmdoc_decompress(PyObject *self, PyObject *args) {
|
||||
// Map chars to bytes
|
||||
for (j = 0; j < input_len; j++)
|
||||
input[j] = (_input[j] < 0) ? _input[j]+256 : _input[j];
|
||||
output = (char *)PyMem_Malloc(sizeof(char)*BUFFER);
|
||||
output = (char *)PyMem_Malloc(sizeof(char)*(MAX(BUFFER, 5*input_len)));
|
||||
if (output == NULL) return PyErr_NoMemory();
|
||||
|
||||
while (i < input_len) {
|
||||
|
@ -175,18 +175,18 @@ class EPUBInput(InputFormatPlugin):
|
||||
raise ValueError(
|
||||
'EPUB files with DTBook markup are not supported')
|
||||
|
||||
not_for_spine = set()
|
||||
for y in opf.itermanifest():
|
||||
id_ = y.get('id', None)
|
||||
if id_ and y.get('media-type', None) in \
|
||||
('application/vnd.adobe-page-template+xml',):
|
||||
not_for_spine.add(id_)
|
||||
|
||||
for x in list(opf.iterspine()):
|
||||
ref = x.get('idref', None)
|
||||
if ref is None:
|
||||
if ref is None or ref in not_for_spine:
|
||||
x.getparent().remove(x)
|
||||
continue
|
||||
for y in opf.itermanifest():
|
||||
if y.get('id', None) == ref and y.get('media-type', None) in \
|
||||
('application/vnd.adobe-page-template+xml',):
|
||||
p = x.getparent()
|
||||
if p is not None:
|
||||
p.remove(x)
|
||||
break
|
||||
|
||||
with open('content.opf', 'wb') as nopf:
|
||||
nopf.write(opf.render())
|
||||
|
@ -28,7 +28,7 @@ class FB2Output(OutputFormatPlugin):
|
||||
'sf_horror', # Horror & mystic
|
||||
'sf_humor', # Humor
|
||||
'sf_fantasy', # Fantasy
|
||||
'sf', # Science Fiction
|
||||
'sf', # Science Fiction
|
||||
# Detectives & Thrillers
|
||||
'det_classic', # Classical detectives
|
||||
'det_police', # Police Stories
|
||||
@ -41,20 +41,20 @@ class FB2Output(OutputFormatPlugin):
|
||||
'det_maniac', # Maniacs
|
||||
'det_hard', # Hard#boiled
|
||||
'thriller', # Thrillers
|
||||
'detective', # Detectives
|
||||
'detective', # Detectives
|
||||
# Prose
|
||||
'prose_classic', # Classics prose
|
||||
'prose_history', # Historical prose
|
||||
'prose_contemporary', # Contemporary prose
|
||||
'prose_counter', # Counterculture
|
||||
'prose_rus_classic', # Russial classics prose
|
||||
'prose_su_classics', # Soviet classics prose
|
||||
'prose_su_classics', # Soviet classics prose
|
||||
# Romance
|
||||
'love_contemporary', # Contemporary Romance
|
||||
'love_history', # Historical Romance
|
||||
'love_detective', # Detective Romance
|
||||
'love_short', # Short Romance
|
||||
'love_erotica', # Erotica
|
||||
'love_erotica', # Erotica
|
||||
# Adventure
|
||||
'adv_western', # Western
|
||||
'adv_history', # History
|
||||
@ -62,7 +62,7 @@ class FB2Output(OutputFormatPlugin):
|
||||
'adv_maritime', # Maritime Fiction
|
||||
'adv_geo', # Travel & geography
|
||||
'adv_animal', # Nature & animals
|
||||
'adventure', # Other
|
||||
'adventure', # Other
|
||||
# Children's
|
||||
'child_tale', # Fairy Tales
|
||||
'child_verse', # Verses
|
||||
@ -71,17 +71,17 @@ class FB2Output(OutputFormatPlugin):
|
||||
'child_det', # Detectives & Thrillers
|
||||
'child_adv', # Adventures
|
||||
'child_education', # Educational
|
||||
'children', # Other
|
||||
'children', # Other
|
||||
# Poetry & Dramaturgy
|
||||
'poetry', # Poetry
|
||||
'dramaturgy', # Dramaturgy
|
||||
'dramaturgy', # Dramaturgy
|
||||
# Antique literature
|
||||
'antique_ant', # Antique
|
||||
'antique_european', # European
|
||||
'antique_russian', # Old russian
|
||||
'antique_east', # Old east
|
||||
'antique_myths', # Myths. Legends. Epos
|
||||
'antique', # Other
|
||||
'antique', # Other
|
||||
# Scientific#educational
|
||||
'sci_history', # History
|
||||
'sci_psychology', # Psychology
|
||||
@ -98,7 +98,7 @@ class FB2Output(OutputFormatPlugin):
|
||||
'sci_chem', # Chemistry
|
||||
'sci_biology', # Biology
|
||||
'sci_tech', # Technical
|
||||
'science', # Other
|
||||
'science', # Other
|
||||
# Computers & Internet
|
||||
'comp_www', # Internet
|
||||
'comp_programming', # Programming
|
||||
@ -106,29 +106,29 @@ class FB2Output(OutputFormatPlugin):
|
||||
'comp_soft', # Software
|
||||
'comp_db', # Databases
|
||||
'comp_osnet', # OS & Networking
|
||||
'computers', # Other
|
||||
'computers', # Other
|
||||
# Reference
|
||||
'ref_encyc', # Encyclopedias
|
||||
'ref_dict', # Dictionaries
|
||||
'ref_ref', # Reference
|
||||
'ref_guide', # Guidebooks
|
||||
'reference', # Other
|
||||
'reference', # Other
|
||||
# Nonfiction
|
||||
'nonf_biography', # Biography & Memoirs
|
||||
'nonf_publicism', # Publicism
|
||||
'nonf_criticism', # Criticism
|
||||
'design', # Art & design
|
||||
'nonfiction', # Other
|
||||
'nonfiction', # Other
|
||||
# Religion & Inspiration
|
||||
'religion_rel', # Religion
|
||||
'religion_esoterics', # Esoterics
|
||||
'religion_self', # Self#improvement
|
||||
'religion', # Other
|
||||
'religion', # Other
|
||||
# Humor
|
||||
'humor_anecdote', # Anecdote (funny stories)
|
||||
'humor_prose', # Prose
|
||||
'humor_verse', # Verses
|
||||
'humor', # Other
|
||||
'humor', # Other
|
||||
# Home & Family
|
||||
'home_cooking', # Cooking
|
||||
'home_pets', # Pets
|
||||
@ -155,14 +155,14 @@ class FB2Output(OutputFormatPlugin):
|
||||
OptionRecommendation(name='fb2_genre',
|
||||
recommended_value='antique', level=OptionRecommendation.LOW,
|
||||
choices=FB2_GENRES,
|
||||
help=_('Genre for the book. Choices: %s\n\n See: ' % FB2_GENRES) + 'http://www.fictionbook.org/index.php/Eng:FictionBook_2.1_genres ' \
|
||||
help=(_('Genre for the book. Choices: %s\n\n See: ') % FB2_GENRES) + 'http://www.fictionbook.org/index.php/Eng:FictionBook_2.1_genres ' \
|
||||
+ _('for a complete list with descriptions.')),
|
||||
])
|
||||
|
||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||
from calibre.ebooks.oeb.transforms.jacket import linearize_jacket
|
||||
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
|
||||
|
||||
|
||||
try:
|
||||
rasterizer = SVGRasterizer()
|
||||
rasterizer(oeb_book, opts)
|
||||
|
@ -10,6 +10,7 @@ import os
|
||||
|
||||
from calibre import walk
|
||||
from calibre.customize.conversion import InputFormatPlugin
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
|
||||
class HTMLZInput(InputFormatPlugin):
|
||||
@ -34,6 +35,13 @@ class HTMLZInput(InputFormatPlugin):
|
||||
html = tf.read()
|
||||
break
|
||||
|
||||
# Encoding
|
||||
if options.input_encoding:
|
||||
ienc = options.input_encoding
|
||||
else:
|
||||
ienc = xml_to_unicode(html[:4096])[-1]
|
||||
html = html.decode(ienc, 'replace')
|
||||
|
||||
# Run the HTML through the html processing plugin.
|
||||
from calibre.customize.ui import plugin_for_input_format
|
||||
html_input = plugin_for_input_format('html')
|
||||
|
@ -12,7 +12,7 @@ from lxml import etree
|
||||
|
||||
from calibre.customize.conversion import OutputFormatPlugin, \
|
||||
OptionRecommendation
|
||||
from calibre.ebooks.oeb.base import OEB_IMAGES
|
||||
from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
|
||||
@ -71,9 +71,13 @@ class HTMLZOutput(OutputFormatPlugin):
|
||||
os.makedirs(os.path.join(tdir, 'images'))
|
||||
for item in oeb_book.manifest:
|
||||
if item.media_type in OEB_IMAGES and item.href in images:
|
||||
if item.media_type == SVG_MIME:
|
||||
data = unicode(etree.tostring(item.data, encoding=unicode))
|
||||
else:
|
||||
data = item.data
|
||||
fname = os.path.join(tdir, 'images', images[item.href])
|
||||
with open(fname, 'wb') as img:
|
||||
img.write(item.data)
|
||||
img.write(data)
|
||||
|
||||
# Metadata
|
||||
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
|
||||
|
@ -6,8 +6,8 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, textwrap, sys
|
||||
from copy import deepcopy
|
||||
import os, textwrap, sys, operator
|
||||
from copy import deepcopy, copy
|
||||
|
||||
from lxml import etree
|
||||
|
||||
@ -149,9 +149,65 @@ class TextBlock(etree.XSLTExtension):
|
||||
self.root = root
|
||||
self.parent = root
|
||||
self.add_text_to = (self.parent, 'text')
|
||||
self.fix_deep_nesting(node)
|
||||
for child in node:
|
||||
self.process_child(child)
|
||||
|
||||
def fix_deep_nesting(self, node):
|
||||
deepest = 1
|
||||
|
||||
def depth(node):
|
||||
parent = node.getparent()
|
||||
ans = 1
|
||||
while parent is not None:
|
||||
ans += 1
|
||||
parent = parent.getparent()
|
||||
return ans
|
||||
|
||||
for span in node.xpath('descendant::Span'):
|
||||
d = depth(span)
|
||||
if d > deepest:
|
||||
deepest = d
|
||||
if d > 500:
|
||||
break
|
||||
|
||||
if deepest < 500:
|
||||
return
|
||||
|
||||
self.log.warn('Found deeply nested spans. Flattening.')
|
||||
#with open('/t/before.xml', 'wb') as f:
|
||||
# f.write(etree.tostring(node, method='xml'))
|
||||
|
||||
spans = [(depth(span), span) for span in node.xpath('descendant::Span')]
|
||||
spans.sort(key=operator.itemgetter(0), reverse=True)
|
||||
|
||||
for depth, span in spans:
|
||||
if depth < 3:
|
||||
continue
|
||||
p = span.getparent()
|
||||
gp = p.getparent()
|
||||
idx = p.index(span)
|
||||
pidx = gp.index(p)
|
||||
children = list(p)[idx:]
|
||||
t = children[-1].tail
|
||||
t = t if t else ''
|
||||
children[-1].tail = t + (p.tail if p.tail else '')
|
||||
p.tail = ''
|
||||
pattrib = dict(**p.attrib) if p.tag == 'Span' else {}
|
||||
for child in children:
|
||||
p.remove(child)
|
||||
if pattrib and child.tag == "Span":
|
||||
attrib = copy(pattrib)
|
||||
attrib.update(child.attrib)
|
||||
child.attrib.update(attrib)
|
||||
|
||||
|
||||
for child in reversed(children):
|
||||
gp.insert(pidx+1, child)
|
||||
|
||||
#with open('/t/after.xml', 'wb') as f:
|
||||
# f.write(etree.tostring(node, method='xml'))
|
||||
|
||||
def add_text(self, text):
|
||||
if text:
|
||||
if getattr(self.add_text_to[0], self.add_text_to[1]) is None:
|
||||
|
@ -483,7 +483,7 @@ class Metadata(object):
|
||||
self_tags = self.get(x, [])
|
||||
self.set_user_metadata(x, meta) # get... did the deepcopy
|
||||
other_tags = other.get(x, [])
|
||||
if meta['is_multiple']:
|
||||
if meta['datatype'] == 'text' and meta['is_multiple']:
|
||||
# Case-insensitive but case preserving merging
|
||||
lotags = [t.lower() for t in other_tags]
|
||||
lstags = [t.lower() for t in self_tags]
|
||||
|
@ -8,12 +8,13 @@ Read meta information from extZ (TXTZ, HTMLZ...) files.
|
||||
'''
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
|
||||
from cStringIO import StringIO
|
||||
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.zipfile import ZipFile, safe_replace
|
||||
|
||||
def get_metadata(stream, extract_cover=True):
|
||||
@ -23,16 +24,75 @@ def get_metadata(stream, extract_cover=True):
|
||||
mi = MetaInformation(_('Unknown'), [_('Unknown')])
|
||||
stream.seek(0)
|
||||
|
||||
with TemporaryDirectory('_untxtz_mdata') as tdir:
|
||||
try:
|
||||
zf = ZipFile(stream)
|
||||
zf.extract('metadata.opf', tdir)
|
||||
with open(os.path.join(tdir, 'metadata.opf'), 'rb') as opff:
|
||||
mi = OPF(opff).to_book_metadata()
|
||||
except:
|
||||
return mi
|
||||
try:
|
||||
with ZipFile(stream) as zf:
|
||||
opf_name = get_first_opf_name(zf)
|
||||
opf_stream = StringIO(zf.read(opf_name))
|
||||
opf = OPF(opf_stream)
|
||||
mi = opf.to_book_metadata()
|
||||
if extract_cover:
|
||||
cover_name = opf.raster_cover
|
||||
if cover_name:
|
||||
mi.cover_data = ('jpg', zf.read(cover_name))
|
||||
except:
|
||||
return mi
|
||||
return mi
|
||||
|
||||
def set_metadata(stream, mi):
|
||||
opf = StringIO(metadata_to_opf(mi))
|
||||
safe_replace(stream, 'metadata.opf', opf)
|
||||
replacements = {}
|
||||
|
||||
# Get the OPF in the archive.
|
||||
with ZipFile(stream) as zf:
|
||||
opf_path = get_first_opf_name(zf)
|
||||
opf_stream = StringIO(zf.read(opf_path))
|
||||
opf = OPF(opf_stream)
|
||||
|
||||
# Cover.
|
||||
new_cdata = None
|
||||
try:
|
||||
new_cdata = mi.cover_data[1]
|
||||
if not new_cdata:
|
||||
raise Exception('no cover')
|
||||
except:
|
||||
try:
|
||||
new_cdata = open(mi.cover, 'rb').read()
|
||||
except:
|
||||
pass
|
||||
if new_cdata:
|
||||
raster_cover = opf.raster_cover
|
||||
if not raster_cover:
|
||||
raster_cover = 'cover.jpg'
|
||||
cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover)
|
||||
new_cover = _write_new_cover(new_cdata, cpath)
|
||||
replacements[cpath] = open(new_cover.name, 'rb')
|
||||
|
||||
# Update the metadata.
|
||||
opf.smart_update(mi, replace_metadata=True)
|
||||
newopf = StringIO(opf.render())
|
||||
safe_replace(stream, opf_path, newopf, extra_replacements=replacements)
|
||||
|
||||
# Cleanup temporary files.
|
||||
try:
|
||||
if cpath is not None:
|
||||
replacements[cpath].close()
|
||||
os.remove(replacements[cpath].name)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_first_opf_name(zf):
|
||||
names = zf.namelist()
|
||||
opfs = []
|
||||
for n in names:
|
||||
if n.endswith('.opf') and '/' not in n:
|
||||
opfs.append(n)
|
||||
if not opfs:
|
||||
raise Exception('No OPF found')
|
||||
opfs.sort()
|
||||
return opfs[0]
|
||||
|
||||
def _write_new_cover(new_cdata, cpath):
|
||||
from calibre.utils.magick.draw import save_cover_data_to
|
||||
new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1])
|
||||
new_cover.close()
|
||||
save_cover_data_to(new_cdata, new_cover.name)
|
||||
return new_cover
|
||||
|
@ -259,6 +259,7 @@ class MetadataUpdater(object):
|
||||
trail = len(new_record0.getvalue()) % 4
|
||||
pad = '\0' * (4 - trail) # Always pad w/ at least 1 byte
|
||||
new_record0.write(pad)
|
||||
new_record0.write('\0'*(1024*8))
|
||||
|
||||
# Rebuild the stream, update the pdbrecords pointers
|
||||
self.patchSection(0,new_record0.getvalue())
|
||||
|
@ -279,12 +279,13 @@ class Worker(Thread): # Get details {{{
|
||||
|
||||
class Amazon(Source):
|
||||
|
||||
name = 'Amazon Web'
|
||||
name = 'Amazon.com'
|
||||
description = _('Downloads metadata from Amazon')
|
||||
|
||||
capabilities = frozenset(['identify', 'cover'])
|
||||
touched_fields = frozenset(['title', 'authors', 'identifier:amazon',
|
||||
'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate'])
|
||||
'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate',
|
||||
'language'])
|
||||
has_html_comments = True
|
||||
supports_gzip_transfer_encoding = True
|
||||
|
||||
@ -341,9 +342,10 @@ class Amazon(Source):
|
||||
# Insufficient metadata to make an identify query
|
||||
return None
|
||||
|
||||
utf8q = dict([(x.encode('utf-8'), y.encode('utf-8')) for x, y in
|
||||
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
|
||||
'ignore')) for x, y in
|
||||
q.iteritems()])
|
||||
url = 'http://www.amazon.%s/s/?'%domain + urlencode(utf8q)
|
||||
url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q)
|
||||
return url
|
||||
|
||||
# }}}
|
||||
|
@ -24,6 +24,7 @@ msprefs.defaults['ignore_fields'] = []
|
||||
msprefs.defaults['max_tags'] = 20
|
||||
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
|
||||
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
|
||||
msprefs.defaults['swap_author_names'] = False
|
||||
|
||||
# Google covers are often poor quality (scans/errors) but they have high
|
||||
# resolution, so they trump covers from better sources. So make sure they
|
||||
@ -78,8 +79,8 @@ class InternalMetadataCompareKeyGen(object):
|
||||
exact_title = 1 if title and \
|
||||
cleanup_title(title) == cleanup_title(mi.title) else 2
|
||||
|
||||
has_cover = 2 if source_plugin.get_cached_cover_url(mi.identifiers)\
|
||||
is None else 1
|
||||
has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or
|
||||
source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1
|
||||
|
||||
self.base = (isbn, has_cover, all_fields, exact_title)
|
||||
self.comments_len = len(mi.comments.strip() if mi.comments else '')
|
||||
@ -131,7 +132,22 @@ def fixcase(x):
|
||||
x = titlecase(x)
|
||||
return x
|
||||
|
||||
class Option(object):
|
||||
__slots__ = ['type', 'default', 'label', 'desc', 'name', 'choices']
|
||||
|
||||
def __init__(self, name, type_, default, label, desc, choices=None):
|
||||
'''
|
||||
:param name: The name of this option. Must be a valid python identifier
|
||||
:param type_: The type of this option, one of ('number', 'string',
|
||||
'bool', 'choices')
|
||||
:param default: The default value for this option
|
||||
:param label: A short (few words) description of this option
|
||||
:param desc: A longer description of this option
|
||||
:param choices: A list of possible values, used only if type='choices'
|
||||
'''
|
||||
self.name, self.type, self.default, self.label, self.desc = (name,
|
||||
type_, default, label, desc)
|
||||
self.choices = choices
|
||||
|
||||
class Source(Plugin):
|
||||
|
||||
@ -157,6 +173,20 @@ class Source(Plugin):
|
||||
#: correctly first
|
||||
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 often the case
|
||||
#: with this source set to False
|
||||
cached_cover_url_is_reliable = True
|
||||
|
||||
#: A list of :class:`Option` objects. They will be used to automatically
|
||||
#: construct the configuration widget for this plugin
|
||||
options = ()
|
||||
|
||||
#: A string that is displayed at the top of the config widget for this
|
||||
#: plugin
|
||||
config_help_message = None
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Plugin.__init__(self, *args, **kwargs)
|
||||
self._isbn_to_identifier_cache = {}
|
||||
@ -164,6 +194,9 @@ class Source(Plugin):
|
||||
self.cache_lock = threading.RLock()
|
||||
self._config_obj = None
|
||||
self._browser = None
|
||||
self.prefs.defaults['ignore_fields'] = []
|
||||
for opt in self.options:
|
||||
self.prefs.defaults[opt.name] = opt.default
|
||||
|
||||
# Configuration {{{
|
||||
|
||||
@ -174,6 +207,16 @@ class Source(Plugin):
|
||||
'''
|
||||
return True
|
||||
|
||||
def is_customizable(self):
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
from calibre.gui2.metadata.config import ConfigWidget
|
||||
return ConfigWidget(self)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
config_widget.commit()
|
||||
|
||||
@property
|
||||
def prefs(self):
|
||||
if self._config_obj is None:
|
||||
@ -251,8 +294,24 @@ class Source(Plugin):
|
||||
Excludes connectives and punctuation.
|
||||
'''
|
||||
if title:
|
||||
pat = re.compile(r'''[-,:;+!@#$%^&*(){}.`~"'\s\[\]/]''')
|
||||
title = pat.sub(' ', title)
|
||||
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||
[
|
||||
# Remove things like: (2010) (Omnibus) etc.
|
||||
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''),
|
||||
# Remove any strings that contain the substring edition inside
|
||||
# parentheses
|
||||
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
|
||||
# Remove commas used a separators in numbers
|
||||
(r'(\d+),(\d+)', r'\1\2'),
|
||||
# Remove hyphens only if they have whitespace before them
|
||||
(r'(\s-)', ' '),
|
||||
# Remove single quotes
|
||||
(r"'", ''),
|
||||
# Replace other special chars with a space
|
||||
(r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ')
|
||||
]]
|
||||
for pat, repl in title_patterns:
|
||||
title = pat.sub(repl, title)
|
||||
tokens = title.split()
|
||||
for token in tokens:
|
||||
token = token.strip()
|
||||
|
@ -76,6 +76,11 @@ def run_download(log, results, abort,
|
||||
(plugin, width, height, fmt, bytes)
|
||||
|
||||
'''
|
||||
if title == _('Unknown'):
|
||||
title = None
|
||||
if authors == [_('Unknown')]:
|
||||
authors = None
|
||||
|
||||
plugins = [p for p in metadata_plugins(['cover']) if p.is_configured()]
|
||||
|
||||
rq = Queue()
|
||||
@ -145,7 +150,7 @@ def download_cover(log,
|
||||
Synchronous cover download. Returns the "best" cover as per user
|
||||
prefs/cover resolution.
|
||||
|
||||
Return cover is a tuple: (plugin, width, height, fmt, data)
|
||||
Returned cover is a tuple: (plugin, width, height, fmt, data)
|
||||
|
||||
Returns None if no cover is found.
|
||||
'''
|
||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import time
|
||||
import time, hashlib
|
||||
from urllib import urlencode
|
||||
from functools import partial
|
||||
from Queue import Queue, Empty
|
||||
@ -133,7 +133,7 @@ def to_metadata(browser, log, entry_, timeout): # {{{
|
||||
default = utcnow().replace(day=15)
|
||||
mi.pubdate = parse_date(pubdate, assume_utc=True, default=default)
|
||||
except:
|
||||
log.exception('Failed to parse pubdate')
|
||||
log.error('Failed to parse pubdate %r'%pubdate)
|
||||
|
||||
# Ratings
|
||||
for x in rating(extra):
|
||||
@ -164,9 +164,12 @@ class GoogleBooks(Source):
|
||||
'comments', 'publisher', 'identifier:isbn', 'rating',
|
||||
'identifier:google']) # language currently disabled
|
||||
supports_gzip_transfer_encoding = True
|
||||
cached_cover_url_is_reliable = False
|
||||
|
||||
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:
|
||||
@ -235,7 +238,11 @@ class GoogleBooks(Source):
|
||||
log('Downloading cover from:', cached_url)
|
||||
try:
|
||||
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:
|
||||
log.exception('Failed to download cover from:', cached_url)
|
||||
|
||||
|
@ -114,8 +114,12 @@ class ISBNMerge(object):
|
||||
|
||||
return self.results
|
||||
|
||||
def merge_metadata_results(self):
|
||||
' Merge results with identical title and authors '
|
||||
def merge_metadata_results(self, merge_on_identifiers=False):
|
||||
'''
|
||||
Merge results with identical title and authors or an identical
|
||||
identifier
|
||||
'''
|
||||
# First title/author
|
||||
groups = {}
|
||||
for result in self.results:
|
||||
title = lower(result.title if result.title else '')
|
||||
@ -135,6 +139,44 @@ class ISBNMerge(object):
|
||||
result = rgroup[0]
|
||||
self.results.append(result)
|
||||
|
||||
if merge_on_identifiers:
|
||||
# Now identifiers
|
||||
groups, empty = {}, []
|
||||
for result in self.results:
|
||||
key = set()
|
||||
for typ, val in result.identifiers.iteritems():
|
||||
if typ and val:
|
||||
key.add((typ, val))
|
||||
if key:
|
||||
key = frozenset(key)
|
||||
match = None
|
||||
for candidate in list(groups):
|
||||
if candidate.intersection(key):
|
||||
# We have at least one identifier in common
|
||||
match = candidate.union(key)
|
||||
results = groups.pop(candidate)
|
||||
results.append(result)
|
||||
groups[match] = results
|
||||
break
|
||||
if match is None:
|
||||
groups[key] = [result]
|
||||
else:
|
||||
empty.append(result)
|
||||
|
||||
if len(groups) != len(self.results):
|
||||
self.results = []
|
||||
for rgroup in groups.itervalues():
|
||||
rel = [r.average_source_relevance for r in rgroup]
|
||||
if len(rgroup) > 1:
|
||||
result = self.merge(rgroup, None, do_asr=False)
|
||||
result.average_source_relevance = sum(rel)/len(rel)
|
||||
elif rgroup:
|
||||
result = rgroup[0]
|
||||
self.results.append(result)
|
||||
|
||||
if empty:
|
||||
self.results.extend(empty)
|
||||
|
||||
self.results.sort(key=attrgetter('average_source_relevance'))
|
||||
|
||||
def merge_isbn_results(self):
|
||||
@ -253,6 +295,10 @@ def merge_identify_results(result_map, log):
|
||||
|
||||
def identify(log, abort, # {{{
|
||||
title=None, authors=None, identifiers={}, timeout=30):
|
||||
if title == _('Unknown'):
|
||||
title = None
|
||||
if authors == [_('Unknown')]:
|
||||
authors = None
|
||||
start_time = time.time()
|
||||
plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
|
||||
|
||||
@ -338,8 +384,9 @@ def identify(log, abort, # {{{
|
||||
|
||||
for i, result in enumerate(presults):
|
||||
result.relevance_in_source = i
|
||||
result.has_cached_cover_url = \
|
||||
plugin.get_cached_cover_url(result.identifiers) is not None
|
||||
result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable
|
||||
and plugin.get_cached_cover_url(result.identifiers) is not
|
||||
None)
|
||||
result.identify_plugin = plugin
|
||||
|
||||
log('The identify phase took %.2f seconds'%(time.time() - start_time))
|
||||
@ -356,13 +403,22 @@ def identify(log, abort, # {{{
|
||||
if r.plugin.has_html_comments and r.comments:
|
||||
r.comments = html2text(r.comments)
|
||||
|
||||
dummy = Metadata(_('Unknown'))
|
||||
max_tags = msprefs['max_tags']
|
||||
for r in results:
|
||||
for f in msprefs['ignore_fields']:
|
||||
setattr(r, f, getattr(dummy, f))
|
||||
r.tags = r.tags[:max_tags]
|
||||
|
||||
if msprefs['swap_author_names']:
|
||||
for r in results:
|
||||
def swap_to_ln_fn(a):
|
||||
if ',' in a:
|
||||
return a
|
||||
parts = a.split(None)
|
||||
if len(parts) <= 1:
|
||||
return a
|
||||
surname = parts[-1]
|
||||
return '%s, %s' % (surname, ' '.join(parts[:-1]))
|
||||
r.authors = [swap_to_ln_fn(a) for a in r.authors]
|
||||
|
||||
return results
|
||||
# }}}
|
||||
|
||||
@ -375,6 +431,10 @@ def urls_from_identifiers(identifiers): # {{{
|
||||
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
|
||||
# }}}
|
||||
|
||||
@ -389,7 +449,7 @@ if __name__ == '__main__': # tests {{{
|
||||
# unknown to Amazon
|
||||
{'identifiers':{'isbn': '9780307459671'},
|
||||
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
|
||||
[title_test('The Invisible Gorilla: And Other Ways Our Intuitions Deceive Us',
|
||||
[title_test('The Invisible Gorilla',
|
||||
exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])]
|
||||
|
||||
),
|
||||
@ -398,7 +458,7 @@ if __name__ == '__main__': # tests {{{
|
||||
{'title':'Learning Python',
|
||||
'authors':['Lutz']},
|
||||
[title_test('Learning Python',
|
||||
exact=True), authors_test(['Mark Lutz'])
|
||||
exact=True), authors_test(['Mark J. Lutz', 'David Ascher'])
|
||||
]
|
||||
|
||||
),
|
||||
|
@ -7,7 +7,19 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.ebooks.metadata.sources.base import Source
|
||||
from urllib import quote
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre.ebooks.metadata import check_isbn
|
||||
from calibre.ebooks.metadata.sources.base import Source, Option
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.utils.cleantext import clean_ascii_chars
|
||||
from calibre.utils.icu import lower
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
|
||||
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%s&page_number=1&results=subjects,authors,texts&'
|
||||
|
||||
|
||||
class ISBNDB(Source):
|
||||
|
||||
@ -18,6 +30,20 @@ class ISBNDB(Source):
|
||||
touched_fields = frozenset(['title', 'authors',
|
||||
'identifier:isbn', 'comments', 'publisher'])
|
||||
supports_gzip_transfer_encoding = True
|
||||
# Shortcut, since we have no cached cover URLS
|
||||
cached_cover_url_is_reliable = False
|
||||
|
||||
options = (
|
||||
Option('isbndb_key', 'string', None, _('IsbnDB key:'),
|
||||
_('To use isbndb.com you have to sign up for a free account'
|
||||
'at isbndb.com and get an access key.')),
|
||||
)
|
||||
|
||||
config_help_message = '<p>'+_('To use metadata from isbndb.com you must sign'
|
||||
' up for a free account and get an isbndb key and enter it below.'
|
||||
' Instructions to get the key are '
|
||||
'<a href="http://isbndb.com/docs/api/30-keys.html">here</a>.')
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Source.__init__(self, *args, **kwargs)
|
||||
@ -35,9 +61,186 @@ class ISBNDB(Source):
|
||||
except:
|
||||
pass
|
||||
|
||||
self.isbndb_key = prefs['isbndb_key']
|
||||
@property
|
||||
def isbndb_key(self):
|
||||
return self.prefs['isbndb_key']
|
||||
|
||||
def is_configured(self):
|
||||
return self.isbndb_key is not None
|
||||
|
||||
def create_query(self, title=None, authors=None, identifiers={}): # {{{
|
||||
base_url = BASE_URL%self.isbndb_key
|
||||
isbn = check_isbn(identifiers.get('isbn', None))
|
||||
q = ''
|
||||
if isbn is not None:
|
||||
q = 'index1=isbn&value1='+isbn
|
||||
elif title or authors:
|
||||
tokens = []
|
||||
title_tokens = list(self.get_title_tokens(title))
|
||||
tokens += title_tokens
|
||||
author_tokens = self.get_author_tokens(authors,
|
||||
only_first_author=True)
|
||||
tokens += author_tokens
|
||||
tokens = [quote(t) for t in tokens]
|
||||
q = '+'.join(tokens)
|
||||
q = 'index1=combined&value1='+q
|
||||
|
||||
if not q:
|
||||
return None
|
||||
if isinstance(q, unicode):
|
||||
q = q.encode('utf-8')
|
||||
return base_url + q
|
||||
# }}}
|
||||
|
||||
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
|
||||
identifiers={}, timeout=30):
|
||||
if not self.is_configured():
|
||||
return
|
||||
query = self.create_query(title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
if not query:
|
||||
err = 'Insufficient metadata to construct query'
|
||||
log.error(err)
|
||||
return err
|
||||
|
||||
results = []
|
||||
try:
|
||||
results = self.make_query(query, abort, title=title, authors=authors,
|
||||
identifiers=identifiers, timeout=timeout)
|
||||
except:
|
||||
err = 'Failed to make query to ISBNDb, aborting.'
|
||||
log.exception(err)
|
||||
return err
|
||||
|
||||
if not results and identifiers.get('isbn', False) and title and authors and \
|
||||
not abort.is_set():
|
||||
return self.identify(log, result_queue, abort, title=title,
|
||||
authors=authors, timeout=timeout)
|
||||
|
||||
for result in results:
|
||||
self.clean_downloaded_metadata(result)
|
||||
result_queue.put(result)
|
||||
|
||||
def parse_feed(self, feed, seen, orig_title, orig_authors, identifiers):
|
||||
|
||||
def tostring(x):
|
||||
if x is None:
|
||||
return ''
|
||||
return etree.tostring(x, method='text', encoding=unicode).strip()
|
||||
|
||||
orig_isbn = identifiers.get('isbn', None)
|
||||
title_tokens = list(self.get_title_tokens(orig_title))
|
||||
author_tokens = list(self.get_author_tokens(orig_authors))
|
||||
results = []
|
||||
|
||||
def ismatch(title, authors):
|
||||
authors = lower(' '.join(authors))
|
||||
title = lower(title)
|
||||
match = not title_tokens
|
||||
for t in title_tokens:
|
||||
if lower(t) in title:
|
||||
match = True
|
||||
break
|
||||
amatch = not author_tokens
|
||||
for a in author_tokens:
|
||||
if lower(a) in authors:
|
||||
amatch = True
|
||||
break
|
||||
if not author_tokens: amatch = True
|
||||
return match and amatch
|
||||
|
||||
bl = feed.find('BookList')
|
||||
if bl is None:
|
||||
err = tostring(etree.find('errormessage'))
|
||||
raise ValueError('ISBNDb query failed:' + err)
|
||||
total_results = int(bl.get('total_results'))
|
||||
shown_results = int(bl.get('shown_results'))
|
||||
for bd in bl.xpath('.//BookData'):
|
||||
isbn = check_isbn(bd.get('isbn13', bd.get('isbn', None)))
|
||||
if not isbn:
|
||||
continue
|
||||
if orig_isbn and isbn != orig_isbn:
|
||||
continue
|
||||
title = tostring(bd.find('Title'))
|
||||
if not title:
|
||||
continue
|
||||
authors = []
|
||||
for au in bd.xpath('.//Authors/Person'):
|
||||
au = tostring(au)
|
||||
if au:
|
||||
if ',' in au:
|
||||
ln, _, fn = au.partition(',')
|
||||
au = fn.strip() + ' ' + ln.strip()
|
||||
authors.append(au)
|
||||
if not authors:
|
||||
continue
|
||||
comments = tostring(bd.find('Summary'))
|
||||
if not comments:
|
||||
# Require comments, since without them the result is useless
|
||||
# anyway
|
||||
continue
|
||||
id_ = (title, tuple(authors))
|
||||
if id_ in seen:
|
||||
continue
|
||||
seen.add(id_)
|
||||
if not ismatch(title, authors):
|
||||
continue
|
||||
publisher = tostring(bd.find('PublisherText'))
|
||||
if not publisher: publisher = None
|
||||
if publisher and 'audio' in publisher.lower():
|
||||
continue
|
||||
mi = Metadata(title, authors)
|
||||
mi.isbn = isbn
|
||||
mi.publisher = publisher
|
||||
mi.comments = comments
|
||||
results.append(mi)
|
||||
return total_results, shown_results, results
|
||||
|
||||
def make_query(self, q, abort, title=None, authors=None, identifiers={},
|
||||
max_pages=10, timeout=30):
|
||||
page_num = 1
|
||||
parser = etree.XMLParser(recover=True, no_network=True)
|
||||
br = self.browser
|
||||
|
||||
seen = set()
|
||||
|
||||
candidates = []
|
||||
total_found = 0
|
||||
while page_num <= max_pages and not abort.is_set():
|
||||
url = q.replace('&page_number=1&', '&page_number=%d&'%page_num)
|
||||
page_num += 1
|
||||
raw = br.open_novisit(url, timeout=timeout).read()
|
||||
feed = etree.fromstring(xml_to_unicode(clean_ascii_chars(raw),
|
||||
strip_encoding_pats=True)[0], parser=parser)
|
||||
total, found, results = self.parse_feed(
|
||||
feed, seen, title, authors, identifiers)
|
||||
total_found += found
|
||||
candidates += results
|
||||
if total_found >= total or len(candidates) > 9:
|
||||
break
|
||||
|
||||
return candidates
|
||||
# }}}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# To run these test use:
|
||||
# calibre-debug -e src/calibre/ebooks/metadata/sources/isbndb.py
|
||||
from calibre.ebooks.metadata.sources.test import (test_identify_plugin,
|
||||
title_test, authors_test)
|
||||
test_identify_plugin(ISBNDB.name,
|
||||
[
|
||||
|
||||
|
||||
(
|
||||
{'title':'Great Gatsby',
|
||||
'authors':['Fitzgerald']},
|
||||
[title_test('The great gatsby', exact=True),
|
||||
authors_test(['F. Scott Fitzgerald'])]
|
||||
),
|
||||
|
||||
(
|
||||
{'title': 'Flatland', 'authors':['Abbott']},
|
||||
[title_test('Flatland', exact=False)]
|
||||
),
|
||||
])
|
||||
|
||||
|
@ -12,7 +12,7 @@ from calibre.ebooks.metadata.sources.base import Source
|
||||
class OpenLibrary(Source):
|
||||
|
||||
name = 'Open Library'
|
||||
description = _('Downloads metadata from The Open Library')
|
||||
description = _('Downloads covers from The Open Library')
|
||||
|
||||
capabilities = frozenset(['cover'])
|
||||
|
||||
|
@ -218,11 +218,11 @@ def test_identify_plugin(name, tests): # {{{
|
||||
'')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ',
|
||||
'_')))
|
||||
with open(cover, 'wb') as f:
|
||||
f.write(cdata)
|
||||
f.write(cdata[-1])
|
||||
|
||||
prints('Cover downloaded to:', cover)
|
||||
|
||||
if len(cdata) < 10240:
|
||||
if len(cdata[-1]) < 10240:
|
||||
prints('Downloaded cover too small')
|
||||
raise SystemExit(1)
|
||||
|
||||
|
408
src/calibre/ebooks/mobi/debug.py
Normal file
408
src/calibre/ebooks/mobi/debug.py
Normal file
@ -0,0 +1,408 @@
|
||||
#!/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 struct, datetime
|
||||
from calibre.utils.date import utc_tz
|
||||
from calibre.ebooks.mobi.langcodes import main_language, sub_language
|
||||
|
||||
class PalmDOCAttributes(object):
|
||||
|
||||
class Attr(object):
|
||||
|
||||
def __init__(self, name, field, val):
|
||||
self.name = name
|
||||
self.val = val & field
|
||||
|
||||
def __str__(self):
|
||||
return '%s: %s'%(self.name, bool(self.val))
|
||||
|
||||
def __init__(self, raw):
|
||||
self.val = struct.unpack(b'<H', raw)[0]
|
||||
self.attributes = []
|
||||
for name, field in [('Read Only', 0x02), ('Dirty AppInfoArea', 0x04),
|
||||
('Backup this database', 0x08),
|
||||
('Okay to install newer over existing copy, if present on PalmPilot', 0x10),
|
||||
('Force the PalmPilot to reset after this database is installed', 0x12),
|
||||
('Don\'t allow copy of file to be beamed to other Pilot',
|
||||
0x14)]:
|
||||
self.attributes.append(PalmDOCAttributes.Attr(name, field,
|
||||
self.val))
|
||||
|
||||
def __str__(self):
|
||||
attrs = '\n\t'.join([str(x) for x in self.attributes])
|
||||
return 'PalmDOC Attributes: %s\n\t%s'%(bin(self.val), attrs)
|
||||
|
||||
class PalmDB(object):
|
||||
|
||||
def __init__(self, raw):
|
||||
self.raw = raw
|
||||
|
||||
if self.raw.startswith(b'TPZ'):
|
||||
raise ValueError('This is a Topaz file')
|
||||
|
||||
self.name = self.raw[:32].replace(b'\x00', b'')
|
||||
self.attributes = PalmDOCAttributes(self.raw[32:34])
|
||||
self.version = struct.unpack(b'>H', self.raw[34:36])[0]
|
||||
|
||||
palm_epoch = datetime.datetime(1904, 1, 1, tzinfo=utc_tz)
|
||||
self.creation_date_raw = struct.unpack(b'>I', self.raw[36:40])[0]
|
||||
self.creation_date = (palm_epoch +
|
||||
datetime.timedelta(seconds=self.creation_date_raw))
|
||||
self.modification_date_raw = struct.unpack(b'>I', self.raw[40:44])[0]
|
||||
self.modification_date = (palm_epoch +
|
||||
datetime.timedelta(seconds=self.modification_date_raw))
|
||||
self.last_backup_date_raw = struct.unpack(b'>I', self.raw[44:48])[0]
|
||||
self.last_backup_date = (palm_epoch +
|
||||
datetime.timedelta(seconds=self.last_backup_date_raw))
|
||||
self.modification_number = struct.unpack(b'>I', self.raw[48:52])[0]
|
||||
self.app_info_id = self.raw[52:56]
|
||||
self.sort_info_id = self.raw[56:60]
|
||||
self.type = self.raw[60:64]
|
||||
self.creator = self.raw[64:68]
|
||||
self.ident = self.type + self.creator
|
||||
if self.ident not in (b'BOOKMOBI', b'TEXTREAD'):
|
||||
raise ValueError('Unknown book ident: %r'%self.ident)
|
||||
self.uid_seed = self.raw[68:72]
|
||||
self.next_rec_list_id = self.raw[72:76]
|
||||
|
||||
self.number_of_records, = struct.unpack(b'>H', self.raw[76:78])
|
||||
|
||||
def __str__(self):
|
||||
ans = ['*'*20 + ' PalmDB Header '+ '*'*20]
|
||||
ans.append('Name: %r'%self.name)
|
||||
ans.append(str(self.attributes))
|
||||
ans.append('Version: %s'%self.version)
|
||||
ans.append('Creation date: %s (%s)'%(self.creation_date.isoformat(),
|
||||
self.creation_date_raw))
|
||||
ans.append('Modification date: %s (%s)'%(self.modification_date.isoformat(),
|
||||
self.modification_date_raw))
|
||||
ans.append('Backup date: %s (%s)'%(self.last_backup_date.isoformat(),
|
||||
self.last_backup_date_raw))
|
||||
ans.append('Modification number: %s'%self.modification_number)
|
||||
ans.append('App Info ID: %r'%self.app_info_id)
|
||||
ans.append('Sort Info ID: %r'%self.sort_info_id)
|
||||
ans.append('Type: %r'%self.type)
|
||||
ans.append('Creator: %r'%self.creator)
|
||||
ans.append('UID seed: %r'%self.uid_seed)
|
||||
ans.append('Next record list id: %r'%self.next_rec_list_id)
|
||||
ans.append('Number of records: %s'%self.number_of_records)
|
||||
|
||||
return '\n'.join(ans)
|
||||
|
||||
class Record(object):
|
||||
|
||||
def __init__(self, raw, header):
|
||||
self.offset, self.flags, self.uid = header
|
||||
self.raw = raw
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return 'Offset: %d Flags: %d UID: %d'%(self.offset, self.flags,
|
||||
self.uid)
|
||||
|
||||
class EXTHRecord(object):
|
||||
|
||||
def __init__(self, type_, data):
|
||||
self.type = type_
|
||||
self.data = data
|
||||
self.name = {
|
||||
1 : 'DRM Server id',
|
||||
2 : 'DRM Commerce id',
|
||||
3 : 'DRM ebookbase book id',
|
||||
100 : 'author',
|
||||
101 : 'publisher',
|
||||
102 : 'imprint',
|
||||
103 : 'description',
|
||||
104 : 'isbn',
|
||||
105 : 'subject',
|
||||
106 : 'publishingdate',
|
||||
107 : 'review',
|
||||
108 : 'contributor',
|
||||
109 : 'rights',
|
||||
110 : 'subjectcode',
|
||||
111 : 'type',
|
||||
112 : 'source',
|
||||
113 : 'asin',
|
||||
114 : 'versionnumber',
|
||||
115 : 'sample',
|
||||
116 : 'startreading',
|
||||
117 : 'adult',
|
||||
118 : 'retailprice',
|
||||
119 : 'retailpricecurrency',
|
||||
201 : 'coveroffset',
|
||||
202 : 'thumboffset',
|
||||
203 : 'hasfakecover',
|
||||
204 : 'Creator Software',
|
||||
205 : 'Creator Major Version', # '>I'
|
||||
206 : 'Creator Minor Version', # '>I'
|
||||
207 : 'Creator Build Number', # '>I'
|
||||
208 : 'watermark',
|
||||
209 : 'tamper_proof_keys',
|
||||
300 : 'fontsignature',
|
||||
301 : 'clippinglimit', # percentage '>B'
|
||||
402 : 'publisherlimit',
|
||||
404 : 'TTS flag', # '>B' 1 - TTS disabled 0 - TTS enabled
|
||||
501 : 'cdetype', # 4 chars (PDOC or EBOK)
|
||||
502 : 'lastupdatetime',
|
||||
503 : 'updatedtitle',
|
||||
}.get(self.type, repr(self.type))
|
||||
|
||||
if self.name in ('coveroffset', 'thumboffset', 'hasfakecover',
|
||||
'Creator Major Version', 'Creator Minor Version',
|
||||
'Creator Build Number', 'Creator Software', 'startreading'):
|
||||
self.data, = struct.unpack(b'>I', self.data)
|
||||
|
||||
def __str__(self):
|
||||
return '%s (%d): %r'%(self.name, self.type, self.data)
|
||||
|
||||
class EXTHHeader(object):
|
||||
|
||||
def __init__(self, raw):
|
||||
self.raw = raw
|
||||
if not self.raw.startswith(b'EXTH'):
|
||||
raise ValueError('EXTH header does not start with EXTH')
|
||||
self.length, = struct.unpack(b'>I', self.raw[4:8])
|
||||
self.count, = struct.unpack(b'>I', self.raw[8:12])
|
||||
|
||||
pos = 12
|
||||
self.records = []
|
||||
for i in xrange(self.count):
|
||||
pos = self.read_record(pos)
|
||||
|
||||
def read_record(self, pos):
|
||||
type_, length = struct.unpack(b'>II', self.raw[pos:pos+8])
|
||||
data = self.raw[(pos+8):(pos+length)]
|
||||
self.records.append(EXTHRecord(type_, data))
|
||||
return pos + length
|
||||
|
||||
def __str__(self):
|
||||
ans = ['*'*20 + ' EXTH Header '+ '*'*20]
|
||||
ans.append('EXTH header length: %d'%self.length)
|
||||
ans.append('Number of EXTH records: %d'%self.count)
|
||||
ans.append('EXTH records...')
|
||||
for r in self.records:
|
||||
ans.append(str(r))
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
class MOBIHeader(object):
|
||||
|
||||
def __init__(self, record0):
|
||||
self.raw = record0.raw
|
||||
|
||||
self.compression_raw = self.raw[:2]
|
||||
self.compression = {1: 'No compression', 2: 'PalmDoc compression',
|
||||
17480: 'HUFF/CDIC compression'}.get(struct.unpack(b'>H',
|
||||
self.compression_raw)[0],
|
||||
repr(self.compression_raw))
|
||||
self.unused = self.raw[2:4]
|
||||
self.text_length, = struct.unpack(b'>I', self.raw[4:8])
|
||||
self.number_of_text_records, self.text_record_size = \
|
||||
struct.unpack(b'>HH', self.raw[8:12])
|
||||
self.encryption_type_raw, = struct.unpack(b'>H', self.raw[12:14])
|
||||
self.encryption_type = {0: 'No encryption',
|
||||
1: 'Old mobipocket encryption',
|
||||
2:'Mobipocket encryption'}.get(self.encryption_type_raw,
|
||||
repr(self.encryption_type_raw))
|
||||
self.unknown = self.raw[14:16]
|
||||
|
||||
self.identifier = self.raw[16:20]
|
||||
if self.identifier != b'MOBI':
|
||||
raise ValueError('Identifier %r unknown'%self.identifier)
|
||||
|
||||
self.length, = struct.unpack(b'>I', self.raw[20:24])
|
||||
self.type_raw, = struct.unpack(b'>I', self.raw[24:28])
|
||||
self.type = {
|
||||
2 : 'Mobipocket book',
|
||||
3 : 'PalmDOC book',
|
||||
4 : 'Audio',
|
||||
257 : 'News',
|
||||
258 : 'News Feed',
|
||||
259 : 'News magazine',
|
||||
513 : 'PICS',
|
||||
514 : 'Word',
|
||||
515 : 'XLS',
|
||||
516 : 'PPT',
|
||||
517 : 'TEXT',
|
||||
518 : 'HTML',
|
||||
}.get(self.type_raw, repr(self.type_raw))
|
||||
|
||||
self.encoding_raw, = struct.unpack(b'>I', self.raw[28:32])
|
||||
self.encoding = {
|
||||
1252 : 'cp1252',
|
||||
65001: 'utf-8',
|
||||
}.get(self.encoding_raw, repr(self.encoding_raw))
|
||||
self.uid = self.raw[32:36]
|
||||
self.file_version = struct.unpack(b'>I', self.raw[36:40])
|
||||
self.reserved = self.raw[40:48]
|
||||
self.secondary_index_record, = struct.unpack(b'>I', self.raw[48:52])
|
||||
self.reserved2 = self.raw[52:80]
|
||||
self.first_non_book_record, = struct.unpack(b'>I', self.raw[80:84])
|
||||
self.fullname_offset, = struct.unpack(b'>I', self.raw[84:88])
|
||||
self.fullname_length, = struct.unpack(b'>I', self.raw[88:92])
|
||||
self.locale_raw, = struct.unpack(b'>I', self.raw[92:96])
|
||||
langcode = self.locale_raw
|
||||
langid = langcode & 0xFF
|
||||
sublangid = (langcode >> 10) & 0xFF
|
||||
self.language = main_language.get(langid, 'ENGLISH')
|
||||
self.sublanguage = sub_language.get(sublangid, 'NEUTRAL')
|
||||
|
||||
self.input_language = self.raw[96:100]
|
||||
self.output_langauage = self.raw[100:104]
|
||||
self.min_version, = struct.unpack(b'>I', self.raw[104:108])
|
||||
self.first_image_index, = struct.unpack(b'>I', self.raw[108:112])
|
||||
self.huffman_record_offset, = struct.unpack(b'>I', self.raw[112:116])
|
||||
self.huffman_record_count, = struct.unpack(b'>I', self.raw[116:120])
|
||||
self.unknown2 = self.raw[120:128]
|
||||
self.exth_flags, = struct.unpack(b'>I', self.raw[128:132])
|
||||
self.has_exth = bool(self.exth_flags & 0x40)
|
||||
self.has_drm_data = self.length >= 174 and len(self.raw) >= 180
|
||||
if self.has_drm_data:
|
||||
self.unknown3 = self.raw[132:164]
|
||||
self.drm_offset, = struct.unpack(b'>I', self.raw[164:168])
|
||||
self.drm_count, = struct.unpack(b'>I', self.raw[168:172])
|
||||
self.drm_size, = struct.unpack(b'>I', self.raw[172:176])
|
||||
self.drm_flags = bin(struct.unpack(b'>I', self.raw[176:180])[0])
|
||||
self.has_extra_data_flags = self.length >= 232 and len(self.raw) >= 232+16
|
||||
self.has_fcis_flis = False
|
||||
if self.has_extra_data_flags:
|
||||
self.unknown4 = self.raw[180:192]
|
||||
self.first_content_record, self.last_content_record = \
|
||||
struct.unpack(b'>HH', self.raw[192:196])
|
||||
self.unknown5, = struct.unpack(b'>I', self.raw[196:200])
|
||||
(self.fcis_number, self.fcis_count, self.flis_number,
|
||||
self.flis_count) = struct.unpack(b'>IIII',
|
||||
self.raw[200:216])
|
||||
self.unknown6 = self.raw[216:240]
|
||||
self.extra_data_flags = bin(struct.unpack(b'>I',
|
||||
self.raw[240:244])[0])
|
||||
self.primary_index_record, = struct.unpack(b'>I',
|
||||
self.raw[244:248])
|
||||
|
||||
if self.has_exth:
|
||||
self.exth_offset = 16 + self.length
|
||||
|
||||
self.exth = EXTHHeader(self.raw[self.exth_offset:])
|
||||
|
||||
self.end_of_exth = self.exth_offset + self.exth.length
|
||||
self.bytes_after_exth = self.fullname_offset - self.end_of_exth
|
||||
|
||||
def __str__(self):
|
||||
ans = ['*'*20 + ' MOBI Header '+ '*'*20]
|
||||
ans.append('Compression: %s'%self.compression)
|
||||
ans.append('Unused: %r'%self.unused)
|
||||
ans.append('Number of text records: %d'%self.number_of_text_records)
|
||||
ans.append('Text record size: %d'%self.text_record_size)
|
||||
ans.append('Encryption: %s'%self.encryption_type)
|
||||
ans.append('Unknown: %r'%self.unknown)
|
||||
ans.append('Identifier: %r'%self.identifier)
|
||||
ans.append('Header length: %d'% self.length)
|
||||
ans.append('Type: %s'%self.type)
|
||||
ans.append('Encoding: %s'%self.encoding)
|
||||
ans.append('UID: %r'%self.uid)
|
||||
ans.append('File version: %d'%self.file_version)
|
||||
ans.append('Reserved: %r'%self.reserved)
|
||||
ans.append('Secondary index record: %d (null val: %d)'%(
|
||||
self.secondary_index_record, 0xffffffff))
|
||||
ans.append('Reserved2: %r'%self.reserved2)
|
||||
ans.append('First non-book record: %d'% self.first_non_book_record)
|
||||
ans.append('Full name offset: %d'%self.fullname_offset)
|
||||
ans.append('Full name length: %d bytes'%self.fullname_length)
|
||||
ans.append('Langcode: %r'%self.locale_raw)
|
||||
ans.append('Language: %s'%self.language)
|
||||
ans.append('Sub language: %s'%self.sublanguage)
|
||||
ans.append('Input language: %r'%self.input_language)
|
||||
ans.append('Output language: %r'%self.output_langauage)
|
||||
ans.append('Min version: %d'%self.min_version)
|
||||
ans.append('First Image index: %d'%self.first_image_index)
|
||||
ans.append('Huffman record offset: %d'%self.huffman_record_offset)
|
||||
ans.append('Huffman record count: %d'%self.huffman_record_count)
|
||||
ans.append('Unknown2: %r'%self.unknown2)
|
||||
ans.append('EXTH flags: %r (%s)'%(self.exth_flags, self.has_exth))
|
||||
if self.has_drm_data:
|
||||
ans.append('Unknown3: %r'%self.unknown3)
|
||||
ans.append('DRM Offset: %s'%self.drm_offset)
|
||||
ans.append('DRM Count: %s'%self.drm_count)
|
||||
ans.append('DRM Size: %s'%self.drm_size)
|
||||
ans.append('DRM Flags: %r'%self.drm_flags)
|
||||
if self.has_extra_data_flags:
|
||||
ans.append('Unknown4: %r'%self.unknown4)
|
||||
ans.append('First content record: %d'% self.first_content_record)
|
||||
ans.append('Last content record: %d'% self.last_content_record)
|
||||
ans.append('Unknown5: %d'% self.unknown5)
|
||||
ans.append('FCIS number: %d'% self.fcis_number)
|
||||
ans.append('FCIS count: %d'% self.fcis_count)
|
||||
ans.append('FLIS number: %d'% self.flis_number)
|
||||
ans.append('FLIS count: %d'% self.flis_count)
|
||||
ans.append('Unknown6: %r'% self.unknown6)
|
||||
ans.append('Extra data flags: %r'%self.extra_data_flags)
|
||||
ans.append('Primary index record: %d'%self.primary_index_record)
|
||||
|
||||
ans = '\n'.join(ans)
|
||||
|
||||
if self.has_exth:
|
||||
ans += '\n\n' + str(self.exth)
|
||||
ans += '\n\nBytes after EXTH: %d'%self.bytes_after_exth
|
||||
|
||||
ans += '\nNumber of bytes after full name: %d' % (len(self.raw) - (self.fullname_offset +
|
||||
self.fullname_length))
|
||||
|
||||
ans += '\nRecord 0 length: %d'%len(self.raw)
|
||||
return ans
|
||||
|
||||
class MOBIFile(object):
|
||||
|
||||
def __init__(self, stream):
|
||||
self.raw = stream.read()
|
||||
|
||||
self.palmdb = PalmDB(self.raw[:78])
|
||||
|
||||
self.record_headers = []
|
||||
self.records = []
|
||||
for i in xrange(self.palmdb.number_of_records):
|
||||
pos = 78 + i * 8
|
||||
offset, a1, a2, a3, a4 = struct.unpack(b'>LBBBB', self.raw[pos:pos+8])
|
||||
flags, val = a1, a2 << 16 | a3 << 8 | a4
|
||||
self.record_headers.append((offset, flags, val))
|
||||
|
||||
def section(section_number):
|
||||
if section_number == self.palmdb.number_of_records - 1:
|
||||
end_off = len(self.raw)
|
||||
else:
|
||||
end_off = self.record_headers[section_number + 1][0]
|
||||
off = self.record_headers[section_number][0]
|
||||
return self.raw[off:end_off]
|
||||
|
||||
for i in range(self.palmdb.number_of_records):
|
||||
self.records.append(Record(section(i), self.record_headers[i]))
|
||||
|
||||
self.mobi_header = MOBIHeader(self.records[0])
|
||||
|
||||
|
||||
def print_header(self):
|
||||
print (str(self.palmdb).encode('utf-8'))
|
||||
print ()
|
||||
print ('Record headers:')
|
||||
for i, r in enumerate(self.records):
|
||||
print ('%6d. %s'%(i, r.header))
|
||||
|
||||
print ()
|
||||
print (str(self.mobi_header).encode('utf-8'))
|
||||
|
||||
def inspect_mobi(path_or_stream):
|
||||
stream = (path_or_stream if hasattr(path_or_stream, 'read') else
|
||||
open(path_or_stream, 'rb'))
|
||||
f = MOBIFile(stream)
|
||||
f.print_header()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
f = MOBIFile(open(sys.argv[1], 'rb'))
|
||||
f.print_header()
|
||||
|
@ -463,9 +463,9 @@ class MobiMLizer(object):
|
||||
text = COLLAPSE.sub(' ', elem.text)
|
||||
valign = style['vertical-align']
|
||||
not_baseline = valign in ('super', 'sub', 'text-top',
|
||||
'text-bottom') or (
|
||||
'text-bottom', 'top', 'bottom') or (
|
||||
isinstance(valign, (float, int)) and abs(valign) != 0)
|
||||
issup = valign in ('super', 'text-top') or (
|
||||
issup = valign in ('super', 'text-top', 'top') or (
|
||||
isinstance(valign, (float, int)) and valign > 0)
|
||||
vtag = 'sup' if issup else 'sub'
|
||||
if not_baseline and not ignore_valign and tag not in NOT_VTAGS and not isblock:
|
||||
@ -484,6 +484,7 @@ class MobiMLizer(object):
|
||||
parent = bstate.para if bstate.inline is None else bstate.inline
|
||||
if parent is not None:
|
||||
vtag = etree.SubElement(parent, XHTML(vtag))
|
||||
vtag = etree.SubElement(vtag, XHTML('small'))
|
||||
# Add anchors
|
||||
for child in vbstate.body:
|
||||
if child is not vbstate.para:
|
||||
@ -495,6 +496,10 @@ class MobiMLizer(object):
|
||||
vtag.append(child)
|
||||
return
|
||||
|
||||
if tag == 'blockquote':
|
||||
old_mim = self.opts.mobi_ignore_margins
|
||||
self.opts.mobi_ignore_margins = False
|
||||
|
||||
if text or tag in CONTENT_TAGS or tag in NESTABLE_TAGS:
|
||||
self.mobimlize_content(tag, text, bstate, istates)
|
||||
for child in elem:
|
||||
@ -510,6 +515,8 @@ class MobiMLizer(object):
|
||||
if tail:
|
||||
self.mobimlize_content(tag, tail, bstate, istates)
|
||||
|
||||
if tag == 'blockquote':
|
||||
self.opts.mobi_ignore_margins = old_mim
|
||||
|
||||
if bstate.content and style['page-break-after'] in PAGE_BREAKS:
|
||||
bstate.pbreak = True
|
||||
|
@ -716,6 +716,7 @@ class MobiReader(object):
|
||||
ent_pat = re.compile(r'&(\S+?);')
|
||||
if elems:
|
||||
tocobj = TOC()
|
||||
found = False
|
||||
reached = False
|
||||
for x in root.iter():
|
||||
if x == elems[-1]:
|
||||
@ -732,7 +733,8 @@ class MobiReader(object):
|
||||
text = ent_pat.sub(entity_to_unicode, text)
|
||||
tocobj.add_item(toc.partition('#')[0], href[1:],
|
||||
text)
|
||||
if reached and x.get('class', None) == 'mbp_pagebreak':
|
||||
found = True
|
||||
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
||||
break
|
||||
if tocobj is not None:
|
||||
opf.set_toc(tocobj)
|
||||
|
@ -7,8 +7,6 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.cam> and \
|
||||
Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
|
||||
from collections import defaultdict
|
||||
from itertools import count
|
||||
from itertools import izip
|
||||
import random
|
||||
import re
|
||||
from struct import pack
|
||||
@ -310,10 +308,11 @@ class Serializer(object):
|
||||
if href not in id_offsets:
|
||||
self.logger.warn('Hyperlink target %r not found' % href)
|
||||
href, _ = urldefrag(href)
|
||||
ioff = self.id_offsets[href]
|
||||
for hoff in hoffs:
|
||||
buffer.seek(hoff)
|
||||
buffer.write('%010d' % ioff)
|
||||
if href in self.id_offsets:
|
||||
ioff = self.id_offsets[href]
|
||||
for hoff in hoffs:
|
||||
buffer.seek(hoff)
|
||||
buffer.write('%010d' % ioff)
|
||||
|
||||
class MobiWriter(object):
|
||||
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
|
||||
@ -1510,7 +1509,7 @@ class MobiWriter(object):
|
||||
record0.write(exth)
|
||||
record0.write(title)
|
||||
record0 = record0.getvalue()
|
||||
self._records[0] = record0 + ('\0' * (2452 - len(record0)))
|
||||
self._records[0] = record0 + ('\0' * (1024*8))
|
||||
|
||||
def _build_exth(self):
|
||||
oeb = self._oeb
|
||||
@ -1629,8 +1628,8 @@ class MobiWriter(object):
|
||||
self._write(title, pack('>HHIIIIII', 0, 0, now, now, 0, 0, 0, 0),
|
||||
'BOOK', 'MOBI', pack('>IIH', nrecords, 0, nrecords))
|
||||
offset = self._tell() + (8 * nrecords) + 2
|
||||
for id, record in izip(count(), self._records):
|
||||
self._write(pack('>I', offset), '\0', pack('>I', id)[1:])
|
||||
for i, record in enumerate(self._records):
|
||||
self._write(pack('>I', offset), '\0', pack('>I', 2*i)[1:])
|
||||
offset += len(record)
|
||||
self._write('\0\0')
|
||||
|
||||
|
@ -24,7 +24,7 @@ from calibre.translations.dynamic import translate
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
|
||||
from calibre import isbytestring
|
||||
from calibre import isbytestring, as_unicode
|
||||
|
||||
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
|
||||
|
||||
@ -643,7 +643,7 @@ class Metadata(object):
|
||||
return unicode(self.value).encode('ascii', 'xmlcharrefreplace')
|
||||
|
||||
def __unicode__(self):
|
||||
return unicode(self.value)
|
||||
return as_unicode(self.value)
|
||||
|
||||
def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}):
|
||||
attrib = {}
|
||||
|
@ -20,8 +20,9 @@ class RemoveAdobeMargins(object):
|
||||
self.oeb, self.opts, self.log = oeb, opts, log
|
||||
|
||||
for item in self.oeb.manifest:
|
||||
if item.media_type in ('application/vnd.adobe-page-template+xml',
|
||||
'application/vnd.adobe.page-template+xml'):
|
||||
if (item.media_type in ('application/vnd.adobe-page-template+xml',
|
||||
'application/vnd.adobe.page-template+xml') and
|
||||
hasattr(item.data, 'xpath')):
|
||||
self.log('Removing page margins specified in the'
|
||||
' Adobe page template')
|
||||
for elem in item.data.xpath(
|
||||
|
@ -11,6 +11,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os
|
||||
import re
|
||||
import StringIO
|
||||
from copy import deepcopy
|
||||
|
||||
from calibre import my_unichr, prepare_string_for_xml
|
||||
from calibre.ebooks.metadata.toc import TOC
|
||||
@ -25,6 +26,7 @@ class PML_HTMLizer(object):
|
||||
'sp',
|
||||
'sb',
|
||||
'h1',
|
||||
'h1c',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
@ -58,6 +60,7 @@ class PML_HTMLizer(object):
|
||||
|
||||
STATES_TAGS = {
|
||||
'h1': ('<h1 style="page-break-before: always;">', '</h1>'),
|
||||
'h1c': ('<h1>', '</h1>'),
|
||||
'h2': ('<h2>', '</h2>'),
|
||||
'h3': ('<h3>', '</h3>'),
|
||||
'h4': ('<h4>', '</h4>'),
|
||||
@ -140,6 +143,10 @@ class PML_HTMLizer(object):
|
||||
'd',
|
||||
'b',
|
||||
]
|
||||
|
||||
NEW_LINE_EXCHANGE_STATES = {
|
||||
'h1': 'h1c',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.state = {}
|
||||
@ -219,11 +226,17 @@ class PML_HTMLizer(object):
|
||||
def start_line(self):
|
||||
start = u''
|
||||
|
||||
state = deepcopy(self.state)
|
||||
div = []
|
||||
span = []
|
||||
other = []
|
||||
|
||||
for key, val in state.items():
|
||||
if key in self.NEW_LINE_EXCHANGE_STATES and val[0]:
|
||||
state[self.NEW_LINE_EXCHANGE_STATES[key]] = val
|
||||
state[key] = [False, '']
|
||||
|
||||
for key, val in self.state.items():
|
||||
for key, val in state.items():
|
||||
if val[0]:
|
||||
if key in self.DIV_STATES:
|
||||
div.append((key, val[1]))
|
||||
|
@ -357,6 +357,7 @@ class FileIconProvider(QFileIconProvider):
|
||||
'bmp' : 'bmp',
|
||||
'svg' : 'svg',
|
||||
'html' : 'html',
|
||||
'htmlz' : 'html',
|
||||
'htm' : 'html',
|
||||
'xhtml' : 'html',
|
||||
'xhtm' : 'html',
|
||||
@ -647,6 +648,18 @@ def open_url(qurl):
|
||||
if isfrozen and islinux and paths:
|
||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
||||
|
||||
def get_current_db():
|
||||
'''
|
||||
This method will try to return the current database in use by the user as
|
||||
efficiently as possible, i.e. without constructing duplicate
|
||||
LibraryDatabase objects.
|
||||
'''
|
||||
from calibre.gui2.ui import get_gui
|
||||
gui = get_gui()
|
||||
if gui is not None and gui.current_db is not None:
|
||||
return gui.current_db
|
||||
from calibre.library import db
|
||||
return db()
|
||||
|
||||
def open_local_file(path):
|
||||
if iswindows:
|
||||
|
@ -22,6 +22,8 @@ from calibre.constants import preferred_encoding, filesystem_encoding
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
from calibre.gui2 import config, question_dialog
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.utils.config import test_eight_code
|
||||
from calibre.ebooks.metadata.sources.base import msprefs
|
||||
|
||||
def get_filters():
|
||||
return [
|
||||
@ -178,13 +180,26 @@ class AddAction(InterfaceAction):
|
||||
except IndexError:
|
||||
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
|
||||
self.isbn_add_dialog.accept()
|
||||
orig = config['overwrite_author_title_metadata']
|
||||
config['overwrite_author_title_metadata'] = True
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].do_download_metadata(
|
||||
self.add_by_isbn_ids)
|
||||
finally:
|
||||
config['overwrite_author_title_metadata'] = orig
|
||||
if test_eight_code:
|
||||
orig = msprefs['ignore_fields']
|
||||
new = list(orig)
|
||||
for x in ('title', 'authors'):
|
||||
if x in new:
|
||||
new.remove(x)
|
||||
msprefs['ignore_fields'] = new
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].download_metadata(
|
||||
ids=self.add_by_isbn_ids)
|
||||
finally:
|
||||
msprefs['ignore_fields'] = orig
|
||||
else:
|
||||
orig = config['overwrite_author_title_metadata']
|
||||
config['overwrite_author_title_metadata'] = True
|
||||
try:
|
||||
self.gui.iactions['Edit Metadata'].do_download_metadata(
|
||||
self.add_by_isbn_ids)
|
||||
finally:
|
||||
config['overwrite_author_title_metadata'] = orig
|
||||
return
|
||||
|
||||
|
||||
|
@ -10,7 +10,7 @@ from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QMenu, QModelIndex
|
||||
|
||||
from calibre.gui2 import error_dialog, config
|
||||
from calibre.gui2 import error_dialog, config, Dispatcher
|
||||
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
|
||||
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
@ -35,16 +35,23 @@ class EditMetadataAction(InterfaceAction):
|
||||
md.addAction(_('Edit metadata in bulk'),
|
||||
partial(self.edit_metadata, False, bulk=True))
|
||||
md.addSeparator()
|
||||
md.addAction(_('Download metadata and covers'),
|
||||
partial(self.download_metadata, False, covers=True),
|
||||
if test_eight_code:
|
||||
dall = self.download_metadata
|
||||
dident = partial(self.download_metadata, covers=False)
|
||||
dcovers = partial(self.download_metadata, identify=False)
|
||||
else:
|
||||
dall = partial(self.download_metadata_old, False, covers=True)
|
||||
dident = partial(self.download_metadata_old, False, covers=False)
|
||||
dcovers = partial(self.download_metadata_old, False, covers=True,
|
||||
set_metadata=False, set_social_metadata=False)
|
||||
|
||||
md.addAction(_('Download metadata and covers'), dall,
|
||||
Qt.ControlModifier+Qt.Key_D)
|
||||
md.addAction(_('Download only metadata'),
|
||||
partial(self.download_metadata, False, covers=False))
|
||||
md.addAction(_('Download only covers'),
|
||||
partial(self.download_metadata, False, covers=True,
|
||||
set_metadata=False, set_social_metadata=False))
|
||||
md.addAction(_('Download only social metadata'),
|
||||
partial(self.download_metadata, False, covers=False,
|
||||
md.addAction(_('Download only metadata'), dident)
|
||||
md.addAction(_('Download only covers'), dcovers)
|
||||
if not test_eight_code:
|
||||
md.addAction(_('Download only social metadata'),
|
||||
partial(self.download_metadata_old, False, covers=False,
|
||||
set_metadata=False, set_social_metadata=True))
|
||||
self.metadata_menu = md
|
||||
|
||||
@ -73,7 +80,26 @@ class EditMetadataAction(InterfaceAction):
|
||||
self.qaction.setEnabled(enabled)
|
||||
self.action_merge.setEnabled(enabled)
|
||||
|
||||
def download_metadata(self, checked, covers=True, set_metadata=True,
|
||||
def download_metadata(self, identify=True, covers=True, ids=None):
|
||||
if ids is None:
|
||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
return error_dialog(self.gui, _('Cannot download metadata'),
|
||||
_('No books selected'), show=True)
|
||||
db = self.gui.library_view.model().db
|
||||
ids = [db.id(row.row()) for row in rows]
|
||||
from calibre.gui2.metadata.bulk_download2 import start_download
|
||||
start_download(self.gui, ids,
|
||||
Dispatcher(self.bulk_metadata_downloaded), identify, covers)
|
||||
|
||||
def bulk_metadata_downloaded(self, job):
|
||||
if job.failed:
|
||||
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
|
||||
return
|
||||
from calibre.gui2.metadata.bulk_download2 import proceed
|
||||
proceed(self.gui, job)
|
||||
|
||||
def download_metadata_old(self, checked, covers=True, set_metadata=True,
|
||||
set_social_metadata=None):
|
||||
rows = self.gui.library_view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
|
39
src/calibre/gui2/actions/store.py
Normal file
39
src/calibre/gui2/actions/store.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- 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'
|
||||
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu
|
||||
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
class StoreAction(InterfaceAction):
|
||||
|
||||
name = 'Store'
|
||||
action_spec = (_('Get books'), 'store.png', None, None)
|
||||
|
||||
def genesis(self):
|
||||
self.qaction.triggered.connect(self.search)
|
||||
self.store_menu = QMenu()
|
||||
self.load_menu()
|
||||
|
||||
def load_menu(self):
|
||||
self.store_menu.clear()
|
||||
self.store_menu.addAction(_('Search'), self.search)
|
||||
self.store_menu.addSeparator()
|
||||
for n, p in self.gui.istores.items():
|
||||
self.store_menu.addAction(n, partial(self.open_store, p))
|
||||
self.qaction.setMenu(self.store_menu)
|
||||
|
||||
def search(self):
|
||||
from calibre.gui2.store.search import SearchDialog
|
||||
sd = SearchDialog(self.gui.istores, self.gui)
|
||||
sd.exec_()
|
||||
|
||||
def open_store(self, store_plugin):
|
||||
store_plugin.open(self.gui)
|
@ -6,9 +6,8 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, time
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QMenu
|
||||
from PyQt4.Qt import Qt, QMenu, QAction, pyqtSignal
|
||||
|
||||
from calibre.constants import isosx
|
||||
from calibre.gui2 import error_dialog, Dispatcher, question_dialog, config, \
|
||||
@ -18,6 +17,19 @@ from calibre.utils.config import prefs
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
class HistoryAction(QAction):
|
||||
|
||||
view_historical = pyqtSignal(object)
|
||||
|
||||
def __init__(self, id_, title, parent):
|
||||
QAction.__init__(self, title, parent)
|
||||
self.id = id_
|
||||
self.triggered.connect(self._triggered)
|
||||
|
||||
def _triggered(self):
|
||||
self.view_historical.emit(self.id)
|
||||
|
||||
|
||||
class ViewAction(InterfaceAction):
|
||||
|
||||
name = 'View'
|
||||
@ -28,18 +40,51 @@ class ViewAction(InterfaceAction):
|
||||
self.persistent_files = []
|
||||
self.qaction.triggered.connect(self.view_book)
|
||||
self.view_menu = QMenu()
|
||||
self.view_menu.addAction(_('View'), partial(self.view_book, False))
|
||||
ac = self.view_menu.addAction(_('View specific format'))
|
||||
ac.setShortcut((Qt.ControlModifier if isosx else Qt.AltModifier)+Qt.Key_V)
|
||||
ac = self.view_specific_action = QAction(_('View specific format'),
|
||||
self.gui)
|
||||
self.qaction.setMenu(self.view_menu)
|
||||
ac.setShortcut(Qt.AltModifier+Qt.Key_V)
|
||||
ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection)
|
||||
|
||||
self.view_menu.addSeparator()
|
||||
ac = self.view_action = QAction(self.qaction.icon(),
|
||||
self.qaction.text(), self.gui)
|
||||
ac.triggered.connect(self.view_book)
|
||||
ac = self.create_action(spec=(_('Read a random book'), 'catalog.png',
|
||||
None, None), attr='action_pick_random')
|
||||
ac.triggered.connect(self.view_random)
|
||||
self.view_menu.addAction(ac)
|
||||
ac = self.clear_history_action = QAction(
|
||||
_('Clear recently viewed list'), self.gui)
|
||||
ac.triggered.connect(self.clear_history)
|
||||
|
||||
def initialization_complete(self):
|
||||
self.build_menus(self.gui.current_db)
|
||||
|
||||
def build_menus(self, db):
|
||||
self.view_menu.clear()
|
||||
self.view_menu.addAction(self.qaction)
|
||||
self.view_menu.addAction(self.view_specific_action)
|
||||
self.view_menu.addSeparator()
|
||||
self.view_menu.addAction(self.action_pick_random)
|
||||
self.history_actions = []
|
||||
history = db.prefs.get('gui_view_history', [])
|
||||
if history:
|
||||
self.view_menu.addSeparator()
|
||||
for id_, title in history:
|
||||
ac = HistoryAction(id_, title, self.view_menu)
|
||||
self.view_menu.addAction(ac)
|
||||
ac.view_historical.connect(self.view_historical)
|
||||
self.view_menu.addSeparator()
|
||||
self.view_menu.addAction(self.clear_history_action)
|
||||
|
||||
def clear_history(self):
|
||||
db = self.gui.current_db
|
||||
db.prefs['gui_view_history'] = []
|
||||
self.build_menus(db)
|
||||
|
||||
def view_historical(self, id_):
|
||||
self._view_calibre_books([id_])
|
||||
|
||||
def library_changed(self, db):
|
||||
self.build_menus(db)
|
||||
|
||||
def location_selected(self, loc):
|
||||
enabled = loc == 'library'
|
||||
@ -47,15 +92,17 @@ class ViewAction(InterfaceAction):
|
||||
action.setEnabled(enabled)
|
||||
|
||||
def view_format(self, row, format):
|
||||
fmt_path = self.gui.library_view.model().db.format_abspath(row, format)
|
||||
if fmt_path:
|
||||
self._view_file(fmt_path)
|
||||
id_ = self.gui.library_view.model().id(row)
|
||||
self.view_format_by_id(id_, format)
|
||||
|
||||
def view_format_by_id(self, id_, format):
|
||||
fmt_path = self.gui.library_view.model().db.format_abspath(id_, format,
|
||||
db = self.gui.current_db
|
||||
fmt_path = db.format_abspath(id_, format,
|
||||
index_is_id=True)
|
||||
if fmt_path:
|
||||
title = db.title(id_, index_is_id=True)
|
||||
self._view_file(fmt_path)
|
||||
self.update_history([(id_, title)])
|
||||
|
||||
def book_downloaded_for_viewing(self, job):
|
||||
if job.failed:
|
||||
@ -162,6 +209,54 @@ class ViewAction(InterfaceAction):
|
||||
self.gui.iactions['Choose Library'].pick_random()
|
||||
self._view_books([self.gui.library_view.currentIndex()])
|
||||
|
||||
def _view_calibre_books(self, ids):
|
||||
db = self.gui.current_db
|
||||
views = []
|
||||
for id_ in ids:
|
||||
try:
|
||||
formats = db.formats(id_, index_is_id=True)
|
||||
except:
|
||||
error_dialog(self.gui, _('Cannot view'),
|
||||
_('This book no longer exists in your library'), show=True)
|
||||
self.update_history([], remove=set([id_]))
|
||||
continue
|
||||
|
||||
title = db.title(id_, index_is_id=True)
|
||||
if not formats:
|
||||
error_dialog(self.gui, _('Cannot view'),
|
||||
_('%s has no available formats.')%(title,), show=True)
|
||||
continue
|
||||
|
||||
formats = formats.upper().split(',')
|
||||
|
||||
fmt = formats[0]
|
||||
for format in prefs['input_format_order']:
|
||||
if format in formats:
|
||||
fmt = format
|
||||
break
|
||||
views.append((id_, title))
|
||||
self.view_format_by_id(id_, fmt)
|
||||
|
||||
self.update_history(views)
|
||||
|
||||
def update_history(self, views, remove=frozenset()):
|
||||
db = self.gui.current_db
|
||||
if views:
|
||||
seen = set()
|
||||
history = []
|
||||
for id_, title in views + db.prefs.get('gui_view_history', []):
|
||||
if title not in seen:
|
||||
seen.add(title)
|
||||
history.append((id_, title))
|
||||
|
||||
db.prefs['gui_view_history'] = history[:10]
|
||||
self.build_menus(db)
|
||||
if remove:
|
||||
history = db.prefs.get('gui_view_history', [])
|
||||
history = [x for x in history if x[0] not in remove]
|
||||
db.prefs['gui_view_history'] = history[:10]
|
||||
self.build_menus(db)
|
||||
|
||||
def _view_books(self, rows):
|
||||
if not rows or len(rows) == 0:
|
||||
self._launch_viewer()
|
||||
@ -171,28 +266,8 @@ class ViewAction(InterfaceAction):
|
||||
return
|
||||
|
||||
if self.gui.current_view() is self.gui.library_view:
|
||||
for row in rows:
|
||||
if hasattr(row, 'row'):
|
||||
row = row.row()
|
||||
|
||||
formats = self.gui.library_view.model().db.formats(row)
|
||||
title = self.gui.library_view.model().db.title(row)
|
||||
if not formats:
|
||||
error_dialog(self.gui, _('Cannot view'),
|
||||
_('%s has no available formats.')%(title,), show=True)
|
||||
continue
|
||||
|
||||
formats = formats.upper().split(',')
|
||||
|
||||
|
||||
in_prefs = False
|
||||
for format in prefs['input_format_order']:
|
||||
if format in formats:
|
||||
in_prefs = True
|
||||
self.view_format(row, format)
|
||||
break
|
||||
if not in_prefs:
|
||||
self.view_format(row, formats[0])
|
||||
ids = list(map(self.gui.library_view.model().id, rows))
|
||||
self._view_calibre_books(ids)
|
||||
else:
|
||||
paths = self.gui.current_view().model().paths(rows)
|
||||
for path in paths:
|
||||
|
@ -483,8 +483,15 @@ class BookDetails(QWidget): # {{{
|
||||
self.book_info.show_data(data)
|
||||
self.cover_view.show_data(data)
|
||||
self._layout.do_layout(self.rect())
|
||||
self.setToolTip('<p>'+_('Double-click to open Book Details window') +
|
||||
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
|
||||
try:
|
||||
sz = self.cover_view.pixmap.size()
|
||||
except:
|
||||
sz = QSize(0, 0)
|
||||
self.setToolTip(
|
||||
'<p>'+_('Double-click to open Book Details window') +
|
||||
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), '') +
|
||||
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
|
||||
)
|
||||
|
||||
def reset_info(self):
|
||||
self.show_data({})
|
||||
|
@ -193,7 +193,10 @@ class PluginWidget(QWidget,Ui_Form):
|
||||
opts_dict['header_note_source_field'] = self.header_note_source_field_name
|
||||
|
||||
# Append the output profile
|
||||
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
|
||||
try:
|
||||
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
|
||||
except:
|
||||
opts_dict['output_profile'] = ['default']
|
||||
if False:
|
||||
print "opts_dict"
|
||||
for opt in sorted(opts_dict.keys()):
|
||||
|
@ -109,6 +109,8 @@ class BookInfo(QDialog, Ui_BookInfo):
|
||||
pixmap = pixmap.scaled(new_width, new_height,
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.cover.set_pixmap(pixmap)
|
||||
sz = pixmap.size()
|
||||
self.cover.setToolTip(_('Cover size: %dx%d')%(sz.width(), sz.height()))
|
||||
|
||||
def refresh(self, row):
|
||||
if isinstance(row, QModelIndex):
|
||||
|
@ -1,7 +1,8 @@
|
||||
<ui version="4.0" >
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog" >
|
||||
<property name="geometry" >
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
@ -9,38 +10,41 @@
|
||||
<height>462</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle" >
|
||||
<property name="windowTitle">
|
||||
<string>Details of job</string>
|
||||
</property>
|
||||
<property name="windowIcon" >
|
||||
<iconset resource="../../../../resources/images.qrc" >
|
||||
<property name="windowIcon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/view.png</normaloff>:/images/view.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" >
|
||||
<item row="0" column="0" >
|
||||
<widget class="QPlainTextEdit" name="log" >
|
||||
<property name="undoRedoEnabled" >
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QPlainTextEdit" name="log">
|
||||
<property name="undoRedoEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="lineWrapMode" >
|
||||
<property name="lineWrapMode">
|
||||
<enum>QPlainTextEdit::NoWrap</enum>
|
||||
</property>
|
||||
<property name="readOnly" >
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" >
|
||||
<widget class="QDialogButtonBox" name="buttonBox" >
|
||||
<property name="standardButtons" >
|
||||
<item row="2" column="0">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QTextBrowser" name="tb"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc" />
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
@ -49,11 +53,11 @@
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel" >
|
||||
<hint type="sourcelabel">
|
||||
<x>617</x>
|
||||
<y>442</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel" >
|
||||
<hint type="destinationlabel">
|
||||
<x>206</x>
|
||||
<y>-5</y>
|
||||
</hint>
|
||||
|
@ -519,6 +519,8 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
||||
val = [] if fm['is_multiple'] else ['']
|
||||
elif not fm['is_multiple']:
|
||||
val = [val]
|
||||
elif fm['datatype'] == 'composite':
|
||||
val = [v.strip() for v in val.split(fm['is_multiple'])]
|
||||
elif field == 'authors':
|
||||
val = [v.replace('|', ',') for v in val]
|
||||
else:
|
||||
|
@ -90,7 +90,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -68,7 +68,7 @@ class DaysOfWeek(Base):
|
||||
def initialize(self, typ=None, val=None):
|
||||
if typ is None:
|
||||
typ = 'day/time'
|
||||
val = (-1, 9, 0)
|
||||
val = (-1, 6, 0)
|
||||
if typ == 'day/time':
|
||||
val = convert_day_time_schedule(val)
|
||||
|
||||
@ -118,7 +118,7 @@ class DaysOfMonth(Base):
|
||||
|
||||
def initialize(self, typ=None, val=None):
|
||||
if val is None:
|
||||
val = ((1,), 9, 0)
|
||||
val = ((1,), 6, 0)
|
||||
days_of_month, hour, minute = val
|
||||
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
||||
self.time.setTime(QTime(hour, minute))
|
||||
@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
if d < timedelta(days=366):
|
||||
ld_text = tm
|
||||
else:
|
||||
typ, sch = 'day/time', (-1, 9, 0)
|
||||
typ, sch = 'day/time', (-1, 6, 0)
|
||||
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
|
||||
'interval':2}[typ]
|
||||
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
||||
|
@ -79,7 +79,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
106
src/calibre/gui2/ebook_download.py
Normal file
106
src/calibre/gui2/ebook_download.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- 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 os
|
||||
import shutil
|
||||
from contextlib import closing
|
||||
from mechanize import MozillaCookieJar
|
||||
|
||||
from calibre import browser, get_download_filename
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.gui2 import Dispatcher
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class EbookDownload(object):
|
||||
|
||||
def __call__(self, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], log=None, abort=None, notifications=None):
|
||||
dfilename = ''
|
||||
try:
|
||||
dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib)
|
||||
self._add(dfilename, gui, add_to_lib, tags)
|
||||
self._save_as(dfilename, save_loc)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
try:
|
||||
if dfilename:
|
||||
os.remove(dfilename)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _download(self, cookie_file, url, filename, save_loc, add_to_lib):
|
||||
dfilename = ''
|
||||
|
||||
if not url:
|
||||
raise Exception(_('No file specified to download.'))
|
||||
if not save_loc and not add_to_lib:
|
||||
# Nothing to do.
|
||||
return dfilename
|
||||
|
||||
if not filename:
|
||||
filename = get_download_filename(url, cookie_file)
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
with closing(br.open(url)) as r:
|
||||
tf = PersistentTemporaryFile(suffix=filename)
|
||||
tf.write(r.read())
|
||||
dfilename = tf.name
|
||||
|
||||
return dfilename
|
||||
|
||||
def _add(self, filename, gui, add_to_lib, tags):
|
||||
if not add_to_lib or not filename:
|
||||
return
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in BOOK_EXTENSIONS:
|
||||
raise Exception(_('Not a support ebook format.'))
|
||||
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
with open(filename) as f:
|
||||
mi = get_metadata(f, ext)
|
||||
mi.tags.extend(tags)
|
||||
|
||||
id = gui.library_view.model().db.create_book_entry(mi)
|
||||
gui.library_view.model().db.add_format_with_hooks(id, ext.upper(), filename, index_is_id=True)
|
||||
gui.library_view.model().books_added(1)
|
||||
gui.library_view.model().count_changed()
|
||||
|
||||
def _save_as(self, dfilename, save_loc):
|
||||
if not save_loc or not dfilename:
|
||||
return
|
||||
shutil.copy(dfilename, save_loc)
|
||||
|
||||
|
||||
gui_ebook_download = EbookDownload()
|
||||
|
||||
def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
description = _('Downloading %s') % filename if filename else url
|
||||
job = ThreadedJob('ebook_download', description, gui_ebook_download, (gui, cookie_file, url, filename, save_loc, add_to_lib, tags), {}, callback, max_concurrent_count=2, killable=False)
|
||||
job_manager.run_threaded_job(job)
|
||||
|
||||
|
||||
class EbookDownloadMixin(object):
|
||||
|
||||
def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
if tags:
|
||||
if isinstance(tags, basestring):
|
||||
tags = tags.split(',')
|
||||
start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags)
|
||||
self.status_bar.show_message(_('Downloading') + ' ' + filename if filename else url, 3000)
|
||||
|
||||
def downloaded_ebook(self, job):
|
||||
if job.failed:
|
||||
self.job_exception(job, dialog_title=_('Failed to download ebook'))
|
||||
return
|
||||
|
||||
self.status_bar.show_message(job.description + ' ' + _('finished'), 5000)
|
@ -6,9 +6,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, socket, time, cStringIO
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
import os, socket, time
|
||||
from binascii import unhexlify
|
||||
from functools import partial
|
||||
from itertools import repeat
|
||||
@ -16,67 +14,20 @@ from itertools import repeat
|
||||
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
||||
config as email_config
|
||||
from calibre.utils.filenames import ascii_filename
|
||||
from calibre.utils.ipc.job import BaseJob
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.customize.ui import available_input_formats, available_output_formats
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.gui2 import config, Dispatcher, warning_dialog
|
||||
from calibre.library.save_to_disk import get_components
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
|
||||
class EmailJob(BaseJob): # {{{
|
||||
|
||||
def __init__(self, callback, description, attachment, aname, to, subject, text, job_manager):
|
||||
BaseJob.__init__(self, description)
|
||||
self.exception = None
|
||||
self.job_manager = job_manager
|
||||
self.email_args = (attachment, aname, to, subject, text)
|
||||
self.email_sent_callback = callback
|
||||
self.log_path = None
|
||||
self._log_file = cStringIO.StringIO()
|
||||
self._log_file.write(self.description.encode('utf-8') + '\n')
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
if self.log_path is not None:
|
||||
return open(self.log_path, 'rb')
|
||||
return cStringIO.StringIO(self._log_file.getvalue())
|
||||
|
||||
def start_work(self):
|
||||
self.start_time = time.time()
|
||||
self.job_manager.changed_queue.put(self)
|
||||
|
||||
def job_done(self):
|
||||
self.duration = time.time() - self.start_time
|
||||
self.percent = 1
|
||||
# Dump log onto disk
|
||||
lf = PersistentTemporaryFile('email_log')
|
||||
lf.write(self._log_file.getvalue())
|
||||
lf.close()
|
||||
self.log_path = lf.name
|
||||
self._log_file.close()
|
||||
self._log_file = None
|
||||
|
||||
self.job_manager.changed_queue.put(self)
|
||||
|
||||
def log_write(self, what):
|
||||
self._log_file.write(what)
|
||||
|
||||
# }}}
|
||||
|
||||
class Emailer(Thread): # {{{
|
||||
class Sendmail(object):
|
||||
|
||||
MAX_RETRIES = 1
|
||||
|
||||
def __init__(self, job_manager):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.jobs = Queue()
|
||||
self.job_manager = job_manager
|
||||
self._run = True
|
||||
def __init__(self):
|
||||
self.calculate_rate_limit()
|
||||
|
||||
self.last_send_time = time.time() - self.rate_limit
|
||||
|
||||
def calculate_rate_limit(self):
|
||||
@ -87,70 +38,28 @@ class Emailer(Thread): # {{{
|
||||
'gmail.com' in rh or 'live.com' in rh):
|
||||
self.rate_limit = tweaks['public_smtp_relay_delay']
|
||||
|
||||
def stop(self):
|
||||
self._run = False
|
||||
self.jobs.put(None)
|
||||
def __call__(self, attachment, aname, to, subject, text, log=None,
|
||||
abort=None, notifications=None):
|
||||
|
||||
def run(self):
|
||||
while self._run:
|
||||
try_count = 0
|
||||
while try_count <= self.MAX_RETRIES:
|
||||
if try_count > 0:
|
||||
log('\nRetrying in %d seconds...\n' %
|
||||
self.rate_limit)
|
||||
try:
|
||||
job = self.jobs.get()
|
||||
self.sendmail(attachment, aname, to, subject, text, log)
|
||||
try_count = self.MAX_RETRIES
|
||||
log('Email successfully sent')
|
||||
except:
|
||||
break
|
||||
if job is None or not self._run:
|
||||
break
|
||||
try_count = 0
|
||||
failed, exc = False, None
|
||||
job.start_work()
|
||||
if job.kill_on_start:
|
||||
job.log_write('Aborted\n')
|
||||
job.failed = failed
|
||||
job.killed = True
|
||||
job.job_done()
|
||||
continue
|
||||
if abort.is_set():
|
||||
return
|
||||
if try_count == self.MAX_RETRIES:
|
||||
raise
|
||||
log.exception('\nSending failed...\n')
|
||||
|
||||
while try_count <= self.MAX_RETRIES:
|
||||
failed = False
|
||||
if try_count > 0:
|
||||
job.log_write('\nRetrying in %d seconds...\n' %
|
||||
self.rate_limit)
|
||||
try:
|
||||
self.sendmail(job)
|
||||
break
|
||||
except Exception as e:
|
||||
if not self._run:
|
||||
return
|
||||
import traceback
|
||||
failed = True
|
||||
exc = e
|
||||
job.log_write('\nSending failed...\n')
|
||||
job.log_write(traceback.format_exc())
|
||||
try_count += 1
|
||||
|
||||
try_count += 1
|
||||
|
||||
if not self._run:
|
||||
break
|
||||
|
||||
job.failed = failed
|
||||
job.exception = exc
|
||||
job.job_done()
|
||||
try:
|
||||
job.email_sent_callback(job)
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
|
||||
texts, attachment_names):
|
||||
for name, attachment, to, subject, text, aname in zip(jobnames,
|
||||
attachments, to_s, subjects, texts, attachment_names):
|
||||
description = _('Email %s to %s') % (name, to)
|
||||
job = EmailJob(callback, description, attachment, aname, to,
|
||||
subject, text, self.job_manager)
|
||||
self.job_manager.add_job(job)
|
||||
self.jobs.put(job)
|
||||
|
||||
def sendmail(self, job):
|
||||
def sendmail(self, attachment, aname, to, subject, text, log):
|
||||
while time.time() - self.last_send_time <= self.rate_limit:
|
||||
time.sleep(1)
|
||||
try:
|
||||
@ -158,7 +67,6 @@ class Emailer(Thread): # {{{
|
||||
from_ = opts.from_
|
||||
if not from_:
|
||||
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
|
||||
attachment, aname, to, subject, text = job.email_args
|
||||
msg = compose_mail(from_, to, text, subject, open(attachment, 'rb'),
|
||||
aname)
|
||||
efrom, eto = map(extract_email_address, (from_, to))
|
||||
@ -169,48 +77,56 @@ class Emailer(Thread): # {{{
|
||||
username=opts.relay_username,
|
||||
password=unhexlify(opts.relay_password), port=opts.relay_port,
|
||||
encryption=opts.encryption,
|
||||
debug_output=partial(print, file=job._log_file))
|
||||
debug_output=log.debug)
|
||||
finally:
|
||||
self.last_send_time = time.time()
|
||||
|
||||
def email_news(self, mi, remove, get_fmts, done):
|
||||
opts = email_config().parse()
|
||||
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
|
||||
for account, x in opts.accounts.items() if x[1]]
|
||||
sent_mails = []
|
||||
for i, x in enumerate(accounts):
|
||||
account, fmts = x
|
||||
files = get_fmts(fmts)
|
||||
files = [f for f in files if f is not None]
|
||||
if not files:
|
||||
continue
|
||||
attachment = files[0]
|
||||
to_s = [account]
|
||||
subjects = [_('News:')+' '+mi.title]
|
||||
texts = [
|
||||
_('Attached is the %s periodical downloaded by calibre.')
|
||||
% (mi.title,)
|
||||
]
|
||||
attachment_names = [ascii_filename(mi.title)+os.path.splitext(attachment)[1]]
|
||||
attachments = [attachment]
|
||||
jobnames = [mi.title]
|
||||
do_remove = []
|
||||
if i == len(accounts) - 1:
|
||||
do_remove = remove
|
||||
self.send_mails(jobnames,
|
||||
Dispatcher(partial(done, remove=do_remove)),
|
||||
attachments, to_s, subjects, texts, attachment_names)
|
||||
sent_mails.append(to_s[0])
|
||||
return sent_mails
|
||||
gui_sendmail = Sendmail()
|
||||
|
||||
|
||||
# }}}
|
||||
def send_mails(jobnames, callback, attachments, to_s, subjects,
|
||||
texts, attachment_names, job_manager):
|
||||
for name, attachment, to, subject, text, aname in zip(jobnames,
|
||||
attachments, to_s, subjects, texts, attachment_names):
|
||||
description = _('Email %s to %s') % (name, to)
|
||||
job = ThreadedJob('email', description, gui_sendmail, (attachment, aname, to,
|
||||
subject, text), {}, callback, killable=False)
|
||||
job_manager.run_threaded_job(job)
|
||||
|
||||
|
||||
def email_news(mi, remove, get_fmts, done, job_manager):
|
||||
opts = email_config().parse()
|
||||
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
|
||||
for account, x in opts.accounts.items() if x[1]]
|
||||
sent_mails = []
|
||||
for i, x in enumerate(accounts):
|
||||
account, fmts = x
|
||||
files = get_fmts(fmts)
|
||||
files = [f for f in files if f is not None]
|
||||
if not files:
|
||||
continue
|
||||
attachment = files[0]
|
||||
to_s = [account]
|
||||
subjects = [_('News:')+' '+mi.title]
|
||||
texts = [
|
||||
_('Attached is the %s periodical downloaded by calibre.')
|
||||
% (mi.title,)
|
||||
]
|
||||
attachment_names = [ascii_filename(mi.title)+os.path.splitext(attachment)[1]]
|
||||
attachments = [attachment]
|
||||
jobnames = [mi.title]
|
||||
do_remove = []
|
||||
if i == len(accounts) - 1:
|
||||
do_remove = remove
|
||||
send_mails(jobnames,
|
||||
Dispatcher(partial(done, remove=do_remove)),
|
||||
attachments, to_s, subjects, texts, attachment_names,
|
||||
job_manager)
|
||||
sent_mails.append(to_s[0])
|
||||
return sent_mails
|
||||
|
||||
class EmailMixin(object): # {{{
|
||||
|
||||
def __init__(self):
|
||||
self.emailer = Emailer(self.job_manager)
|
||||
|
||||
def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None,
|
||||
do_auto_convert=True, specific_format=None):
|
||||
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
|
||||
@ -246,8 +162,7 @@ class EmailMixin(object): # {{{
|
||||
components = get_components(subject, mi, id)
|
||||
if not components:
|
||||
components = [mi.title]
|
||||
subject = os.path.join(*components)
|
||||
subjects.append(subject)
|
||||
subjects.append(os.path.join(*components))
|
||||
a = authors_to_string(mi.authors if mi.authors else \
|
||||
[_('Unknown')])
|
||||
texts.append(_('Attached, you will find the e-book') + \
|
||||
@ -262,11 +177,10 @@ class EmailMixin(object): # {{{
|
||||
|
||||
to_s = list(repeat(to, len(attachments)))
|
||||
if attachments:
|
||||
if not self.emailer.is_alive():
|
||||
self.emailer.start()
|
||||
self.emailer.send_mails(jobnames,
|
||||
send_mails(jobnames,
|
||||
Dispatcher(partial(self.email_sent, remove=remove)),
|
||||
attachments, to_s, subjects, texts, attachment_names)
|
||||
attachments, to_s, subjects, texts, attachment_names,
|
||||
self.job_manager)
|
||||
self.status_bar.show_message(_('Sending email to')+' '+to, 3000)
|
||||
|
||||
auto = []
|
||||
@ -334,10 +248,8 @@ class EmailMixin(object): # {{{
|
||||
files, auto = self.library_view.model().\
|
||||
get_preferred_formats_from_ids([id_], fmts)
|
||||
return files
|
||||
if not self.emailer.is_alive():
|
||||
self.emailer.start()
|
||||
sent_mails = self.emailer.email_news(mi, remove,
|
||||
get_fmts, self.email_sent)
|
||||
sent_mails = email_news(mi, remove,
|
||||
get_fmts, self.email_sent, self.job_manager)
|
||||
if sent_mails:
|
||||
self.status_bar.show_message(_('Sent news to')+' '+\
|
||||
', '.join(sent_mails), 3000)
|
||||
|
@ -247,6 +247,11 @@ class LayoutMixin(object): # {{{
|
||||
for x in ('cb', 'tb', 'bd'):
|
||||
button = getattr(self, x+'_splitter').button
|
||||
button.setIconSize(QSize(24, 24))
|
||||
if isosx:
|
||||
button.setStyleSheet('''
|
||||
QToolButton { background: none; border:none; padding: 0px; }
|
||||
QToolButton:checked { background: rgba(0, 0, 0, 25%); }
|
||||
''')
|
||||
self.status_bar.addPermanentWidget(button)
|
||||
self.status_bar.addPermanentWidget(self.jobs_button)
|
||||
self.setStatusBar(self.status_bar)
|
||||
|
@ -8,14 +8,13 @@ Job management.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from Queue import Empty, Queue
|
||||
|
||||
from PyQt4.Qt import QAbstractTableModel, QVariant, QModelIndex, Qt, \
|
||||
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication, \
|
||||
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame, \
|
||||
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication, QAction, \
|
||||
QByteArray
|
||||
from PyQt4.Qt import (QAbstractTableModel, QVariant, QModelIndex, Qt,
|
||||
QTimer, pyqtSignal, QIcon, QDialog, QAbstractItemDelegate, QApplication,
|
||||
QSize, QStyleOptionProgressBarV2, QString, QStyle, QToolTip, QFrame,
|
||||
QHBoxLayout, QVBoxLayout, QSizePolicy, QLabel, QCoreApplication, QAction,
|
||||
QByteArray)
|
||||
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
@ -25,8 +24,9 @@ from calibre.gui2.dialogs.jobs_ui import Ui_JobsDialog
|
||||
from calibre import __appname__
|
||||
from calibre.gui2.dialogs.job_view_ui import Ui_Dialog
|
||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||
from calibre.gui2.threaded_jobs import ThreadedJobServer, ThreadedJob
|
||||
|
||||
class JobManager(QAbstractTableModel):
|
||||
class JobManager(QAbstractTableModel): # {{{
|
||||
|
||||
job_added = pyqtSignal(int)
|
||||
job_done = pyqtSignal(int)
|
||||
@ -42,6 +42,7 @@ class JobManager(QAbstractTableModel):
|
||||
self.add_job = Dispatcher(self._add_job)
|
||||
self.server = Server(limit=int(config['worker_limit']/2.0),
|
||||
enforce_cpu_limit=config['enforce_cpu_limit'])
|
||||
self.threaded_server = ThreadedJobServer()
|
||||
self.changed_queue = Queue()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
@ -146,12 +147,21 @@ class JobManager(QAbstractTableModel):
|
||||
jobs.add(self.server.changed_jobs_queue.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
# Update device jobs
|
||||
while True:
|
||||
try:
|
||||
jobs.add(self.changed_queue.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
# Update threaded jobs
|
||||
while True:
|
||||
try:
|
||||
jobs.add(self.threaded_server.changed_jobs.get_nowait())
|
||||
except Empty:
|
||||
break
|
||||
|
||||
if jobs:
|
||||
needs_reset = False
|
||||
for job in jobs:
|
||||
@ -207,11 +217,22 @@ class JobManager(QAbstractTableModel):
|
||||
self.server.add_job(job)
|
||||
return job
|
||||
|
||||
def run_threaded_job(self, job):
|
||||
self.add_job(job)
|
||||
self.threaded_server.add_job(job)
|
||||
|
||||
def launch_gui_app(self, name, args=[], kwargs={}, description=''):
|
||||
job = ParallelJob(name, description, lambda x: x,
|
||||
args=args, kwargs=kwargs)
|
||||
self.server.run_job(job, gui=True, redirect_output=False)
|
||||
|
||||
def _kill_job(self, job):
|
||||
if isinstance(job, ParallelJob):
|
||||
self.server.kill_job(job)
|
||||
elif isinstance(job, ThreadedJob):
|
||||
self.threaded_server.kill_job(job)
|
||||
else:
|
||||
job.kill_on_start = True
|
||||
|
||||
def kill_job(self, row, view):
|
||||
job = self.jobs[row]
|
||||
@ -221,29 +242,29 @@ class JobManager(QAbstractTableModel):
|
||||
if job.duration is not None:
|
||||
return error_dialog(view, _('Cannot kill job'),
|
||||
_('Job has already run')).exec_()
|
||||
if isinstance(job, ParallelJob):
|
||||
self.server.kill_job(job)
|
||||
else:
|
||||
job.kill_on_start = True
|
||||
if not getattr(job, 'killable', True):
|
||||
return error_dialog(view, _('Cannot kill job'),
|
||||
_('This job cannot be stopped'), show=True)
|
||||
self._kill_job(job)
|
||||
|
||||
def kill_all_jobs(self):
|
||||
for job in self.jobs:
|
||||
if isinstance(job, DeviceJob) or job.duration is not None:
|
||||
if (isinstance(job, DeviceJob) or job.duration is not None or
|
||||
not getattr(job, 'killable', True)):
|
||||
continue
|
||||
if isinstance(job, ParallelJob):
|
||||
self.server.kill_job(job)
|
||||
else:
|
||||
job.kill_on_start = True
|
||||
self._kill_job(job)
|
||||
|
||||
def terminate_all_jobs(self):
|
||||
self.server.killall()
|
||||
for job in self.jobs:
|
||||
if isinstance(job, DeviceJob) or job.duration is not None:
|
||||
if (isinstance(job, DeviceJob) or job.duration is not None or
|
||||
not getattr(job, 'killable', True)):
|
||||
continue
|
||||
if not isinstance(job, ParallelJob):
|
||||
job.kill_on_start = True
|
||||
|
||||
self._kill_job(job)
|
||||
# }}}
|
||||
|
||||
# Jobs UI {{{
|
||||
class ProgressBarDelegate(QAbstractItemDelegate):
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
@ -269,6 +290,11 @@ class DetailView(QDialog, Ui_Dialog):
|
||||
self.setupUi(self)
|
||||
self.setWindowTitle(job.description)
|
||||
self.job = job
|
||||
self.html_view = hasattr(job, 'html_details')
|
||||
if self.html_view:
|
||||
self.log.setVisible(False)
|
||||
else:
|
||||
self.tb.setVisible(False)
|
||||
self.next_pos = 0
|
||||
self.update()
|
||||
self.timer = QTimer(self)
|
||||
@ -277,12 +303,19 @@ class DetailView(QDialog, Ui_Dialog):
|
||||
|
||||
|
||||
def update(self):
|
||||
f = self.job.log_file
|
||||
f.seek(self.next_pos)
|
||||
more = f.read()
|
||||
self.next_pos = f.tell()
|
||||
if more:
|
||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
||||
if self.html_view:
|
||||
html = self.job.html_details
|
||||
if len(html) > self.next_pos:
|
||||
self.next_pos = len(html)
|
||||
self.tb.setHtml(
|
||||
'<pre style="font-family:monospace">%s</pre>'%html)
|
||||
else:
|
||||
f = self.job.log_file
|
||||
f.seek(self.next_pos)
|
||||
more = f.read()
|
||||
self.next_pos = f.tell()
|
||||
if more:
|
||||
self.log.appendPlainText(more.decode('utf-8', 'replace'))
|
||||
|
||||
class JobsButton(QFrame):
|
||||
|
||||
@ -441,3 +474,5 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
def hide(self, *args):
|
||||
self.save_state()
|
||||
return QDialog.hide(self, *args)
|
||||
# }}}
|
||||
|
||||
|
@ -196,11 +196,9 @@ class SearchBar(QWidget): # {{{
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Reset Quick Search"))
|
||||
|
||||
x = parent.search_options_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I('config.png')))
|
||||
x.setObjectName("search_option_button")
|
||||
x = parent.highlight_only_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I('arrow-down.png')))
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Change the way searching for books works"))
|
||||
|
||||
x = parent.saved_search = SavedSearchBox(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
@ -220,13 +218,6 @@ class SearchBar(QWidget): # {{{
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Save current search under the name shown in the box"))
|
||||
|
||||
x = parent.delete_search_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I("search_delete_saved.png")))
|
||||
x.setObjectName("delete_search_button")
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Delete current saved search"))
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class Spacer(QWidget): # {{{
|
||||
@ -326,6 +317,8 @@ class BaseToolBar(QToolBar): # {{{
|
||||
QToolBar.resizeEvent(self, ev)
|
||||
style = self.get_text_style()
|
||||
self.setToolButtonStyle(style)
|
||||
if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'):
|
||||
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
|
||||
|
||||
def get_text_style(self):
|
||||
style = Qt.ToolButtonTextUnderIcon
|
||||
@ -408,6 +401,10 @@ class ToolBar(BaseToolBar): # {{{
|
||||
self.d_widget.layout().addWidget(self.donate_button)
|
||||
if isosx:
|
||||
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
||||
self.d_widget.layout().setContentsMargins(0,0,0,0)
|
||||
self.d_widget.setContentsMargins(0,0,0,0)
|
||||
self.d_widget.filler = QLabel(u'\u00a0')
|
||||
self.d_widget.layout().addWidget(self.d_widget.filler)
|
||||
bar.addWidget(self.d_widget)
|
||||
self.showing_donate = True
|
||||
elif what in self.gui.iactions:
|
||||
|
@ -310,10 +310,17 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
def sort(self, col, order, reset=True):
|
||||
if not self.db:
|
||||
return
|
||||
self.about_to_be_sorted.emit(self.db.id)
|
||||
if not isinstance(order, bool):
|
||||
order = order == Qt.AscendingOrder
|
||||
label = self.column_map[col]
|
||||
self._sort(label, order, reset)
|
||||
|
||||
def sort_by_named_field(self, field, order, reset=True):
|
||||
if field in self.db.field_metadata.keys():
|
||||
self._sort(field, order, reset)
|
||||
|
||||
def _sort(self, label, order, reset):
|
||||
self.about_to_be_sorted.emit(self.db.id)
|
||||
self.db.sort(label, order)
|
||||
if reset:
|
||||
self.reset()
|
||||
@ -604,7 +611,10 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
def size(r, idx=-1):
|
||||
size = self.db.data[r][idx]
|
||||
if size:
|
||||
return QVariant('%.1f'%(float(size)/(1024*1024)))
|
||||
ans = '%.1f'%(float(size)/(1024*1024))
|
||||
if size > 0 and ans == '0.0':
|
||||
ans = '<0.1'
|
||||
return QVariant(ans)
|
||||
return None
|
||||
|
||||
def rating_type(r, idx=-1):
|
||||
|
@ -236,6 +236,46 @@ class BooksView(QTableView): # {{{
|
||||
sm.select(idx, sm.Select|sm.Rows)
|
||||
self.scroll_to_row(indices[0].row())
|
||||
self.selected_ids = []
|
||||
|
||||
def sort_by_named_field(self, field, order, reset=True):
|
||||
if field in self.column_map:
|
||||
idx = self.column_map.index(field)
|
||||
if order:
|
||||
self.sortByColumn(idx, Qt.AscendingOrder)
|
||||
else:
|
||||
self.sortByColumn(idx, Qt.DescendingOrder)
|
||||
else:
|
||||
self._model.sort_by_named_field(field, order, reset)
|
||||
|
||||
def multisort(self, fields, reset=True, only_if_different=False):
|
||||
if len(fields) == 0:
|
||||
return
|
||||
sh = self.cleanup_sort_history(self._model.sort_history,
|
||||
ignore_column_map=True)
|
||||
if only_if_different and len(sh) >= len(fields):
|
||||
ret=True
|
||||
for i,t in enumerate(fields):
|
||||
if t[0] != sh[i][0]:
|
||||
ret = False
|
||||
break
|
||||
if ret:
|
||||
return
|
||||
|
||||
for n,d in reversed(fields):
|
||||
if n in self._model.db.field_metadata.keys():
|
||||
sh.insert(0, (n, d))
|
||||
sh = self.cleanup_sort_history(sh, ignore_column_map=True)
|
||||
self._model.sort_history = [tuple(x) for x in sh]
|
||||
self._model.resort(reset=reset)
|
||||
col = fields[0][0]
|
||||
dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder
|
||||
if col in self.column_map:
|
||||
col = self.column_map.index(col)
|
||||
hdrs = self.horizontalHeader()
|
||||
try:
|
||||
hdrs.setSortIndicator(col, dir)
|
||||
except:
|
||||
pass
|
||||
# }}}
|
||||
|
||||
# Ondevice column {{{
|
||||
@ -280,14 +320,14 @@ class BooksView(QTableView): # {{{
|
||||
state = self.get_state()
|
||||
self.write_state(state)
|
||||
|
||||
def cleanup_sort_history(self, sort_history):
|
||||
def cleanup_sort_history(self, sort_history, ignore_column_map=False):
|
||||
history = []
|
||||
for col, order in sort_history:
|
||||
if not isinstance(order, bool):
|
||||
continue
|
||||
if col == 'date':
|
||||
col = 'timestamp'
|
||||
if col in self.column_map:
|
||||
if ignore_column_map or col in self.column_map:
|
||||
if (not history or history[-1][0] != col):
|
||||
history.append([col, order])
|
||||
return history
|
||||
@ -621,7 +661,7 @@ class BooksView(QTableView): # {{{
|
||||
h = self.horizontalHeader()
|
||||
for i in range(h.count()):
|
||||
if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0:
|
||||
self.scrollTo(self.model().index(row, i))
|
||||
self.scrollTo(self.model().index(row, i), self.PositionAtCenter)
|
||||
break
|
||||
|
||||
def set_current_row(self, row, select=True):
|
||||
@ -703,6 +743,8 @@ class BooksView(QTableView): # {{{
|
||||
id_to_select = self._model.get_current_highlighted_id()
|
||||
if id_to_select is not None:
|
||||
self.select_rows([id_to_select], using_ids=True)
|
||||
elif self._model.highlight_only:
|
||||
self.clearSelection()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def connect_to_search_box(self, sb, search_done):
|
||||
|
@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap, re, os
|
||||
|
||||
from PyQt4.Qt import (Qt, QDateEdit, QDate,
|
||||
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal,
|
||||
QIcon, QToolButton, QWidget, QLabel, QGridLayout,
|
||||
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
|
||||
QPushButton, QSpinBox, QLineEdit, QSizePolicy)
|
||||
@ -172,6 +172,7 @@ class AuthorsEdit(MultiCompleteComboBox):
|
||||
self.books_to_refresh = set([])
|
||||
all_authors = db.all_authors()
|
||||
all_authors.sort(key=lambda x : sort_key(x[1]))
|
||||
self.clear()
|
||||
for i in all_authors:
|
||||
id, name = i
|
||||
name = [name.strip().replace('|', ',') for n in name.split(',')]
|
||||
@ -221,7 +222,8 @@ class AuthorSortEdit(EnLineEdit):
|
||||
'red, then the authors and this text do not match.')
|
||||
LABEL = _('Author s&ort:')
|
||||
|
||||
def __init__(self, parent, authors_edit, autogen_button, db):
|
||||
def __init__(self, parent, authors_edit, autogen_button, db,
|
||||
copy_a_to_as_action, copy_as_to_a_action):
|
||||
EnLineEdit.__init__(self, parent)
|
||||
self.authors_edit = authors_edit
|
||||
self.db = db
|
||||
@ -240,6 +242,8 @@ class AuthorSortEdit(EnLineEdit):
|
||||
self.textChanged.connect(self.update_state)
|
||||
|
||||
autogen_button.clicked.connect(self.auto_generate)
|
||||
copy_a_to_as_action.triggered.connect(self.auto_generate)
|
||||
copy_as_to_a_action.triggered.connect(self.copy_to_authors)
|
||||
self.update_state()
|
||||
|
||||
@dynamic_property
|
||||
@ -272,6 +276,14 @@ class AuthorSortEdit(EnLineEdit):
|
||||
self.setToolTip(tt)
|
||||
self.setWhatsThis(tt)
|
||||
|
||||
def copy_to_authors(self):
|
||||
aus = self.current_val
|
||||
if aus:
|
||||
ln, _, rest = aus.partition(',')
|
||||
if rest:
|
||||
au = rest.strip() + ' ' + ln.strip()
|
||||
self.authors_edit.current_val = [au]
|
||||
|
||||
def auto_generate(self, *args):
|
||||
au = unicode(self.authors_edit.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
@ -315,7 +327,7 @@ class SeriesEdit(MultiCompleteComboBox):
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -326,6 +338,7 @@ class SeriesEdit(MultiCompleteComboBox):
|
||||
self.update_items_cache([x[1] for x in all_series])
|
||||
series_id = db.series_id(id_, index_is_id=True)
|
||||
idx, c = None, 0
|
||||
self.clear()
|
||||
for i in all_series:
|
||||
id, name = i
|
||||
if id == series_id:
|
||||
@ -613,6 +626,8 @@ class FormatsManager(QWidget): # {{{
|
||||
|
||||
class Cover(ImageView): # {{{
|
||||
|
||||
download_cover = pyqtSignal()
|
||||
|
||||
def __init__(self, parent):
|
||||
ImageView.__init__(self, parent)
|
||||
self.dialog = parent
|
||||
@ -703,9 +718,6 @@ class Cover(ImageView): # {{{
|
||||
cdata = im.export('png')
|
||||
self.current_val = cdata
|
||||
|
||||
def download_cover(self, *args):
|
||||
pass # TODO: Implement this
|
||||
|
||||
def generate_cover(self, *args):
|
||||
from calibre.ebooks import calibre_cover
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
@ -845,7 +857,7 @@ class RatingEdit(QSpinBox): # {{{
|
||||
class TagsEdit(MultiCompleteLineEdit): # {{{
|
||||
LABEL = _('Ta&gs:')
|
||||
TOOLTIP = '<p>'+_('Tags categorize the book. This is particularly '
|
||||
'useful while searching. <br><br>They can be any words'
|
||||
'useful while searching. <br><br>They can be any words '
|
||||
'or phrases, separated by commas.')
|
||||
|
||||
def __init__(self, parent):
|
||||
@ -862,6 +874,7 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
|
||||
if not val:
|
||||
val = []
|
||||
self.setText(', '.join([x.strip() for x in val]))
|
||||
self.setCursorPosition(0)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
@ -928,6 +941,7 @@ class IdentifiersEdit(QLineEdit): # {{{
|
||||
val = {}
|
||||
txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()])
|
||||
self.setText(txt.strip())
|
||||
self.setCursorPosition(0)
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
def initialize(self, db, id_):
|
||||
@ -977,7 +991,7 @@ class PublisherEdit(MultiCompleteComboBox): # {{{
|
||||
if not val:
|
||||
val = ''
|
||||
self.setEditText(val.strip())
|
||||
self.setCursorPosition(0)
|
||||
self.lineEdit().setCursorPosition(0)
|
||||
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
@ -987,13 +1001,13 @@ class PublisherEdit(MultiCompleteComboBox): # {{{
|
||||
all_publishers.sort(key=lambda x : sort_key(x[1]))
|
||||
self.update_items_cache([x[1] for x in all_publishers])
|
||||
publisher_id = db.publisher_id(id_, index_is_id=True)
|
||||
idx, c = None, 0
|
||||
for i in all_publishers:
|
||||
id, name = i
|
||||
if id == publisher_id:
|
||||
idx = c
|
||||
idx = None
|
||||
self.clear()
|
||||
for i, x in enumerate(all_publishers):
|
||||
id_, name = x
|
||||
if id_ == publisher_id:
|
||||
idx = i
|
||||
self.addItem(name)
|
||||
c += 1
|
||||
|
||||
self.setEditText('')
|
||||
if idx is not None:
|
||||
|
320
src/calibre/gui2/metadata/bulk_download2.py
Normal file
320
src/calibre/gui2/metadata/bulk_download2.py
Normal file
@ -0,0 +1,320 @@
|
||||
#!/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
|
||||
from functools import partial
|
||||
from itertools import izip
|
||||
|
||||
from PyQt4.Qt import (QIcon, QDialog, QVBoxLayout, QTextBrowser, QSize,
|
||||
QDialogButtonBox, QApplication, QTimer, QLabel, QProgressBar)
|
||||
|
||||
from calibre.gui2.dialogs.message_box import MessageBox
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
from calibre.utils.icu import lower
|
||||
from calibre.ebooks.metadata import authors_to_string
|
||||
from calibre.gui2 import question_dialog, error_dialog
|
||||
from calibre.ebooks.metadata.sources.identify import identify, msprefs
|
||||
from calibre.ebooks.metadata.sources.covers import download_cover
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.customize.ui import metadata_plugins
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
def show_config(gui, parent):
|
||||
from calibre.gui2.preferences import show_config_widget
|
||||
show_config_widget('Sharing', 'Metadata download', parent=parent,
|
||||
gui=gui, never_shutdown=True)
|
||||
|
||||
def start_download(gui, ids, callback, identify, covers):
|
||||
q = MessageBox(MessageBox.QUESTION, _('Schedule download?'),
|
||||
'<p>'+_('The download of metadata for the <b>%d selected book(s)</b> will'
|
||||
' run in the background. Proceed?')%len(ids) +
|
||||
'<p>'+_('You can monitor the progress of the download '
|
||||
'by clicking the rotating spinner in the bottom right '
|
||||
'corner.') +
|
||||
'<p>'+_('When the download completes you will be asked for'
|
||||
' confirmation before calibre applies the downloaded metadata.'),
|
||||
show_copy_button=False, parent=gui)
|
||||
b = q.bb.addButton(_('Configure download'), q.bb.ActionRole)
|
||||
b.setIcon(QIcon(I('config.png')))
|
||||
b.clicked.connect(partial(show_config, gui, q))
|
||||
q.det_msg_toggle.setVisible(False)
|
||||
|
||||
ret = q.exec_()
|
||||
b.clicked.disconnect()
|
||||
if ret != q.Accepted:
|
||||
return
|
||||
|
||||
job = ThreadedJob('metadata bulk download',
|
||||
_('Download metadata for %d books')%len(ids),
|
||||
download, (ids, gui.current_db, identify, covers), {}, callback)
|
||||
gui.job_manager.run_threaded_job(job)
|
||||
gui.status_bar.show_message(_('Metadata download started'), 3000)
|
||||
|
||||
|
||||
class ViewLog(QDialog): # {{{
|
||||
|
||||
def __init__(self, html, parent=None):
|
||||
QDialog.__init__(self, parent)
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.tb = QTextBrowser(self)
|
||||
self.tb.setHtml('<pre style="font-family: monospace">%s</pre>' % html)
|
||||
l.addWidget(self.tb)
|
||||
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
|
||||
self.bb.ActionRole)
|
||||
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
|
||||
self.copy_button.clicked.connect(self.copy_to_clipboard)
|
||||
l.addWidget(self.bb)
|
||||
self.setModal(False)
|
||||
self.resize(QSize(700, 500))
|
||||
self.setWindowTitle(_('Download log'))
|
||||
self.setWindowIcon(QIcon(I('debug.png')))
|
||||
self.show()
|
||||
|
||||
def copy_to_clipboard(self):
|
||||
txt = self.tb.toPlainText()
|
||||
QApplication.clipboard().setText(txt)
|
||||
|
||||
_vl = None
|
||||
def view_log(job, parent):
|
||||
global _vl
|
||||
_vl = ViewLog(job.html_details, parent)
|
||||
|
||||
# }}}
|
||||
|
||||
class ApplyDialog(QDialog):
|
||||
|
||||
def __init__(self, id_map, gui):
|
||||
QDialog.__init__(self, gui)
|
||||
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
l.addWidget(QLabel(_('Applying downloaded metadata to your library')))
|
||||
|
||||
self.pb = QProgressBar(self)
|
||||
l.addWidget(self.pb)
|
||||
self.pb.setMinimum(0)
|
||||
self.pb.setMaximum(len(id_map))
|
||||
|
||||
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||
self.bb.rejected.connect(self.reject)
|
||||
self.bb.accepted.connect(self.accept)
|
||||
l.addWidget(self.bb)
|
||||
|
||||
self.gui = gui
|
||||
self.id_map = list(id_map.iteritems())
|
||||
self.current_idx = 0
|
||||
|
||||
self.failures = []
|
||||
self.ids = []
|
||||
self.canceled = False
|
||||
|
||||
QTimer.singleShot(20, self.do_one)
|
||||
|
||||
def do_one(self):
|
||||
if self.canceled:
|
||||
return
|
||||
i, mi = self.id_map[self.current_idx]
|
||||
db = self.gui.current_db
|
||||
try:
|
||||
set_title = not mi.is_null('title')
|
||||
set_authors = not mi.is_null('authors')
|
||||
db.set_metadata(i, mi, commit=False, set_title=set_title,
|
||||
set_authors=set_authors)
|
||||
self.ids.append(i)
|
||||
except:
|
||||
import traceback
|
||||
self.failures.append((i, traceback.format_exc()))
|
||||
|
||||
try:
|
||||
if mi.cover:
|
||||
os.remove(mi.cover)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.pb.setValue(self.pb.value()+1)
|
||||
|
||||
if self.current_idx >= len(self.id_map) - 1:
|
||||
self.finalize()
|
||||
else:
|
||||
self.current_idx += 1
|
||||
QTimer.singleShot(20, self.do_one)
|
||||
|
||||
def reject(self):
|
||||
self.canceled = True
|
||||
QDialog.reject(self)
|
||||
|
||||
def finalize(self):
|
||||
if self.canceled:
|
||||
return
|
||||
if self.failures:
|
||||
msg = []
|
||||
db = self.gui.current_db
|
||||
for i, tb in self.failures:
|
||||
title = db.title(i, index_is_id=True)
|
||||
authors = db.authors(i, index_is_id=True)
|
||||
if authors:
|
||||
authors = [x.replace('|', ',') for x in authors.split(',')]
|
||||
title += ' - ' + authors_to_string(authors)
|
||||
msg.append(title+'\n\n'+tb+'\n'+('*'*80))
|
||||
|
||||
error_dialog(self, _('Some failures'),
|
||||
_('Failed to apply updated metadata for some books'
|
||||
' in your library. Click "Show Details" to see '
|
||||
'details.'), det_msg='\n\n'.join(msg), show=True)
|
||||
self.accept()
|
||||
if self.ids:
|
||||
cr = self.gui.library_view.currentIndex().row()
|
||||
self.gui.library_view.model().refresh_ids(
|
||||
self.ids, cr)
|
||||
if self.gui.cover_flow:
|
||||
self.gui.cover_flow.dataChanged()
|
||||
|
||||
_amd = None
|
||||
def apply_metadata(job, gui, q, result):
|
||||
global _amd
|
||||
q.vlb.clicked.disconnect()
|
||||
q.finished.disconnect()
|
||||
if result != q.Accepted:
|
||||
return
|
||||
id_map, failed_ids, failed_covers, title_map = job.result
|
||||
id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in
|
||||
failed_ids])
|
||||
if not id_map:
|
||||
return
|
||||
|
||||
modified = set()
|
||||
db = gui.current_db
|
||||
|
||||
for i, mi in id_map.iteritems():
|
||||
lm = db.metadata_last_modified(i, index_is_id=True)
|
||||
if lm > mi.last_modified:
|
||||
title = db.title(i, index_is_id=True)
|
||||
authors = db.authors(i, index_is_id=True)
|
||||
if authors:
|
||||
authors = [x.replace('|', ',') for x in authors.split(',')]
|
||||
title += ' - ' + authors_to_string(authors)
|
||||
modified.add(title)
|
||||
|
||||
if modified:
|
||||
modified = sorted(modified, key=lower)
|
||||
if not question_dialog(gui, _('Some books changed'), '<p>'+
|
||||
_('The metadata for some books in your library has'
|
||||
' changed since you started the download. If you'
|
||||
' proceed, some of those changes may be overwritten. '
|
||||
'Click "Show details" to see the list of changed books. '
|
||||
'Do you want to proceed?'), det_msg='\n'.join(modified)):
|
||||
return
|
||||
|
||||
_amd = ApplyDialog(id_map, gui)
|
||||
_amd.exec_()
|
||||
|
||||
def proceed(gui, job):
|
||||
gui.status_bar.show_message(_('Metadata download completed'), 3000)
|
||||
id_map, failed_ids, failed_covers, title_map = job.result
|
||||
fmsg = det_msg = ''
|
||||
if failed_ids or failed_covers:
|
||||
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
|
||||
' "Show details" to see which books.')%len(failed_ids)
|
||||
det_msg = []
|
||||
for i in failed_ids | failed_covers:
|
||||
title = title_map[i]
|
||||
if i in failed_ids:
|
||||
title += (' ' + _('(Failed metadata)'))
|
||||
if i in failed_covers:
|
||||
title += (' ' + _('(Failed cover)'))
|
||||
det_msg.append(title)
|
||||
msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. '
|
||||
'Proceed with updating the metadata in your library?')%len(id_map)
|
||||
q = MessageBox(MessageBox.QUESTION, _('Download complete'),
|
||||
msg + fmsg, det_msg='\n'.join(det_msg), show_copy_button=bool(failed_ids),
|
||||
parent=gui)
|
||||
q.vlb = q.bb.addButton(_('View log'), q.bb.ActionRole)
|
||||
q.vlb.setIcon(QIcon(I('debug.png')))
|
||||
q.vlb.clicked.connect(partial(view_log, job, q))
|
||||
q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers))
|
||||
q.setModal(False)
|
||||
q.show()
|
||||
q.finished.connect(partial(apply_metadata, job, gui, q))
|
||||
|
||||
def merge_result(oldmi, newmi):
|
||||
dummy = Metadata(_('Unknown'))
|
||||
for f in msprefs['ignore_fields']:
|
||||
setattr(newmi, f, getattr(dummy, f))
|
||||
fields = set()
|
||||
for plugin in metadata_plugins(['identify']):
|
||||
fields |= plugin.touched_fields
|
||||
|
||||
for f in fields:
|
||||
# Optimize so that set_metadata does not have to do extra work later
|
||||
if not f.startswith('identifier:'):
|
||||
if (not newmi.is_null(f) and getattr(newmi, f) == getattr(oldmi, f)):
|
||||
setattr(newmi, f, getattr(dummy, f))
|
||||
|
||||
newmi.last_modified = oldmi.last_modified
|
||||
|
||||
return newmi
|
||||
|
||||
def download(ids, db, do_identify, covers,
|
||||
log=None, abort=None, notifications=None):
|
||||
ids = list(ids)
|
||||
metadata = [db.get_metadata(i, index_is_id=True, get_user_categories=False)
|
||||
for i in ids]
|
||||
failed_ids = set()
|
||||
failed_covers = set()
|
||||
title_map = {}
|
||||
ans = {}
|
||||
count = 0
|
||||
for i, mi in izip(ids, metadata):
|
||||
if abort.is_set():
|
||||
log.error('Aborting...')
|
||||
break
|
||||
title, authors, identifiers = mi.title, mi.authors, mi.identifiers
|
||||
title_map[i] = title
|
||||
if do_identify:
|
||||
results = []
|
||||
try:
|
||||
results = identify(log, abort, title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
except:
|
||||
pass
|
||||
if results:
|
||||
mi = merge_result(mi, results[0])
|
||||
identifiers = mi.identifiers
|
||||
if not mi.is_null('rating'):
|
||||
# set_metadata expects a rating out of 10
|
||||
mi.rating *= 2
|
||||
else:
|
||||
log.error('Failed to download metadata for', title)
|
||||
failed_ids.add(i)
|
||||
# We don't want set_metadata operating on anything but covers
|
||||
mi = merge_result(mi, mi)
|
||||
if covers:
|
||||
cdata = download_cover(log, title=title, authors=authors,
|
||||
identifiers=identifiers)
|
||||
if cdata is not None:
|
||||
with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
|
||||
f.write(cdata[-1])
|
||||
mi.cover = f.name
|
||||
else:
|
||||
failed_covers.add(i)
|
||||
ans[i] = mi
|
||||
count += 1
|
||||
notifications.put((count/len(ids),
|
||||
_('Downloaded %d of %d')%(count, len(ids))))
|
||||
log('Download complete, with %d failures'%len(failed_ids))
|
||||
return (ans, failed_ids, failed_covers, title_map)
|
||||
|
||||
|
||||
|
127
src/calibre/gui2/metadata/config.py
Normal file
127
src/calibre/gui2/metadata/config.py
Normal file
@ -0,0 +1,127 @@
|
||||
#!/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 textwrap
|
||||
|
||||
from PyQt4.Qt import (QWidget, QGridLayout, QGroupBox, QListView, Qt, QSpinBox,
|
||||
QDoubleSpinBox, QCheckBox, QLineEdit, QComboBox, QLabel)
|
||||
|
||||
from calibre.gui2.preferences.metadata_sources import FieldsModel as FM
|
||||
|
||||
class FieldsModel(FM): # {{{
|
||||
|
||||
def __init__(self, plugin):
|
||||
FM.__init__(self)
|
||||
self.plugin = plugin
|
||||
self.exclude = frozenset(['title', 'authors']) | self.exclude
|
||||
self.prefs = self.plugin.prefs
|
||||
|
||||
def initialize(self):
|
||||
fields = self.plugin.touched_fields
|
||||
self.fields = []
|
||||
for x in fields:
|
||||
if not x.startswith('identifier:') and x not in self.exclude:
|
||||
self.fields.append(x)
|
||||
self.fields.sort(key=lambda x:self.descs.get(x, x))
|
||||
self.reset()
|
||||
|
||||
def state(self, field, defaults=False):
|
||||
src = self.prefs.defaults if defaults else self.prefs
|
||||
return (Qt.Unchecked if field in src['ignore_fields']
|
||||
else Qt.Checked)
|
||||
|
||||
def restore_defaults(self):
|
||||
self.overrides = dict([(f, self.state(f, True)) for f in self.fields])
|
||||
self.reset()
|
||||
|
||||
def commit(self):
|
||||
val = [k for k, v in self.overrides.iteritems() if v == Qt.Unchecked]
|
||||
self.prefs['ignore_fields'] = val
|
||||
|
||||
# }}}
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
|
||||
def __init__(self, plugin):
|
||||
QWidget.__init__(self)
|
||||
self.plugin = plugin
|
||||
|
||||
self.l = l = QGridLayout()
|
||||
self.setLayout(l)
|
||||
|
||||
self.gb = QGroupBox(_('Downloaded metadata fields'), self)
|
||||
if plugin.config_help_message:
|
||||
self.pchm = QLabel(plugin.config_help_message)
|
||||
self.pchm.setWordWrap(True)
|
||||
self.pchm.setOpenExternalLinks(True)
|
||||
l.addWidget(self.pchm, 0, 0, 1, 2)
|
||||
l.addWidget(self.gb, l.rowCount(), 0, 1, 2)
|
||||
self.gb.l = QGridLayout()
|
||||
self.gb.setLayout(self.gb.l)
|
||||
self.fields_view = v = QListView(self)
|
||||
self.gb.l.addWidget(v, 0, 0)
|
||||
v.setFlow(v.LeftToRight)
|
||||
v.setWrapping(True)
|
||||
v.setResizeMode(v.Adjust)
|
||||
self.fields_model = FieldsModel(self.plugin)
|
||||
self.fields_model.initialize()
|
||||
v.setModel(self.fields_model)
|
||||
|
||||
self.memory = []
|
||||
self.widgets = []
|
||||
for opt in plugin.options:
|
||||
self.create_widgets(opt)
|
||||
|
||||
def create_widgets(self, opt):
|
||||
val = self.plugin.prefs[opt.name]
|
||||
if opt.type == 'number':
|
||||
c = QSpinBox if isinstance(opt.default, int) else QDoubleSpinBox
|
||||
widget = c(self)
|
||||
widget.setValue(val)
|
||||
elif opt.type == 'string':
|
||||
widget = QLineEdit(self)
|
||||
widget.setText(val if val else '')
|
||||
elif opt.type == 'bool':
|
||||
widget = QCheckBox(opt.label, self)
|
||||
widget.setChecked(bool(val))
|
||||
elif opt.type == 'choices':
|
||||
widget = QComboBox(self)
|
||||
for x in opt.choices:
|
||||
widget.addItem(x)
|
||||
idx = opt.choices.index(val)
|
||||
widget.setCurrentIndex(idx)
|
||||
widget.opt = opt
|
||||
widget.setToolTip(textwrap.fill(opt.desc))
|
||||
self.widgets.append(widget)
|
||||
r = self.l.rowCount()
|
||||
if opt.type == 'bool':
|
||||
self.l.addWidget(widget, r, 0, 1, self.l.columnCount())
|
||||
else:
|
||||
l = QLabel(opt.label)
|
||||
l.setToolTip(widget.toolTip())
|
||||
self.memory.append(l)
|
||||
l.setBuddy(widget)
|
||||
self.l.addWidget(l, r, 0, 1, 1)
|
||||
self.l.addWidget(widget, r, 1, 1, 1)
|
||||
|
||||
|
||||
def commit(self):
|
||||
self.fields_model.commit()
|
||||
for w in self.widgets:
|
||||
if isinstance(w, (QSpinBox, QDoubleSpinBox)):
|
||||
val = w.value()
|
||||
elif isinstance(w, QLineEdit):
|
||||
val = unicode(w.text())
|
||||
elif isinstance(w, QCheckBox):
|
||||
val = w.isChecked()
|
||||
elif isinstance(w, QComboBox):
|
||||
val = unicode(w.currentText())
|
||||
self.plugin.prefs[w.opt.name] = val
|
||||
|
||||
|
@ -13,16 +13,18 @@ from functools import partial
|
||||
from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
|
||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
|
||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
|
||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
|
||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu)
|
||||
|
||||
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,
|
||||
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit,
|
||||
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit,
|
||||
BuddyLabel, DateEdit, PubdateEdit)
|
||||
from calibre.gui2.metadata.single_download import FullFetch
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre.utils.config import tweaks
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
|
||||
class MetadataSingleDialogBase(ResizableDialog):
|
||||
|
||||
@ -100,15 +102,19 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.deduce_title_sort_button)
|
||||
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
||||
|
||||
self.authors = AuthorsEdit(self)
|
||||
self.deduce_author_sort_button = QToolButton(self)
|
||||
self.deduce_author_sort_button.setToolTip(_(
|
||||
self.deduce_author_sort_button = b = QToolButton(self)
|
||||
b.setToolTip(_(
|
||||
'Automatically create the author sort entry based on the current'
|
||||
' author entry.\n'
|
||||
'Using this button to create author sort will change author sort from'
|
||||
' red to green.'))
|
||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
||||
self.deduce_author_sort_button, self.db)
|
||||
b.m = m = QMenu()
|
||||
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
||||
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
||||
b.setMenu(m)
|
||||
self.authors = AuthorsEdit(self)
|
||||
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||
ac2)
|
||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||
|
||||
self.swap_title_author_button = QToolButton(self)
|
||||
@ -132,6 +138,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.formats_manager.cover_from_format_button.clicked.connect(
|
||||
self.cover_from_format)
|
||||
self.cover = Cover(self)
|
||||
self.cover.download_cover.connect(self.download_cover)
|
||||
self.basic_metadata_widgets.append(self.cover)
|
||||
|
||||
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
|
||||
@ -158,12 +165,17 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
|
||||
|
||||
self.fetch_metadata_button = QPushButton(
|
||||
_('&Fetch metadata from server'), self)
|
||||
_('&Download metadata'), self)
|
||||
self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
|
||||
font = self.fmb_font = QFont()
|
||||
font.setBold(True)
|
||||
self.fetch_metadata_button.setFont(font)
|
||||
|
||||
self.config_metadata_button = QToolButton(self)
|
||||
self.config_metadata_button.setIcon(QIcon(I('config.png')))
|
||||
self.config_metadata_button.clicked.connect(self.configure_metadata)
|
||||
self.config_metadata_button.setToolTip(
|
||||
_('Change how calibre downloads metadata'))
|
||||
|
||||
# }}}
|
||||
|
||||
@ -303,7 +315,36 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.comments.current_val = mi.comments
|
||||
|
||||
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:
|
||||
from calibre.ebooks.metadata.sources.base import msprefs
|
||||
mi = d.book
|
||||
dummy = Metadata(_('Unknown'))
|
||||
for f in msprefs['ignore_fields']:
|
||||
setattr(mi, f, getattr(dummy, f))
|
||||
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 configure_metadata(self):
|
||||
from calibre.gui2.preferences import show_config_widget
|
||||
gui = self.parent()
|
||||
show_config_widget('Sharing', 'Metadata download', parent=self,
|
||||
gui=gui, never_shutdown=True)
|
||||
|
||||
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):
|
||||
@ -430,7 +471,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||
|
||||
sto = QWidget.setTabOrder
|
||||
sto(self.button_box, self.fetch_metadata_button)
|
||||
sto(self.fetch_metadata_button, self.title)
|
||||
sto(self.fetch_metadata_button, self.config_metadata_button)
|
||||
sto(self.config_metadata_button, self.title)
|
||||
|
||||
def create_row(row, one, two, three, col=1, icon='forward.png'):
|
||||
ql = BuddyLabel(one)
|
||||
@ -509,7 +551,8 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding)
|
||||
l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3)
|
||||
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3)
|
||||
l.addWidget(self.fetch_metadata_button, 9, 0, 1, 2)
|
||||
l.addWidget(self.config_metadata_button, 9, 2, 1, 1)
|
||||
|
||||
self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self)
|
||||
gb.l = l = QVBoxLayout()
|
||||
@ -521,18 +564,35 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class DragTrackingWidget(QWidget): # {{{
|
||||
|
||||
def __init__(self, parent, on_drag_enter):
|
||||
QWidget.__init__(self, parent)
|
||||
self.on_drag_enter = on_drag_enter
|
||||
|
||||
def dragEnterEvent(self, ev):
|
||||
self.on_drag_enter.emit()
|
||||
|
||||
# }}}
|
||||
|
||||
class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
|
||||
cc_two_column = False
|
||||
one_line_comments_toolbar = True
|
||||
|
||||
on_drag_enter = pyqtSignal()
|
||||
|
||||
def handle_drag_enter(self):
|
||||
self.central_widget.setCurrentIndex(1)
|
||||
|
||||
def do_layout(self):
|
||||
self.central_widget.clear()
|
||||
self.tabs = []
|
||||
self.labels = []
|
||||
sto = QWidget.setTabOrder
|
||||
|
||||
self.tabs.append(QWidget(self))
|
||||
self.on_drag_enter.connect(self.handle_drag_enter)
|
||||
self.tabs.append(DragTrackingWidget(self, self.on_drag_enter))
|
||||
self.central_widget.addTab(self.tabs[0], _("&Metadata"))
|
||||
self.tabs[0].l = QGridLayout()
|
||||
self.tabs[0].setLayout(self.tabs[0].l)
|
||||
@ -542,6 +602,10 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
self.tabs[1].l = QGridLayout()
|
||||
self.tabs[1].setLayout(self.tabs[1].l)
|
||||
|
||||
# accept drop events so we can automatically switch to the second tab to
|
||||
# drop covers and formats
|
||||
self.tabs[0].setAcceptDrops(True)
|
||||
|
||||
# Tab 0
|
||||
tab0 = self.tabs[0]
|
||||
|
||||
@ -550,6 +614,12 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
|
||||
gb.setLayout(tl)
|
||||
|
||||
self.button_box.addButton(self.fetch_metadata_button,
|
||||
QDialogButtonBox.ActionRole)
|
||||
self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
self.config_metadata_button.setText(_('Configure metadata downloading'))
|
||||
self.button_box.addButton(self.config_metadata_button,
|
||||
QDialogButtonBox.ActionRole)
|
||||
sto(self.button_box, self.title)
|
||||
|
||||
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
|
||||
@ -639,7 +709,6 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
|
||||
wgl.addWidget(gb)
|
||||
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
wgl.addWidget(self.fetch_metadata_button)
|
||||
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
|
||||
QSizePolicy.Expanding))
|
||||
wgl.addWidget(self.formats_manager)
|
||||
|
@ -46,9 +46,8 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
|
||||
def sizeHint(self, option, index):
|
||||
doc = self.to_doc(index)
|
||||
ans = doc.size().toSize()
|
||||
if ans.width() > 250:
|
||||
doc.setTextWidth(250)
|
||||
ans = doc.size().toSize()
|
||||
if ans.width() > 150:
|
||||
ans.setWidth(160)
|
||||
ans.setHeight(ans.height()+10)
|
||||
return ans
|
||||
|
||||
@ -181,6 +180,13 @@ class ResultsModel(QAbstractTableModel): # {{{
|
||||
return self.yes_icon
|
||||
elif role == Qt.UserRole:
|
||||
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
|
||||
|
||||
def sort(self, col, order=Qt.AscendingOrder):
|
||||
@ -190,7 +196,7 @@ class ResultsModel(QAbstractTableModel): # {{{
|
||||
elif col == 1:
|
||||
key = attrgetter('title')
|
||||
elif col == 2:
|
||||
key = attrgetter('authors')
|
||||
key = attrgetter('pubdate')
|
||||
elif col == 3:
|
||||
key = attrgetter('has_cached_cover_url')
|
||||
elif key == 4:
|
||||
@ -429,7 +435,7 @@ class IdentifyWidget(QWidget): # {{{
|
||||
if authors:
|
||||
parts.append('authors:'+authors_to_string(authors))
|
||||
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)
|
||||
self.query.setText(_('Query: ')+'; '.join(parts))
|
||||
self.log(unicode(self.query.text()))
|
||||
@ -561,16 +567,23 @@ class CoversModel(QAbstractListModel): # {{{
|
||||
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 = {}
|
||||
for i, x in enumerate(self.covers):
|
||||
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
|
||||
good = [x for x in self.covers if not x[-1]]
|
||||
self.covers = good
|
||||
self.plugin_map = pmap
|
||||
self.reset()
|
||||
@ -665,7 +678,8 @@ class CoversWidget(QWidget): # {{{
|
||||
def start(self, book, current_cover, title, authors):
|
||||
self.book, self.current_cover = book, current_cover
|
||||
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()
|
||||
|
||||
@ -749,6 +763,10 @@ class LogViewer(QDialog): # {{{
|
||||
|
||||
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)
|
||||
|
||||
@ -759,10 +777,13 @@ class LogViewer(QDialog): # {{{
|
||||
self.keep_updating = True
|
||||
self.last_html = None
|
||||
self.finished.connect(self.stop)
|
||||
QTimer.singleShot(1000, self.update_log)
|
||||
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
|
||||
|
||||
@ -772,16 +793,17 @@ class LogViewer(QDialog): # {{{
|
||||
html = self.log.html
|
||||
if html != self.last_html:
|
||||
self.last_html = html
|
||||
self.tb.setHtml('<pre>%s</pre>'%html)
|
||||
self.tb.setHtml('<pre style="font-family:monospace">%s</pre>'%html)
|
||||
QTimer.singleShot(1000, self.update_log)
|
||||
|
||||
# }}}
|
||||
|
||||
class FullFetch(QDialog): # {{{
|
||||
|
||||
def __init__(self, log, current_cover=None, parent=None):
|
||||
def __init__(self, current_cover=None, parent=None):
|
||||
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...'))
|
||||
@ -807,7 +829,7 @@ class FullFetch(QDialog): # {{{
|
||||
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.results_found.connect(self.identify_results_found)
|
||||
self.identify_widget.book_selected.connect(self.book_selected)
|
||||
@ -829,6 +851,7 @@ class FullFetch(QDialog): # {{{
|
||||
self.ok_button.setVisible(True)
|
||||
self.book = book
|
||||
self.stack.setCurrentIndex(1)
|
||||
self.log('\n\n')
|
||||
self.covers_widget.start(book, self.current_cover,
|
||||
self.title, self.authors)
|
||||
|
||||
@ -838,6 +861,7 @@ class FullFetch(QDialog): # {{{
|
||||
|
||||
def reject(self):
|
||||
self.identify_widget.cancel()
|
||||
self.covers_widget.cancel()
|
||||
return QDialog.reject(self)
|
||||
|
||||
def cleanup(self):
|
||||
@ -864,12 +888,65 @@ class FullFetch(QDialog): # {{{
|
||||
self.title, self.authors = title, authors
|
||||
self.identify_widget.start(title=title, authors=authors,
|
||||
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__':
|
||||
#DEBUG_DIALOG = True
|
||||
app = QApplication([])
|
||||
d = FullFetch(Log())
|
||||
d.start(title='great gatsby', authors=['Fitzgerald'])
|
||||
d = FullFetch()
|
||||
d.start(title='great gatsby', authors=['fitzgerald'])
|
||||
|
||||
|
@ -7,8 +7,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap
|
||||
|
||||
from PyQt4.Qt import QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox, \
|
||||
QLineEdit, QComboBox, QVariant, Qt
|
||||
from PyQt4.Qt import (QWidget, pyqtSignal, QCheckBox, QAbstractSpinBox,
|
||||
QLineEdit, QComboBox, QVariant, Qt, QIcon, QDialog, QVBoxLayout,
|
||||
QDialogButtonBox)
|
||||
|
||||
from calibre.customize.ui import preferences_plugins
|
||||
from calibre.utils.config import ConfigProxy
|
||||
@ -21,7 +22,7 @@ class ConfigWidgetInterface(object):
|
||||
'''
|
||||
This class defines the interface that all widgets displayed in the
|
||||
Preferences dialog must implement. See :class:`ConfigWidgetBase` for
|
||||
a base class that implements this interface and defines various conveninece
|
||||
a base class that implements this interface and defines various convenience
|
||||
methods as well.
|
||||
'''
|
||||
|
||||
@ -284,7 +285,14 @@ def get_plugin(category, name):
|
||||
'No Preferences Plugin with category: %s and name: %s found' %
|
||||
(category, name))
|
||||
|
||||
# Testing {{{
|
||||
class ConfigDialog(QDialog):
|
||||
def set_widget(self, w): self.w = w
|
||||
def accept(self):
|
||||
try:
|
||||
self.restart_required = self.w.commit()
|
||||
except AbortCommit:
|
||||
return
|
||||
QDialog.accept(self)
|
||||
|
||||
def init_gui():
|
||||
from calibre.gui2.ui import Main
|
||||
@ -298,21 +306,27 @@ def init_gui():
|
||||
gui.initialize(db.library_path, db, None, actions, show_gui=False)
|
||||
return gui
|
||||
|
||||
def test_widget(category, name, gui=None):
|
||||
from PyQt4.Qt import QDialog, QVBoxLayout, QDialogButtonBox
|
||||
class Dialog(QDialog):
|
||||
def set_widget(self, w): self.w = w
|
||||
def accept(self):
|
||||
try:
|
||||
self.restart_required = self.w.commit()
|
||||
except AbortCommit:
|
||||
return
|
||||
QDialog.accept(self)
|
||||
def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
||||
parent=None, never_shutdown=False):
|
||||
'''
|
||||
Show the preferences plugin identified by category and name
|
||||
|
||||
:param gui: gui instance, if None a hidden gui is created
|
||||
:param show_restart_msg: If True and the preferences plugin indicates a
|
||||
restart is required, show a message box telling the user to restart
|
||||
:param parent: The parent of the displayed dialog
|
||||
|
||||
:return: True iff a restart is required for the changes made by the user to
|
||||
take effect
|
||||
'''
|
||||
from calibre.gui2 import gprefs
|
||||
pl = get_plugin(category, name)
|
||||
d = Dialog()
|
||||
d = ConfigDialog(parent)
|
||||
d.resize(750, 550)
|
||||
d.setWindowTitle(category + " - " + name)
|
||||
conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name)
|
||||
geom = gprefs.get(conf_name, None)
|
||||
d.setWindowTitle(_('Configure ') + name)
|
||||
d.setWindowIcon(QIcon(I('config.png')))
|
||||
bb = QDialogButtonBox(d)
|
||||
bb.setStandardButtons(bb.Apply|bb.Cancel|bb.RestoreDefaults)
|
||||
bb.accepted.connect(d.accept)
|
||||
@ -334,12 +348,23 @@ def test_widget(category, name, gui=None):
|
||||
mygui = True
|
||||
w.genesis(gui)
|
||||
w.initialize()
|
||||
if geom is not None:
|
||||
d.restoreGeometry(geom)
|
||||
d.exec_()
|
||||
if getattr(d, 'restart_required', False):
|
||||
geom = bytearray(d.saveGeometry())
|
||||
gprefs[conf_name] = geom
|
||||
rr = getattr(d, 'restart_required', False)
|
||||
if show_restart_msg and rr:
|
||||
from calibre.gui2 import warning_dialog
|
||||
warning_dialog(gui, 'Restart required', 'Restart required', show=True)
|
||||
if mygui:
|
||||
if mygui and not never_shutdown:
|
||||
gui.shutdown()
|
||||
return rr
|
||||
|
||||
# Testing {{{
|
||||
|
||||
def test_widget(category, name, gui=None):
|
||||
show_config_widget(category, name, gui=gui, show_restart_msg=True)
|
||||
|
||||
def test_all():
|
||||
from PyQt4.Qt import QApplication
|
||||
|
@ -43,6 +43,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
r('overwrite_author_title_metadata', config)
|
||||
r('get_social_metadata', config)
|
||||
if test_eight_code:
|
||||
self.opt_overwrite_author_title_metadata.setVisible(False)
|
||||
self.opt_get_social_metadata.setVisible(False)
|
||||
r('new_version_notification', config)
|
||||
r('upload_news_to_device', config)
|
||||
r('delete_news_from_library_on_upload', config)
|
||||
|
@ -163,8 +163,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
elif '*edited' in self.custcols[c]:
|
||||
cc = self.custcols[c]
|
||||
db.set_custom_column_metadata(cc['colnum'], name=cc['name'],
|
||||
label=cc['label'],
|
||||
display = self.custcols[c]['display'])
|
||||
label=cc['label'],
|
||||
display = self.custcols[c]['display'],
|
||||
notify=False)
|
||||
if '*must_restart' in self.custcols[c]:
|
||||
must_restart = True
|
||||
return must_restart
|
||||
|
@ -79,7 +79,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
|
@ -41,6 +41,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
'text':_('Yes/No'), 'is_multiple':False},
|
||||
10:{'datatype':'composite',
|
||||
'text':_('Column built from other columns'), 'is_multiple':False},
|
||||
11:{'datatype':'*composite',
|
||||
'text':_('Column built from other columns, behaves like tags'), 'is_multiple':True},
|
||||
}
|
||||
|
||||
def __init__(self, parent, editing, standard_colheads, standard_colnames):
|
||||
@ -99,7 +101,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
c = parent.custcols[col]
|
||||
self.column_name_box.setText(c['label'])
|
||||
self.column_heading_box.setText(c['name'])
|
||||
ct = c['datatype'] if not c['is_multiple'] else '*text'
|
||||
ct = c['datatype']
|
||||
if c['is_multiple']:
|
||||
ct = '*' + ct
|
||||
self.orig_column_number = c['colnum']
|
||||
self.orig_column_name = col
|
||||
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x),
|
||||
@ -109,7 +113,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
if ct == 'datetime':
|
||||
if c['display'].get('date_format', None):
|
||||
self.date_format_box.setText(c['display'].get('date_format', ''))
|
||||
elif ct == 'composite':
|
||||
elif ct in ['composite', '*composite']:
|
||||
self.composite_box.setText(c['display'].get('composite_template', ''))
|
||||
sb = c['display'].get('composite_sort', 'text')
|
||||
vals = ['text', 'number', 'date', 'bool']
|
||||
@ -167,7 +171,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
|
||||
for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label',
|
||||
'make_category'):
|
||||
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
|
||||
getattr(self, 'composite_'+x).setVisible(col_type in ['composite', '*composite'])
|
||||
for x in ('box', 'default_label', 'label'):
|
||||
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
|
||||
self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration'])
|
||||
@ -187,8 +191,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
'because these names are reserved for the index of a series column.'))
|
||||
col_heading = unicode(self.column_heading_box.text()).strip()
|
||||
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
|
||||
if col_type == '*text':
|
||||
col_type='text'
|
||||
if col_type[0] == '*':
|
||||
col_type = col_type[1:]
|
||||
is_multiple = True
|
||||
else:
|
||||
is_multiple = False
|
||||
@ -249,11 +253,10 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
elif col_type == 'text' and is_multiple:
|
||||
display_dict = {'is_names': self.is_names.isChecked()}
|
||||
|
||||
if col_type in ['text', 'composite', 'enumeration']:
|
||||
if col_type in ['text', 'composite', 'enumeration'] and not is_multiple:
|
||||
display_dict['use_decorations'] = self.use_decorations.checkState()
|
||||
|
||||
if not self.editing_col:
|
||||
db.field_metadata
|
||||
self.parent.custcols[key] = {
|
||||
'label':col,
|
||||
'name':col_heading,
|
||||
|
@ -84,11 +84,11 @@ class EmailAccounts(QAbstractTableModel): # {{{
|
||||
account = self.account_order[row]
|
||||
if col == 3:
|
||||
self.accounts[account][1] ^= True
|
||||
if col == 2:
|
||||
elif col == 2:
|
||||
self.subjects[account] = unicode(value.toString())
|
||||
elif col == 1:
|
||||
self.accounts[account][0] = unicode(value.toString()).upper()
|
||||
else:
|
||||
elif col == 0:
|
||||
na = unicode(value.toString())
|
||||
from email.utils import parseaddr
|
||||
addr = parseaddr(na)[-1]
|
||||
@ -100,7 +100,7 @@ class EmailAccounts(QAbstractTableModel): # {{{
|
||||
self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
|
||||
|
||||
self.dataChanged.emit(
|
||||
self.index(index.row(), 0), self.index(index.row(), 2))
|
||||
self.index(index.row(), 0), self.index(index.row(), 3))
|
||||
return True
|
||||
|
||||
def make_default(self, index):
|
||||
@ -202,7 +202,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.changed_signal.emit()
|
||||
|
||||
def refresh_gui(self, gui):
|
||||
gui.emailer.calculate_rate_limit()
|
||||
from calibre.gui2.email import gui_sendmail
|
||||
gui_sendmail.calculate_rate_limit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -73,13 +73,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
choices=sorted(list(choices), key=sort_key))
|
||||
|
||||
|
||||
self.current_font = None
|
||||
self.current_font = self.initial_font = None
|
||||
self.change_font_button.clicked.connect(self.change_font)
|
||||
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
self.current_font = gprefs['font']
|
||||
self.current_font = self.initial_font = gprefs['font']
|
||||
self.update_font_display()
|
||||
|
||||
def restore_defaults(self):
|
||||
@ -119,7 +119,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def commit(self, *args):
|
||||
rr = ConfigWidgetBase.commit(self, *args)
|
||||
if self.current_font != gprefs['font']:
|
||||
if self.current_font != self.initial_font:
|
||||
gprefs['font'] = self.current_font
|
||||
QApplication.setFont(self.font_display.font())
|
||||
rr = True
|
||||
|
316
src/calibre/gui2/preferences/metadata_sources.py
Normal file
316
src/calibre/gui2/preferences/metadata_sources.py
Normal file
@ -0,0 +1,316 @@
|
||||
#!/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'
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
from PyQt4.Qt import (QAbstractTableModel, Qt, QAbstractListModel, QWidget,
|
||||
pyqtSignal, QVBoxLayout, QDialogButtonBox, QFrame, QLabel, QIcon)
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences.metadata_sources_ui import Ui_Form
|
||||
from calibre.ebooks.metadata.sources.base import msprefs
|
||||
from calibre.customize.ui import (all_metadata_plugins, is_disabled,
|
||||
enable_plugin, disable_plugin, default_disabled_plugins)
|
||||
from calibre.gui2 import NONE, error_dialog
|
||||
|
||||
class SourcesModel(QAbstractTableModel): # {{{
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QAbstractTableModel.__init__(self, parent)
|
||||
|
||||
self.plugins = []
|
||||
self.enabled_overrides = {}
|
||||
self.cover_overrides = {}
|
||||
|
||||
def initialize(self):
|
||||
self.plugins = list(all_metadata_plugins())
|
||||
self.plugins.sort(key=attrgetter('name'))
|
||||
self.enabled_overrides = {}
|
||||
self.cover_overrides = {}
|
||||
self.reset()
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.plugins)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return 2
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
if section == 0:
|
||||
return _('Source')
|
||||
if section == 1:
|
||||
return _('Cover priority')
|
||||
return NONE
|
||||
|
||||
def data(self, index, role):
|
||||
try:
|
||||
plugin = self.plugins[index.row()]
|
||||
except:
|
||||
return NONE
|
||||
col = index.column()
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
if col == 0:
|
||||
return plugin.name
|
||||
elif col == 1:
|
||||
orig = msprefs['cover_priorities'].get(plugin.name, 1)
|
||||
return self.cover_overrides.get(plugin, orig)
|
||||
elif role == Qt.CheckStateRole and col == 0:
|
||||
orig = Qt.Unchecked if is_disabled(plugin) else Qt.Checked
|
||||
return self.enabled_overrides.get(plugin, orig)
|
||||
elif role == Qt.UserRole:
|
||||
return plugin
|
||||
elif (role == Qt.DecorationRole and col == 0 and not
|
||||
plugin.is_configured()):
|
||||
return QIcon(I('list_remove.png'))
|
||||
elif role == Qt.ToolTipRole:
|
||||
if plugin.is_configured():
|
||||
return _('This source is configured and ready to go')
|
||||
return _('This source needs configuration')
|
||||
return NONE
|
||||
|
||||
def setData(self, index, val, role):
|
||||
try:
|
||||
plugin = self.plugins[index.row()]
|
||||
except:
|
||||
return False
|
||||
col = index.column()
|
||||
ret = False
|
||||
if col == 0 and role == Qt.CheckStateRole:
|
||||
val, ok = val.toInt()
|
||||
if ok:
|
||||
self.enabled_overrides[plugin] = val
|
||||
ret = True
|
||||
if col == 1 and role == Qt.EditRole:
|
||||
val, ok = val.toInt()
|
||||
if ok:
|
||||
self.cover_overrides[plugin] = val
|
||||
ret = True
|
||||
if ret:
|
||||
self.dataChanged.emit(index, index)
|
||||
return ret
|
||||
|
||||
|
||||
def flags(self, index):
|
||||
col = index.column()
|
||||
ans = QAbstractTableModel.flags(self, index)
|
||||
if col == 0:
|
||||
return ans | Qt.ItemIsUserCheckable
|
||||
return Qt.ItemIsEditable | ans
|
||||
|
||||
def commit(self):
|
||||
for plugin, val in self.enabled_overrides.iteritems():
|
||||
if val == Qt.Checked:
|
||||
enable_plugin(plugin)
|
||||
elif val == Qt.Unchecked:
|
||||
disable_plugin(plugin)
|
||||
|
||||
if self.cover_overrides:
|
||||
cp = msprefs['cover_priorities']
|
||||
for plugin, val in self.cover_overrides.iteritems():
|
||||
if val == 1:
|
||||
cp.pop(plugin.name, None)
|
||||
else:
|
||||
cp[plugin.name] = val
|
||||
msprefs['cover_priorities'] = cp
|
||||
|
||||
self.enabled_overrides = {}
|
||||
self.cover_overrides = {}
|
||||
|
||||
def restore_defaults(self):
|
||||
self.enabled_overrides = dict([(p, (Qt.Unchecked if p.name in
|
||||
default_disabled_plugins else Qt.Checked)) for p in self.plugins])
|
||||
self.cover_overrides = dict([(p,
|
||||
msprefs.defaults['cover_priorities'].get(p.name, 1))
|
||||
for p in self.plugins])
|
||||
self.reset()
|
||||
|
||||
# }}}
|
||||
|
||||
class FieldsModel(QAbstractListModel): # {{{
|
||||
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QAbstractTableModel.__init__(self, parent)
|
||||
|
||||
self.fields = []
|
||||
self.descs = {
|
||||
'authors': _('Authors'),
|
||||
'comments': _('Comments'),
|
||||
'pubdate': _('Published date'),
|
||||
'publisher': _('Publisher'),
|
||||
'rating' : _('Rating'),
|
||||
'tags' : _('Tags'),
|
||||
'title': _('Title'),
|
||||
'series': _('Series'),
|
||||
'language': _('Language'),
|
||||
}
|
||||
self.overrides = {}
|
||||
self.exclude = frozenset(['series_index'])
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self.fields)
|
||||
|
||||
def initialize(self):
|
||||
fields = set()
|
||||
for p in all_metadata_plugins():
|
||||
fields |= p.touched_fields
|
||||
self.fields = []
|
||||
for x in fields:
|
||||
if not x.startswith('identifier:') and x not in self.exclude:
|
||||
self.fields.append(x)
|
||||
self.fields.sort(key=lambda x:self.descs.get(x, x))
|
||||
self.reset()
|
||||
|
||||
def state(self, field, defaults=False):
|
||||
src = msprefs.defaults if defaults else msprefs
|
||||
return (Qt.Unchecked if field in src['ignore_fields']
|
||||
else Qt.Checked)
|
||||
|
||||
def data(self, index, role):
|
||||
try:
|
||||
field = self.fields[index.row()]
|
||||
except:
|
||||
return None
|
||||
if role == Qt.DisplayRole:
|
||||
return self.descs.get(field, field)
|
||||
if role == Qt.CheckStateRole:
|
||||
return self.overrides.get(field, self.state(field))
|
||||
return NONE
|
||||
|
||||
def flags(self, index):
|
||||
ans = QAbstractTableModel.flags(self, index)
|
||||
return ans | Qt.ItemIsUserCheckable
|
||||
|
||||
def restore_defaults(self):
|
||||
self.overrides = dict([(f, self.state(f, True)) for f in self.fields])
|
||||
self.reset()
|
||||
|
||||
def setData(self, index, val, role):
|
||||
try:
|
||||
field = self.fields[index.row()]
|
||||
except:
|
||||
return False
|
||||
ret = False
|
||||
if role == Qt.CheckStateRole:
|
||||
val, ok = val.toInt()
|
||||
if ok:
|
||||
self.overrides[field] = val
|
||||
ret = True
|
||||
if ret:
|
||||
self.dataChanged.emit(index, index)
|
||||
return ret
|
||||
|
||||
def commit(self):
|
||||
val = [k for k, v in self.overrides.iteritems() if v == Qt.Unchecked]
|
||||
msprefs['ignore_fields'] = val
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class PluginConfig(QWidget): # {{{
|
||||
|
||||
finished = pyqtSignal()
|
||||
|
||||
def __init__(self, plugin, parent):
|
||||
QWidget.__init__(self, parent)
|
||||
|
||||
self.plugin = plugin
|
||||
|
||||
self.l = l = QVBoxLayout()
|
||||
self.setLayout(l)
|
||||
self.c = c = QLabel(_('<b>Configure %s</b><br>%s') % (plugin.name,
|
||||
plugin.description))
|
||||
c.setAlignment(Qt.AlignHCenter)
|
||||
l.addWidget(c)
|
||||
|
||||
self.config_widget = plugin.config_widget()
|
||||
self.l.addWidget(self.config_widget)
|
||||
|
||||
self.bb = QDialogButtonBox(
|
||||
QDialogButtonBox.Save|QDialogButtonBox.Cancel,
|
||||
parent=self)
|
||||
self.bb.accepted.connect(self.finished)
|
||||
self.bb.rejected.connect(self.finished)
|
||||
self.bb.accepted.connect(self.commit)
|
||||
l.addWidget(self.bb)
|
||||
|
||||
self.f = QFrame(self)
|
||||
self.f.setFrameShape(QFrame.HLine)
|
||||
l.addWidget(self.f)
|
||||
|
||||
def commit(self):
|
||||
self.plugin.save_settings(self.config_widget)
|
||||
# }}}
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def genesis(self, gui):
|
||||
r = self.register
|
||||
r('txt_comments', msprefs)
|
||||
r('max_tags', msprefs)
|
||||
r('wait_after_first_identify_result', msprefs)
|
||||
r('wait_after_first_cover_result', msprefs)
|
||||
r('swap_author_names', msprefs)
|
||||
|
||||
self.configure_plugin_button.clicked.connect(self.configure_plugin)
|
||||
self.sources_model = SourcesModel(self)
|
||||
self.sources_view.setModel(self.sources_model)
|
||||
self.sources_model.dataChanged.connect(self.changed_signal)
|
||||
|
||||
self.fields_model = FieldsModel(self)
|
||||
self.fields_view.setModel(self.fields_model)
|
||||
self.fields_model.dataChanged.connect(self.changed_signal)
|
||||
|
||||
def configure_plugin(self):
|
||||
for index in self.sources_view.selectionModel().selectedRows():
|
||||
plugin = self.sources_model.data(index, Qt.UserRole)
|
||||
if plugin is not NONE:
|
||||
return self.do_config(plugin)
|
||||
error_dialog(self, _('No source selected'),
|
||||
_('No source selected, cannot configure.'), show=True)
|
||||
|
||||
def do_config(self, plugin):
|
||||
self.pc = PluginConfig(plugin, self)
|
||||
self.stack.insertWidget(1, self.pc)
|
||||
self.stack.setCurrentIndex(1)
|
||||
self.pc.finished.connect(self.pc_finished)
|
||||
|
||||
def pc_finished(self):
|
||||
try:
|
||||
self.pc.finished.diconnect()
|
||||
except:
|
||||
pass
|
||||
self.stack.setCurrentIndex(0)
|
||||
self.stack.removeWidget(self.pc)
|
||||
self.pc = None
|
||||
|
||||
def initialize(self):
|
||||
ConfigWidgetBase.initialize(self)
|
||||
self.sources_model.initialize()
|
||||
self.sources_view.resizeColumnsToContents()
|
||||
self.fields_model.initialize()
|
||||
|
||||
def restore_defaults(self):
|
||||
ConfigWidgetBase.restore_defaults(self)
|
||||
self.sources_model.restore_defaults()
|
||||
self.fields_model.restore_defaults()
|
||||
self.changed_signal.emit()
|
||||
|
||||
def commit(self):
|
||||
self.sources_model.commit()
|
||||
self.fields_model.commit()
|
||||
return ConfigWidgetBase.commit(self)
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
test_widget('Sharing', 'Metadata download')
|
||||
|
166
src/calibre/gui2/preferences/metadata_sources.ui
Normal file
166
src/calibre/gui2/preferences/metadata_sources.ui
Normal file
@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>781</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<widget class="QWidget" name="page">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" rowspan="6">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Metadata sources</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Disable any metadata sources you do not want by unchecking them. You can also set the cover priority. Covers from sources that have a higher (smaller) priority will be preferred when bulk downloading metadata.
|
||||
</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="sources_view">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Sources with a red X next to their names must be configured before they will be used. </string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="configure_plugin_button">
|
||||
<property name="text">
|
||||
<string>Configure selected source</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/plugins.png</normaloff>:/images/plugins.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Downloaded metadata fields</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QListView" name="fields_view">
|
||||
<property name="toolTip">
|
||||
<string>If you uncheck any fields, metadata for those fields will not be downloaded</string>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_txt_comments">
|
||||
<property name="text">
|
||||
<string>Convert all downloaded comments to plain &text</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_swap_author_names">
|
||||
<property name="text">
|
||||
<string>Swap author names from FN LN to LN, FN</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Max. number of &tags to download:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_max_tags</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QSpinBox" name="opt_max_tags"/>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Max. &time to wait after first match is found:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_wait_after_first_identify_result</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QSpinBox" name="opt_wait_after_first_identify_result">
|
||||
<property name="suffix">
|
||||
<string> secs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Max. time to wait after first &cover is found:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_wait_after_first_cover_result</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QSpinBox" name="opt_wait_after_first_cover_result">
|
||||
<property name="suffix">
|
||||
<string> secs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="page_2"/>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
@ -218,6 +218,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.search.search.connect(self.find)
|
||||
self.next_button.clicked.connect(self.find_next)
|
||||
self.previous_button.clicked.connect(self.find_previous)
|
||||
self.changed_signal.connect(self.reload_store_plugins)
|
||||
|
||||
def find(self, query):
|
||||
idx = self._plugin_model.find(query)
|
||||
@ -344,6 +345,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
plugin.name + _(' cannot be removed. It is a '
|
||||
'builtin plugin. Try disabling it instead.')).exec_()
|
||||
|
||||
def reload_store_plugins(self):
|
||||
self.gui.load_store_plugins()
|
||||
if self.gui.iactions.has_key('Store'):
|
||||
self.gui.iactions['Store'].load_menu()
|
||||
|
||||
def check_for_add_to_toolbars(self, plugin):
|
||||
from calibre.gui2.preferences.toolbar import ConfigWidget
|
||||
from calibre.customize import InterfaceActionBase
|
||||
@ -376,6 +382,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
installed_actions.append(plugin_action.name)
|
||||
gprefs['action-layout-'+key] = tuple(installed_actions)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
|
@ -171,10 +171,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
return ConfigWidgetBase.commit(self)
|
||||
|
||||
def refresh_gui(self, gui):
|
||||
gui.set_highlight_only_button_icon()
|
||||
if self.muc_changed:
|
||||
gui.tags_view.set_new_model()
|
||||
gui.search.search_as_you_type(config['search_as_you_type'])
|
||||
gui.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||
gui.search.do_search()
|
||||
|
||||
def clear_histories(self, *args):
|
||||
|
@ -14,9 +14,9 @@ from calibre.utils.config import read_raw_tweaks, write_tweaks
|
||||
from calibre.gui2.widgets import PythonHighlighter
|
||||
from calibre import isbytestring
|
||||
|
||||
from PyQt4.Qt import QAbstractListModel, Qt, QStyledItemDelegate, QStyle, \
|
||||
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog, \
|
||||
QVBoxLayout, QPlainTextEdit, QLabel
|
||||
from PyQt4.Qt import (QAbstractListModel, Qt, QStyledItemDelegate, QStyle,
|
||||
QStyleOptionViewItem, QFont, QDialogButtonBox, QDialog,
|
||||
QVBoxLayout, QPlainTextEdit, QLabel)
|
||||
|
||||
class Delegate(QStyledItemDelegate): # {{{
|
||||
def __init__(self, view):
|
||||
@ -35,8 +35,9 @@ class Delegate(QStyledItemDelegate): # {{{
|
||||
class Tweak(object): # {{{
|
||||
|
||||
def __init__(self, name, doc, var_names, defaults, custom):
|
||||
self.name = name
|
||||
self.doc = doc.strip()
|
||||
translate = __builtins__['_']
|
||||
self.name = translate(name)
|
||||
self.doc = translate(doc.strip())
|
||||
self.var_names = var_names
|
||||
self.default_values = {}
|
||||
for x in var_names:
|
||||
|
@ -10,10 +10,9 @@ import re
|
||||
|
||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \
|
||||
pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, \
|
||||
QString
|
||||
QString, QIcon
|
||||
|
||||
from calibre.gui2 import config
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
@ -316,23 +315,6 @@ class SavedSearchBox(QComboBox): # {{{
|
||||
self.addItems(qnames)
|
||||
self.setCurrentIndex(-1)
|
||||
|
||||
# SIGNALed from the main UI
|
||||
def delete_search_button_clicked(self):
|
||||
if not confirm('<p>'+_('The selected search will be '
|
||||
'<b>permanently deleted</b>. Are you sure?')
|
||||
+'</p>', 'saved_search_delete', self):
|
||||
return
|
||||
idx = self.currentIndex
|
||||
if idx < 0:
|
||||
return
|
||||
ss = saved_searches().lookup(unicode(self.currentText()))
|
||||
if ss is None:
|
||||
return
|
||||
saved_searches().delete(unicode(self.currentText()))
|
||||
self.clear()
|
||||
self.search_box.clear()
|
||||
self.changed.emit()
|
||||
|
||||
# SIGNALed from the main UI
|
||||
def save_search_button_clicked(self):
|
||||
name = unicode(self.currentText())
|
||||
@ -382,7 +364,24 @@ class SearchBoxMixin(object): # {{{
|
||||
unicode(self.search.toolTip())))
|
||||
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
||||
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
||||
self.search_options_button.clicked.connect(self.search_options_button_clicked)
|
||||
self.set_highlight_only_button_icon()
|
||||
self.highlight_only_button.clicked.connect(self.highlight_only_clicked)
|
||||
tt = _('Enable or disable search highlighting.') + '<br><br>'
|
||||
tt += config.help('highlight_search_matches')
|
||||
self.highlight_only_button.setToolTip(tt)
|
||||
|
||||
def highlight_only_clicked(self, state):
|
||||
config['highlight_search_matches'] = not config['highlight_search_matches']
|
||||
self.set_highlight_only_button_icon()
|
||||
self.search.do_search()
|
||||
self.focus_to_library()
|
||||
|
||||
def set_highlight_only_button_icon(self):
|
||||
if config['highlight_search_matches']:
|
||||
self.highlight_only_button.setIcon(QIcon(I('highlight_only_on.png')))
|
||||
else:
|
||||
self.highlight_only_button.setIcon(QIcon(I('highlight_only_off.png')))
|
||||
self.library_view.model().set_highlight_only(config['highlight_search_matches'])
|
||||
|
||||
def focus_search_box(self, *args):
|
||||
self.search.setFocus(Qt.OtherFocusReason)
|
||||
@ -406,10 +405,6 @@ class SearchBoxMixin(object): # {{{
|
||||
self.search.do_search()
|
||||
self.focus_to_library()
|
||||
|
||||
def search_options_button_clicked(self):
|
||||
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
|
||||
'Search'), close_after_initial=True)
|
||||
|
||||
def focus_to_library(self):
|
||||
self.current_view().setFocus(Qt.OtherFocusReason)
|
||||
|
||||
@ -422,8 +417,6 @@ class SavedSearchBoxMixin(object): # {{{
|
||||
self.clear_button.clicked.connect(self.saved_search.clear)
|
||||
self.save_search_button.clicked.connect(
|
||||
self.saved_search.save_search_button_clicked)
|
||||
self.delete_search_button.clicked.connect(
|
||||
self.saved_search.delete_search_button_clicked)
|
||||
self.copy_search_button.clicked.connect(
|
||||
self.saved_search.copy_search_button_clicked)
|
||||
self.saved_searches_changed()
|
||||
@ -432,18 +425,20 @@ class SavedSearchBoxMixin(object): # {{{
|
||||
self.saved_search.setToolTip(
|
||||
_('Choose saved search or enter name for new saved search'))
|
||||
self.saved_search.setStatusTip(self.saved_search.toolTip())
|
||||
for x in ('copy', 'save', 'delete'):
|
||||
for x in ('copy', 'save'):
|
||||
b = getattr(self, x+'_search_button')
|
||||
b.setStatusTip(b.toolTip())
|
||||
|
||||
def saved_searches_changed(self, set_restriction=None):
|
||||
def saved_searches_changed(self, set_restriction=None, recount=True):
|
||||
p = sorted(saved_searches().names(), key=sort_key)
|
||||
if set_restriction is None:
|
||||
set_restriction = unicode(self.search_restriction.currentText())
|
||||
# rebuild the restrictions combobox using current saved searches
|
||||
self.search_restriction.clear()
|
||||
self.search_restriction.addItem('')
|
||||
self.tags_view.recount()
|
||||
self.search_restriction.addItem(_('*Current search'))
|
||||
if recount:
|
||||
self.tags_view.recount()
|
||||
for s in p:
|
||||
self.search_restriction.addItem(s)
|
||||
if set_restriction: # redo the search restriction if there was one
|
||||
|
@ -25,16 +25,36 @@ class SearchRestrictionMixin(object):
|
||||
r = self.search_restriction.findText(name)
|
||||
if r < 0:
|
||||
r = 0
|
||||
self.search_restriction.setCurrentIndex(r)
|
||||
self.apply_search_restriction(r)
|
||||
if r != self.search_restriction.currentIndex():
|
||||
self.search_restriction.setCurrentIndex(r)
|
||||
self.apply_search_restriction(r)
|
||||
|
||||
def apply_text_search_restriction(self, search):
|
||||
if not search:
|
||||
self.search_restriction.setItemText(1, _('*Current search'))
|
||||
self.search_restriction.setCurrentIndex(0)
|
||||
else:
|
||||
self.search_restriction.setCurrentIndex(1)
|
||||
self.search_restriction.setItemText(1, search)
|
||||
self._apply_search_restriction(search)
|
||||
|
||||
def apply_search_restriction(self, i):
|
||||
r = unicode(self.search_restriction.currentText())
|
||||
if r is not None and r != '':
|
||||
restriction = 'search:"%s"'%(r)
|
||||
self.search_restriction.setItemText(1, _('*Current search'))
|
||||
if i == 1:
|
||||
restriction = unicode(self.search.currentText())
|
||||
if not restriction:
|
||||
self.search_restriction.setCurrentIndex(0)
|
||||
else:
|
||||
self.search_restriction.setItemText(1, restriction)
|
||||
else:
|
||||
restriction = ''
|
||||
r = unicode(self.search_restriction.currentText())
|
||||
if r is not None and r != '':
|
||||
restriction = 'search:"%s"'%(r)
|
||||
else:
|
||||
restriction = ''
|
||||
self._apply_search_restriction(restriction)
|
||||
|
||||
def _apply_search_restriction(self, restriction):
|
||||
self.saved_search.clear()
|
||||
# The order below is important. Set the restriction, force a '' search
|
||||
# to apply it, reset the tag browser to take it into account, then set
|
||||
|
139
src/calibre/gui2/store/__init__.py
Normal file
139
src/calibre/gui2/store/__init__.py
Normal file
@ -0,0 +1,139 @@
|
||||
# -*- 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'
|
||||
|
||||
class StorePlugin(object): # {{{
|
||||
'''
|
||||
A plugin representing an online ebook repository (store). The store can
|
||||
be a comercial store that sells ebooks or a source of free downloadable
|
||||
ebooks.
|
||||
|
||||
Note that this class is the base class for these plugins, however, to
|
||||
integrate the plugin with calibre's plugin system, you have to make a
|
||||
wrapper class that references the actual plugin. See the
|
||||
:mod:`calibre.customize.builtins` module for examples.
|
||||
|
||||
If two :class:`StorePlugin` objects have the same name, the one with higher
|
||||
priority takes precedence.
|
||||
|
||||
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
||||
|
||||
Regarding :meth:`open`. Most stores only make themselves available
|
||||
though a web site thus most store plugins will open using
|
||||
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
||||
open a modal window and display the store website in a QWebView.
|
||||
|
||||
Sub-classes should implement and use the :meth:`genesis` if they require
|
||||
plugin specific initialization. They should not override or otherwise
|
||||
reimplement :meth:`__init__`.
|
||||
|
||||
Once initialized, this plugin has access to the main calibre GUI via the
|
||||
:attr:`gui` member. You can access other plugins by name, for example::
|
||||
|
||||
self.gui.istores['Amazon Kindle']
|
||||
|
||||
Plugin authors can use affiliate programs within their plugin. The
|
||||
distribution of money earned from a store plugin is 70/30. 70% going
|
||||
to the pluin author / maintainer and 30% going to the calibre project.
|
||||
|
||||
The easiest way to handle affiliate money payouts is to randomly select
|
||||
between the author's affiliate id and calibre's affiliate id so that
|
||||
70% of the time the author's id is used.
|
||||
'''
|
||||
|
||||
def __init__(self, gui, name):
|
||||
self.gui = gui
|
||||
self.name = name
|
||||
self.base_plugin = None
|
||||
|
||||
def open(self, gui, parent=None, detail_item=None, external=False):
|
||||
'''
|
||||
Open the store.
|
||||
|
||||
:param gui: The main GUI. This will be used to have the job
|
||||
system start downloading an item from the store.
|
||||
|
||||
:param parent: The parent of the store dialog. This is used
|
||||
to create modal dialogs.
|
||||
|
||||
:param detail_item: A plugin specific reference to an item
|
||||
in the store that the user should be shown.
|
||||
|
||||
:param external: When False open an internal dialog with the
|
||||
store. When True open the users default browser to the store's
|
||||
web site. :param:`detail_item` should still be respected when external
|
||||
is True.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
Searches the store for items matching query. This should
|
||||
return items as a generator.
|
||||
|
||||
Don't be lazy with the search! Load as much data as possible in the
|
||||
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
|
||||
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
||||
isn't available because the store does not display cover images then it's okay to
|
||||
ignore it.
|
||||
|
||||
Also, by default search results can only include ebooks. A plugin can offer users
|
||||
an option to include physical books in the search results but this must be
|
||||
disabled by default.
|
||||
|
||||
If a store doesn't provide search on it's own use something like a site specific
|
||||
google search to get search results for this funtion.
|
||||
|
||||
:param query: The string query search with.
|
||||
:param max_results: The maximum number of results to return.
|
||||
:param timeout: The maximum amount of time in seconds to spend download the search results.
|
||||
|
||||
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
||||
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_settings(self):
|
||||
'''
|
||||
This is only useful for plugins that implement
|
||||
:attr:`config_widget` that is the only way to save
|
||||
settings. This is used by plugins to get the saved
|
||||
settings and apply when necessary.
|
||||
|
||||
:return: A dictionary filled with the settings used
|
||||
by this plugin.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def do_genesis(self):
|
||||
self.genesis()
|
||||
|
||||
def genesis(self):
|
||||
'''
|
||||
Plugin specific initialization.
|
||||
'''
|
||||
pass
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
@ -0,0 +1,172 @@
|
||||
# -*- 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 random
|
||||
import re
|
||||
import urllib2
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class AmazonKindleStore(StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
'''
|
||||
Amazon comes with a number of difficulties.
|
||||
|
||||
QWebView has major issues with Amazon.com. The largest of
|
||||
issues is it simply doesn't work on a number of pages.
|
||||
|
||||
When connecting to a number parts of Amazon.com (Kindle library
|
||||
for instance) QNetworkAccessManager fails to connect with a
|
||||
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
||||
when I check QNetworkRequest.HttpStatusCodeAttribute when the
|
||||
399 error is returned the status code is 200 (Ok). However, once
|
||||
the QNetworkAccessManager decides there was a NetworkError it
|
||||
does not download the page from Amazon. So I can't even set the
|
||||
HTML in the QWebView myself.
|
||||
|
||||
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
||||
open bug about the issue but it is not correct. We can set the
|
||||
useragent (Arora does) to something else and the above issue
|
||||
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
||||
gives a bit more information about the issue but as of now (27/Feb/2011)
|
||||
there is no solution or work around.
|
||||
|
||||
We cannot change the The linkDelegationPolicy to allow us to avoid
|
||||
QNetworkAccessManager because it only works links. Forms aren't
|
||||
included so the same issue persists on any part of the site (login)
|
||||
that use a form to load a new page.
|
||||
|
||||
Using an aStore was evaluated but I've decided against using it.
|
||||
There are three major issues with an aStore. Because checkout is
|
||||
handled by sending the user to Amazon we can't put it in a QWebView.
|
||||
If we're sending the user to Amazon sending them there directly is
|
||||
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
||||
redirection the users default browser because the cookies with the
|
||||
shopping cart won't transfer.
|
||||
|
||||
Another issue with the aStore is how it handles the referral. It only
|
||||
counts the referral for the items in the shopping card / the item
|
||||
that directed the user to Amazon. Kindle books do not use the shopping
|
||||
cart and send the user directly to Amazon for the purchase. In this
|
||||
instance we would only get referral credit for the one book that the
|
||||
aStore directs to Amazon that the user buys. Any other purchases we
|
||||
won't get credit for.
|
||||
|
||||
The last issue with the aStore is performance. Even though it's an
|
||||
Amazon site it's alow. So much slower than Amazon.com that it makes
|
||||
me not want to browse books using it. The look and feel are lesser
|
||||
issues. So is the fact that it almost seems like the purchase is
|
||||
with calibre. This can cause some support issues because we can't
|
||||
do much for issues with Amazon.com purchase hiccups.
|
||||
|
||||
Another option that was evaluated was the Product Advertising API.
|
||||
The reasons against this are complexity. It would take a lot of work
|
||||
to basically re-create Amazon.com within calibre. The Product
|
||||
Advertising API is also designed with being run on a server not
|
||||
in an app. The signing keys would have to be made avaliable to ever
|
||||
calibre user which means bad things could be done with our account.
|
||||
|
||||
The Product Advertising API also assumes the same browser for easy
|
||||
shopping cart transfer to Amazon. With QWebView not working and there
|
||||
not being an easy way to transfer cookies between a QWebView and the
|
||||
users default browser this won't work well.
|
||||
|
||||
We could create our own website on the calibre server and create an
|
||||
Amazon Product Advertising API store. However, this goes back to the
|
||||
complexity argument. Why spend the time recreating Amazon.com
|
||||
|
||||
The final and largest issue against using the Product Advertising API
|
||||
is the Efficiency Guidelines:
|
||||
|
||||
"Each account used to access the Product Advertising API will be allowed
|
||||
an initial usage limit of 2,000 requests per hour. Each account will
|
||||
receive an additional 500 requests per hour (up to a maximum of 25,000
|
||||
requests per hour) for every $1 of shipped item revenue driven per hour
|
||||
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
||||
on revenue performance."
|
||||
|
||||
With over two million users a limit of 2,000 request per hour could
|
||||
render our store unusable for no other reason than Amazon rate
|
||||
limiting our traffic.
|
||||
|
||||
The best (I use the term lightly here) solution is to open Amazon.com
|
||||
in the users default browser and set the affiliate id as part of the url.
|
||||
'''
|
||||
aff_id = {'tag': 'josbl0e-cpb-20'}
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id['tag'] = 'calibrebs-20'
|
||||
store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id
|
||||
if detail_item:
|
||||
aff_id['asin'] = detail_item
|
||||
store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id
|
||||
open_url(QUrl(store_link))
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(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('//div[@class="productData"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
# Even though we are searching digital-text only Amazon will still
|
||||
# put in results for non Kindle books (author pages). Se we need
|
||||
# to explicitly check if the item is a Kindle book and ignore it
|
||||
# if it isn't.
|
||||
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
||||
if 'kindle' not in type.lower():
|
||||
continue
|
||||
|
||||
# We must have an asin otherwise we can't easily reference the
|
||||
# book later.
|
||||
asin_href = None
|
||||
asin_a = data.xpath('div[@class="productTitle"]/a[1]')
|
||||
if asin_a:
|
||||
asin_href = asin_a[0].get('href', '')
|
||||
m = re.search(r'/dp/(?P<asin>.+?)(/|$)', asin_href)
|
||||
if m:
|
||||
asin = m.group('asin')
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
cover_url = ''
|
||||
if asin_href:
|
||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||
if cover_img:
|
||||
cover_url = cover_img[0]
|
||||
|
||||
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||
author = author.split('by')[-1]
|
||||
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = asin.strip()
|
||||
|
||||
yield s
|
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
@ -0,0 +1,89 @@
|
||||
# -*- 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 re
|
||||
import urllib2
|
||||
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 BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.webscription.net/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.webscription.net/searchadv.aspx?IsSubmit=true&SearchTerm=' + urllib2.quote(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('//table/tr/td/img[@src="skins/Skin_1/images/matchingproducts.gif"]/..//tr'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('./td[1]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('./td[1]/a/text()'))
|
||||
|
||||
author = ''
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open('http://www.webscription.net/' + id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
author = ''.join(idata.xpath('//span[@class="ProductNameText"]/../b/text()'))
|
||||
author = author.split('by ')[-1]
|
||||
price = ''.join(idata.xpath('//span[@class="variantprice"]/text()'))
|
||||
a, b, price = price.partition('$')
|
||||
price = b + price
|
||||
|
||||
pnum = ''
|
||||
mo = re.search(r'p-(?P<num>\d+)-', id.strip())
|
||||
if mo:
|
||||
pnum = mo.group('num')
|
||||
if pnum:
|
||||
cover_url = 'http://www.webscription.net/' + ''.join(idata.xpath('//img[@id="ProductPic%s"]/@src' % pnum))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
52
src/calibre/gui2/store/basic_config.py
Normal file
52
src/calibre/gui2/store/basic_config.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- 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'
|
||||
|
||||
from PyQt4.Qt import QWidget
|
||||
|
||||
from calibre.gui2 import gprefs
|
||||
from calibre.gui2.store.basic_config_widget_ui import Ui_Form
|
||||
|
||||
def save_settings(config_widget):
|
||||
gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked()
|
||||
tags = unicode(config_widget.tags.text())
|
||||
gprefs[config_widget.store.name + '_tags'] = tags
|
||||
|
||||
class BasicStoreConfigWidget(QWidget, Ui_Form):
|
||||
|
||||
def __init__(self, store):
|
||||
QWidget.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.store = store
|
||||
|
||||
self.load_setings()
|
||||
|
||||
def load_setings(self):
|
||||
settings = self.store.get_settings()
|
||||
|
||||
self.open_external.setChecked(settings.get(self.store.name + '_open_external'))
|
||||
self.tags.setText(settings.get(self.store.name + '_tags', ''))
|
||||
|
||||
class BasicStoreConfig(object):
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Customize the behavior of this store.'
|
||||
|
||||
def config_widget(self):
|
||||
return BasicStoreConfigWidget(self)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
save_settings(config_widget)
|
||||
|
||||
def get_settings(self):
|
||||
settings = {}
|
||||
|
||||
settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False)
|
||||
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
|
||||
|
||||
return settings
|
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>460</width>
|
||||
<height>69</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Added Tags:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="tags"/>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="open_external">
|
||||
<property name="text">
|
||||
<string>Open store in external web browswer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
80
src/calibre/gui2/store/bewrite_plugin.py
Normal file
80
src/calibre/gui2/store/bewrite_plugin.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- 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 urllib2
|
||||
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 BeWriteStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(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('//div[@id="content"]//table/tr[position() > 1]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
heading = ''.join(data.xpath('./td[2]//text()'))
|
||||
title, q, author = heading.partition('by ')
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
||||
price = '$' + price.split('$')[-1]
|
||||
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
||||
if cover_img:
|
||||
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url.strip()
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user