Sycn to trunk.

This commit is contained in:
John Schember 2011-03-28 19:52:42 -04:00
commit 223cb00e61
55 changed files with 1402 additions and 422 deletions

View File

@ -0,0 +1,83 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Cracked(BasicNewsRecipe):
title = u'Cracked.com'
__author__ = u'Nudgenudge'
language = 'en'
description = 'America''s Only Humor and Video Site, since 1958'
publisher = 'Cracked'
category = 'comedy, lists'
oldest_article = 2
delay = 10
max_articles_per_feed = 2
no_stylesheets = True
encoding = 'cp1252'
remove_javascript = True
use_embedded_content = False
INDEX = u'http://www.cracked.com'
extra_css = """
.pageheader_type{font-size: x-large; font-weight: bold; color: #828D74}
.pageheader_title{font-size: xx-large; color: #394128}
.pageheader_byline{font-size: small; font-weight: bold; color: #394128}
.score_bg {display: inline; width: 100%; margin-bottom: 2em}
.score_column_1{ padding-left: 10px; font-size: small; width: 50%}
.score_column_2{ padding-left: 10px; font-size: small; width: 50%}
.score_column_3{ padding-left: 10px; font-size: small; width: 50%}
.score_header{font-size: large; color: #50544A}
.bodytext{display: block}
body{font-family: Helvetica,Arial,sans-serif}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
keep_only_tags = [
dict(name='div', attrs={'class':['Column1']})
]
feeds = [(u'Articles', u'http://feeds.feedburner.com/CrackedRSS')]
def get_article_url(self, article):
return article.get('guid', None)
def cleanup_page(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
for div_to_remove in soup.findAll('div', attrs={'id':['googlead_1','fb-like-article','comments_section']}):
div_to_remove.extract()
for div_to_remove in soup.findAll('div', attrs={'class':['share_buttons_col_1','GenericModule1']}):
div_to_remove.extract()
for div_to_remove in soup.findAll('div', attrs={'class':re.compile("prev_next")}):
div_to_remove.extract()
for ul_to_remove in soup.findAll('ul', attrs={'class':['Nav6']}):
ul_to_remove.extract()
for image in soup.findAll('img', attrs={'alt': 'article image'}):
image.extract()
def append_page(self, soup, appendtag, position):
pager = soup.find('a',attrs={'class':'next_arrow_active'})
if pager:
nexturl = self.INDEX + pager['href']
soup2 = self.index_to_soup(nexturl)
texttag = soup2.find('div', attrs={'class':re.compile("userStyled")})
newpos = len(texttag.contents)
self.append_page(soup2,texttag,newpos)
texttag.extract()
self.cleanup_page(appendtag)
appendtag.insert(position,texttag)
else:
self.cleanup_page(appendtag)
def preprocess_html(self, soup):
self.append_page(soup, soup.body, 3)
return self.adeify_images(soup)

View File

@ -1,63 +1,134 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, elsuave'
'''
estadao.com.br
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from datetime import datetime, timedelta
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
from calibre.utils.magick import Image, PixelWand
from urllib2 import Request, urlopen, URLError
class Estadao(BasicNewsRecipe): class Estadao(BasicNewsRecipe):
title = 'O Estado de S. Paulo' THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/
__author__ = 'elsuave (modified from Darko Miletic)' LANGUAGE = 'pt_br'
description = 'News from Brasil in Portuguese' language = 'pt'
publisher = 'O Estado de S. Paulo' LANGHTM = 'pt-br'
category = 'news, politics, Brasil' ENCODING = 'utf'
oldest_article = 2 ENCHTM = 'utf-8'
max_articles_per_feed = 25 directionhtm = 'ltr'
requires_version = (0,8,47)
news = True
publication_type = 'newsportal'
title = u'Estadao'
__author__ = 'Euler Alves'
description = u'Brazilian news from Estad\xe3o'
publisher = u'Estad\xe3o'
category = 'news, rss'
oldest_article = 4
max_articles_per_feed = 100
summary_length = 1000
remove_javascript = True
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
encoding = 'utf8' remove_empty_feeds = True
cover_url = 'http://www.estadao.com.br/img/logo_estadao.png' timefmt = ' [%d %b %Y (%a)]'
remove_javascript = True
html2lrf_options = [ html2lrf_options = [
'--comment', description '--comment', description
, '--category', category ,'--category', category
, '--publisher', publisher ,'--publisher', publisher
] ]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"' html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [ hoje = datetime.now()-timedelta(days=2)
dict(name='div', attrs={'class':['bb-md-noticia','c5']}) pubdate = hoje.strftime('%a, %d %b')
] if hoje.hour<10:
hoje = hoje-timedelta(days=1)
CAPA = 'http://www.estadao.com.br/estadaodehoje/'+hoje.strftime('%Y%m%d')+'/img/capadodia.jpg'
SCREENSHOT = 'http://estadao.com.br/'
cover_margins = (0,0,'white')
masthead_url = 'http://www.estadao.com.br/estadao/novo/img/logo.png'
keep_only_tags = [dict(name='div', attrs={'class':['bb-md-noticia','corpo']})]
remove_tags = [ remove_tags = [
dict(name=['script','object','form','ul']) dict(name='div',
,dict(name='div', attrs={'class':['fnt2 Color_04 bold','right fnt2 innerTop15 dvTmFont','™_01 right outerLeft15','tituloBox','tags']}) attrs={'id':[
,dict(name='div', attrs={'id':['bb-md-noticia-subcom']}) 'bb-md-noticia-tabs'
] ]})
,dict(name='div',
attrs={'class':[
'tags'
,'discussion'
,'bb-gg adsense_container'
]})
,dict(name='a')
,dict(name='iframe')
,dict(name='link')
,dict(name='script')
]
feeds = [ feeds = [
(u'Manchetes Estadao', u'http://www.estadao.com.br/rss/manchetes.xml') (u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml')
,(u'Ultimas noticias', u'http://www.estadao.com.br/rss/ultimas.xml') ,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml')
,(u'Nacional', u'http://www.estadao.com.br/rss/nacional.xml') ,(u'Brasil', u'http://www.estadao.com.br/rss/brasil.xml')
,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml') ,(u'Internacional', u'http://www.estadao.com.br/rss/internacional.xml')
,(u'Cidades', u'http://www.estadao.com.br/rss/cidades.xml') ,(u'Cinema', u'http://blogs.estadao.com.br/cinema/feed/')
,(u'Esportes', u'http://www.estadao.com.br/rss/esportes.xml') ,(u'Planeta', u'http://www.estadao.com.br/rss/planeta.xml')
,(u'Arte & Lazer', u'http://www.estadao.com.br/rss/arteelazer.xml') ,(u'Ci\xEAncia', u'http://www.estadao.com.br/rss/ciencia.xml')
,(u'Economia', u'http://www.estadao.com.br/rss/economia.xml') ,(u'Sa\xFAde', u'http://www.estadao.com.br/rss/saude.xml')
,(u'Vida &', u'http://www.estadao.com.br/rss/vidae.xml') ,(u'Pol\xEDtica', u'http://www.estadao.com.br/rss/politica.xml')
] ]
conversion_options = {
'title' : title
,'comments' : description
,'publisher' : publisher
,'tags' : category
,'language' : LANGUAGE
,'linearize_tables': True
}
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
if not soup.find(attrs={'http-equiv':'Content-Language'}):
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
soup.head.insert(0,meta0)
if not soup.find(attrs={'http-equiv':'Content-Type'}):
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
soup.head.insert(0,meta1)
return soup
language = 'pt' def postprocess_html(self, soup, first):
#process all the images. assumes that the new html has the correct path
def get_article_url(self, article): for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
url = BasicNewsRecipe.get_article_url(self, article) iurl = tag['src']
if '/Multimidia/' not in url: img = Image()
return url img.open(iurl)
width, height = img.size
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
pw = PixelWand()
if( width > height and width > 590) :
print 'Rotate image'
img.rotate(pw, -90)
img.save(iurl)
return soup
def get_cover_url(self):
cover_url = self.CAPA
pedido = Request(self.CAPA)
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
pedido.add_header('Accept-Charset',self.ENCHTM)
pedido.add_header('Referer',self.SCREENSHOT)
try:
resposta = urlopen(pedido)
soup = BeautifulSoup(resposta)
cover_item = soup.find('body')
if cover_item:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url
except URLError:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url

View File

@ -1,74 +1,149 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Saverio Palmieri Neto <saverio.palmieri at gmail.com>'
'''
folha.uol.com.br
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from datetime import datetime, timedelta
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
from calibre.utils.magick import Image, PixelWand
from urllib2 import Request, urlopen, URLError
class FolhaOnline(BasicNewsRecipe): class FolhaOnline(BasicNewsRecipe):
title = 'Folha de Sao Paulo' THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/
__author__ = 'Saverio Palmieri Neto' LANGUAGE = 'pt_br'
description = 'Brazilian news from Folha de Sao Paulo Online' language = 'pt'
publisher = 'Folha de Sao Paulo' LANGHTM = 'pt-br'
category = 'Brasil, news' ENCODING = 'cp1252'
oldest_article = 2 ENCHTM = 'iso-8859-1'
max_articles_per_feed = 1000 directionhtm = 'ltr'
summary_length = 2048 requires_version = (0,8,47)
news = True
publication_type = 'newsportal'
title = u'Folha de S\xE3o Paulo'
__author__ = 'Euler Alves'
description = u'Brazilian news from Folha de S\xE3o Paulo'
publisher = u'Folha de S\xE3o Paulo'
category = 'news, rss'
oldest_article = 4
max_articles_per_feed = 100
summary_length = 1000
remove_javascript = True
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
remove_empty_feeds = True
timefmt = ' [%d %b %Y (%a)]' timefmt = ' [%d %b %Y (%a)]'
encoding = 'cp1252'
cover_url = 'http://lh5.ggpht.com/_hEb7sFmuBvk/TFoiKLRS5dI/AAAAAAAAADM/kcVKggZwKnw/capa_folha.jpg'
cover_margins = (5,5,'white')
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})] html2lrf_options = [
'--comment', description
,'--category', category
,'--publisher', publisher
]
remove_tags = [ html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
dict(name='script')
,dict(name='div',
attrs={'id':[
'articleButton'
,'bookmarklets'
,'ad-180x150-1'
,'contextualAdsArticle'
,'articleEnd'
,'articleComments'
]})
,dict(name='div',
attrs={'class':[
'openBox adslibraryArticle'
]})
,dict(name='a')
,dict(name='iframe')
,dict(name='link')
]
hoje = datetime.now()
pubdate = hoje.strftime('%a, %d %b')
if hoje.hour<6:
hoje = hoje-timedelta(days=1)
CAPA = 'http://www1.folha.uol.com.br/fsp/images/cp'+hoje.strftime('%d%m%Y')+'.jpg'
SCREENSHOT = 'http://www1.folha.uol.com.br/'
cover_margins = (0,0,'white')
masthead_url = 'http://f.i.uol.com.br/fsp/furniture/images/lgo-fsp-430x50-ffffff.gif'
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})]
remove_tags = [
dict(name='div',
attrs={'id':[
'articleButton'
,'bookmarklets'
,'ad-180x150-1'
,'contextualAdsArticle'
,'articleEnd'
,'articleComments'
]})
,dict(name='div',
attrs={'class':[
'openBox adslibraryArticle'
]})
,dict(name='a')
,dict(name='iframe')
,dict(name='link')
,dict(name='script')
]
feeds = [ feeds = [
(u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml') (u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml')
,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml') ,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml')
,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml') ,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml')
,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml') ,(u'Ci\xEAncia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml')
,(u'Ciencia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml') ,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml')
,(u'Cotidiano', u'http://feeds.folha.uol.com.br/cotidiado/rss091.xml') ,(u'Equil\xEDbrio e Sa\xFAde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml')
,(u'Saber', u'http://feeds.folha.uol.com.br/saber/rss091.xml') ,(u'Turismo', u'http://feeds.folha.uol.com.br/folha/turismo/rss091.xml')
,(u'Equilíbrio e Saúde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml') ,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml')
,(u'Esporte', u'http://feeds.folha.uol.com.br/esporte/rss091.xml') ,(u'Pelo Mundo', u'http://feeds.folha.uol.com.br/pelomundo.folha.rssblog.uol.com.br/')
,(u'Ilustrada', u'http://feeds.folha.uol.com.br/ilustrada/rss091.xml') ,(u'Circuito integrado', u'http://feeds.folha.uol.com.br/circuitointegrado.folha.rssblog.uol.com.br/')
,(u'Ilustríssima', u'http://feeds.folha.uol.com.br/ilustrissima/rss091.xml') ,(u'Blog do Fred', u'http://feeds.folha.uol.com.br/blogdofred.folha.rssblog.uol.com.br/')
,(u'Mercado', u'http://feeds.folha.uol.com.br/mercado/rss091.xml') ,(u'Maria In\xEAs Dolci', u'http://feeds.folha.uol.com.br/mariainesdolci.folha.blog.uol.com.br/')
,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml') ,(u'Eduardo Ohata', u'http://feeds.folha.uol.com.br/folha/pensata/eduardoohata/rss091.xml')
,(u'Tec', u'http://feeds.folha.uol.com.br/tec/rss091.xml') ,(u'Kennedy Alencar', u'http://feeds.folha.uol.com.br/folha/pensata/kennedyalencar/rss091.xml')
,(u'Turismo', u'http://feeds.folha.uol.com.br/turismo/rss091.xml') ,(u'Eliane Catanh\xEAde', u'http://feeds.folha.uol.com.br/folha/pensata/elianecantanhede/rss091.xml')
] ,(u'Fernado Canzian', u'http://feeds.folha.uol.com.br/folha/pensata/fernandocanzian/rss091.xml')
,(u'Gilberto Dimenstein', u'http://feeds.folha.uol.com.br/folha/pensata/gilbertodimenstein/rss091.xml')
,(u'H\xE9lio Schwartsman', u'http://feeds.folha.uol.com.br/folha/pensata/helioschwartsman/rss091.xml')
,(u'Jo\xE3o Pereira Coutinho', u'http://http://feeds.folha.uol.com.br/folha/pensata/joaopereiracoutinho/rss091.xml')
,(u'Luiz Caversan', u'http://http://feeds.folha.uol.com.br/folha/pensata/luizcaversan/rss091.xml')
,(u'S\xE9rgio Malbergier', u'http://http://feeds.folha.uol.com.br/folha/pensata/sergiomalbergier/rss091.xml')
,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml')
]
conversion_options = {
'title' : title
,'comments' : description
,'publisher' : publisher
,'tags' : category
,'language' : LANGUAGE
,'linearize_tables': True
}
def preprocess_html(self, soup): def preprocess_html(self, soup):
for item in soup.findAll(style=True): for item in soup.findAll(style=True):
del item['style'] del item['style']
if not soup.find(attrs={'http-equiv':'Content-Language'}):
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
soup.head.insert(0,meta0)
if not soup.find(attrs={'http-equiv':'Content-Type'}):
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
soup.head.insert(0,meta1)
return soup return soup
language = 'pt' def postprocess_html(self, soup, first):
#process all the images. assumes that the new html has the correct path
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
iurl = tag['src']
img = Image()
img.open(iurl)
width, height = img.size
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
pw = PixelWand()
if( width > height and width > 590) :
print 'Rotate image'
img.rotate(pw, -90)
img.save(iurl)
return soup
def get_cover_url(self):
cover_url = self.CAPA
pedido = Request(self.CAPA)
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
pedido.add_header('Accept-Charset',self.ENCHTM)
pedido.add_header('Referer',self.SCREENSHOT)
try:
resposta = urlopen(pedido)
soup = BeautifulSoup(resposta)
cover_item = soup.find('body')
if cover_item:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url
except URLError:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url

View File

@ -39,14 +39,26 @@ class WashingtonPost(BasicNewsRecipe):
{'class':lambda x: x and 'also-read' in x.split()}, {'class':lambda x: x and 'also-read' in x.split()},
{'class':lambda x: x and 'partners-content' in x.split()}, {'class':lambda x: x and 'partners-content' in x.split()},
{'class':['module share', 'module ads', 'comment-vars', 'hidden', {'class':['module share', 'module ads', 'comment-vars', 'hidden',
'share-icons-wrap', 'comments']}, 'share-icons-wrap', 'comments', 'flipper']},
{'id':['right-rail']}, {'id':['right-rail', 'save-and-share']},
{'width':'1', 'height':'1'},
]
]
keep_only_tags = dict(id=['content', 'article']) keep_only_tags = dict(id=['content', 'article'])
def get_article_url(self, *args):
ans = BasicNewsRecipe.get_article_url(self, *args)
ans = ans.rpartition('?')[0]
if ans.endswith('_video.html'):
return None
if 'ads.pheedo.com' in ans:
return None
#if not ans.endswith('_blog.html'):
# return None
return ans
def print_version(self, url): def print_version(self, url):
url = url.rpartition('?')[0]
return url.replace('_story.html', '_singlePage.html') return url.replace('_story.html', '_singlePage.html')

View File

@ -363,3 +363,11 @@ maximum_cover_size = (1200, 1600)
# the files will be sent to the location with the most free space. # the files will be sent to the location with the most free space.
send_news_to_device_location = "main" send_news_to_device_location = "main"
#: What interfaces should the content server listen on
# By default, the calibre content server listens on '0.0.0.0' which means that it
# accepts IPv4 connections on all interfaces. You can change this to, for
# example, '127.0.0.1' to only listen for connections from the local machine, or
# to '::' to listen to all incoming IPv6 and IPv4 connections (this may not
# work on all operating systems)
server_listen_on = '0.0.0.0'

View File

@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, glob
from subprocess import check_call from subprocess import check_call
from tempfile import NamedTemporaryFile, mkdtemp from tempfile import NamedTemporaryFile, mkdtemp
from zipfile import ZipFile
from setup import Command, __version__, installer_name, __appname__ from setup import Command, __version__, installer_name, __appname__
@ -341,7 +341,25 @@ class UploadUserManual(Command): # {{{
description = 'Build and upload the User Manual' description = 'Build and upload the User Manual'
sub_commands = ['manual'] sub_commands = ['manual']
def build_plugin_example(self, path):
from calibre import CurrentDir
with NamedTemporaryFile(suffix='.zip') as f:
with CurrentDir(self.d(path)):
with ZipFile(f, 'w') as zf:
for x in os.listdir('.'):
zf.write(x)
if os.path.isdir(x):
for y in os.listdir(x):
zf.write(os.path.join(x, y))
bname = self.b(path) + '_plugin.zip'
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS,
bname)])
def run(self, opts): def run(self, opts):
path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples')
for x in glob.glob(self.j(path, '*')):
self.build_plugin_example(x)
check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*', check_call(' '.join(['scp', '-r', 'src/calibre/manual/.build/html/*',
'divok:%s'%USER_MANUAL]), shell=True) 'divok:%s'%USER_MANUAL]), shell=True)
# }}} # }}}

View File

@ -61,6 +61,9 @@ def osx_version():
if m: if m:
return int(m.group(1)), int(m.group(2)), int(m.group(3)) return int(m.group(1)), int(m.group(2)), int(m.group(3))
def confirm_config_name(name):
return name + '_again'
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]') _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
_filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<', _filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<',
u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32)))) u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32))))

View File

@ -525,13 +525,21 @@ class InterfaceActionBase(Plugin): # {{{
actual_plugin = None actual_plugin = None
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.actual_plugin_ = None
def load_actual_plugin(self, gui): def load_actual_plugin(self, gui):
''' '''
This method must return the actual interface action plugin object. This method must return the actual interface action plugin object.
''' '''
mod, cls = self.actual_plugin.split(':') ac = self.actual_plugin_
return getattr(importlib.import_module(mod), cls)(gui, if ac is None:
self.site_customization) mod, cls = self.actual_plugin.split(':')
ac = getattr(importlib.import_module(mod), cls)(gui,
self.site_customization)
self.actual_plugin_ = ac
return ac
# }}} # }}}

View File

@ -853,7 +853,7 @@ class Columns(PreferencesPlugin):
class Toolbar(PreferencesPlugin): class Toolbar(PreferencesPlugin):
name = 'Toolbar' name = 'Toolbar'
icon = I('wizard.png') icon = I('wizard.png')
gui_name = _('Customize the toolbar') gui_name = _('Toolbar')
category = 'Interface' category = 'Interface'
gui_category = _('Interface') gui_category = _('Interface')
category_order = 1 category_order = 1
@ -865,7 +865,7 @@ class Toolbar(PreferencesPlugin):
class Search(PreferencesPlugin): class Search(PreferencesPlugin):
name = 'Search' name = 'Search'
icon = I('search.png') icon = I('search.png')
gui_name = _('Customize searching') gui_name = _('Searching')
category = 'Interface' category = 'Interface'
gui_category = _('Interface') gui_category = _('Interface')
category_order = 1 category_order = 1

View File

@ -2,14 +2,17 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import, from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
from future_builtins import map
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, zipfile, posixpath, importlib, threading, re import os, zipfile, posixpath, importlib, threading, re, imp, sys
from collections import OrderedDict from collections import OrderedDict
from functools import partial
from calibre import as_unicode
from calibre.customize import (Plugin, numeric_version, platform, from calibre.customize import (Plugin, numeric_version, platform,
InvalidPlugin, PluginNotFound) InvalidPlugin, PluginNotFound)
@ -17,31 +20,138 @@ from calibre.customize import (Plugin, numeric_version, platform,
# python 2.x that prevents importing from zip files in locations whose paths # python 2.x that prevents importing from zip files in locations whose paths
# have non ASCII characters # have non ASCII characters
def get_resources(zfp, name_or_list_of_names):
'''
Load resources from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : file_contents}``. Any names
that were not found in the zip file will not be present in the
dictionary. If a single path is passed in the return value will
be just the bytes of the resource or None if it wasn't found.
'''
names = name_or_list_of_names
if isinstance(names, basestring):
names = [names]
ans = {}
with zipfile.ZipFile(zfp) as zf:
for name in names:
try:
ans[name] = zf.read(name)
except:
import traceback
traceback.print_exc()
if len(names) == 1:
ans = ans.pop(names[0], None)
return ans
def get_icons(zfp, name_or_list_of_names):
'''
Load icons from the plugin zip file
:param name_or_list_of_names: List of paths to resources in the zip file using / as
separator, or a single path
:return: A dictionary of the form ``{name : QIcon}``. Any names
that were not found in the zip file will be null QIcons.
If a single path is passed in the return value will
be A QIcon.
'''
from PyQt4.Qt import QIcon, QPixmap
names = name_or_list_of_names
ans = get_resources(zfp, names)
if isinstance(names, basestring):
names = [names]
if ans is None:
ans = {}
if isinstance(ans, basestring):
ans = dict([(names[0], ans)])
ians = {}
for name in names:
p = QPixmap()
raw = ans.get(name, None)
if raw:
p.loadFromData(raw)
ians[name] = QIcon(p)
if len(names) == 1:
ians = ians.pop(names[0])
return ians
class PluginLoader(object): class PluginLoader(object):
'''
The restrictions that a zip file must obey to be a valid calibre plugin
are:
* The .py file that defines the main plugin class must have a name
that:
* Ends in plugin.py
* Is a valid python identifier (contains only English alphabets,
underscores and numbers and starts with an alphabet). This
applies to the file name minus the .py extension, obviously.
* Try to make this name as distinct as possible, as it will be
put into a global namespace of all plugins.
* The zip file must contain a .py file that defines the main plugin
class at the top level. That is, it must not be in a subdirectory.
The filename must follow the restrictions outlined above.
'''
def __init__(self): def __init__(self):
self.loaded_plugins = {} self.loaded_plugins = {}
self._lock = threading.RLock() self._lock = threading.RLock()
self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*') self._identifier_pat = re.compile(r'[a-zA-Z][_0-9a-zA-Z]*')
def _get_actual_fullname(self, fullname):
parts = fullname.split('.')
if parts[0] == 'calibre_plugins':
if len(parts) == 1:
return parts[0], None
plugin_name = parts[1]
with self._lock:
names = self.loaded_plugins.get(plugin_name, None)
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
names = names[1]
fullname = '.'.join(parts[2:])
if not fullname:
fullname = '__init__'
if fullname in names:
return fullname, plugin_name
if fullname+'.__init__' in names:
return fullname+'.__init__', plugin_name
return None, None
def find_module(self, fullname, path=None):
fullname, plugin_name = self._get_actual_fullname(fullname)
if fullname is None and plugin_name is None:
return None
return self
def load_module(self, fullname):
import_name, plugin_name = self._get_actual_fullname(fullname)
if import_name is None and plugin_name is None:
raise ImportError('No plugin named %r is loaded'%fullname)
mod = sys.modules.setdefault(fullname, imp.new_module(fullname))
mod.__file__ = "<calibre Plugin Loader>"
mod.__loader__ = self
if import_name.endswith('.__init__') or import_name in ('__init__',
'calibre_plugins'):
# We have a package
mod.__path__ = []
if plugin_name is not None:
# We have some actual code to load
with self._lock:
zfp, names = self.loaded_plugins.get(plugin_name, (None, None))
if names is None:
raise ImportError('No plugin named %r loaded'%plugin_name)
zinfo = names.get(import_name, None)
if zinfo is None:
raise ImportError('Plugin %r has no module named %r' %
(plugin_name, import_name))
with zipfile.ZipFile(zfp) as zf:
try:
code = zf.read(zinfo)
except:
# Maybe the zip file changed from under us
code = zf.read(zinfo.filename)
compiled = compile(code, 'calibre_plugins.%s.%s'%(plugin_name,
import_name), 'exec', dont_inherit=True)
mod.__dict__['get_resources'] = partial(get_resources, zfp)
mod.__dict__['get_icons'] = partial(get_icons, zfp)
exec compiled in mod.__dict__
return mod
def load(self, path_to_zip_file): def load(self, path_to_zip_file):
if not os.access(path_to_zip_file, os.R_OK): if not os.access(path_to_zip_file, os.R_OK):
raise PluginNotFound('Cannot access %r'%path_to_zip_file) raise PluginNotFound('Cannot access %r'%path_to_zip_file)
@ -51,26 +161,31 @@ class PluginLoader(object):
try: try:
ans = None ans = None
m = importlib.import_module( plugin_module = 'calibre_plugins.%s'%plugin_name
'calibre_plugins.%s.__init__'%plugin_name) m = sys.modules.get(plugin_module, None)
if m is not None:
reload(m)
else:
m = importlib.import_module(plugin_module)
for obj in m.__dict__.itervalues(): for obj in m.__dict__.itervalues():
if isinstance(obj, type) and issubclass(obj, Plugin) and \ if isinstance(obj, type) and issubclass(obj, Plugin) and \
obj.name != 'Trivial Plugin': obj.name != 'Trivial Plugin':
ans = obj ans = obj
break break
if ans is None: if ans is None:
raise InvalidPlugin('No plugin class found in %r:%r'%( raise InvalidPlugin('No plugin class found in %s:%s'%(
path_to_zip_file, plugin_name)) as_unicode(path_to_zip_file), plugin_name))
if ans.minimum_calibre_version < numeric_version: if ans.minimum_calibre_version > numeric_version:
raise InvalidPlugin( raise InvalidPlugin(
'The plugin at %r needs a version of calibre >= %r' % 'The plugin at %s needs a version of calibre >= %s' %
(path_to_zip_file, '.'.join(ans.minimum_calibre_version))) (as_unicode(path_to_zip_file), '.'.join(map(unicode,
ans.minimum_calibre_version))))
if platform not in ans.supported_platforms: if platform not in ans.supported_platforms:
raise InvalidPlugin( raise InvalidPlugin(
'The plugin at %r cannot be used on %s' % 'The plugin at %s cannot be used on %s' %
(path_to_zip_file, platform)) (as_unicode(path_to_zip_file), platform))
return ans return ans
except: except:
@ -98,10 +213,6 @@ class PluginLoader(object):
if plugin_name not in self.loaded_plugins: if plugin_name not in self.loaded_plugins:
break break
else: else:
if plugin_name in self.loaded_plugins:
raise InvalidPlugin((
'The plugin in %r uses an import name %r that is already'
' used by another plugin') % (path_to_zip_file, plugin_name))
if self._identifier_pat.match(plugin_name) is None: if self._identifier_pat.match(plugin_name) is None:
raise InvalidPlugin(( raise InvalidPlugin((
'The plugin at %r uses an invalid import name: %r' % 'The plugin at %r uses an invalid import name: %r' %
@ -123,7 +234,7 @@ class PluginLoader(object):
names = OrderedDict() names = OrderedDict()
for candidate in names: for candidate in pynames:
parts = posixpath.splitext(candidate)[0].split('/') parts = posixpath.splitext(candidate)[0].split('/')
package = '.'.join(parts[:-1]) package = '.'.join(parts[:-1])
if package and package not in valid_packages: if package and package not in valid_packages:
@ -150,5 +261,24 @@ class PluginLoader(object):
loader = PluginLoader() loader = PluginLoader()
sys.meta_path.insert(0, loader)
if __name__ == '__main__':
from tempfile import NamedTemporaryFile
from calibre.customize.ui import add_plugin
from calibre import CurrentDir
path = sys.argv[-1]
with NamedTemporaryFile(suffix='.zip') as f:
with zipfile.ZipFile(f, 'w') as zf:
with CurrentDir(path):
for x in os.listdir('.'):
if x[0] != '.':
print ('Adding', x)
zf.write(x)
if os.path.isdir(x):
for y in os.listdir(x):
zf.write(os.path.join(x, y))
add_plugin(f.name)
print ('Added plugin from', sys.argv[-1])

View File

@ -94,14 +94,14 @@ class ANDROID(USBMS):
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE'] 'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC', 'LGE', 'NVIDIA']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM'] '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7'] 'A70S', 'A101IT', '7']

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time
from calibre.constants import __appname__, __version__, DEBUG from calibre.constants import __appname__, __version__, DEBUG
from calibre import fit_image from calibre import fit_image, confirm_config_name
from calibre.constants import isosx, iswindows from calibre.constants import isosx, iswindows
from calibre.devices.errors import OpenFeedback, UserFeedback from calibre.devices.errors import OpenFeedback, UserFeedback
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
@ -18,12 +18,77 @@ from calibre.ebooks.metadata import authors_to_string, MetaInformation, \
from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.epub import set_metadata from calibre.ebooks.metadata.epub import set_metadata
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime
from calibre.utils.config import config_dir, prefs from calibre.utils.config import config_dir, dynamic, prefs
from calibre.utils.date import now, parse_date from calibre.utils.date import now, parse_date
from calibre.utils.logging import Log from calibre.utils.logging import Log
from calibre.utils.zipfile import ZipFile from calibre.utils.zipfile import ZipFile
class AppleOpenFeedback(OpenFeedback):
def __init__(self, plugin):
OpenFeedback.__init__(self, u'')
self.log = plugin.log
self.plugin = plugin
def custom_dialog(self, parent):
from PyQt4.Qt import (QDialog, QDialogButtonBox, QIcon,
QLabel, QPushButton, QVBoxLayout)
class Dialog(QDialog):
def __init__(self, p, cd, pixmap='dialog_information.png'):
QDialog.__init__(self, p)
self.cd = cd
self.setWindowTitle("Apple iDevice detected")
self.l = l = QVBoxLayout()
self.setLayout(l)
msg = QLabel()
msg.setText(_(
'<p>If you do not want calibre to recognize your Apple iDevice '
'when it is connected to your computer, '
'click <b>Disable Apple Driver</b>.</p>'
'<p>To transfer books to your iDevice, '
'click <b>Disable Apple Driver</b>, '
"then use the 'Connect to iTunes' method recommended in the "
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">Calibre + iDevices FAQ</a>, '
'using the <em>Connect/Share</em>|<em>Connect to iTunes</em> menu item.</p>'
'<p>Enabling the Apple driver for direct connection to iDevices '
'is an unsupported advanced user mode.</p>'
'<p></p>'
))
msg.setOpenExternalLinks(True)
msg.setWordWrap(True)
l.addWidget(msg)
self.bb = QDialogButtonBox()
disable_driver = QPushButton(_("Disable Apple driver"))
disable_driver.setDefault(True)
self.bb.addButton(disable_driver, QDialogButtonBox.RejectRole)
enable_driver = QPushButton(_("Enable Apple driver"))
self.bb.addButton(enable_driver, QDialogButtonBox.AcceptRole)
l.addWidget(self.bb)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.setWindowIcon(QIcon(I(pixmap)))
self.resize(self.sizeHint())
self.finished.connect(self.do_it)
def do_it(self, return_code):
if return_code == self.Accepted:
self.cd.log.info(" Apple driver ENABLED")
dynamic[confirm_config_name(self.cd.plugin.DISPLAY_DISABLE_DIALOG)] = False
else:
from calibre.customize.ui import disable_plugin
self.cd.log.info(" Apple driver DISABLED")
disable_plugin(self.cd.plugin)
return Dialog(parent, self)
from PIL import Image as PILImage from PIL import Image as PILImage
from lxml import etree from lxml import etree
@ -54,15 +119,11 @@ class DriverBase(DeviceConfig, DevicePlugin):
'iBooks Category'), 'iBooks Category'),
_('Cache covers from iTunes/iBooks') + _('Cache covers from iTunes/iBooks') +
':::' + ':::' +
_('Enable to cache and display covers from iTunes/iBooks'), _('Enable to cache and display covers from iTunes/iBooks')
_("Skip 'Connect to iTunes' recommendation") +
':::' +
_("Enable to skip the 'Connect to iTunes' recommendation dialog")
] ]
EXTRA_CUSTOMIZATION_DEFAULT = [ EXTRA_CUSTOMIZATION_DEFAULT = [
True, True,
True, True,
False,
] ]
@ -118,12 +179,13 @@ class ITUNES(DriverBase):
supported_platforms = ['osx','windows'] supported_platforms = ['osx','windows']
author = 'GRiker' author = 'GRiker'
#: The version of this plugin as a 3-tuple (major, minor, revision) #: The version of this plugin as a 3-tuple (major, minor, revision)
version = (0,9,0) version = (1,0,0)
DISPLAY_DISABLE_DIALOG = "display_disable_apple_driver_dialog"
# EXTRA_CUSTOMIZATION_MESSAGE indexes # EXTRA_CUSTOMIZATION_MESSAGE indexes
USE_SERIES_AS_CATEGORY = 0 USE_SERIES_AS_CATEGORY = 0
CACHE_COVERS = 1 CACHE_COVERS = 1
SKIP_CONNECT_TO_ITUNES_DIALOG = 2
OPEN_FEEDBACK_MESSAGE = _( OPEN_FEEDBACK_MESSAGE = _(
'Apple device detected, launching iTunes, please wait ...') 'Apple device detected, launching iTunes, please wait ...')
@ -739,21 +801,17 @@ class ITUNES(DriverBase):
Note that most of the initialization is necessarily performed in can_handle(), as Note that most of the initialization is necessarily performed in can_handle(), as
we need to talk to iTunes to discover if there's a connected iPod we need to talk to iTunes to discover if there's a connected iPod
''' '''
if DEBUG: if DEBUG:
self.log.info("ITUNES.open()") self.log.info("ITUNES.open()")
# Display a dialog recommending using 'Connect to iTunes' # Display a dialog recommending using 'Connect to iTunes' if user hasn't
if False and not self.settings().extra_customization[self.SKIP_CONNECT_TO_ITUNES_DIALOG]: # previously disabled the dialog
raise OpenFeedback('<p>' + ('Click the "Connect/Share" button and choose' if dynamic.get(confirm_config_name(self.DISPLAY_DISABLE_DIALOG),True):
' "Connect to iTunes" to send books from your calibre library' raise AppleOpenFeedback(self)
' to your Apple iDevice.<p>For more information, see ' else:
'<a href="http://www.mobileread.com/forums/showthread.php?t=118559">' if DEBUG:
'Calibre + Apple iDevices FAQ</a>.<p>' self.log.info(" advanced user mode, directly connecting to iDevice")
'After following the Quick Start steps outlined in the FAQ, '
'restart calibre.'))
if DEBUG:
self.log.info(" advanced user mode, directly connecting to iDevice")
# Confirm/create thumbs archive # Confirm/create thumbs archive
if not os.path.exists(self.cache_dir): if not os.path.exists(self.cache_dir):

View File

@ -41,6 +41,13 @@ class OpenFeedback(DeviceError):
self.feedback_msg = msg self.feedback_msg = msg
DeviceError.__init__(self, msg) DeviceError.__init__(self, msg)
def custom_dialog(self, parent):
'''
If you need to show the user a custom dialog, instead of just
displaying the feedback_msg, create and return it here.
'''
raise NotImplementedError
class DeviceBusy(ProtocolError): class DeviceBusy(ProtocolError):
""" Raised when device is busy """ """ Raised when device is busy """
def __init__(self, uerr=""): def __init__(self, uerr=""):

View File

@ -57,6 +57,7 @@ class PRS505(USBMS):
MUST_READ_METADATA = True MUST_READ_METADATA = True
SUPPORTS_USE_AUTHOR_SORT = True SUPPORTS_USE_AUTHOR_SORT = True
EBOOK_DIR_MAIN = 'database/media/books' EBOOK_DIR_MAIN = 'database/media/books'
SCAN_FROM_ROOT = False
ALL_BY_TITLE = _('All by title') ALL_BY_TITLE = _('All by title')
ALL_BY_AUTHOR = _('All by author') ALL_BY_AUTHOR = _('All by author')
@ -87,18 +88,27 @@ class PRS505(USBMS):
_('Set this option if you want the cover thumbnails to have ' _('Set this option if you want the cover thumbnails to have '
'the same aspect ratio (width to height) as the cover. ' 'the same aspect ratio (width to height) as the cover. '
'Unset it if you want the thumbnail to be the maximum size, ' 'Unset it if you want the thumbnail to be the maximum size, '
'ignoring aspect ratio.') 'ignoring aspect ratio.'),
_('Search for books in all folders') +
':::' +
_('Setting this option tells calibre to look for books in all '
'folders on the device and its cards. This permits calibre to '
'find books put on the device by other software and by '
'wireless download.')
] ]
EXTRA_CUSTOMIZATION_DEFAULT = [ EXTRA_CUSTOMIZATION_DEFAULT = [
', '.join(['series', 'tags']), ', '.join(['series', 'tags']),
False, False,
False, False,
True,
True True
] ]
OPT_COLLECTIONS = 0 OPT_COLLECTIONS = 0
OPT_UPLOAD_COVERS = 1 OPT_UPLOAD_COVERS = 1
OPT_REFRESH_COVERS = 2 OPT_REFRESH_COVERS = 2
OPT_PRESERVE_ASPECT_RATIO = 3
OPT_SCAN_FROM_ROOT = 4
plugboard = None plugboard = None
plugboard_func = None plugboard_func = None
@ -147,11 +157,13 @@ class PRS505(USBMS):
self.booklist_class.rebuild_collections = self.rebuild_collections self.booklist_class.rebuild_collections = self.rebuild_collections
# Set the thumbnail width to the theoretical max if the user has asked # Set the thumbnail width to the theoretical max if the user has asked
# that we do not preserve aspect ratio # that we do not preserve aspect ratio
if not self.settings().extra_customization[3]: if not self.settings().extra_customization[self.OPT_PRESERVE_ASPECT_RATIO]:
self.THUMBNAIL_WIDTH = 168 self.THUMBNAIL_WIDTH = 168
# Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be # Set WANTS_UPDATED_THUMBNAILS if the user has asked that thumbnails be
# updated on every connect # updated on every connect
self.WANTS_UPDATED_THUMBNAILS = self.settings().extra_customization[2] self.WANTS_UPDATED_THUMBNAILS = \
self.settings().extra_customization[self.OPT_REFRESH_COVERS]
self.SCAN_FROM_ROOT = self.settings().extra_customization[self.OPT_SCAN_FROM_ROOT]
def filename_callback(self, fname, mi): def filename_callback(self, fname, mi):
if getattr(mi, 'application_id', None) is not None: if getattr(mi, 'application_id', None) is not None:

View File

@ -55,6 +55,8 @@ class USBMS(CLI, Device):
METADATA_CACHE = 'metadata.calibre' METADATA_CACHE = 'metadata.calibre'
DRIVEINFO = 'driveinfo.calibre' DRIVEINFO = 'driveinfo.calibre'
SCAN_FROM_ROOT = False
def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None): def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None):
if not isinstance(dinfo, dict): if not isinstance(dinfo, dict):
dinfo = {} dinfo = {}
@ -173,9 +175,13 @@ class USBMS(CLI, Device):
ebook_dirs = [ebook_dirs] ebook_dirs = [ebook_dirs]
for ebook_dir in ebook_dirs: for ebook_dir in ebook_dirs:
ebook_dir = self.path_to_unicode(ebook_dir) ebook_dir = self.path_to_unicode(ebook_dir)
ebook_dir = self.normalize_path( \ if self.SCAN_FROM_ROOT:
ebook_dir = self.normalize_path(prefix)
else:
ebook_dir = self.normalize_path( \
os.path.join(prefix, *(ebook_dir.split('/'))) \ os.path.join(prefix, *(ebook_dir.split('/'))) \
if ebook_dir else prefix) if ebook_dir else prefix)
debug_print('USBMS: scan from root', self.SCAN_FROM_ROOT, ebook_dir)
if not os.path.exists(ebook_dir): continue if not os.path.exists(ebook_dir): continue
# Get all books in the ebook_dir directory # Get all books in the ebook_dir directory
if self.SUPPORTS_SUB_DIRS: if self.SUPPORTS_SUB_DIRS:

View File

@ -1003,8 +1003,10 @@ OptionRecommendation(name='sr3_replace',
self.opts.insert_blank_line = oibl self.opts.insert_blank_line = oibl
self.opts.remove_paragraph_spacing = orps self.opts.remove_paragraph_spacing = orps
from calibre.ebooks.oeb.transforms.page_margin import RemoveFakeMargins from calibre.ebooks.oeb.transforms.page_margin import \
RemoveFakeMargins, RemoveAdobeMargins
RemoveFakeMargins()(self.oeb, self.log, self.opts) RemoveFakeMargins()(self.oeb, self.log, self.opts)
RemoveAdobeMargins()(self.oeb, self.log, self.opts)
pr(0.9) pr(0.9)
self.flush() self.flush()

View File

@ -182,6 +182,19 @@ def metadata_from_filename(name, pat=None):
mi.isbn = si mi.isbn = si
except (IndexError, ValueError): except (IndexError, ValueError):
pass pass
try:
publisher = match.group('publisher')
mi.publisher = publisher
except (IndexError, ValueError):
pass
try:
pubdate = match.group('published')
if pubdate:
from calibre.utils.date import parse_date
mi.pubdate = parse_date(pubdate)
except:
pass
if mi.is_null('title'): if mi.is_null('title'):
mi.title = name mi.title = name
return mi return mi

View File

@ -323,6 +323,7 @@ class MetadataUpdater(object):
"\tThis is a '%s' file of type '%s'" % (self.type[0:4], self.type[4:8])) "\tThis is a '%s' file of type '%s'" % (self.type[0:4], self.type[4:8]))
recs = [] recs = []
added_501 = False
try: try:
from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks.conversion.config import load_defaults
prefs = load_defaults('mobi_output') prefs = load_defaults('mobi_output')
@ -355,6 +356,7 @@ class MetadataUpdater(object):
update_exth_record((105, normalize(subjects).encode(self.codec, 'replace'))) update_exth_record((105, normalize(subjects).encode(self.codec, 'replace')))
if kindle_pdoc and kindle_pdoc in mi.tags: if kindle_pdoc and kindle_pdoc in mi.tags:
added_501 = True
update_exth_record((501, str('PDOC'))) update_exth_record((501, str('PDOC')))
if mi.pubdate: if mi.pubdate:
@ -370,7 +372,10 @@ class MetadataUpdater(object):
update_exth_record((203, pack('>I', 0))) update_exth_record((203, pack('>I', 0)))
if self.thumbnail_record is not None: if self.thumbnail_record is not None:
update_exth_record((202, pack('>I', self.thumbnail_rindex))) update_exth_record((202, pack('>I', self.thumbnail_rindex)))
if 113 not in self.original_exth_records and 501 in self.original_exth_records and self.original_exth_records[501] == 'EBOK' and not recs.has_key(501): # Add a 113 record if not present to allow Amazon syncing
if (113 not in self.original_exth_records and
self.original_exth_records.get(501, None) == 'EBOK' and
not added_501):
from uuid import uuid4 from uuid import uuid4
update_exth_record((113, str(uuid4()))) update_exth_record((113, str(uuid4())))
if 503 in self.original_exth_records: if 503 in self.original_exth_records:

View File

@ -79,7 +79,7 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
time.sleep(0.2) time.sleep(0.2)
if get_results() and first_result_at is None: if get_results() and first_result_at is None:
first_result_at = time.time() first_result_at = time.time()
if not is_worker_alive(workers): if not is_worker_alive(workers):
break break

View File

@ -102,6 +102,7 @@ class MobiMLizer(object):
def __call__(self, oeb, context): def __call__(self, oeb, context):
oeb.logger.info('Converting XHTML to Mobipocket markup...') oeb.logger.info('Converting XHTML to Mobipocket markup...')
self.oeb = oeb self.oeb = oeb
self.log = self.oeb.logger
self.opts = context self.opts = context
self.profile = profile = context.dest self.profile = profile = context.dest
self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items()) self.fnums = fnums = dict((v, k) for k, v in profile.fnums.items())
@ -118,6 +119,10 @@ class MobiMLizer(object):
del oeb.guide['cover'] del oeb.guide['cover']
item = oeb.manifest.hrefs[href] item = oeb.manifest.hrefs[href]
if item.spine_position is not None: if item.spine_position is not None:
self.log.warn('Found an HTML cover,', item.href, 'removing it.',
'If you find some content missing from the output MOBI, it '
'is because you misidentified the HTML cover in the input '
'document')
oeb.spine.remove(item) oeb.spine.remove(item)
if item.media_type in OEB_DOCS: if item.media_type in OEB_DOCS:
self.oeb.manifest.remove(item) self.oeb.manifest.remove(item)
@ -206,7 +211,11 @@ class MobiMLizer(object):
vspace = bstate.vpadding + bstate.vmargin vspace = bstate.vpadding + bstate.vmargin
bstate.vpadding = bstate.vmargin = 0 bstate.vpadding = bstate.vmargin = 0
if tag not in TABLE_TAGS: if tag not in TABLE_TAGS:
wrapper.attrib['height'] = self.mobimlize_measure(vspace) if tag in ('ul', 'ol') and vspace > 0:
wrapper.addprevious(etree.Element(XHTML('div'),
height=self.mobimlize_measure(vspace)))
else:
wrapper.attrib['height'] = self.mobimlize_measure(vspace)
para.attrib['width'] = self.mobimlize_measure(indent) para.attrib['width'] = self.mobimlize_measure(indent)
elif tag == 'table' and vspace > 0: elif tag == 'table' and vspace > 0:
vspace = int(round(vspace / self.profile.fbase)) vspace = int(round(vspace / self.profile.fbase))

View File

@ -103,8 +103,8 @@ class OEBReader(object):
data = self.oeb.container.read(None) data = self.oeb.container.read(None)
data = self.oeb.decode(data) data = self.oeb.decode(data)
data = XMLDECL_RE.sub('', data) data = XMLDECL_RE.sub('', data)
data = data.replace('http://openebook.org/namespaces/oeb-package/1.0', data = re.sub(r'http://openebook.org/namespaces/oeb-package/1.0(/*)',
OPF1_NS) OPF1_NS, data)
try: try:
opf = etree.fromstring(data) opf = etree.fromstring(data)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
class RemoveFakeMargins(object):
'''
Try to detect and remove fake margins inserted by asinine ebook creation
software on each paragraph/wrapper div. Can be used only after CSS
flattening.
'''
def __call__(self, oeb, opts, log):
self.oeb, self.opts, self.log = oeb, opts, log
from calibre.ebooks.oeb.base import XPath, OEB_STYLES
stylesheet = None
for item in self.oeb.manifest:
if item.media_type.lower() in OEB_STYLES:
stylesheet = item.data
break
if stylesheet is None:
return
top_level_elements = {}
second_level_elements = {}
for x in self.oeb.spine:
root = x.data
body = XPath('//h:body')(root)
if body:
body = body[0]
if not hasattr(body, 'xpath'):
continue
# Check for margins on top level elements
for lb in XPath('./h:div|./h:p|./*/h:div|./*/h:p')(body):
cls = lb.get('class', '')
level = top_level_elements if lb.getparent() is body else \
second_level_elements
if cls not in level:
level[cls] = []
top_level_elements[cls] = []
level[cls].append(lb)
def get_margins(self, stylesheet, cls):
pass

View File

@ -11,6 +11,26 @@ from collections import Counter
from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath from calibre.ebooks.oeb.base import OEB_STYLES, barename, XPath
class RemoveAdobeMargins(object):
'''
Remove margins specified in Adobe's page templates.
'''
def __call__(self, oeb, log, opts):
self.oeb, self.opts, self.log = oeb, opts, log
for item in self.oeb.manifest:
if item.media_type == 'application/vnd.adobe-page-template+xml':
self.log('Removing page margins specified in the'
' Adobe page template')
for elem in item.data.xpath(
'//*[@margin-bottom or @margin-top '
'or @margin-left or @margin-right]'):
for margin in ('left', 'right', 'top', 'bottom'):
attr = 'margin-'+margin
elem.attrib.pop(attr, None)
class RemoveFakeMargins(object): class RemoveFakeMargins(object):
''' '''

View File

@ -291,9 +291,12 @@ class MakeLists:
if self.__list_of_lists: # older RTF won't generate a list_of_lists if self.__list_of_lists: # older RTF won't generate a list_of_lists
index_of_list = self.__get_index_of_list(id) index_of_list = self.__get_index_of_list(id)
if index_of_list != None:# found a matching id if index_of_list != None:# found a matching id
list_dict = self.__list_of_lists[index_of_list][0] curlist = self.__list_of_lists[index_of_list]
list_dict = curlist[0]
level = int(self.__level) + 1 level = int(self.__level) + 1
level_dict = self.__list_of_lists[index_of_list][level][0] if level >= len(curlist):
level = len(curlist) - 1
level_dict = curlist[level][0]
list_type = level_dict.get('numbering-type') list_type = level_dict.get('numbering-type')
if list_type == 'bullet': if list_type == 'bullet':
list_type = 'unordered' list_type = 'unordered'

View File

@ -54,7 +54,6 @@ gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium' gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['automerge'] = 'ignore' gprefs.defaults['automerge'] = 'ignore'
gprefs.defaults['toolbar_text'] = 'auto' gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['show_child_bar'] = False
gprefs.defaults['font'] = None gprefs.defaults['font'] = None
gprefs.defaults['tags_browser_partition_method'] = 'first letter' gprefs.defaults['tags_browser_partition_method'] = 'first letter'
gprefs.defaults['tags_browser_collapse_at'] = 100 gprefs.defaults['tags_browser_collapse_at'] = 100

View File

@ -75,7 +75,7 @@ class InterfaceAction(QObject):
dont_remove_from = frozenset([]) dont_remove_from = frozenset([])
all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu', all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu',
'context-menu-device']) 'context-menu-device', 'toolbar-child'])
#: Type of action #: Type of action
#: 'current' means acts on the current view #: 'current' means acts on the current view

View File

@ -12,7 +12,7 @@ class AddToLibraryAction(InterfaceAction):
name = 'Add To Library' name = 'Add To Library'
action_spec = (_('Add books to library'), 'add_book.png', action_spec = (_('Add books to library'), 'add_book.png',
_('Add books to your calibre library from the connected device'), None) _('Add books to your calibre library from the connected device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu']) dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -121,7 +121,7 @@ class SendToDeviceAction(InterfaceAction):
name = 'Send To Device' name = 'Send To Device'
action_spec = (_('Send to device'), 'sync.png', None, _('D')) action_spec = (_('Send to device'), 'sync.png', None, _('D'))
dont_remove_from = frozenset(['toolbar-device']) dont_remove_from = frozenset(['toolbar-device'])
dont_add_to = frozenset(['toolbar', 'context-menu']) dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
def genesis(self): def genesis(self):
self.qaction.triggered.connect(self.do_sync) self.qaction.triggered.connect(self.do_sync)

View File

@ -12,7 +12,7 @@ class EditCollectionsAction(InterfaceAction):
name = 'Edit Collections' name = 'Edit Collections'
action_spec = (_('Manage collections'), None, action_spec = (_('Manage collections'), None,
_('Manage the collections on this device'), None) _('Manage the collections on this device'), None)
dont_add_to = frozenset(['toolbar', 'context-menu']) dont_add_to = frozenset(['toolbar', 'context-menu', 'toolbar-child'])
action_type = 'current' action_type = 'current'
def genesis(self): def genesis(self):

View File

@ -164,7 +164,7 @@ class DeviceManager(Thread): # {{{
dev.open(self.current_library_uuid) dev.open(self.current_library_uuid)
except OpenFeedback as e: except OpenFeedback as e:
if dev not in self.ejected_devices: if dev not in self.ejected_devices:
self.open_feedback_msg(dev.get_gui_name(), e.feedback_msg) self.open_feedback_msg(dev.get_gui_name(), e)
self.ejected_devices.add(dev) self.ejected_devices.add(dev)
continue continue
except: except:
@ -618,8 +618,11 @@ class DeviceMixin(object): # {{{
if tweaks['auto_connect_to_folder']: if tweaks['auto_connect_to_folder']:
self.connect_to_folder_named(tweaks['auto_connect_to_folder']) self.connect_to_folder_named(tweaks['auto_connect_to_folder'])
def show_open_feedback(self, devname, msg): def show_open_feedback(self, devname, e):
self.__of_dev_mem__ = d = info_dialog(self, devname, msg) try:
self.__of_dev_mem__ = d = e.custom_dialog(self)
except NotImplementedError:
self.__of_dev_mem__ = d = info_dialog(self, devname, e.feedback_msg)
d.show() d.show()
def auto_convert_question(self, msg, autos): def auto_convert_question(self, msg, autos):

View File

@ -3,12 +3,11 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from calibre.gui2 import dynamic
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon
def _config_name(name): from calibre import confirm_config_name
return name + '_again' from calibre.gui2 import dynamic
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
class Dialog(QDialog, Ui_Dialog): class Dialog(QDialog, Ui_Dialog):
@ -22,11 +21,11 @@ class Dialog(QDialog, Ui_Dialog):
self.buttonBox.setFocus(Qt.OtherFocusReason) self.buttonBox.setFocus(Qt.OtherFocusReason)
def toggle(self, *args): def toggle(self, *args):
dynamic[_config_name(self.name)] = self.again.isChecked() dynamic[confirm_config_name(self.name)] = self.again.isChecked()
def confirm(msg, name, parent=None, pixmap='dialog_warning.png'): def confirm(msg, name, parent=None, pixmap='dialog_warning.png'):
if not dynamic.get(_config_name(name), True): if not dynamic.get(confirm_config_name(name), True):
return True return True
d = Dialog(msg, name, parent) d = Dialog(msg, name, parent)
d.label.setPixmap(QPixmap(I(pixmap))) d.label.setPixmap(QPixmap(I(pixmap)))

View File

@ -206,6 +206,46 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Publisher:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="publisher">
<property name="toolTip">
<string>Regular expression (?P&lt;publisher&gt;)</string>
</property>
<property name="text">
<string>No match</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>Published:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="pubdate">
<property name="toolTip">
<string>Regular expression (?P&lt;published&gt;)</string>
</property>
<property name="text">
<string>No match</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</widget> </widget>

View File

@ -284,9 +284,7 @@ class ToolBar(QToolBar): # {{{
mactions = gprefs['action-layout-toolbar'+mactions] mactions = gprefs['action-layout-toolbar'+mactions]
cactions = gprefs['action-layout-toolbar-child'] cactions = gprefs['action-layout-toolbar-child']
show_child = gprefs['show_child_bar'] show_child = len(cactions) > 0
if not len(cactions):
show_child = False
self.child_bar.setVisible(show_child) self.child_bar.setVisible(show_child)
for ac in self.added_actions: for ac in self.added_actions:
@ -306,7 +304,7 @@ class ToolBar(QToolBar): # {{{
for ac in self.location_manager.available_actions: for ac in self.location_manager.available_actions:
bar.addAction(ac) bar.addAction(ac)
bar.added_actions.append(ac) bar.added_actions.append(ac)
bar.setup_tool_button(ac, QToolButton.MenuButtonPopup) bar.setup_tool_button(bar, ac, QToolButton.MenuButtonPopup)
elif what == 'Donate': elif what == 'Donate':
self.d_widget = QWidget() self.d_widget = QWidget()
self.d_widget.setLayout(QVBoxLayout()) self.d_widget.setLayout(QVBoxLayout())
@ -317,10 +315,10 @@ class ToolBar(QToolBar): # {{{
action = self.gui.iactions[what] action = self.gui.iactions[what]
bar.addAction(action.qaction) bar.addAction(action.qaction)
self.added_actions.append(action.qaction) self.added_actions.append(action.qaction)
self.setup_tool_button(action.qaction, action.popup_type) self.setup_tool_button(bar, action.qaction, action.popup_type)
def setup_tool_button(self, ac, menu_mode=None): def setup_tool_button(self, bar, ac, menu_mode=None):
ch = self.widgetForAction(ac) ch = bar.widgetForAction(ac)
if ch is None: if ch is None:
ch = self.child_bar.widgetForAction(ac) ch = self.child_bar.widgetForAction(ac)
ch.setCursor(Qt.PointingHandCursor) ch.setCursor(Qt.PointingHandCursor)
@ -336,7 +334,7 @@ class ToolBar(QToolBar): # {{{
style = Qt.ToolButtonIconOnly style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35 and \ if p == 'auto' and self.preferred_width > self.width()+35 and \
not gprefs['show_child_bar']: not gprefs['action-layout-toolbar-child']:
style = Qt.ToolButtonIconOnly style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style) self.setToolButtonStyle(style)

View File

@ -48,7 +48,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('disable_tray_notification', config) r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True) r('separate_cover_flow', config, restart_required=True)
r('show_child_bar', gprefs)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')] (_('Large'), 'large')]

View File

@ -244,13 +244,6 @@ then the tags will be displayed each on their own line.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_show_child_bar">
<property name="text">
<string>Add a &amp;second toolbar</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -71,11 +71,11 @@ class Category(QWidget): # {{{
plugin_activated = pyqtSignal(object) plugin_activated = pyqtSignal(object)
def __init__(self, name, plugins, parent=None): def __init__(self, name, plugins, gui_name, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self._layout = QVBoxLayout() self._layout = QVBoxLayout()
self.setLayout(self._layout) self.setLayout(self._layout)
self.label = QLabel(name) self.label = QLabel(gui_name)
self.sep = QFrame(self) self.sep = QFrame(self)
self.bf = QFont() self.bf = QFont()
self.bf.setBold(True) self.bf.setBold(True)
@ -118,12 +118,17 @@ class Browser(QScrollArea): # {{{
QScrollArea.__init__(self, parent) QScrollArea.__init__(self, parent)
self.setWidgetResizable(True) self.setWidgetResizable(True)
category_map = {} category_map, category_names = {}, {}
for plugin in preferences_plugins(): for plugin in preferences_plugins():
if plugin.category not in category_map: if plugin.category not in category_map:
category_map[plugin.category] = plugin.category_order category_map[plugin.category] = plugin.category_order
if category_map[plugin.category] < plugin.category_order: if category_map[plugin.category] < plugin.category_order:
category_map[plugin.category] = plugin.category_order category_map[plugin.category] = plugin.category_order
if plugin.category not in category_names:
category_names[plugin.category] = (plugin.gui_category if
plugin.gui_category else plugin.category)
self.category_names = category_names
categories = list(category_map.keys()) categories = list(category_map.keys())
categories.sort(cmp=lambda x, y: cmp(category_map[x], category_map[y])) categories.sort(cmp=lambda x, y: cmp(category_map[x], category_map[y]))
@ -145,7 +150,7 @@ class Browser(QScrollArea): # {{{
self.setWidget(self.container) self.setWidget(self.container)
for name, plugins in self.category_map.items(): for name, plugins in self.category_map.items():
w = Category(name, plugins, self) w = Category(name, plugins, self.category_names[name], parent=self)
self.widgets.append(w) self.widgets.append(w)
self._layout.addWidget(w) self._layout.addWidget(w)
w.plugin_activated.connect(self.show_plugin.emit) w.plugin_activated.connect(self.show_plugin.emit)

View File

@ -208,7 +208,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
LOCATIONS = [ LOCATIONS = [
('toolbar', _('The main toolbar')), ('toolbar', _('The main toolbar')),
('toolbar-child', _('The second toolbar')), ('toolbar-child', _('The optional second toolbar')),
('toolbar-device', _('The main toolbar when a device is connected')), ('toolbar-device', _('The main toolbar when a device is connected')),
('context-menu', _('The context menu for the books in the ' ('context-menu', _('The context menu for the books in the '
'calibre library')), 'calibre library')),

View File

@ -158,8 +158,11 @@ class TagsView(QTreeView): # {{{
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
pop = config['sort_tags_by'] pop = config['sort_tags_by']
self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
match_pop = config['match_tags_type'] try:
self.tag_match.setCurrentIndex(self.db.MATCH_TYPE.index(match_pop)) match_pop = self.db.MATCH_TYPE.index(config['match_tags_type'])
except ValueError:
match_pop = 0
self.tag_match.setCurrentIndex(match_pop)
if not self.made_connections: if not self.made_connections:
self.clicked.connect(self.toggle) self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu) self.customContextMenuRequested.connect(self.show_context_menu)
@ -182,9 +185,12 @@ class TagsView(QTreeView): # {{{
def sort_changed(self, pop): def sort_changed(self, pop):
config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop]) config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
self.recount() self.recount()
def match_changed(self, pop): def match_changed(self, pop):
config.set('match_tags_type', self.db.MATCH_TYPE[pop]) try:
config.set('match_tags_type', self.db.MATCH_TYPE[pop])
except:
pass
def set_search_restriction(self, s): def set_search_restriction(self, s):
if s: if s:

View File

@ -382,6 +382,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
error_dialog(self, _('Failed to start content server'), error_dialog(self, _('Failed to start content server'),
unicode(self.content_server.exception)).exec_() unicode(self.content_server.exception)).exec_()
@property
def current_db(self):
return self.library_view.model().db
def another_instance_wants_to_talk(self): def another_instance_wants_to_talk(self):
try: try:

View File

@ -121,6 +121,12 @@ class FilenamePattern(QWidget, Ui_Form):
else: else:
self.series_index.setText(_('No match')) self.series_index.setText(_('No match'))
if mi.publisher:
self.publisher.setText(mi.publisher)
if mi.pubdate:
self.pubdate.setText(mi.pubdate.strftime('%Y-%m-%d'))
self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn)) self.isbn.setText(_('No match') if mi.isbn is None else str(mi.isbn))

View File

@ -76,6 +76,8 @@ class CustomColumns(object):
'num':record[6], 'num':record[6],
'is_multiple':record[7], 'is_multiple':record[7],
} }
if data['display'] is None:
data['display'] = {}
table, lt = self.custom_table_names(data['num']) table, lt = self.custom_table_names(data['num'])
if table not in custom_tables or (data['normalized'] and lt not in if table not in custom_tables or (data['normalized'] and lt not in
custom_tables): custom_tables):

View File

@ -10,7 +10,7 @@ import os
from calibre.utils.config import Config, StringConfig, config_dir, tweaks from calibre.utils.config import Config, StringConfig, config_dir, tweaks
listen_on = '0.0.0.0' listen_on = tweaks['server_listen_on']
log_access_file = os.path.join(config_dir, 'server_access_log.txt') log_access_file = os.path.join(config_dir, 'server_access_log.txt')

View File

@ -0,0 +1,212 @@
.. include:: global.rst
.. _pluginstutorial:
Writing your own plugins to extend |app|'s functionality
====================================================================
|app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`.
Here, we will teach you how to create your own plugins to add new features to |app|.
.. contents:: Contents
:depth: 2
:local:
.. note:: This only applies to calibre releases >= 0.7.53
Anatomy of a |app| plugin
---------------------------
A |app| plugin is very simple, it's just a zip file that contains some python code
and any other resources like image files needed by the plugin. Without further ado,
let's see a basic example.
Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and MOBI
formats. You would like all files generated by |app| to have their publisher set as "Hello world", here's how to do it.
Create a file named :file:`__init__.py` (this is a special name and must always be used for the main file of your plugin)
and enter the following Python code into it:
.. literalinclude:: plugin_examples/helloworld/__init__.py
:lines: 10-
That's all. To add this code to |app| as a plugin, simply create a zip file with::
zip plugin.zip __init__.py
Add this plugin to |app| via :guilabel:`Preferences->Plugins`.
You can download the Hello World plugin from
`helloworld_plugin.zip <http://calibre-ebook.com/downloads/helloworld_plugin.zip>`_.
Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the
converted book will have its publisher set to "Hello World". This is a trivial plugin, lets move on to
a more complex example that actually adds a component to the user interface.
A User Interface plugin
-------------------------
This plugin will be spread over a few files (to keep the code clean). It will show you how to get resources
(images or data files) from the plugin zip file, allow users to configure your plugin,
how to create elements in the |app| user interface and how to access
and query the books database in |app|.
You can download this plugin from `interface_demo_plugin.zip <http://calibre-ebook.com/downloads/interface_demo_plugin.zip>`_
The first thing to note is that this zip file has a lot more files in it, explained below, pay particular attention to
``plugin-import-name-interface_demo.txt``.
**plugin-import-name-interface_demo.txt**
An empty text file used to enable the multi-file plugin magic. This file must be present in all plugins that use
more than one .py file. It should be empty and its filename must be of the form: plugin-import-name-**some_name**.txt
The presence of this file allows you to import code from the .py files present inside the zip file, using a statement like::
from calibre_plugins.some_name.some_module import some_object
The prefix ``calibre_plugins`` must always be present. ``some_name`` comes from the filename of the empty text file.
``some_module`` refers to :file:`some_module.py` file inside the zip file. Note that this importing is just as
powerful as regular python imports. You can create packages and subpackages of .py modules inside the zip file,
just like you would normally (by defining __init__.py in each sub directory), and everything should Just Work.
The name you use for ``some_name`` enters a global namespace shared by all plugins, **so make it as unique as possible**.
But remember that it must be a valid python identifier (only alphabets, numbers and the underscore).
**__init__.py**
As before, the file that defines the plugin class
**main.py**
This file contains the actual code that does something useful
**ui.py**
This file defines the interface part of the plugin
**images/icon.png**
The icon for this plugin
**about.txt**
A text file with information about the plugin
Now let's look at the code.
__init__.py
^^^^^^^^^^^^^
First, the obligatory ``__init__.py`` to define the plugin metadata:
.. literalinclude:: plugin_examples/interface_demo/__init__.py
:lines: 10-
The only noteworthy feature is the field :attr:`actual_plugin`. Since |app| has both command line and GUI interfaces,
GUI plugins like this one should not load any GUI libraries in __init__.py. The actual_plugin field does this for you,
by telling |app| that the actual plugin is to be found in another file inside your zip archive, which will only be loaded
in a GUI context.
Remember that for this to work, you must have a plugin-import-name-some_name.txt file in your plugin zip file,
as discussed above.
Also there are a couple of methods for enabling user configuration of the plugin. These are discussed below.
ui.py
^^^^^^^^
Now let's look at ui.py which defines the actual GUI plugin. The source code is heavily commented and should be self explanatory:
.. literalinclude:: plugin_examples/interface_demo/ui.py
:lines: 16-
main.py
^^^^^^^^^
The actual logic to implement the Interface Plugin Demo dialog.
.. literalinclude:: plugin_examples/interface_demo/main.py
:lines: 16-
Getting resources from the plugin zip file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|app|'s plugin loading system defines a couple of builtin functions that allow you to conveniently get files from the plugin zip file.
**get_resources(name_or_list_of_names)**
This function should be called with a list of paths to files inside the zip file. For example to access the file icon.png in
the directory images in the zip file, you would use: ``images/icon.png``. Always use a forward slash as the path separator,
even on windows. When you pass in a single name, the function will return the raw bytes of that file or None if the name
was not found in the zip file. If you pass in more than one name then it returns a dict mapping the names to bytes.
If a name is not found, it will not be present in the returned dict.
**get_icons(name_or_list_of_names)**
A convenience wrapper for get_resources() that creates QIcon objects from the raw bytes returned by get_resources.
If a name is not found in the zip file the corresponding QIcon will be null.
Enabling user configuration of your plugin
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To allow users to configure your plugin, you must define three methods in your base plugin class, '**is_customizable**, **config_widget** and **save_settings** as shown below:
.. literalinclude:: plugin_examples/interface_demo/__init__.py
:pyobject: InterfacePluginDemo.is_customizable
.. literalinclude:: plugin_examples/interface_demo/__init__.py
:pyobject: InterfacePluginDemo.config_widget
.. literalinclude:: plugin_examples/interface_demo/__init__.py
:pyobject: InterfacePluginDemo.save_settings
|app| has many different ways to store configuration data (a legacy of its long history). The recommended way is to use the **JSONConfig** class, which stores your configuration information in a .json file.
The code to manage configuration data in the demo plugin is in config.py:
.. literalinclude:: plugin_examples/interface_demo/config.py
:lines: 10-
The ``prefs`` object is now available throughout the plugin code by a simple::
from calibre_plugins.interface_demo.config import prefs
You can see the ``prefs`` object being used in main.py:
.. literalinclude:: plugin_examples/interface_demo/main.py
:pyobject: DemoDialog.config
The different types of plugins
--------------------------------
As you may have noticed above, a plugin in |app| is a class. There are different classes for the different types of plugins in |app|.
Details on each class, including the base class of all plugins can be found in :ref:`plugins`.
Debugging plugins
-------------------
The first, most important step is to run |app| in debug mode. You can do this from the command line with::
calibre-debug -g
Or from within calibre by clicking the arrow next to the preferences button or using the `Ctrl+Shift+R` keyboard shortcut.
When running from the command line, debug output will be printed to the console, when running from within |app| the output will go to a txt file.
You can insert print statements anywhere in your plugin code, they will be output in debug mode. Remember, this is python, you really shouldn't need anything more than print statements to debug ;) I developed all of |app| using just this debugging technique.
It can get tiresome to keep re-adding a plugin to calibre to test small changes. The plugin zip files are stored in the calibre config directory in plugins/ (goto Preferences->Misc and click open config directory to see the config directory).
Once you've located the zip file of your plugin you can then directly update it with your changes instead of re-adding it each time. To do so from the command line, in the directory that contains your plugin source code, use::
zip -R /path/to/plugin/zip/file.zip *
This will automatically update all changed files. It relies on the freely available zip command line tool.
More plugin examples
----------------------
You can find a list of many, sophisticated |app| plugins `here <http://www.mobileread.com/forums/showthread.php?t=118764>`_.
Sharing your plugins with others
----------------------------------
If you would like to share the plugins you have created with other users of |app|, post your plugin in a new thread in the
`calibre plugins forum <http://www.mobileread.com/forums/forumdisplay.php?f=237>`_.

View File

@ -17,6 +17,11 @@ use *plugins* to add functionality to |app|.
:depth: 2 :depth: 2
:local: :local:
.. toctree::
:hidden:
plugins
Environment variables Environment variables
----------------------- -----------------------
@ -53,148 +58,10 @@ You should not change the files in this resources folder, as your changes will g
For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is For example, if you wanted to change the icon for the :guilabel:`Remove books` action, you would first look in the builtin resources folder and see that the relevant file is
:file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders. :file:`resources/images/trash.svg`. Assuming you have an alternate icon in svg format called :file:`mytrash.svg` you would save it in the configuration directory as :file:`resources/images/trash.svg`. All the icons used by the calibre user interface are in :file:`resources/images` and its sub-folders.
A Hello World plugin Customizing |app| with plugins
------------------------ --------------------------------
Suppose you have an installation of |app| that you are using to self publish various e-documents in EPUB and LRF |app| has a very modular design. Almost all functionality in |app| comes in the form of plugins. Plugins are used for conversion, for downloading news (though these are called recipes), for various components of the user interface, to connect to different devices, to process files when adding them to |app| and so on. You can get a complete list of all the builtin plugins in |app| by going to :guilabel:`Preferences->Plugins`.
format. You would like all file generated by |app| to have their publisher set as "Hello world", here's how to do it.
Create a file name :file:`my_plugin.py` (the file name must end with plugin.py) and enter the following Python code into it:
.. code-block:: python You can write your own plugins to customize and extend the behavior of |app|. The plugin architecture in |app| is very simple, see the tutorial :ref:`pluginstutorial`.
import os
from calibre.customize import FileTypePlugin
class HelloWorld(FileTypePlugin):
name = 'Hello World Plugin' # Name of the plugin
description = 'Set the publisher to Hello World for all new conversions'
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Acme Inc.' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
file_types = set(['epub', 'lrf']) # The file types that this plugin will be applied to
on_postprocess = True # Run this plugin after conversion is complete
def run(self, path_to_ebook):
from calibre.ebooks.metadata.meta import get_metadata, set_metadata
file = open(path_to_ebook, 'r+b')
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
mi = get_metadata(file, ext)
mi.publisher = 'Hello World'
set_metadata(file, mi, ext)
return path_to_ebook
That's all. To add this code to |app| as a plugin, simply create a zip file with::
zip plugin.zip my_plugin.py
You can download the Hello World plugin from
`helloworld_plugin.zip <http://calibre-ebook.com/downloads/helloworld_plugin.zip>`_.
Now either use the configuration dialog in |app| GUI to add this zip file as a plugin, or
use the command::
calibre-customize -a plugin.zip
Every time you use calibre to convert a book, the plugin's :meth:`run` method will be called and the
converted book will have its publisher set to "Hello World". For more information about
|app|'s plugin system, read on...
A Hello World GUI plugin
---------------------------
Here's a simple Hello World plugin for the |app| GUI. It will cause a box to popup with the message "Hellooo World!" when you press Ctrl+Shift+H
.. note:: Only available in calibre versions ``>= 0.7.32``.
.. code-block:: python
from calibre.customize import InterfaceActionBase
class HelloWorldBase(InterfaceActionBase):
name = 'Hello World GUI'
author = 'The little green man'
def load_actual_plugin(self, gui):
from calibre.gui2.actions import InterfaceAction
class HelloWorld(InterfaceAction):
name = 'Hello World GUI'
action_spec = ('Hello World!', 'add_book.png', None,
_('Ctrl+Shift+H'))
def genesis(self):
self.qaction.triggered.connect(self.hello_world)
def hello_world(self, *args):
from calibre.gui2 import info_dialog
info_dialog(self.gui, 'Hello World!', 'Hellooo World!',
show=True)
return HelloWorld(gui, self.site_customization)
You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in.
While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can achieve. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``.
The Plugin base class
------------------------
As you may have noticed above, all |app| plugins are classes. The Plugin classes are organized in a hierarchy at the top of which
is :class:`calibre.customize.Plugin`. The has excellent in source documentation for its various features, here I will discuss a
few of the important ones.
First, all plugins must supply a list of platforms they have been tested on by setting the ``supported_platforms`` member as in the
example above.
If the plugin needs to do any initialization, it should implement the :meth:`initialize` method. The path to the plugin zip file
is available as ``self.plugin_path``. The initialization method could be used to load any needed resources from the zip file.
If the plugin needs to be customized (i.e. it needs some information from the user), it should implement the :meth:`customization_help`
method, to indicate to |app| that it needs user input. This can be useful, for example, to ask the user to input the path to a needed system
binary or the URL of a website, etc. When |app| asks the user for the customization information, the string retuned by the :meth:`customization_help`
method is used as help text to le thte user know what information is needed.
Another useful method is :meth:`temporary_file`, which returns a file handle to an opened temporary file. If your plugin needs to make use
of temporary files, it should use this method. Temporary file cleanup is then taken care of automatically.
In addition, whenever plugins are run, their zip files are automatically added to the start of ``sys.path``, so you can directly import
any python files you bundle in the zip files. Note that this is not available when the plugin is being initialized, only when it is being run.
Finally, plugins can have a priority (a positive integer). Higher priority plugins are run in preference tolower priority ones in a given context.
By default all plugins have priority 1. You can change that by setting the member :attr:'priority` in your subclass.
See :ref:`pluginsPlugin` for details.
File type plugins
-------------------
File type plugins are intended to be associated with specific file types (as identified by extension). They can be run on several different occassions.
* When books/formats are added ot the |app| database (if :attr:`on_import` is set to True).
* Just before an any2whatever converter is run on an input file (if :attr:`on_preprocess` is set to True).
* After an any2whatever converter has run, on the output file (if :attr:`on_postprocess` is set to True).
File type plugins specify which file types they are associated with by specifying the :attr:`file_types` member as in the above example.
the actual work should be done in the :meth:`run` method, which must return the path to the modified ebook (it can be the same as the original
if the modifcations are done in place).
See :ref:`pluginsFTPlugin` for details.
Metadata plugins
-------------------
Metadata plugins add the ability to read/write metadata from ebook files to |app|. See :ref:`pluginsMetadataPlugin` for details.
.. toctree::
:hidden:
plugins
Metadata download plugins
----------------------------
Metadata download plugins add various sources that |app| uses to download metadata based on title/author/isbn etc. See :ref:`pluginsMetadataSource`
for details.

View File

@ -0,0 +1,33 @@
#!/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 calibre.customize import FileTypePlugin
class HelloWorld(FileTypePlugin):
name = 'Hello World Plugin' # Name of the plugin
description = 'Set the publisher to Hello World for all new conversions'
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Acme Inc.' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
file_types = set(['epub', 'mobi']) # The file types that this plugin will be applied to
on_postprocess = True # Run this plugin after conversion is complete
minimum_calibre_version = (0, 7, 53)
def run(self, path_to_ebook):
from calibre.ebooks.metadata.meta import get_metadata, set_metadata
file = open(path_to_ebook, 'r+b')
ext = os.path.splitext(path_to_ebook)[-1][1:].lower()
mi = get_metadata(file, ext)
mi.publisher = 'Hello World'
set_metadata(file, mi, ext)
return path_to_ebook

View File

@ -0,0 +1,80 @@
#!/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'
# The class that all Interface Action plugin wrappers must inherit from
from calibre.customize import InterfaceActionBase
class InterfacePluginDemo(InterfaceActionBase):
'''
This class is a simple wrapper that provides information about the actual
plugin class. The actual interface plugin class is called InterfacePlugin
and is defined in the ui.py file, as specified in the actual_plugin field
below.
The reason for having two classes is that it allows the command line
calibre utilities to run without needing to load the GUI libraries.
'''
name = 'Interface Plugin Demo'
description = 'An advanced plugin demo'
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
version = (1, 0, 0)
minimum_calibre_version = (0, 7, 53)
#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
#: The specified class must be defined in the specified module.
actual_plugin = 'calibre_plugins.interface_demo.ui:InterfacePlugin'
def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.
This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.
If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.
The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
# It is important to put this import statement here rather than at the
# top of the module as importing the config class will also cause the
# GUI libraries to be loaded, which we do not want when using calibre
# from the command line
from calibre_plugins.interface_demo.config import ConfigWidget
return ConfigWidget()
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.
:param config_widget: The widget returned by :meth:`config_widget`.
'''
config_widget.save_settings()
# Apply the changes
ac = self.actual_plugin_
if ac is not None:
ac.apply_settings()

View File

@ -0,0 +1,7 @@
The Interface Plugin Demo
===========================
Created by Kovid Goyal
Requires calibre >= 0.7.53

View File

@ -0,0 +1,41 @@
#!/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 PyQt4.Qt import QWidget, QHBoxLayout, QLabel, QLineEdit
from calibre.utils.config import JSONConfig
# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')
# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'
class ConfigWidget(QWidget):
def __init__(self):
QWidget.__init__(self)
self.l = QHBoxLayout()
self.setLayout(self.l)
self.label = QLabel('Hello world &message:')
self.l.addWidget(self.label)
self.msg = QLineEdit(self)
self.msg.setText(prefs['hello_world_msg'])
self.l.addWidget(self.msg)
self.label.setBuddy(self.msg)
def save_settings(self):
prefs['hello_world_msg'] = unicode(self.msg.text())

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,116 @@
#!/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'
if False:
# This is here to keep my python error checker from complaining about
# the builtin functions that will be defined by the plugin loading system
# You do not need this code in your plugins
get_icons = get_resources = None
from PyQt4.Qt import QDialog, QVBoxLayout, QPushButton, QMessageBox, QLabel
from calibre_plugins.interface_demo.config import prefs
class DemoDialog(QDialog):
def __init__(self, gui, icon, do_user_config):
QDialog.__init__(self, gui)
self.gui = gui
self.do_user_config = do_user_config
# The current database shown in the GUI
# db is an instance of the class LibraryDatabase2 from database.py
# This class has many, many methods that allow you to do a lot of
# things.
self.db = gui.current_db
self.l = QVBoxLayout()
self.setLayout(self.l)
self.label = QLabel(prefs['hello_world_msg'])
self.l.addWidget(self.label)
self.setWindowTitle('Interface Plugin Demo')
self.setWindowIcon(icon)
self.about_button = QPushButton('About', self)
self.about_button.clicked.connect(self.about)
self.l.addWidget(self.about_button)
self.marked_button = QPushButton(
'Show books with only one format in the calibre GUI', self)
self.marked_button.clicked.connect(self.marked)
self.l.addWidget(self.marked_button)
self.view_button = QPushButton(
'View the most recently added book', self)
self.view_button.clicked.connect(self.view)
self.l.addWidget(self.view_button)
self.conf_button = QPushButton(
'Configure this plugin', self)
self.conf_button.clicked.connect(self.config)
self.l.addWidget(self.conf_button)
self.resize(self.sizeHint())
def about(self):
# Get the about text from a file inside the plugin zip file
# The get_resources function is a builtin function defined for all your
# plugin code. It loads files from the plugin zip file. It returns
# the bytes from the specified file.
#
# Note that if you are loading more than one file, for performance, you
# should pass a list of names to get_resources. In this case,
# get_resources will return a dictionary mapping names to bytes. Names that
# are not found in the zip file will not be in the returned dictionary.
text = get_resources('about.txt')
QMessageBox.about(self, 'About the Interface Plugin Demo',
text.decode('utf-8'))
def marked(self):
fmt_idx = self.db.FIELD_MAP['formats']
matched_ids = set()
for record in self.db.data.iterall():
# Iterate over all records
fmts = record[fmt_idx]
# fmts is either None or a comma separated list of formats
if fmts and ',' not in fmts:
matched_ids.add(record[0])
# Mark the records with the matching ids
self.db.set_marked_ids(matched_ids)
# Tell the GUI to search for all marked records
self.gui.search.setEditText('marked:true')
self.gui.search.do_search()
def view(self):
most_recent = most_recent_id = None
timestamp_idx = self.db.FIELD_MAP['timestamp']
for record in self.db.data:
# Iterate over all currently showing records
timestamp = record[timestamp_idx]
if most_recent is None or timestamp > most_recent:
most_recent = timestamp
most_recent_id = record[0]
if most_recent_id is not None:
# Get the row number of the id as shown in the GUI
row_number = self.db.row(most_recent_id)
# Get a reference to the View plugin
view_plugin = self.gui.iactions['View']
# Ask the view plugin to launch the viewer for row_number
view_plugin._view_books([row_number])
def config(self):
self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

View File

@ -0,0 +1,71 @@
#!/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'
if False:
# This is here to keep my python error checker from complaining about
# the builtin functions that will be defined by the plugin loading system
# You do not need this code in your plugins
get_icons = get_resources = None
# The class that all interface action plugins must inherit from
from calibre.gui2.actions import InterfaceAction
from calibre_plugins.interface_demo.main import DemoDialog
class InterfacePlugin(InterfaceAction):
name = 'Interface Plugin Demo'
# Declare the main action associated with this plugin
# The keyboard shortcut can be None if you dont want to use a keyboard
# shortcut. Remember that currently calibre has no central management for
# keyboard shortcuts, so try to use an unusual/unused shortcut.
action_spec = ('Interface Plugin Demo', None,
'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')
def genesis(self):
# This method is called once per plugin, do initial setup here
# Set the icon for this interface action
# The get_icons function is a builtin function defined for all your
# plugin code. It loads icons from the plugin zip file. It returns
# QIcon objects, if you want the actual data, use the analogous
# get_resources builtin function.
#
# Note that if you are loading more than one icon, for performance, you
# should pass a list of names to get_icons. In this case, get_icons
# will return a dictionary mapping names to QIcons. Names that
# are not found in the zip file will result in null QIcons.
icon = get_icons('images/icon.png')
# The qaction is automatically created from the action_spec defined
# above
self.qaction.setIcon(icon)
self.qaction.triggered.connect(self.show_dialog)
def show_dialog(self):
# The base plugin object defined in __init__.py
base_plugin_object = self.interface_action_base_plugin
# Show the config dialog
# The config dialog can also be shown from within
# Preferences->Plugins, which is why the do_user_config
# method is defined on the base plugin class
do_user_config = base_plugin_object.do_user_config
# self.gui is the main calibre GUI. It acts as the gateway to access
# all the elements of the calibre user interface, it should also be the
# parent of the dialog
d = DemoDialog(self.gui, self.qaction.icon(), do_user_config)
d.show()
def apply_settings(self):
from calibre_plugins.interface_demo.config import prefs
# In an actual non trivial plugin, you would probably need to
# do something based on the settings in prefs
prefs

View File

@ -18,4 +18,5 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
regexp regexp
portable portable
server server
creating_plugins

View File

@ -31,8 +31,11 @@ class cmd_commit(_cmd_commit):
summary = '' summary = ''
raw = urllib.urlopen('https://bugs.launchpad.net/calibre/+bug/' + raw = urllib.urlopen('https://bugs.launchpad.net/calibre/+bug/' +
bug).read() bug).read()
h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0] try:
summary = html.tostring(h1, method='text', encoding=unicode).strip() h1 = html.fromstring(raw).xpath('//h1[@id="edit-title"]')[0]
summary = html.tostring(h1, method='text', encoding=unicode).strip()
except:
summary = 'Private bug'
print 'Working on bug:', summary print 'Working on bug:', summary
if summary: if summary:
msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary)) msg = msg.replace('#%s'%bug, '#%s (%s)'%(bug, summary))

View File

@ -12,6 +12,8 @@ from threading import Thread
from Queue import Queue from Queue import Queue
from contextlib import closing from contextlib import closing
from binascii import unhexlify from binascii import unhexlify
from zipimport import ZipImportError
from calibre import prints from calibre import prints
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx
@ -75,7 +77,14 @@ class Progress(Thread):
def get_func(name): def get_func(name):
module, func, notification = PARALLEL_FUNCS[name] module, func, notification = PARALLEL_FUNCS[name]
module = importlib.import_module(module) try:
module = importlib.import_module(module)
except ZipImportError:
# Something windows weird happened, try clearing the zip import cache
# incase the zipfile was changed from under us
from zipimport import _zip_directory_cache as zdc
zdc.clear()
module = importlib.import_module(module)
func = getattr(module, func) func = getattr(module, func)
return func, notification return func, notification