Sync to trunk.

This commit is contained in:
John Schember 2011-01-01 10:04:34 -05:00
commit c17b189f56
77 changed files with 3001 additions and 971 deletions

View File

@ -4,6 +4,11 @@ License: GPL-3
The full text of the GPL is distributed as in The full text of the GPL is distributed as in
/usr/share/common-licenses/GPL-3 on Debian systems. /usr/share/common-licenses/GPL-3 on Debian systems.
Files: src/calibre/ebooks/pdf/*.h,*.cpp
License: GPL-2 or later
The full text of the GPL is distributed as in
/usr/share/common-licenses/GPL-2 on Debian systems.
Files: src/calibre/ebooks/BeautifulSoup.py Files: src/calibre/ebooks/BeautifulSoup.py
Copyright: Copyright (c) 2004-2007, Leonard Richardson Copyright: Copyright (c) 2004-2007, Leonard Richardson
License: BSD License: BSD

View File

@ -2,19 +2,29 @@ body { background-color: white; }
p.title { p.title {
margin-top:0em; margin-top:0em;
margin-bottom:1em; margin-bottom:0em;
text-align:center; text-align:center;
font-style:italic; font-style:italic;
font-size:xx-large; font-size:xx-large;
border-bottom: solid black 2px; }
p.series_id {
margin-top:0em;
margin-bottom:0em;
text-align:center;
}
a.series_id {
font-style:normal;
font-size:large;
} }
p.author { p.author {
font-size:large;
margin-top:0em; margin-top:0em;
margin-bottom:0em; margin-bottom:0em;
text-align: center; text-align: center;
text-indent: 0em; text-indent: 0em;
font-size:large;
} }
p.author_index { p.author_index {
@ -26,7 +36,8 @@ p.author_index {
text-indent: 0em; text-indent: 0em;
} }
p.tags { p.genres {
font-style:normal;
margin-top:0.5em; margin-top:0.5em;
margin-bottom:0em; margin-bottom:0em;
text-align: left; text-align: left;
@ -108,6 +119,13 @@ p.date_read {
text-indent:-6em; text-indent:-6em;
} }
hr.annotations_divider {
width:50%;
margin-left:1em;
margin-top:0em;
margin-bottom:0em;
}
hr.description_divider { hr.description_divider {
width:90%; width:90%;
margin-left:5%; margin-left:5%;
@ -117,20 +135,37 @@ hr.description_divider {
border-left: solid white 0px; border-left: solid white 0px;
} }
hr.annotations_divider { hr.header_divider {
width:50%; width:100%;
margin-left:1em; border-top: solid white 1px;
margin-top:0em; border-right: solid white 0px;
margin-bottom:0em; border-bottom: solid black 2px;
border-left: solid white 0px;
}
hr.merged_comments_divider {
width:80%;
margin-left:10%;
border-top: solid white 0px;
border-right: solid white 0px;
border-bottom: dashed gray 2px;
border-left: solid white 0px;
} }
td.publisher, td.date { td.publisher, td.date {
font-weight:bold; font-weight:bold;
text-align:center; text-align:center;
} }
td.rating {
text-align: center; td.rating{
text-align:center;
} }
td.notes {
font-size: 100%;
text-align:center;
}
td.thumbnail img { td.thumbnail img {
-webkit-box-shadow: 4px 4px 12px #999; -webkit-box-shadow: 4px 4px 12px #999;
} }

View File

@ -0,0 +1,41 @@
<html xmlns="{xmlns}">
<head>
<title>{title_str}</title>
<meta name="catalog description header" http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" href="stylesheet.css" media="screen" />
</head>
<body>
<p class="title">{title}</p>
<p class="series_id"><a class="series_id">{series} [{series_index}]</a></p>
<hr class="header_divider" />
<p class="author">{author_prefix}<a class="author">{author}</a></p>
<p class="genres">{genres}</p>
<p class="formats">{formats}</p>
<table width="100%" border="0">
<tr>
<td class="thumbnail" rowspan="7"></td>
<td>&#160;</td>
</tr>
<tr>
<td>&#160;</td>
</tr>
<tr>
<td class="publisher">{publisher}</td>
</tr>
<tr>
<td class="date">{pubyear}</td>
</tr>
<tr>
<td class="rating">{rating}</td>
</tr>
<tr>
<td class="notes">{note_source}: {note_content}</td>
</tr>
<tr>
<td>&#160;</td>
</tr>
</table>
<hr class="description_divider" />
<div class="description"></div>
</body>
</html>

View File

@ -55,6 +55,32 @@ author_sort_copy_method = 'invert'
# categories_use_field_for_author_name = 'author_sort' # categories_use_field_for_author_name = 'author_sort'
categories_use_field_for_author_name = 'author' categories_use_field_for_author_name = 'author'
# Control how the tags pane displays categories containing many items. If the
# number of items is larger than categories_collapse_more_than, a sub-category
# will be added. If sorting by name, then the subcategories can be organized by
# first letter (categories_collapse_model = 'first letter') or into equal-sized
# groups (categories_collapse_model = 'partition'). If sorting by average rating
# or by popularity, then 'partition' is always used. The addition of
# subcategories can be disabled by setting categories_collapse_more_than = 0.
# When using partition, the format of the subcategory label is controlled by a
# template: categories_collapsed_name_template if sorting by name,
# categories_collapsed_rating_template if sorting by average rating, and
# categories_collapsed_popularity_template if sorting by popularity. There are
# two variables available to the template: first and last. The variable 'first'
# is the initial item in the subcategory, and the variable 'last' is the final
# item in the subcategory. Both variables are 'objects'; they each have multiple
# values that are obtained by using a suffix. For example, first.name for an
# author category will be the name of the author. The sub-values available are:
# name: the printable name of the item
# count: the number of books that references this item
# avg_rating: the averate rating of all the books referencing this item
# sort: the sort value. For authors, this is the author_sort for that author
# category: the category (e.g., authors, series) that the item is in.
categories_collapse_more_than = 50
categories_collapsed_name_template = '{first.name:shorten(4,'',0)} - {last.name::shorten(4,'',0)}'
categories_collapsed_rating_template = '{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = '{first.count:d} - {last.count:d}'
categories_collapse_model = 'first letter'
# Set whether boolean custom columns are two- or three-valued. # Set whether boolean custom columns are two- or three-valued.
# Two-values for true booleans # Two-values for true booleans
@ -289,3 +315,11 @@ locale_for_sorting = ''
# metadata one book at a time. If True, then the fields are laid out using two # metadata one book at a time. If True, then the fields are laid out using two
# columns. If False, one column is used. # columns. If False, one column is used.
metadata_single_use_2_cols_for_custom_fields = True metadata_single_use_2_cols_for_custom_fields = True
# The number of seconds to wait before sending emails when using a
# public email server like gmail or hotmail. Default is: 5 minutes
# Setting it to lower may cause the server's SPAM controls to kick in,
# making email sending fail. Changes will take effect only after a restart of
# calibre.
public_smtp_relay_delay = 301

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,69 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.businessinsider.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Business_insider(BasicNewsRecipe):
title = 'Business Insider'
__author__ = 'Darko Miletic'
description = 'Noticias de Argentina y el resto del mundo'
publisher = 'Business Insider, Inc.'
category = 'news, politics, finances, world'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = True
language = 'en'
remove_empty_feeds = True
publication_type = 'newsportal'
masthead_url = 'http://static.businessinsider.com/assets/images/logos/tbi_print.jpg'
extra_css = """
body{font-family: Arial,Helvetica,sans-serif }
img{margin-bottom: 0.4em; display:block}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(name=['meta','link'])
,dict(attrs={'class':'feedflare'})
]
remove_attributes=['lang','border']
feeds = [
(u'Latest' , u'http://feeds2.feedburner.com/businessinsider' )
,(u'Markets' , u'http://feeds.feedburner.com/TheMoneyGame' )
,(u'Wall Street' , u'http://feeds.feedburner.com/clusterstock' )
,(u'Tech' , u'http://feeds.feedburner.com/typepad/alleyinsider/silicon_alley_insider')
,(u'The Wire' , u'http://feeds.feedburner.com/businessinsider/thewire' )
,(u'War Room' , u'http://feeds.feedburner.com/businessinsider/warroom' )
,(u'Sports' , u'http://feeds.feedburner.com/businessinsider/sportspage' )
,(u'Tools' , u'http://feeds.feedburner.com/businessinsider/tools' )
,(u'Travel' , u'http://feeds.feedburner.com/businessinsider/travel' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
if item['href'].startswith('http://feedads'):
item.extract()
else:
if item.string is not None:
tstr = item.string
item.replaceWith(tstr)
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
www.businessworld.in www.businessworld.in
''' '''
@ -22,7 +20,11 @@ class BusinessWorldMagazine(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
encoding = 'utf-8' encoding = 'utf-8'
language = 'en_IN' language = 'en_IN'
extra_css = """
img{display: block; margin-bottom: 0.5em}
body{font-family: Arial,Helvetica,sans-serif}
h2{color: gray; display: block}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
@ -43,6 +45,25 @@ class BusinessWorldMagazine(BasicNewsRecipe):
linklist = [] linklist = []
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.INDEX)
tough = soup.find('div', attrs={'id':'tough'})
if tough:
for item in tough.findAll('h1'):
description = ''
title_prefix = ''
feed_link = item.find('a')
if feed_link and feed_link.has_key('href'):
url = self.ROOT + feed_link['href']
if not self.is_in_list(linklist,url):
title = title_prefix + self.tag_to_string(feed_link)
date = strftime(self.timefmt)
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
linklist.append(url)
for item in soup.findAll('div', attrs={'class':'nametitle'}): for item in soup.findAll('div', attrs={'class':'nametitle'}):
description = '' description = ''
title_prefix = '' title_prefix = ''
@ -62,8 +83,8 @@ class BusinessWorldMagazine(BasicNewsRecipe):
return [(soup.head.title.string, articles)] return [(soup.head.title.string, articles)]
keep_only_tags = [dict(name='div', attrs={'id':['register-panel','printwrapper']})] keep_only_tags = [dict(name='div', attrs={'id':'printwrapper'})]
remove_tags = [dict(name=['object','link'])] remove_tags = [dict(name=['object','link','meta','base','iframe','link','table'])]
def print_version(self, url): def print_version(self, url):
return url.replace('/bw/','/bw/storyContent/') return url.replace('/bw/','/bw/storyContent/')

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '04 December 2010, desUBIKado'
__author__ = 'desUBIKado'
__description__ = 'Daily newspaper from Aragon'
__version__ = 'v0.05'
__date__ = '07, December 2010'
'''
elperiodicodearagon.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class elperiodicodearagon(BasicNewsRecipe):
title = u'El Periodico de Aragon'
__author__ = u'desUBIKado'
description = u'Noticias desde Aragon'
publisher = u'elperiodicodearagon.com'
category = u'news, politics, Spain, Aragon'
oldest_article = 2
delay = 0
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
language = 'es'
encoding = 'utf8'
remove_empty_feeds = True
remove_javascript = True
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
feeds = [(u'Arag\xf3n', u'http://elperiodicodearagon.com/RSS/2.xml'),
(u'Internacional', u'http://elperiodicodearagon.com/RSS/4.xml'),
(u'Espa\xf1a', u'http://elperiodicodearagon.com/RSS/3.xml'),
(u'Econom\xeda', u'http://elperiodicodearagon.com/RSS/5.xml'),
(u'Deportes', u'http://elperiodicodearagon.com/RSS/7.xml'),
(u'Real Zaragoza', u'http://elperiodicodearagon.com/RSS/10.xml'),
(u'Opini\xf3n', u'http://elperiodicodearagon.com/RSS/103.xml'),
(u'Escenarios', u'http://elperiodicodearagon.com/RSS/105.xml'),
(u'Sociedad', u'http://elperiodicodearagon.com/RSS/104.xml'),
(u'Gente', u'http://elperiodicodearagon.com/RSS/330.xml')]
extra_css = '''
h3{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
dd{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
'''
remove_attributes = ['height','width']
keep_only_tags = [dict(name='div', attrs={'id':'contenidos'})]
# Quitar toda la morralla
remove_tags = [dict(name='ul', attrs={'class':'herramientasDeNoticia'}),
dict(name='span', attrs={'class':'MasInformacion '}),
dict(name='span', attrs={'class':'MasInformacion'}),
dict(name='div', attrs={'class':'Middle'}),
dict(name='div', attrs={'class':'MenuCabeceraRZaragoza'}),
dict(name='div', attrs={'id':'MenuCabeceraRZaragoza'}),
dict(name='div', attrs={'class':'MenuEquipo'}),
dict(name='div', attrs={'class':'TemasRelacionados'}),
dict(name='div', attrs={'class':'GaleriaEnNoticia'}),
dict(name='div', attrs={'class':'Recorte'}),
dict(name='div', attrs={'id':'NoticiasenRecursos'}),
dict(name='div', attrs={'id':'NoticiaEnPapel'}),
dict(name='p', attrs={'class':'RecorteEnNoticias'}),
dict(name='div', attrs={'id':'Comparte'}),
dict(name='div', attrs={'id':'CajaComparte'}),
dict(name='a', attrs={'class':'EscribirComentario'}),
dict(name='a', attrs={'class':'AvisoComentario'}),
dict(name='div', attrs={'class':'CajaAvisoComentario'}),
dict(name='div', attrs={'class':'navegaNoticias'}),
dict(name='div', attrs={'id':'PaginadorDiCom'}),
dict(name='div', attrs={'id':'CajaAccesoCuentaUsuario'}),
dict(name='div', attrs={'id':'CintilloComentario'}),
dict(name='div', attrs={'id':'EscribeComentario'}),
dict(name='div', attrs={'id':'FormularioComentario'}),
dict(name='div', attrs={'id':'FormularioNormas'})]
# Recuperamos la portada de papel (la imagen format=1 tiene mayor resolucion)
def get_cover_url(self):
index = 'http://pdf.elperiodicodearagon.com/'
soup = self.index_to_soup(index)
for image in soup.findAll('img',src=True):
if image['src'].startswith('http://pdf.elperiodicodearagon.com/funciones/portada-preview.php?eid='):
return image['src'].rstrip('format=2') + 'format=1'
return None
# Para quitar espacios entre la noticia y los comentarios (lineas 1 y 2)
# El indice no apuntaba correctamente al empiece de la noticia (linea 3)
preprocess_regexps = [
(re.compile(r'<p>&nbsp;</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p> </p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'<p id="">', re.DOTALL|re.IGNORECASE), lambda match: '<p>')
]

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
eluniversal.com.mx eluniversal.com.mx
''' '''
@ -18,75 +16,25 @@ class ElUniversal(BasicNewsRecipe):
category = 'news, politics, Mexico' category = 'news, politics, Mexico'
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
encoding = 'cp1252' encoding = 'utf8'
remove_javascript = True remove_javascript = True
remove_empty_feeds = True
publication_type = 'newspaper'
language = 'es' language = 'es'
extra_css = ''' extra_css = '''
body{font-family:Arial,Helvetica,sans-serif; font-size:x-small;} body{font-family:Arial,Helvetica,sans-serif}
.geoGris30{font-family:Georgia,"Times New Roman",Times,serif; font-size:large; color:#003366; font-weight:bold;} .noteTitle{font-family: Georgia,"Times New Roman",Times,serif; color: #336699; font-size: xx-large; font-weight: bold}
.arnegro16{font-family:Georgia,"Times New Roman",Times,serif; font-weight:bold; font-size:small;} .noteInfo{display: block; color: gray}
.tbazull2{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color:#336699; font-size:xx-small;}
.tbgrisf11{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color: #666666; font-size:xx-small;}
.verrojo13{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; color: #CC0033; font-size:xx-small;}
.trnegro13{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; font-size:xx-small;}
.txt-fotogaleria{font-family:"trebuchet ms",Arial,Helvetica,sans-serif; font-size:xx-small;}
''' '''
keep_only_tags = [ dict(name='table', attrs={'width':"633"}),dict(name='table', attrs={'width':"629"}),] keep_only_tags = [ dict(name='div', attrs={'id':'noteContent'})]
remove_tags_after = dict(attrs={'class':'noteText'})
remove_tags = [ remove_tags = [
dict(name='table', attrs={'bgcolor':"#f5f5f5"}), dict(attrs={'class':'noteExtras'}),
dict(name='td', attrs={'bgcolor':"#f7f8f9"}), dict(name=['meta','iframe','base','embed','object']),
dict(name='td', attrs={'bgcolor':"#f5f5f5"}), dict(attrs={'id':'tm_box'})
dict(name='table', attrs={'width':"302"}),
dict(name='table', attrs={'width':"214"}),
dict(name='table', attrs={'width':"112"}),
dict(name='table', attrs={'width':"980"}),
dict(name='td', attrs={'height':"1"}),
dict(name='td', attrs={'height':"4"}),
dict(name='td', attrs={'height':"20"}),
dict(name='td', attrs={'height':"10"}),
dict(name='td', attrs={'class':["trrojo11","trbris11","trrojo12","arrojo12s","tbazul13"]}),
dict(name='div', attrs={'id':["mapg","ver_off_todosloscom","todosloscom"]}),
dict(name='span', attrs={'class':["trazul18b","trrojo11","trnaranja11","trbris11","georojo18b","geogris18"]}),
dict(name='span', attrs={'class':["detalles-opinion"]}),
dict(name='a', attrs={'class':["arnaranja12b","trbris11","arazul12rel","trrojo10"]}),
dict(name='img', src = "/img/icono_imprimir.gif"),
dict(name='img', src = "/img/icono_enviar_mail.gif"),
dict(name='img', src = "/img/icono_fuente_g.gif"),
dict(name='img', src = "/img/icono_fuente_m.gif"),
dict(name='img', src = "/img/icono_fuente_c.gif"),
dict(name='img', src = "/img/icono_compartir.gif"),
dict(name='img', src = "/img/icono_enviar_coment.gif"),
dict(name='img', src = "http://www.eluniversal.com.mx/n_img/bot-notasrel.gif"),
dict(name='img', src = "http://www.eluniversal.com.mx/n_img/fr.gif"),
dict(name='img', src = "/img/espiral2.gif"),
dict(name='img', src = "http://www.eluniversal.com.mx/n_img/b"),
dict(name='img', src = "/img/icono_enviar_coment.gifot-notasrel.gif"),
dict(name='img', src = "/n_img/icono_tipo3.gif"),
dict(name='img', src = "/n_img/icono_tipo2.gif"),
dict(name='img', src = "/n_img/icono_print.gif"),
dict(name='img', src = "/n_img/icono_mail2.gif"),
dict(name='img', src = "/n_img/im-comentarios-2a.gif"),
dict(name='img', src = "/n_img/im-comentarios-1a.gif"),
dict(name='img', src = "/img/icono_coment.gif"),
dict(name='img', src = "http://www.eluniversal.com.mx/n_img/bot-sitiosrel.gif"),
dict(name='img', src = "/n_img/icono_tipomenos.gif"),
dict(name='img', src = "/img/futbol/19.jpg"),
dict(name='img', alt = "Facebook"),
dict(name='img', alt = "Twitter"),
dict(name='img', alt = "Google"),
dict(name='img', alt = "LinkedIn"),
dict(name='img', alt = "Viadeo"),
dict(name='img', alt = "Digg"),
dict(name='img', alt = "Delicious"),
dict(name='img', alt = "Meneame"),
dict(name='img', alt = "Yahoo"),
dict(name='img', alt = "Technorati"),
dict(name='a',text =["Compartir","Facebook","Twitter","Google","LinkedIn","Viadeo","Digg","Delicious","Meneame","Yahoo","Technorati"]),
dict(name='select'),
dict(name='a', attrs={'class':"tbgriscompartir"}),
] ]
remove_attributes=['lang','onclick']
feeds = [ feeds = [
(u'Minuto por Minuto', u'http://www.eluniversal.com.mx/rss/universalmxm.xml' ) (u'Minuto por Minuto', u'http://www.eluniversal.com.mx/rss/universalmxm.xml' )
@ -101,25 +49,3 @@ class ElUniversal(BasicNewsRecipe):
,(u'Computacion' , u'http://www.eluniversal.com.mx/rss/computo.xml' ) ,(u'Computacion' , u'http://www.eluniversal.com.mx/rss/computo.xml' )
,(u'Sociedad' , u'http://www.eluniversal.com.mx/rss/sociedad.xml' ) ,(u'Sociedad' , u'http://www.eluniversal.com.mx/rss/sociedad.xml' )
] ]
# def print_version(self, url):
# return url.replace('/notas/','/notas/vi_')
def preprocess_html(self, soup):
mtag = '<meta http-equiv="Content-Language" content="es-MX"/><meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
for tag in soup.findAll(name='td',attrs={'class': 'arazul50'}):
tag.insert(0,"<h1>")
tag.insert(2,"</h1>")
return soup
def postprocess_html(self, soup,first):
for tag in soup.findAll(name=['table', 'span','i']):
tag.name = 'div'
for item in soup.findAll(align = "right"):
del item['align']
return soup

View File

@ -1,27 +1,31 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
www.elpais.com/diario/ www.elpais.com
''' '''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class ElPaisImpresa(BasicNewsRecipe): class ElPais_RSS(BasicNewsRecipe):
title = u'El Pa\xeds - edicion impresa' title = 'El Pais'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = u'el periodico global en Espa\xf1ol' description = 'el periodico global en Castellano'
publisher = 'EDICIONES EL PAIS, S.L.' publisher = 'EDICIONES EL PAIS, S.L.'
category = 'news, politics,Spain,actualidad,noticias,informacion,videos,fotografias,audios,graficos,nacional,internacional,deportes,economia,tecnologia,cultura,gente,television,sociedad,opinion,blogs,foros,chats,encuestas,entrevistas,participacion' category = 'news, politics, finances, world, spain'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True no_stylesheets = True
encoding = 'latin1' encoding = 'cp1252'
use_embedded_content = False use_embedded_content = False
language = 'es' language = 'es_ES'
remove_empty_feeds = True
publication_type = 'newspaper' publication_type = 'newspaper'
masthead_url = 'http://www.elpais.com/im/tit_logo_global.gif' masthead_url = 'http://www.elpais.com/im/tit_logo.gif'
index = 'http://www.elpais.com/diario/' extra_css = """
extra_css = ' p{text-align: justify} body{ text-align: left; font-family: Georgia,"Times New Roman",Times,serif } h2{font-family: Arial,Helvetica,sans-serif} img{margin-bottom: 0.4em} ' body{font-family: Georgia,"Times New Roman",Times,serif }
h3{font-family: Arial,Helvetica,sans-serif}
img{margin-bottom: 0.4em; display:block}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
@ -30,57 +34,62 @@ class ElPaisImpresa(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
feeds = [ keep_only_tags = [dict(attrs={'class':['cabecera_noticia estirar','cabecera_noticia','','contenido_noticia']})]
(u'Internacional' , index + u'internacional/' ) remove_tags = [
,(u'Espa\xf1a' , index + u'espana/' ) dict(name=['meta','link','base','iframe','embed','object'])
,(u'Economia' , index + u'economia/' ) ,dict(attrs={'class':['info_complementa','estructura_2col_der','votos estirar','votos']})
,(u'Opinion' , index + u'opinion/' ) ,dict(attrs={'id':'utilidades'})
,(u'Vi\xf1etas' , index + u'vineta/' )
,(u'Sociedad' , index + u'sociedad/' )
,(u'Cultura' , index + u'cultura/' )
,(u'Tendencias' , index + u'tendencias/' )
,(u'Gente' , index + u'gente/' )
,(u'Obituarios' , index + u'obituarios/' )
,(u'Deportes' , index + u'deportes/' )
,(u'Pantallas' , index + u'radioytv/' )
,(u'Ultima' , index + u'ultima/' )
,(u'Educacion' , index + u'educacion/' )
,(u'Saludo' , index + u'salud/' )
,(u'Ciberpais' , index + u'ciberpais/' )
,(u'EP3' , index + u'ep3/' )
,(u'Cine' , index + u'cine/' )
,(u'Babelia' , index + u'babelia/' )
,(u'El viajero' , index + u'viajero/' )
,(u'Negocios' , index + u'negocios/' )
,(u'Domingo' , index + u'domingo/' )
,(u'El Pais semanal' , index + u'eps/' )
,(u'Quadern Catalunya' , index + u'quadern-catalunya/' )
] ]
remove_tags_after = dict(attrs={'id':'utilidades'})
remove_attributes = ['lang','border','width','height']
keep_only_tags=[dict(attrs={'class':['cabecera_noticia','contenido_noticia']})] feeds = [
remove_attributes=['width','height'] (u'Lo ultimo' , u'http://www.elpais.com/rss/feed.html?feedId=17046')
remove_tags=[dict(name='link')] ,(u'America Latina' , u'http://www.elpais.com/rss/feed.html?feedId=17041')
,(u'Mexico' , u'http://www.elpais.com/rss/feed.html?feedId=17042')
def parse_index(self): ,(u'Europa' , u'http://www.elpais.com/rss/feed.html?feedId=17043')
totalfeeds = [] ,(u'Estados Unidos' , u'http://www.elpais.com/rss/feed.html?feedId=17044')
lfeeds = self.get_feeds() ,(u'Oriente proximo' , u'http://www.elpais.com/rss/feed.html?feedId=17045')
for feedobj in lfeeds: ,(u'Espana' , u'http://www.elpais.com/rss/feed.html?feedId=1002' )
feedtitle, feedurl = feedobj ,(u'Andalucia' , u'http://www.elpais.com/rss/feed.html?feedId=17057')
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl)) ,(u'Catalunia' , u'http://www.elpais.com/rss/feed.html?feedId=17059')
articles = [] ,(u'Comunidad Valenciana' , u'http://www.elpais.com/rss/feed.html?feedId=17061')
soup = self.index_to_soup(feedurl) ,(u'Madrid' , u'http://www.elpais.com/rss/feed.html?feedId=1016' )
for item in soup.findAll('a',attrs={'class':['g19r003','g19i003','g17r003','g17i003']}): ,(u'Pais Vasco' , u'http://www.elpais.com/rss/feed.html?feedId=17062')
url = 'http://www.elpais.com' + item['href'].rpartition('/')[0] ,(u'Galicia' , u'http://www.elpais.com/rss/feed.html?feedId=17063')
title = self.tag_to_string(item) ,(u'Opinion' , u'http://www.elpais.com/rss/feed.html?feedId=1003' )
date = strftime(self.timefmt) ,(u'Sociedad' , u'http://www.elpais.com/rss/feed.html?feedId=1004' )
articles.append({ ,(u'Deportes' , u'http://www.elpais.com/rss/feed.html?feedId=1007' )
'title' :title ,(u'Cultura' , u'http://www.elpais.com/rss/feed.html?feedId=1008' )
,'date' :date ,(u'Cine' , u'http://www.elpais.com/rss/feed.html?feedId=17052')
,'url' :url ,(u'Literatura' , u'http://www.elpais.com/rss/feed.html?feedId=17053')
,'description':'' ,(u'Musica' , u'http://www.elpais.com/rss/feed.html?feedId=17051')
}) ,(u'Arte' , u'http://www.elpais.com/rss/feed.html?feedId=17060')
totalfeeds.append((feedtitle, articles)) ,(u'Tecnologia' , u'http://www.elpais.com/rss/feed.html?feedId=1005' )
return totalfeeds ,(u'Economia' , u'http://www.elpais.com/rss/feed.html?feedId=1006' )
,(u'Ciencia' , u'http://www.elpais.com/rss/feed.html?feedId=17068')
,(u'Salud' , u'http://www.elpais.com/rss/feed.html?feedId=17074')
,(u'Ocio' , u'http://www.elpais.com/rss/feed.html?feedId=17075')
,(u'Justicia y Leyes' , u'http://www.elpais.com/rss/feed.html?feedId=17069')
,(u'Guerras y conflictos' , u'http://www.elpais.com/rss/feed.html?feedId=17070')
,(u'Politica' , u'http://www.elpais.com/rss/feed.html?feedId=17073')
]
def print_version(self, url): def print_version(self, url):
return url + '?print=1' return url + '?print=1'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
if item.string is not None:
tstr = item.string
item.replaceWith(tstr)
else:
item.name='span'
for atrs in ['href','target','alt','title']:
if item.has_key(atrs):
del item[atrs]
for item in soup.findAll('img',alt=False):
item['alt'] = 'image'
return soup

View File

@ -1,50 +1,65 @@
#!/usr/bin/env python #!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__author__ = 'Lorenzo Vigentini' __copyright__ = '04 December 2010, desUBIKado'
__copyright__ = '2009, Lorenzo Vigentini <l.vigentini at gmail.com>' __author__ = 'desUBIKado'
__description__ = 'Daily newspaper from Aragon' __description__ = 'Daily newspaper from Aragon'
__version__ = 'v1.01' __version__ = 'v0.03'
__date__ = '30, January 2010' __date__ = '11, December 2010'
''' '''
http://www.heraldo.es/ [url]http://www.heraldo.es/[/url]
''' '''
import time
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class heraldo(BasicNewsRecipe): class heraldo(BasicNewsRecipe):
author = 'Lorenzo Vigentini' __author__ = 'desUBIKado'
description = 'Daily newspaper from Aragon' description = 'Daily newspaper from Aragon'
cover_url = 'http://www.heraldo.es/MODULOS/global/publico/interfaces/img/logo.gif'
title = u'Heraldo de Aragon' title = u'Heraldo de Aragon'
publisher = 'OJD Nielsen' publisher = 'OJD Nielsen'
category = 'News, politics, culture, economy, general interest' category = 'News, politics, culture, economy, general interest'
language = 'es' language = 'es'
timefmt = '[%a, %d %b, %Y]' timefmt = '[%a, %d %b, %Y]'
oldest_article = 1 oldest_article = 1
max_articles_per_feed = 25 max_articles_per_feed = 100
use_embedded_content = False use_embedded_content = False
recursion = 10
remove_javascript = True remove_javascript = True
no_stylesheets = True no_stylesheets = True
recursion = 10
keep_only_tags = [
dict(name='div', attrs={'class':['titularNoticiaNN','textoGrisVerdanaContenidos']})
]
feeds = [ feeds = [
(u'Portadas ', u'http://www.heraldo.es/index.php/mod.portadas/mem.rss') (u'Portadas', u'http://www.heraldo.es/index.php/mod.portadas/mem.rss')
] ]
keep_only_tags = [dict(name='div', attrs={'id':['dts','com']})]
remove_tags = [dict(name='a', attrs={'class':['com flo-r','enl-if','enl-df']}),
dict(name='div', attrs={'class':['brb-b-s con marg-btt','cnt-rel con']}),
dict(name='form', attrs={'class':'form'})]
remove_tags_before = dict(name='div' , attrs={'id':'dts'})
remove_tags_after = dict(name='div' , attrs={'id':'com'})
def get_cover_url(self):
cover = None
st = time.localtime()
year = str(st.tm_year)
month = "%.2d" % st.tm_mon
day = "%.2d" % st.tm_mday
#[url]http://oldorigin-www.heraldo.es/20101211/primeras/portada_aragon.pdf[/url]
cover='http://oldorigin-www.heraldo.es/'+ year + month + day +'/primeras/portada_aragon.pdf'
br = BasicNewsRecipe.get_browser()
try:
br.open(cover)
except:
self.log("\nPortada no disponible")
cover ='http://www.heraldo.es/MODULOS/global/publico/interfaces/img/logo-Heraldo.png'
return cover
extra_css = ''' extra_css = '''
.articledate {color: gray;font-family: monospace;} h2{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:xx-large;}
.articledescription {display: block;font-family: sans;font-size: 0.7em; text-indent: 0;}
.firma {color: #666;display: block;font-family: verdana, arial, helvetica;font-size: 1em;margin-bottom: 8px;}
.textoGrisVerdanaContenidos {color: #56595c;display: block;font-family: Verdana;font-size: 1.28571em;padding-bottom: 10px}
.titularNoticiaNN {display: block;padding-bottom: 10px;padding-left: 0;padding-right: 0;padding-top: 4px}
.titulo {color: #003066;font-family: Tahoma;font-size: 1.92857em;font-weight: bold;line-height: 1.2em}
''' '''

View File

@ -0,0 +1,52 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class KANewsRecipe(BasicNewsRecipe):
title = u'KA-News.de'
description = u'Nachrichten aus Karlsruhe, Deutschland und der Welt.'
__author__ = 'tfeld'
lang='de'
no_stylesheets = True
oldest_article = 7
max_articles_per_feed = 100
feeds = [
(u'News aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/karlsruhe.xml'),
(u'Kulturnachrichten aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/kultur.xml'),
(u'Durlach: News aus Durlach', 'http://www.ka-news.de/storage/rss/rss/durlach.xml'),
(u'Stutensee: News aus Stutensee Blankenloch, Büchig, Friedrichstal, Staffort, Spöck', 'http://www.ka-news.de/storage/rss/rss/stutensee.xml'),
(u'Bruchsal: News aus Bruchsal', 'http://www.ka-news.de/storage/rss/rss/bruchsal.xml'),
(u'Wirtschaftsnews aus Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/wirtschaft.xml'),
(u'ka-news.de - Sport', 'http://www.ka-news.de/storage/rss/rss/sport.xml'),
(u'KSC-News - News rund um den KSC', 'http://www.ka-news.de/storage/rss/rss/ksc.xml'),
(u'ka-news.de - BG Karlsruhe', 'http://www.ka-news.de/storage/rss/rss/basketball.xml')
]
preprocess_regexps = [
(re.compile(r'width:[0-9]*?px', re.DOTALL|re.IGNORECASE), lambda match: ''),
]
remove_tags_before = dict(id='artdetail_ueberschrift')
remove_tags_after = dict(id='artdetail_unterzeile')
remove_tags = [dict(name=['div'], attrs={'class': 'lbx_table'}),
dict(name=['div'], attrs={'class': 'lk_zumthema'}),
dict(name=['div'], attrs={'class': 'lk_thumb'}),
dict(name=['div'], attrs={'class': 'lk_trenner'}),
dict(name=['div'], attrs={'class': 'lupen_container'}),
dict(name=['script']),
dict(name=['span'], attrs={'style': 'display:none;'}),
dict(name=['span'], attrs={'class': 'comm_info'}),
dict(name=['h3'], attrs={'id': 'artdetail_unterzeile'})]
# removing style attribute _after_ removing specifig tags above
remove_attributes = ['width','height','style']
extra_css = '''
h1{ font-size:large; font-weight:bold; }
h2{ font-size:medium; font-weight:bold; }
'''
def get_cover_url(self):
return 'http://www.ka-news.de/storage/scl/techkanews/logos/434447_m1t1w250q75s1v29681_ka-news-Logo_mit_Schatten_transparent.png'

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '11 December 2010, desUBIKado'
__author__ = 'desUBIKado'
__description__ = 'Entertainment guide from Aragon'
__version__ = 'v0.01'
__date__ = '11, December 2010'
'''
[url]http://www.redaragon.es/[/url]
'''
from calibre.web.feeds.news import BasicNewsRecipe
class heraldo(BasicNewsRecipe):
__author__ = 'desUBIKado'
description = u'Guia de ocio desde Aragon'
title = u'RedAragon'
publisher = 'Grupo Z'
category = 'Concerts, Movies, Entertainment news'
cover_url = 'http://www.redaragon.com/2008_img/logotipo.gif'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 15
max_articles_per_feed = 100
encoding = 'iso-8859-1'
use_embedded_content = False
remove_javascript = True
no_stylesheets = True
feeds = [(u'Conciertos', u'http://redaragon.com/rss/agenda.asp?tid=1'),
(u'Exposiciones', u'http://redaragon.com/rss/agenda.asp?tid=5'),
(u'Teatro', u'http://redaragon.com/rss/agenda.asp?tid=10'),
(u'Conferencias', u'http://redaragon.com/rss/agenda.asp?tid=2'),
(u'Ferias', u'http://redaragon.com/rss/agenda.asp?tid=6'),
(u'Filmotecas/Cineclubs', u'http://redaragon.com/rss/agenda.asp?tid=7'),
(u'Presentaciones', u'http://redaragon.com/rss/agenda.asp?tid=9'),
(u'Fiestas', u'http://redaragon.com/rss/agenda.asp?tid=11'),
(u'Infantil', u'http://redaragon.com/rss/agenda.asp?tid=13'),
(u'Otros', u'http://redaragon.com/rss/agenda.asp?tid=8')]
keep_only_tags = [dict(name='div', attrs={'id':'FichaEventoAgenda'})]
remove_tags = [dict(name='div', attrs={'class':['Comparte','CajaAgenda','Caja','Cintillo']})]
remove_tags_before = dict(name='div' , attrs={'id':'FichaEventoAgenda'})
remove_tags_after = dict(name='div' , attrs={'class':'Cintillo'})

View File

@ -25,22 +25,20 @@ class Salon_com(BasicNewsRecipe):
feeds = [ feeds = [
('News & Politics', 'http://feeds.salon.com/salon/news'), ('News & Politics', 'http://feeds.salon.com/salon/news'),
('War Room', 'http://feeds.salon.com/salon/war_room'), ('War Room', 'http://feeds.feedburner.com/salon/war_room'),
('Arts & Entertainment', 'http://feeds.salon.com/salon/ent'), ('Joan Walsh', 'http://feeds.feedburner.com/Salon_Joan_Walsh'),
('I Like to Watch', 'http://feeds.salon.com/salon/iltw'), ('Glenn Greenwald', 'http://feeds.feedburner.com/salon/greenwald'),
('Beyond Multiplex', 'http://feeds.salon.com/salon/btm'),
('Book Reviews', 'http://feeds.salon.com/salon/books'),
('All Life', 'http://feeds.salon.com/salon/mwt'),
('All Opinion', 'http://feeds.salon.com/salon/opinion'),
('Glenn Greenwald', 'http://feeds.salon.com/salon/greenwald'),
('Garrison Keillor', 'http://dir.salon.com/topics/garrison_keillor/index.rss'),
('Joan Walsh', 'http://www.salon.com/rss/walsh.rss'),
('All Sports', 'http://feeds.salon.com/salon/sports'),
('Tech & Business', 'http://feeds.salon.com/salon/tech'), ('Tech & Business', 'http://feeds.salon.com/salon/tech'),
('How World Works', 'http://feeds.salon.com/salon/htww') ('Ask the Pilot', 'http://feeds.feedburner.com/salon/ask_the_pilot'),
('How World Works', 'http://feeds.feedburner.com/salon/htww'),
('Life', 'http://feeds.feedburner.com/salon/mwt'),
('Broadsheet', 'http://feeds.feedburner.com/salon/broadsheet'),
('Movie Reviews', 'http://feeds.feedburner.com/salon/movie_reviews'),
('Film Salon', 'http://feeds.feedburner.com/Salon/Film_Salon'),
('TV', 'http://feeds.feedburner.com/salon/tv'),
('Books', 'http://feeds.feedburner.com/salon/books')
] ]
def print_version(self, url): def print_version(self, url):
return url.replace('/index.html', '/print.html') return url.replace('/index.html', '/print.html')

View File

@ -17,8 +17,8 @@ class SmithsonianMagazine(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='iframe'), dict(name='iframe'),
dict(name='div', attrs={'class':'article_sidebar_border'}), dict(name='div', attrs={'class':'article_sidebar_border'}),
dict(name='div', attrs={'id':['article_sidebar_border', 'most-popular_large']}), dict(name='div', attrs={'id':['article_sidebar_border', 'most-popular_large', 'most-popular-body_large']}),
#dict(name='ul', attrs={'class':'article-tools'}), ##dict(name='ul', attrs={'class':'article-tools'}),
dict(name='ul', attrs={'class':'cat-breadcrumb col three last'}), dict(name='ul', attrs={'class':'cat-breadcrumb col three last'}),
] ]
@ -37,16 +37,16 @@ class SmithsonianMagazine(BasicNewsRecipe):
] ]
def preprocess_html(self, soup): def preprocess_html(self, soup):
story = soup.find(name='div', attrs={'id':'article-left'}) story = soup.find(name='div', attrs={'id':'article-body'})
#td = heading.findParent(name='td') ##td = heading.findParent(name='td')
#td.extract() ##td.extract()
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>') soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
body = soup.find(name='body') body = soup.find(name='body')
body.insert(0, story) body.insert(0, story)
return soup return soup
def postprocess_html(self, soup, first): #def postprocess_html(self, soup, first):
for p in soup.findAll(id='articlePaginationWrapper'): p.extract() #for p in soup.findAll(id='articlePaginationWrapper'): p.extract()
if not first: #if not first:
for div in soup.findAll(id='article-head'): div.extract() #for div in soup.findAll(id='article-head'): div.extract()
return soup #return soup

View File

@ -1,17 +1,19 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2010, JOlo'
''' '''
www.theweek.com www.theweek.com
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
import re
class TheWeekFree(BasicNewsRecipe): class TheWeek(BasicNewsRecipe):
title = 'The Week Magazine - Free content' title = 'The Week Magazine'
__author__ = 'Darko Miletic' __author__ = 'Jim Olo'
description = "The best of the US and international media. Daily coverage of commentary and analysis of the day's events, as well as arts, entertainment, people and gossip, and political cartoons." description = "The best of the US and international media. Daily coverage of commentary and analysis of the day's events, as well as arts, entertainment, people and gossip, and political cartoons."
publisher = 'The Week Publications, Inc.' publisher = 'The Week Publications, Inc.'
masthead_url = 'http://test.theweek.com/images/logo_theweek.gif'
cover_url = masthead_url
category = 'news, politics, USA' category = 'news, politics, USA'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
@ -19,31 +21,27 @@ class TheWeekFree(BasicNewsRecipe):
encoding = 'utf-8' encoding = 'utf-8'
use_embedded_content = False use_embedded_content = False
language = 'en' language = 'en'
preprocess_regexps = [(re.compile(r'<h3><a href=.*</body>', re.DOTALL), lambda match: '</body>')]
conversion_options = { remove_tags_before = dict(name='h1')
'comment' : description remove_tags_after = dict(name='div', attrs={'class':'articleSubscribe4free'})
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [
dict(name=['h1','h2'])
, dict(name='div', attrs={'class':'basefont'})
, dict(name='div', attrs={'id':'slideshowLoader'})
]
remove_tags = [ remove_tags = [
dict(name='div', attrs={'id':['digg_dugg','articleRight','dateHeader']}) dict(name='div', attrs={'class':['floatLeft','imageCaption','slideshowImageAttribution','postDate','utilities','cartoonInfo','left','middle','col300','articleSubscribe4free',' articleFlyout','articleFlyout floatRight','fourFreeBar']})
,dict(name=['object','embed','iframe']) ,dict(name='div', attrs={'id':['cartoonThumbs','rightColumn','header','partners']})
,dict(name='ul', attrs={'class':['slideshowNav','hotTopicsList topicList']})
] ]
remove_attributes = ['width','height', 'style', 'font', 'color']
extra_css = '''
h1{font-family:Geneva, Arial, Helvetica, sans-serif;color:#154B7A;}
h3{font-size: 14px;color:#999999; font-family:Geneva, Arial, Helvetica, sans-serif;font-weight: bold;}
h2{color:#666666; font-family:Geneva, Arial, Helvetica, sans-serif;font-size:small;}
p {font-family:Arial,Helvetica,sans-serif;}
'''
filter_regexps = [r'www\.palmcoastdata\.com']
feeds = [ feeds = [
(u'News & Opinions' , u'http://www.theweek.com/section/index/news_opinion.rss') (u'News-Opinion', u'http://theweek.com/section/index/news_opinion.rss'),
,(u'Arts & Leisure' , u'http://www.theweek.com/section/index/arts_leisure.rss') (u'Business', u'http://theweek.com/section/index/business.rss'),
,(u'Business' , u'http://www.theweek.com/section/index/business.rss' ) (u'Arts-Life', u'http://theweek.com/section/index/arts_life.rss'),
,(u'Cartoon & Short takes' , u'http://www.theweek.com/section/index/cartoons_wit.rss') (u'Cartoons', u'http://theweek.com/section/index/cartoon_wit/0/all-cartoons.rss')
] ]

View File

@ -38,12 +38,12 @@ class Wired(BasicNewsRecipe):
keep_only_tags = [dict(name='div', attrs={'class':'post'})] keep_only_tags = [dict(name='div', attrs={'class':'post'})]
remove_tags_after = dict(name='div', attrs={'class':'tweetmeme_button'}) remove_tags_after = dict(name='div', attrs={'class':'tweetmeme_button'})
remove_tags = [ remove_tags = [
dict(name=['object','embed','iframe','link']) dict(name=['object','embed','iframe','link','meta','base'])
,dict(name='div', attrs={'class':['podcast_storyboard','tweetmeme_button']}) ,dict(name='div', attrs={'class':['podcast_storyboard','tweetmeme_button']})
,dict(attrs={'id':'ff_bottom_nav'}) ,dict(attrs={'id':'ff_bottom_nav'})
,dict(name='a',attrs={'href':'http://www.wired.com/app'}) ,dict(name='a',attrs={'href':'http://www.wired.com/app'})
] ]
remove_attributes = ['height','width'] remove_attributes = ['height','width','lang','border','clear']
def parse_index(self): def parse_index(self):
@ -78,6 +78,8 @@ class Wired(BasicNewsRecipe):
divurl = item.find('div',attrs={'class':'feature-header'}) divurl = item.find('div',attrs={'class':'feature-header'})
if divurl: if divurl:
divdesc = item.find('div',attrs={'class':'feature-text'}) divdesc = item.find('div',attrs={'class':'feature-text'})
url = divurl.a['href']
if not divurl.a['href'].startswith('http://www.wired.com'):
url = 'http://www.wired.com' + divurl.a['href'] url = 'http://www.wired.com' + divurl.a['href']
title = self.tag_to_string(divurl.a) title = self.tag_to_string(divurl.a)
description = self.tag_to_string(divdesc) description = self.tag_to_string(divdesc)
@ -127,5 +129,17 @@ class Wired(BasicNewsRecipe):
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']
for item in soup.findAll('a'):
if item.string is not None:
tstr = item.string
item.replaceWith(tstr)
else:
item.name='span'
for atrs in ['href','target','alt','title','name','id']:
if item.has_key(atrs):
del item[atrs]
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup return soup

View File

@ -612,8 +612,13 @@ class Py2App(object):
dmg = os.path.join(destdir, volname+'.dmg') dmg = os.path.join(destdir, volname+'.dmg')
if os.path.exists(dmg): if os.path.exists(dmg):
os.unlink(dmg) os.unlink(dmg)
subprocess.check_call(['/usr/bin/hdiutil', 'create', '-srcfolder', os.path.abspath(d), tdir = tempfile.mkdtemp()
shutil.copytree(d, os.path.join(tdir, os.path.basename(d)),
symlinks=True)
os.symlink('/Applications', os.path.join(tdir, 'Applications'))
subprocess.check_call(['/usr/bin/hdiutil', 'create', '-srcfolder', tdir,
'-volname', volname, '-format', format, dmg]) '-volname', volname, '-format', format, dmg])
shutil.rmtree(tdir)
if internet_enable: if internet_enable:
subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg]) subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
size = os.stat(dmg).st_size/(1024*1024.) size = os.stat(dmg).st_size/(1024*1024.)

View File

@ -307,6 +307,14 @@ class CatalogPlugin(Plugin): # {{{
#: cli_options parsed in library.cli:catalog_option_parser() #: cli_options parsed in library.cli:catalog_option_parser()
cli_options = [] cli_options = []
def _field_sorter(self, key):
'''
Custom fields sort after standard fields
'''
if key.startswith('#'):
return '~%s' % key[1:]
else:
return key
def search_sort_db(self, db, opts): def search_sort_db(self, db, opts):
@ -315,18 +323,18 @@ class CatalogPlugin(Plugin): # {{{
if opts.sort_by: if opts.sort_by:
# 2nd arg = ascending # 2nd arg = ascending
db.sort(opts.sort_by, True) db.sort(opts.sort_by, True)
return db.get_data_as_dict(ids=opts.ids) return db.get_data_as_dict(ids=opts.ids)
def get_output_fields(self, opts): def get_output_fields(self, db, opts):
# Return a list of requested fields, with opts.sort_by first # Return a list of requested fields, with opts.sort_by first
all_fields = set( all_std_fields = set(
['author_sort','authors','comments','cover','formats', ['author_sort','authors','comments','cover','formats',
'id','isbn','ondevice','pubdate','publisher','rating', 'id','isbn','ondevice','pubdate','publisher','rating',
'series_index','series','size','tags','timestamp', 'series_index','series','size','tags','timestamp',
'title','uuid']) 'title','uuid'])
all_custom_fields = set(db.custom_field_keys())
all_fields = all_std_fields.union(all_custom_fields)
fields = all_fields
if opts.fields != 'all': if opts.fields != 'all':
# Make a list from opts.fields # Make a list from opts.fields
requested_fields = set(opts.fields.split(',')) requested_fields = set(opts.fields.split(','))
@ -337,7 +345,7 @@ class CatalogPlugin(Plugin): # {{{
if not opts.connected_device['is_device_connected'] and 'ondevice' in fields: if not opts.connected_device['is_device_connected'] and 'ondevice' in fields:
fields.pop(int(fields.index('ondevice'))) fields.pop(int(fields.index('ondevice')))
fields.sort() fields = sorted(fields, key=self._field_sorter)
if opts.sort_by and opts.sort_by in fields: if opts.sort_by and opts.sort_by in fields:
fields.insert(0,fields.pop(int(fields.index(opts.sort_by)))) fields.insert(0,fields.pop(int(fields.index(opts.sort_by))))
return fields return fields

View File

@ -478,7 +478,7 @@ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \ GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR, \
TREKSTOR, EEEREADER TREKSTOR, EEEREADER, NEXTBOOK
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
@ -606,6 +606,7 @@ plugins += [
BAMBOOK, BAMBOOK,
TREKSTOR, TREKSTOR,
EEEREADER, EEEREADER,
NEXTBOOK,
ITUNES, ITUNES,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

@ -439,6 +439,13 @@ class TabletOutput(iPadOutput):
screen_size = (sys.maxint, sys.maxint) screen_size = (sys.maxint, sys.maxint)
comic_screen_size = (sys.maxint, sys.maxint) comic_screen_size = (sys.maxint, sys.maxint)
class SamsungGalaxy(TabletOutput):
name = 'Samsung Galaxy'
shortname = 'galaxy'
description = _('Intended for the Samsung Galaxy and similar tablet devices with '
'a resolution of 600x1280')
screen_size = comic_screen_size = (600, 1280)
class SonyReaderOutput(OutputProfile): class SonyReaderOutput(OutputProfile):
name = 'Sony Reader' name = 'Sony Reader'
@ -617,6 +624,8 @@ class KindleDXOutput(OutputProfile):
#comic_screen_size = (741, 1022) #comic_screen_size = (741, 1022)
supports_mobi_indexing = True supports_mobi_indexing = True
periodical_date_in_title = False periodical_date_in_title = False
missing_char = u'x\u2009'
empty_ratings_char = u'\u2606'
ratings_char = u'\u2605' ratings_char = u'\u2605'
read_char = u'\u2713' read_char = u'\u2713'
mobi_ems_per_blockquote = 2.0 mobi_ems_per_blockquote = 2.0
@ -707,7 +716,7 @@ class BambookOutput(OutputProfile):
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output, output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output, SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput, HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
iPadOutput, KoboReaderOutput, TabletOutput, iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput, IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
BambookOutput, NookColorOutput] BambookOutput, NookColorOutput]

View File

@ -27,8 +27,8 @@ class ANDROID(USBMS):
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] }, 0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216], 0x22b8 : { 0x41d9 : [0x216], 0x2d61: [0x100], 0x2d67 : [0x100],
0x4285 : [0x216], 0x42a3 : [0x216] }, 0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216] },
# Sony Ericsson # Sony Ericsson
0xfce : { 0xd12e : [0x0100]}, 0xfce : { 0xd12e : [0x0100]},
@ -64,7 +64,8 @@ class ANDROID(USBMS):
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'] 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300']
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']

View File

@ -29,12 +29,16 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
booklist_class = BookList booklist_class = BookList
book_class = Book book_class = Book
ip = None
FORMATS = [ "snb" ] FORMATS = [ "snb" ]
VENDOR_ID = 0x230b VENDOR_ID = 0x230b
PRODUCT_ID = 0x0001 PRODUCT_ID = 0x0001
BCD = None BCD = None
CAN_SET_METADATA = False CAN_SET_METADATA = False
THUMBNAIL_HEIGHT = 155 THUMBNAIL_HEIGHT = 155
EXTRA_CUSTOMIZATION_MESSAGE = \
_("Device IP Address (restart calibre after changing)")
icon = I("devices/bambook.png") icon = I("devices/bambook.png")
# OPEN_FEEDBACK_MESSAGE = _( # OPEN_FEEDBACK_MESSAGE = _(
@ -47,6 +51,10 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
METADATA_FILE_GUID = 'calibremetadata.snb' METADATA_FILE_GUID = 'calibremetadata.snb'
bambook = None bambook = None
is_connected = False
def __init__(self, ip):
self.ip = ip
def reset(self, key='-1', log_packets=False, report_progress=None, def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) : detected_device=None) :
@ -60,15 +68,23 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
self.eject() self.eject()
# Connect # Connect
self.bambook = Bambook() self.bambook = Bambook()
self.bambook.Connect() self.bambook.Connect(ip = self.ip, timeout = 10000)
if self.bambook.GetState() != CONN_CONNECTED: if self.bambook.GetState() != CONN_CONNECTED:
self.bambook = None self.bambook = None
raise Exception(_("Unable to connect to Bambook.")) raise OpenFeedback(_("Unable to connect to Bambook. \n"
"If you are trying to connect via Wi-Fi, "
"please make sure the IP address of Bambook has been correctly configured."))
self.is_connected = True
return True
def unmount_device(self):
self.eject()
def eject(self): def eject(self):
if self.bambook: if self.bambook:
self.bambook.Disconnect() self.bambook.Disconnect()
self.bambook = None self.bambook = None
self.is_connected = False
def post_yank_cleanup(self): def post_yank_cleanup(self):
self.eject() self.eject()
@ -475,3 +491,8 @@ class BAMBOOK(DeviceConfig, DevicePlugin):
def get_guid(uuid): def get_guid(uuid):
guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb" guid = hashlib.md5(uuid).hexdigest()[0:15] + ".snb"
return guid return guid
class BAMBOOKWifi(BAMBOOK):
def is_usb_connected(self, devices_on_system, debug=False,
only_presence=False):
return self.is_connected, self

View File

@ -329,6 +329,8 @@ class Bambook:
self.handle = None self.handle = None
def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000): def Connect(self, ip = DEFAULT_BAMBOOK_IP, timeout = 10000):
if ip == None or ip == '':
ip = DEFAULT_BAMBOOK_IP
self.handle = BambookConnect(ip, timeout) self.handle = BambookConnect(ip, timeout)
if self.handle and self.handle != 0: if self.handle and self.handle != 0:
return True return True

View File

@ -230,7 +230,7 @@ class POCKETBOOK301(USBMS):
class POCKETBOOK602(USBMS): class POCKETBOOK602(USBMS):
name = 'PocketBook Pro 602/902 Device Interface' name = 'PocketBook Pro 602/902 Device Interface'
description = _('Communicate with the PocketBook 602 reader.') description = _('Communicate with the PocketBook 602/603/902/903 reader.')
author = 'Kovid Goyal' author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm',
@ -244,7 +244,7 @@ class POCKETBOOK602(USBMS):
BCD = [0x0324] BCD = [0x0324]
VENDOR_NAME = '' VENDOR_NAME = ''
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902', 'PB903']
class POCKETBOOK701(USBMS): class POCKETBOOK701(USBMS):

View File

@ -18,9 +18,9 @@ class FOLDER_DEVICE_FOR_CONFIG(USBMS):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'mobi', 'azw', 'lrf', 'tcr', 'pmlz', 'lit', FORMATS = ['epub', 'fb2', 'mobi', 'azw', 'lrf', 'tcr', 'pmlz', 'lit',
'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb', 'prc'] 'rtf', 'rb', 'pdf', 'oeb', 'txt', 'pdb', 'prc']
VENDOR_ID = 0xffff VENDOR_ID = [0xffff]
PRODUCT_ID = 0xffff PRODUCT_ID = [0xffff]
BCD = 0xffff BCD = [0xffff]
DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE'
@ -34,9 +34,9 @@ class FOLDER_DEVICE(USBMS):
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
FORMATS = FOLDER_DEVICE_FOR_CONFIG.FORMATS FORMATS = FOLDER_DEVICE_FOR_CONFIG.FORMATS
VENDOR_ID = 0xffff VENDOR_ID = [0xffff]
PRODUCT_ID = 0xffff PRODUCT_ID = [0xffff]
BCD = 0xffff BCD = [0xffff]
DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE' DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE'
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device

View File

@ -20,11 +20,11 @@ class IRIVER_STORY(USBMS):
FORMATS = ['epub', 'fb2', 'pdf', 'djvu', 'txt'] FORMATS = ['epub', 'fb2', 'pdf', 'djvu', 'txt']
VENDOR_ID = [0x1006] VENDOR_ID = [0x1006]
PRODUCT_ID = [0x4023, 0x4025] PRODUCT_ID = [0x4023, 0x4024, 0x4025]
BCD = [0x0323] BCD = [0x0323]
VENDOR_NAME = 'IRIVER' VENDOR_NAME = 'IRIVER'
WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05'] WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05', 'STORY_WI-FI']
WINDOWS_CARD_A_MEM = ['STORY', 'STORY_SD'] WINDOWS_CARD_A_MEM = ['STORY', 'STORY_SD']
#OSX_MAIN_MEM = 'Kindle Internal Storage Media' #OSX_MAIN_MEM = 'Kindle Internal Storage Media'

View File

@ -264,3 +264,23 @@ class EEEREADER(USBMS):
VENDOR_NAME = 'LINUX' VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
class NEXTBOOK(USBMS):
name = 'Nextbook device interface'
gui_name = 'Nextbook'
description = _('Communicate with the Nextbook Reader')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
VENDOR_ID = [0x05e3]
PRODUCT_ID = [0x0726]
BCD = [0x021a]
EBOOK_DIR_MAIN = ''
VENDOR_NAME = 'NEXT2'
WINDOWS_MAIN_MEM = '1.0.14'

View File

@ -140,11 +140,19 @@ class CollectionsBookList(BookList):
all_by_author = '' all_by_author = ''
all_by_title = '' all_by_title = ''
ca = [] ca = []
all_by_something = []
for c in collection_attributes: for c in collection_attributes:
if c.startswith('aba:') and c[4:]: if c.startswith('aba:') and c[4:].strip():
all_by_author = c[4:].strip() all_by_author = c[4:].strip()
elif c.startswith('abt:') and c[4:]: elif c.startswith('abt:') and c[4:].strip():
all_by_title = c[4:].strip() all_by_title = c[4:].strip()
elif c.startswith('abs:') and c[4:].strip():
name = c[4:].strip()
sby = self.in_category_sort_rules(name)
if sby is None:
sby = name
if name and sby:
all_by_something.append((name, sby))
else: else:
ca.append(c.lower()) ca.append(c.lower())
collection_attributes = ca collection_attributes = ca
@ -251,6 +259,10 @@ class CollectionsBookList(BookList):
if all_by_title not in collections: if all_by_title not in collections:
collections[all_by_title] = {} collections[all_by_title] = {}
collections[all_by_title][lpath] = (book, tsval, asval) collections[all_by_title][lpath] = (book, tsval, asval)
for (n, sb) in all_by_something:
if n not in collections:
collections[n] = {}
collections[n][lpath] = (book, book.get(sb, ''), tsval)
# Sort collections # Sort collections
result = {} result = {}

View File

@ -41,9 +41,12 @@ class FB2Input(InputFormatPlugin):
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.oeb.base import XLINK_NS, XHTML_NS, RECOVER_PARSER from calibre.ebooks.oeb.base import XLINK_NS, XHTML_NS, RECOVER_PARSER
from calibre.ebooks.chardet import xml_to_unicode
NAMESPACES = {'f':FB2NS, 'l':XLINK_NS} NAMESPACES = {'f':FB2NS, 'l':XLINK_NS}
log.debug('Parsing XML...') log.debug('Parsing XML...')
raw = stream.read().replace('\0', '') raw = stream.read().replace('\0', '')
raw = xml_to_unicode(raw, strip_encoding_pats=True,
assume_utf8=True)[0]
try: try:
doc = etree.fromstring(raw) doc = etree.fromstring(raw)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:

View File

@ -159,6 +159,11 @@ class Metadata(object):
try: try:
return self.__getattribute__(field) return self.__getattribute__(field)
except AttributeError: except AttributeError:
if field.startswith('#') and field.endswith('_index'):
try:
return self.get_extra(field[:-6])
except:
pass
return default return default
def get_extra(self, field): def get_extra(self, field):

View File

@ -9,6 +9,7 @@ import mimetypes, os
from base64 import b64decode from base64 import b64decode
from lxml import etree from lxml import etree
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode
XLINK_NS = 'http://www.w3.org/1999/xlink' XLINK_NS = 'http://www.w3.org/1999/xlink'
def XLINK(name): def XLINK(name):
@ -23,7 +24,10 @@ def get_metadata(stream):
tostring = lambda x : etree.tostring(x, method='text', tostring = lambda x : etree.tostring(x, method='text',
encoding=unicode).strip() encoding=unicode).strip()
parser = etree.XMLParser(recover=True, no_network=True) parser = etree.XMLParser(recover=True, no_network=True)
root = etree.fromstring(stream.read(), parser=parser) raw = stream.read()
raw = xml_to_unicode(raw, strip_encoding_pats=True,
assume_utf8=True)[0]
root = etree.fromstring(raw, parser=parser)
authors, author_sort = [], None authors, author_sort = [], None
for au in XPath('//fb2:author')(root): for au in XPath('//fb2:author')(root):
fname = lname = author = None fname = lname = author = None

View File

@ -27,7 +27,39 @@ def get_metadata(stream):
with TemporaryDirectory() as tdir: with TemporaryDirectory() as tdir:
with CurrentDir(tdir): with CurrentDir(tdir):
path = zf.extract(f) path = zf.extract(f)
return get_metadata(open(path, 'rb'), stream_type) mi = get_metadata(open(path,'rb'), stream_type)
if stream_type == 'opf' and mi.application_id == None:
try:
# zip archive opf files without an application_id were assumed not to have a cover
# reparse the opf and if cover exists read its data from zip archive for the metadata
nmi = zip_opf_metadata(path, zf)
return nmi
except:
pass
return mi
raise ValueError('No ebook found in ZIP archive') raise ValueError('No ebook found in ZIP archive')
def zip_opf_metadata(opfpath, zf):
from calibre.ebooks.metadata.opf2 import OPF
if hasattr(opfpath, 'read'):
f = opfpath
opfpath = getattr(f, 'name', os.getcwd())
else:
f = open(opfpath, 'rb')
opf = OPF(f, os.path.dirname(opfpath))
mi = opf.to_book_metadata()
# This is broken, in that it only works for
# when both the OPF file and the cover file are in the root of the
# zip file and the cover is an actual raster image, but I don't care
# enough to make it more robust
if getattr(mi, 'cover', None):
covername = os.path.basename(mi.cover)
mi.cover = None
names = zf.namelist()
if covername in names:
fmt = covername.rpartition('.')[-1]
data = zf.read(covername)
mi.cover_data = (fmt, data)
return mi

View File

@ -513,11 +513,14 @@ class MobiReader(object):
mobi_version = self.book_header.mobi_version mobi_version = self.book_header.mobi_version
for x in root.xpath('//ncx'): for x in root.xpath('//ncx'):
x.getparent().remove(x) x.getparent().remove(x)
svg_tags = []
for i, tag in enumerate(root.iter(etree.Element)): for i, tag in enumerate(root.iter(etree.Element)):
tag.attrib.pop('xmlns', '') tag.attrib.pop('xmlns', '')
for x in tag.attrib: for x in tag.attrib:
if ':' in x: if ':' in x:
del tag.attrib[x] del tag.attrib[x]
if tag.tag and barename(tag.tag) == 'svg':
svg_tags.append(tag)
if tag.tag and barename(tag.tag.lower()) in \ if tag.tag and barename(tag.tag.lower()) in \
('country-region', 'place', 'placetype', 'placename', ('country-region', 'place', 'placetype', 'placename',
'state', 'city', 'street', 'address', 'content', 'form'): 'state', 'city', 'street', 'address', 'content', 'form'):
@ -628,6 +631,11 @@ class MobiReader(object):
cls = cls + (' ' if cls else '') + ncls cls = cls + (' ' if cls else '') + ncls
attrib['class'] = cls attrib['class'] = cls
for tag in svg_tags:
p = tag.getparent()
if hasattr(p, 'remove'):
p.remove(tag)
def create_opf(self, htmlfile, guide=None, root=None): def create_opf(self, htmlfile, guide=None, root=None):
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi) mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
if mi is None: if mi is None:

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */

View File

@ -1,3 +1,10 @@
/**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v2+
*/
#include <stdio.h> #include <stdio.h>
#include <errno.h> #include <errno.h>
#include <sstream> #include <sstream>

View File

@ -1,3 +1,10 @@
/**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v2+
*/
#pragma once #pragma once
#include <vector> #include <vector>

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */

View File

@ -1,3 +1,10 @@
/**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v2+
*/
#ifndef PDF2XML #ifndef PDF2XML
#define UNICODE #define UNICODE
#define PY_SSIZE_T_CLEAN #define PY_SSIZE_T_CLEAN

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */
#include <Outline.h> #include <Outline.h>

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
* Based on pdftohtml from the poppler project. * Based on pdftohtml from the poppler project.
*/ */

View File

@ -1,6 +1,6 @@
/** /**
* Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net> * Copyright 2009 Kovid Goyal <kovid@kovidgoyal.net>
* License: GNU GPL v3 * License: GNU GPL v2+
*/ */

View File

@ -59,7 +59,13 @@ def get_pdf_printer(opts, for_comic=False):
dpi = opts.output_profile.dpi dpi = opts.output_profile.dpi
printer.setPaperSize(QSizeF(float(w) / dpi, float(h) / dpi), QPrinter.Inch) printer.setPaperSize(QSizeF(float(w) / dpi, float(h) / dpi), QPrinter.Inch)
printer.setPageMargins(opts.margin_left, opts.margin_top, opts.margin_right, opts.margin_bottom, QPrinter.Point) if for_comic:
# Comic pages typically have their own margins, or their background
# color is not white, in which case the margin looks bad
printer.setPageMargins(0, 0, 0, 0, QPrinter.Point)
else:
printer.setPageMargins(opts.margin_left, opts.margin_top,
opts.margin_right, opts.margin_bottom, QPrinter.Point)
printer.setOrientation(orientation(opts.orientation)) printer.setOrientation(orientation(opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(True) printer.setFullPage(True)

View File

@ -402,7 +402,7 @@ class FieldStrings:
Logic: Logic:
self.__link_switch = re.compile(r'\\l\s{1,}(.*?)\s') self.__link_switch = re.compile(r'\\l\s{1,}(.*?)\s')
""" """
self.__link_switch = re.compile(r'\\l\s{1,}(.*?)\s') self.__link_switch = re.compile(r'\\l\s{1,}"{0,1}(.*?)"{0,1}\s')
the_string = name the_string = name
match_group = re.search(self.__link_switch, line) match_group = re.search(self.__link_switch, line)
if match_group: if match_group:

View File

@ -46,14 +46,27 @@ class SNBInput(InputFormatPlugin):
meta = snbFile.GetFileStream('snbf/book.snbf') meta = snbFile.GetFileStream('snbf/book.snbf')
if meta != None: if meta != None:
meta = etree.fromstring(meta) meta = etree.fromstring(meta)
oeb.metadata.add('title', meta.find('.//head/name').text) l = { 'title' : './/head/name',
oeb.metadata.add('creator', meta.find('.//head/author').text, attrib={'role':'aut'}) 'creator' : './/head/author',
oeb.metadata.add('language', meta.find('.//head/language').text.lower().replace('_', '-')) 'language' : './/head/language',
oeb.metadata.add('creator', meta.find('.//head/generator').text) 'generator': './/head/generator',
oeb.metadata.add('publisher', meta.find('.//head/publisher').text) 'publisher': './/head/publisher',
cover = meta.find('.//head/cover') 'cover' : './/head/cover', }
if cover != None and cover.text != None: d = {}
oeb.guide.add('cover', 'Cover', cover.text) for item in l:
node = meta.find(l[item])
if node != None:
d[item] = node.text if node.text != None else ''
else:
d[item] = ''
oeb.metadata.add('title', d['title'])
oeb.metadata.add('creator', d['creator'], attrib={'role':'aut'})
oeb.metadata.add('language', d['language'].lower().replace('_', '-'))
oeb.metadata.add('generator', d['generator'])
oeb.metadata.add('publisher', d['publisher'])
if d['cover'] != '':
oeb.guide.add('cover', 'Cover', d['cover'])
bookid = str(uuid.uuid4()) bookid = str(uuid.uuid4())
oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid') oeb.metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')

View File

@ -57,7 +57,7 @@ class GenerateCatalogAction(InterfaceAction):
if job.result: if job.result:
# Search terms nulled catalog results # Search terms nulled catalog results
return error_dialog(self.gui, _('No books found'), return error_dialog(self.gui, _('No books found'),
_("No books to catalog\nCheck exclude tags"), _("No books to catalog\nCheck exclusion criteria"),
show=True) show=True)
if job.failed: if job.failed:
return self.gui.job_exception(job) return self.gui.job_exception(job)

View File

@ -12,11 +12,15 @@ from PyQt4.Qt import QToolButton, QMenu, pyqtSignal, QIcon
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.utils.smtp import config as email_config from calibre.utils.smtp import config as email_config
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx
from calibre.customize.ui import is_disabled
from calibre.devices.bambook.driver import BAMBOOK
class ShareConnMenu(QMenu): # {{{ class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal() connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal() connect_to_itunes = pyqtSignal()
connect_to_bambook = pyqtSignal()
config_email = pyqtSignal() config_email = pyqtSignal()
toggle_server = pyqtSignal() toggle_server = pyqtSignal()
dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
@ -34,6 +38,17 @@ class ShareConnMenu(QMenu): # {{{
self.connect_to_itunes_action = mitem self.connect_to_itunes_action = mitem
if not (iswindows or isosx): if not (iswindows or isosx):
mitem.setVisible(False) mitem.setVisible(False)
mitem = self.addAction(QIcon(I('devices/bambook.png')), _('Connect to Bambook'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_bambook.emit())
self.connect_to_bambook_action = mitem
bambook_visible = False
if not is_disabled(BAMBOOK):
device_ip = BAMBOOK.settings().extra_customization
if device_ip:
bambook_visible = True
self.connect_to_bambook_action.setVisible(bambook_visible)
self.addSeparator() self.addSeparator()
self.toggle_server_action = \ self.toggle_server_action = \
self.addAction(QIcon(I('network-server.png')), self.addAction(QIcon(I('network-server.png')),
@ -88,6 +103,7 @@ class ShareConnMenu(QMenu): # {{{
def set_state(self, device_connected): def set_state(self, device_connected):
self.connect_to_folder_action.setEnabled(not device_connected) self.connect_to_folder_action.setEnabled(not device_connected)
self.connect_to_itunes_action.setEnabled(not device_connected) self.connect_to_itunes_action.setEnabled(not device_connected)
self.connect_to_bambook_action.setEnabled(not device_connected)
# }}} # }}}
@ -126,6 +142,7 @@ class ConnectShareAction(InterfaceAction):
self.qaction.setMenu(self.share_conn_menu) self.qaction.setMenu(self.share_conn_menu)
self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder) self.share_conn_menu.connect_to_folder.connect(self.gui.connect_to_folder)
self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes) self.share_conn_menu.connect_to_itunes.connect(self.gui.connect_to_itunes)
self.share_conn_menu.connect_to_bambook.connect(self.gui.connect_to_bambook)
def location_selected(self, loc): def location_selected(self, loc):
enabled = loc == 'library' enabled = loc == 'library'

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, collections, sys import os, collections, sys
from Queue import Queue from Queue import Queue
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, \ from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \ QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
@ -18,7 +18,7 @@ from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.gui2 import config, open_local_file from calibre.gui2 import config, open_local_file, open_url
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
# render_rows(data) {{{ # render_rows(data) {{{
@ -412,6 +412,12 @@ class BookDetails(QWidget): # {{{
self.view_specific_format.emit(int(id_), fmt) self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath': elif typ == 'devpath':
open_local_file(val) open_local_file(val)
else:
try:
open_url(QUrl(link, QUrl.TolerantMode))
except:
import traceback
traceback.print_exc()
def mouseDoubleClickEvent(self, ev): def mouseDoubleClickEvent(self, ev):

View File

@ -6,9 +6,9 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from calibre.gui2 import gprefs from calibre.gui2 import gprefs
from calibre.gui2.catalog.catalog_csv_xml_ui import Ui_Form from calibre.gui2.catalog.catalog_csv_xml_ui import Ui_Form
from calibre.library import db as db_
from PyQt4.Qt import QWidget, QListWidgetItem from PyQt4.Qt import QWidget, QListWidgetItem
class PluginWidget(QWidget, Ui_Form): class PluginWidget(QWidget, Ui_Form):
@ -28,6 +28,12 @@ class PluginWidget(QWidget, Ui_Form):
self.all_fields.append(x) self.all_fields.append(x)
QListWidgetItem(x, self.db_fields) QListWidgetItem(x, self.db_fields)
db = db_()
for x in sorted(db.custom_field_keys()):
self.all_fields.append(x)
QListWidgetItem(x, self.db_fields)
def initialize(self, name, db): def initialize(self, name, db):
self.name = name self.name = name
fields = gprefs.get(name+'_db_fields', self.all_fields) fields = gprefs.get(name+'_db_fields', self.all_fields)

View File

@ -17,18 +17,55 @@ class PluginWidget(QWidget,Ui_Form):
TITLE = _('E-book options') TITLE = _('E-book options')
HELP = _('Options specific to')+' EPUB/MOBI '+_('output') HELP = _('Options specific to')+' EPUB/MOBI '+_('output')
OPTION_FIELDS = [('exclude_genre','\[.+\]'),
('exclude_tags','~,'+_('Catalog')), CheckBoxControls = [
('generate_titles', True), 'generate_titles',
('generate_series', True), 'generate_series',
('generate_recently_added', True), 'generate_genres',
('note_tag','*'), 'generate_recently_added',
('numbers_as_text', False), 'generate_descriptions',
('read_pattern','+'), 'include_hr'
('read_source_field_cb','Tag'), ]
('wishlist_tag','Wishlist'), ComboBoxControls = [
'read_source_field',
'exclude_source_field',
'header_note_source_field',
'merge_source_field'
]
LineEditControls = [
'exclude_genre',
'exclude_pattern',
'exclude_tags',
'read_pattern',
'wishlist_tag'
]
RadioButtonControls = [
'merge_before',
'merge_after'
]
SpinBoxControls = [
'thumb_width'
] ]
OPTION_FIELDS = zip(CheckBoxControls,
[True for i in CheckBoxControls],
['check_box' for i in CheckBoxControls])
OPTION_FIELDS += zip(ComboBoxControls,
[None for i in ComboBoxControls],
['combo_box' for i in ComboBoxControls])
OPTION_FIELDS += zip(RadioButtonControls,
[None for i in RadioButtonControls],
['radio_button' for i in RadioButtonControls])
# LineEditControls
OPTION_FIELDS += zip(['exclude_genre'],['\[.+\]'],['line_edit'])
OPTION_FIELDS += zip(['exclude_pattern'],[None],['line_edit'])
OPTION_FIELDS += zip(['exclude_tags'],['~,'+_('Catalog')],['line_edit'])
OPTION_FIELDS += zip(['read_pattern'],['+'],['line_edit'])
OPTION_FIELDS += zip(['wishlist_tag'],['Wishlist'],['line_edit'])
# SpinBoxControls
OPTION_FIELDS += zip(['thumb_width'],[1.00],['spin_box'])
# Output synced to the connected device? # Output synced to the connected device?
sync_enabled = True sync_enabled = True
@ -42,105 +79,203 @@ class PluginWidget(QWidget,Ui_Form):
def initialize(self, name, db): def initialize(self, name, db):
self.name = name self.name = name
self.db = db
# Populate the 'Read book' source fields self.populateComboBoxes()
all_custom_fields = db.custom_field_keys()
custom_fields = {}
custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
for custom_field in all_custom_fields:
field_md = db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.read_source_field_cb.addItem(cf)
self.read_source_fields = custom_fields
self.read_source_field_cb.currentIndexChanged.connect(self.read_source_field_changed)
# Update dialog fields from stored options # Update dialog fields from stored options
for opt in self.OPTION_FIELDS: for opt in self.OPTION_FIELDS:
opt_value = gprefs.get(self.name + '_' + opt[0], opt[1]) c_name, c_def, c_type = opt
if opt[0] in [ opt_value = gprefs.get(self.name + '_' + c_name, c_def)
'generate_recently_added', if c_type in ['check_box']:
'generate_series', getattr(self, c_name).setChecked(eval(str(opt_value)))
'generate_titles', elif c_type in ['combo_box'] and opt_value is not None:
'numbers_as_text', # *** Test this code with combo boxes ***
]: #index = self.read_source_field.findText(opt_value)
getattr(self, opt[0]).setChecked(opt_value) index = getattr(self,c_name).findText(opt_value)
if index == -1 and c_name == 'read_source_field':
index = self.read_source_field.findText('Tag')
#self.read_source_field.setCurrentIndex(index)
getattr(self,c_name).setCurrentIndex(index)
elif c_type in ['line_edit']:
getattr(self, c_name).setText(opt_value if opt_value else '')
elif c_type in ['radio_button'] and opt_value is not None:
getattr(self, c_name).setChecked(opt_value)
elif c_type in ['spin_box']:
getattr(self, c_name).setValue(float(opt_value))
# Combo box # Init self.read_source_field_name
elif opt[0] in ['read_source_field_cb']: cs = unicode(self.read_source_field.currentText())
# Look for last-stored combo box value
index = self.read_source_field_cb.findText(opt_value)
if index == -1:
index = self.read_source_field_cb.findText('Tag')
self.read_source_field_cb.setCurrentIndex(index)
# Text fields
else:
getattr(self, opt[0]).setText(opt_value)
# Init self.read_source_field
cs = unicode(self.read_source_field_cb.currentText())
read_source_spec = self.read_source_fields[cs] read_source_spec = self.read_source_fields[cs]
self.read_source_field = read_source_spec['field'] self.read_source_field_name = read_source_spec['field']
# Init self.exclude_source_field_name
self.exclude_source_field_name = ''
cs = unicode(self.exclude_source_field.currentText())
if cs > '':
exclude_source_spec = self.exclude_source_fields[cs]
self.exclude_source_field_name = exclude_source_spec['field']
# Init self.merge_source_field_name
self.merge_source_field_name = ''
cs = unicode(self.merge_source_field.currentText())
if cs > '':
merge_source_spec = self.merge_source_fields[cs]
self.merge_source_field_name = merge_source_spec['field']
# Init self.header_note_source_field_name
self.header_note_source_field_name = ''
cs = unicode(self.header_note_source_field.currentText())
if cs > '':
header_note_source_spec = self.header_note_source_fields[cs]
self.header_note_source_field_name = header_note_source_spec['field']
# Hook changes to thumb_width
self.thumb_width.valueChanged.connect(self.thumb_width_changed)
def options(self): def options(self):
# Save/return the current options # Save/return the current options
# exclude_genre stores literally # exclude_genre stores literally
# generate_titles, generate_recently_added, numbers_as_text stores as True/False # generate_titles, generate_recently_added, numbers_as_text stores as True/False
# others store as lists # others store as lists
opts_dict = {} opts_dict = {}
for opt in self.OPTION_FIELDS:
# Save values to gprefs # Save values to gprefs
if opt[0] in [ for opt in self.OPTION_FIELDS:
'generate_recently_added', c_name, c_def, c_type = opt
'generate_series', if c_type in ['check_box', 'radio_button']:
'generate_titles', opt_value = getattr(self, c_name).isChecked()
'numbers_as_text', elif c_type in ['combo_box']:
]: opt_value = unicode(getattr(self,c_name).currentText()).strip()
opt_value = getattr(self,opt[0]).isChecked() elif c_type in ['line_edit']:
opt_value = unicode(getattr(self, c_name).text()).strip()
elif c_type in ['spin_box']:
opt_value = unicode(getattr(self, c_name).value())
gprefs.set(self.name + '_' + c_name, opt_value)
# Combo box uses .currentText() # Construct opts object
elif opt[0] in ['read_source_field_cb']: if c_name == 'exclude_tags':
opt_value = unicode(getattr(self, opt[0]).currentText()) # store as list
opts_dict[c_name] = opt_value.split(',')
# text fields use .text()
else: else:
opt_value = unicode(getattr(self, opt[0]).text()) opts_dict[c_name] = opt_value
gprefs.set(self.name + '_' + opt[0], opt_value)
# Construct opts # Generate markers for hybrids
if opt[0] in [ opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field_name,
'exclude_genre', self.read_pattern.text())
'generate_recently_added', opts_dict['exclude_book_marker'] = "%s:%s" % (self.exclude_source_field_name,
'generate_series', self.exclude_pattern.text())
'generate_titles',
'numbers_as_text',
]:
opts_dict[opt[0]] = opt_value
else:
opts_dict[opt[0]] = opt_value.split(',')
# Generate read_book_marker # Generate specs for merge_comments, header_note_source_field
opts_dict['read_book_marker'] = "%s:%s" % (self.read_source_field, self.read_pattern.text()) checked = ''
if self.merge_before.isChecked():
checked = 'before'
elif self.merge_after.isChecked():
checked = 'after'
include_hr = self.include_hr.isChecked()
opts_dict['merge_comments'] = "%s:%s:%s" % \
(self.merge_source_field_name, checked, include_hr)
opts_dict['header_note_source_field'] = self.header_note_source_field_name
# Append the output profile # Append the output profile
opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']] opts_dict['output_profile'] = [load_defaults('page_setup')['output_profile']]
if False:
print "opts_dict"
for opt in sorted(opts_dict.keys()):
print " %s: %s" % (opt, repr(opts_dict[opt]))
return opts_dict return opts_dict
def populateComboBoxes(self):
# Custom column types declared in
# gui2.preferences.create_custom_column:CreateCustomColumn()
# As of 0.7.34:
# bool Yes/No
# comments Long text, like comments, not shown in tag browser
# composite Column built from other columns
# datetime Date
# enumeration Text, but with a fixed set of permitted values
# float Floating point numbers
# int Integers
# rating Ratings, shown with stars
# series Text column for keeping series-like information
# text Column shown in the tag browser
# *text Comma-separated text, like tags, shown in tag browser
all_custom_fields = self.db.custom_field_keys()
# Populate the 'Read book' hybrid
custom_fields = {}
custom_fields['Tag'] = {'field':'tag', 'datatype':u'text'}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.read_source_field.addItem(cf)
self.read_source_fields = custom_fields
self.read_source_field.currentIndexChanged.connect(self.read_source_field_changed)
# Populate the 'Excluded books' hybrid
custom_fields = {}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Blank field first
self.exclude_source_field.addItem('')
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.exclude_source_field.addItem(cf)
self.exclude_source_fields = custom_fields
self.exclude_source_field.currentIndexChanged.connect(self.exclude_source_field_changed)
# Populate the 'Header note' combo box
custom_fields = {}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['bool','composite','datetime','enumeration','text']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Blank field first
self.header_note_source_field.addItem('')
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.header_note_source_field.addItem(cf)
self.header_note_source_fields = custom_fields
self.header_note_source_field.currentIndexChanged.connect(self.header_note_source_field_changed)
# Populate the 'Merge with Comments' combo box
custom_fields = {}
for custom_field in all_custom_fields:
field_md = self.db.metadata_for_field(custom_field)
if field_md['datatype'] in ['text','comments']:
custom_fields[field_md['name']] = {'field':custom_field,
'datatype':field_md['datatype']}
# Blank field first
self.merge_source_field.addItem('')
# Add the sorted eligible fields to the combo box
for cf in sorted(custom_fields):
self.merge_source_field.addItem(cf)
self.merge_source_fields = custom_fields
self.merge_source_field.currentIndexChanged.connect(self.merge_source_field_changed)
self.merge_before.setEnabled(False)
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
def read_source_field_changed(self,new_index): def read_source_field_changed(self,new_index):
''' '''
Process changes in the read_source_field combo box Process changes in the read_source_field combo box
Currently using QLineEdit for all field types Currently using QLineEdit for all field types
Possible to modify to switch QWidget type Possible to modify to switch QWidget type
''' '''
new_source = str(self.read_source_field_cb.currentText()) new_source = str(self.read_source_field.currentText())
read_source_spec = self.read_source_fields[str(new_source)] read_source_spec = self.read_source_fields[str(new_source)]
self.read_source_field = read_source_spec['field'] self.read_source_field_name = read_source_spec['field']
# Change pattern input widget to match the source field datatype # Change pattern input widget to match the source field datatype
if read_source_spec['datatype'] in ['bool','composite','datetime','text']: if read_source_spec['datatype'] in ['bool','composite','datetime','text']:
@ -152,3 +287,63 @@ class PluginWidget(QWidget,Ui_Form):
self.read_pattern = dw self.read_pattern = dw
self.read_spec_hl.addWidget(dw) self.read_spec_hl.addWidget(dw)
def exclude_source_field_changed(self,new_index):
'''
Process changes in the exclude_source_field combo box
Currently using QLineEdit for all field types
Possible to modify to switch QWidget type
'''
new_source = str(self.exclude_source_field.currentText())
self.exclude_source_field_name = new_source
if new_source > '':
exclude_source_spec = self.exclude_source_fields[str(new_source)]
self.exclude_source_field_name = exclude_source_spec['field']
self.exclude_pattern.setEnabled(True)
# Change pattern input widget to match the source field datatype
if exclude_source_spec['datatype'] in ['bool','composite','datetime','text']:
if not isinstance(self.exclude_pattern, QLineEdit):
self.exclude_spec_hl.removeWidget(self.exclude_pattern)
dw = QLineEdit(self)
dw.setObjectName('exclude_pattern')
dw.setToolTip('Exclusion pattern')
self.exclude_pattern = dw
self.exclude_spec_hl.addWidget(dw)
else:
self.exclude_pattern.setEnabled(False)
def header_note_source_field_changed(self,new_index):
'''
Process changes in the header_note_source_field combo box
'''
new_source = str(self.header_note_source_field.currentText())
self.header_note_source_field_name = new_source
if new_source > '':
header_note_source_spec = self.header_note_source_fields[str(new_source)]
self.header_note_source_field_name = header_note_source_spec['field']
def merge_source_field_changed(self,new_index):
'''
Process changes in the header_note_source_field combo box
'''
new_source = str(self.merge_source_field.currentText())
self.merge_source_field_name = new_source
if new_source > '':
merge_source_spec = self.merge_source_fields[str(new_source)]
self.merge_source_field_name = merge_source_spec['field']
if not self.merge_before.isChecked() and not self.merge_after.isChecked():
self.merge_after.setChecked(True)
self.merge_before.setEnabled(True)
self.merge_after.setEnabled(True)
self.include_hr.setEnabled(True)
else:
self.merge_before.setEnabled(False)
self.merge_after.setEnabled(False)
self.include_hr.setEnabled(False)
def thumb_width_changed(self,new_value):
'''
Process changes in the thumb_width spin box
'''
pass

View File

@ -6,150 +6,403 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>627</width> <width>650</width>
<height>549</height> <height>582</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item row="0" column="0"> <item>
<widget class="QLabel" name="label_2"> <widget class="QGroupBox" name="includedSections">
<property name="text"> <property name="sizePolicy">
<string>'Don't include this book' tag:</string> <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property> </property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="exclude_tags">
<property name="toolTip"> <property name="toolTip">
<string extracomment="Default: ~,Catalog"/> <string>Sections to include in catalog. All catalogs include 'Books by Author'.</string>
</property>
<property name="title">
<string>Included sections</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="generate_titles">
<property name="text">
<string>Books by &amp;Title</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="label_4"> <widget class="QCheckBox" name="generate_series">
<property name="text"> <property name="text">
<string>Additional note tag prefix:</string> <string>Books by &amp;Series</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="0" column="2">
<widget class="QLineEdit" name="note_tag"> <widget class="QCheckBox" name="generate_recently_added">
<property name="text">
<string>Recently &amp;Added</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="generate_genres">
<property name="text">
<string>Books by &amp;Genre</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QCheckBox" name="generate_descriptions">
<property name="text">
<string>&amp;Descriptions</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="excludedGenres">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string extracomment="Default: *"/> <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Default pattern &lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Courier New,courier';&quot;&gt;\[.+\]&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;excludes tags of the form [&lt;span style=&quot; font-family:'Courier New,courier';&quot;&gt;tag&lt;/span&gt;], &lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;e.g., [Project Gutenberg]&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="title">
<string>Excluded genres</string>
</property>
<layout class="QFormLayout" name="formLayout_3">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>-1</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Tags to &amp;exclude</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_genre</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item>
<widget class="QLineEdit" name="exclude_genre"> <widget class="QLineEdit" name="exclude_genre">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string extracomment="Default: \[[\w]*\]"/> <string extracomment="Default: \[[\w]*\]"/>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> </layout>
<widget class="QLabel" name="label"> </item>
<property name="text"> </layout>
<string>Regex pattern describing tags to exclude as genres:</string>
</property>
<property name="textFormat">
<enum>Qt::LogText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item row="7" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Regex tips:
- The default regex - \[.+\] - excludes genre tags of the form [tag], e.g., [Amazon Freebie]
- A regex pattern of a single dot excludes all genre tags, generating no Genre Section</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="10" column="0">
<widget class="QCheckBox" name="generate_titles">
<property name="text">
<string>Include 'Titles' Section</string>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QCheckBox" name="generate_recently_added">
<property name="text">
<string>Include 'Recently Added' Section</string>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QCheckBox" name="numbers_as_text">
<property name="text">
<string>Sort numbers as text</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QCheckBox" name="generate_series">
<property name="text">
<string>Include 'Series' Section</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="wishlist_tag"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Wishlist tag:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="read_spec_hl">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item> <item>
<widget class="QComboBox" name="read_source_field_cb"> <widget class="QGroupBox" name="excludedBooks">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Source column for read book</string> <string>Books matching either pattern will not be included in generated catalog. </string>
</property>
<property name="title">
<string>Excluded books</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>Tags to &amp;exclude</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_tags</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="exclude_tags">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Lucida Grande'; font-size:13pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Comma-separated list of tags to exclude.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:12pt;&quot;&gt;Default:&lt;/span&gt;&lt;span style=&quot; font-family:'Courier New,courier'; font-size:12pt;&quot;&gt; ~,Catalog&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="exclude_spec_hl">
<item>
<widget class="QLabel" name="label_7">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Column/value</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>exclude_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="exclude_source_field">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Column containing additional exclusion criteria</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>18</number>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="exclude_pattern">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Exclusion pattern</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="readBooks">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Matching books will be displayed with ✓</string>
</property>
<property name="title">
<string>Read books</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0" colspan="2">
<layout class="QHBoxLayout" name="read_spec_hl">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<widget class="QLabel" name="label_3">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Column/value</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>read_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="read_source_field">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Column containing 'read' status</string>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string/> <string/>
</property> </property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>18</number>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="read_pattern"> <widget class="QLineEdit" name="read_pattern">
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Pattern for read book</string> <string>'read book' pattern</string>
</property> </property>
<property name="statusTip"> <property name="statusTip">
<string/> <string/>
@ -158,11 +411,276 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="2" column="0"> </layout>
<widget class="QLabel" name="label_3"> </widget>
<property name="text"> </item>
<string>Books marked as read:</string> <item>
<widget class="QGroupBox" name="otherOptions">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property> </property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="title">
<string>Other options</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldsStayAtSizeHint</enum>
</property>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_5">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>&amp;Wishlist tag</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>wishlist_tag</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="wishlist_tag">
<property name="toolTip">
<string>Books tagged as Wishlist items will be displayed with ✕</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_4">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Thumbnail width</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>thumb_width</cstring>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="thumb_width">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Size hint for Description cover thumbnails</string>
</property>
<property name="suffix">
<string> inch</string>
</property>
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>2.000000000000000</double>
</property>
<property name="singleStep">
<double>0.100000000000000</double>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_6">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>&amp;Description note</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>header_note_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="header_note_source_field">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Custom column source for note to include in Description header area</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_9">
<property name="minimumSize">
<size>
<width>175</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Merge with Comments</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>merge_source_field</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="merge_source_field">
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Additional content merged with Comments during catalog generation</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="merge_before">
<property name="toolTip">
<string>Merge additional content before Comments</string>
</property>
<property name="text">
<string>&amp;Before</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="merge_after">
<property name="toolTip">
<string>Merge additional content after Comments</string>
</property>
<property name="text">
<string>&amp;After</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="include_hr">
<property name="toolTip">
<string>Separate Comments and additional content with horizontal rule</string>
</property>
<property name="text">
<string>&amp;Separator</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>579</width> <width>650</width>
<height>411</height> <height>575</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">

View File

@ -259,6 +259,19 @@ class EditorWidget(QWebView): # {{{
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def keyPressEvent(self, ev):
if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
ev.ignore()
else:
return QWebView.keyPressed(self, ev)
def keyReleaseEvent(self, ev):
if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
ev.ignore()
else:
return QWebView.keyReleased(self, ev)
# }}} # }}}
# Highlighter {{{ # Highlighter {{{
@ -480,6 +493,9 @@ class Editor(QWidget): # {{{
self.toolbar1 = QToolBar(self) self.toolbar1 = QToolBar(self)
self.toolbar2 = QToolBar(self) self.toolbar2 = QToolBar(self)
self.toolbar3 = QToolBar(self) self.toolbar3 = QToolBar(self)
for i in range(1, 4):
t = getattr(self, 'toolbar%d'%i)
t.setIconSize(QSize(18, 18))
self.editor = EditorWidget(self) self.editor = EditorWidget(self)
self.tabs = QTabWidget(self) self.tabs = QTabWidget(self)
self.tabs.setTabPosition(self.tabs.South) self.tabs.setTabPosition(self.tabs.South)

View File

@ -24,6 +24,7 @@ from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError from calibre.devices.errors import FreeSpaceError
from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.apple.driver import ITUNES_ASYNC
from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.folder_device.driver import FOLDER_DEVICE
from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi
from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
@ -635,6 +636,10 @@ class DeviceMixin(object): # {{{
if dir is not None: if dir is not None:
self.device_manager.mount_device(kls=FOLDER_DEVICE, kind='folder', path=dir) self.device_manager.mount_device(kls=FOLDER_DEVICE, kind='folder', path=dir)
def connect_to_bambook(self):
self.device_manager.mount_device(kls=BAMBOOKWifi, kind='bambook',
path=BAMBOOK.settings().extra_customization)
def connect_to_itunes(self): def connect_to_itunes(self):
self.device_manager.mount_device(kls=ITUNES_ASYNC, kind='itunes', path=None) self.device_manager.mount_device(kls=ITUNES_ASYNC, kind='itunes', path=None)

View File

@ -9,7 +9,7 @@ from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \
QDialog, QPixmap, QGraphicsScene, QIcon, QSize QDialog, QPixmap, QGraphicsScene, QIcon, QSize
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
from calibre.gui2 import dynamic, open_local_file from calibre.gui2 import dynamic, open_local_file, open_url
from calibre import fit_image from calibre import fit_image
from calibre.library.comments import comments_to_html from calibre.library.comments import comments_to_html
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -22,6 +22,8 @@ class BookInfo(QDialog, Ui_BookInfo):
self.setupUi(self) self.setupUi(self)
self.cover_pixmap = None self.cover_pixmap = None
self.comments.sizeHint = self.comments_size_hint self.comments.sizeHint = self.comments_size_hint
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
self.comments.linkClicked(self.link_clicked)
self.view_func = view_func self.view_func = view_func
@ -41,6 +43,8 @@ class BookInfo(QDialog, Ui_BookInfo):
screen_height = desktop.availableGeometry().height() - 100 screen_height = desktop.availableGeometry().height() - 100
self.resize(self.size().width(), screen_height) self.resize(self.size().width(), screen_height)
def link_clicked(self, url):
open_url(url)
def comments_size_hint(self): def comments_size_hint(self):
return QSize(350, 250) return QSize(350, 250)
@ -115,6 +119,7 @@ class BookInfo(QDialog, Ui_BookInfo):
lines = [x if x.strip() else '<br><br>' for x in lines] lines = [x if x.strip() else '<br><br>' for x in lines]
comments = '\n'.join(lines) comments = '\n'.join(lines)
self.comments.setHtml('<div>%s</div>' % comments) self.comments.setHtml('<div>%s</div>' % comments)
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
cdata = info.pop('cover', '') cdata = info.pop('cover', '')
self.cover_pixmap = QPixmap.fromImage(cdata) self.cover_pixmap = QPixmap.fromImage(cdata)
self.resize_cover() self.resize_cover()

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>611</width> <width>674</width>
<height>514</height> <height>660</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -33,6 +33,18 @@
</item> </item>
<item row="1" column="0" colspan="2"> <item row="1" column="0" colspan="2">
<widget class="QTabWidget" name="tabs"> <widget class="QTabWidget" name="tabs">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>650</width>
<height>575</height>
</size>
</property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>0</number>
</property> </property>

View File

@ -4,7 +4,7 @@ from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.tag_editor_ui import Ui_TagEditor from calibre.gui2.dialogs.tag_editor_ui import Ui_TagEditor
from calibre.gui2 import question_dialog, error_dialog from calibre.gui2 import question_dialog, error_dialog, gprefs
from calibre.constants import islinux from calibre.constants import islinux
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
@ -49,6 +49,10 @@ class TagEditor(QDialog, Ui_TagEditor):
self.connect(self.available_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags) self.connect(self.available_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags)
self.connect(self.applied_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags) self.connect(self.applied_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
geom = gprefs.get('tag_editor_geometry', None)
if geom is not None:
self.restoreGeometry(geom)
def delete_tags(self, item=None): def delete_tags(self, item=None):
confirms, deletes = [], [] confirms, deletes = [], []
@ -121,3 +125,15 @@ class TagEditor(QDialog, Ui_TagEditor):
self.applied_tags.addItem(tag) self.applied_tags.addItem(tag)
self.add_tag_input.setText('') self.add_tag_input.setText('')
def accept(self):
self.save_state()
return QDialog.accept(self)
def reject(self):
self.save_state()
return QDialog.reject(self)
def save_state(self):
gprefs['tag_editor_geometry'] = bytearray(self.saveGeometry())

View File

@ -19,7 +19,7 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
if text is not None: if text is not None:
self.textbox.setPlainText(text) self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True) self.textbox.setTabStopWidth(50)
self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK')) self.buttonBox.button(QDialogButtonBox.Ok).setText(_('&OK'))
self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel')) self.buttonBox.button(QDialogButtonBox.Cancel).setText(_('&Cancel'))

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>336</width> <width>500</width>
<height>235</height> <height>235</height>
</rect> </rect>
</property> </property>

View File

@ -22,6 +22,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.gui2 import config, Dispatcher, warning_dialog from calibre.gui2 import config, Dispatcher, warning_dialog
from calibre.utils.config import tweaks
class EmailJob(BaseJob): # {{{ class EmailJob(BaseJob): # {{{
@ -83,7 +84,7 @@ class Emailer(Thread): # {{{
rh = opts.relay_host rh = opts.relay_host
if rh and ( if rh and (
'gmail.com' in rh or 'live.com' in rh): 'gmail.com' in rh or 'live.com' in rh):
self.rate_limit = 301 self.rate_limit = tweaks['public_smtp_relay_delay']
def stop(self): def stop(self):
self._run = False self._run = False

View File

@ -5,6 +5,4 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from PyQt4.Qt import Qt DEFAULT_SORT = ('timestamp', False)
DEFAULT_SORT = ('timestamp', Qt.DescendingOrder)

View File

@ -247,9 +247,10 @@ class BooksModel(QAbstractTableModel): # {{{
if not self.db: if not self.db:
return return
self.about_to_be_sorted.emit(self.db.id) self.about_to_be_sorted.emit(self.db.id)
ascending = order == Qt.AscendingOrder if not isinstance(order, bool):
order = order == Qt.AscendingOrder
label = self.column_map[col] label = self.column_map[col]
self.db.sort(label, ascending) self.db.sort(label, order)
if reset: if reset:
self.reset() self.reset()
self.sorted_on = (label, order) self.sorted_on = (label, order)

View File

@ -165,7 +165,7 @@ class BooksView(QTableView): # {{{
partial(self.column_header_context_handler, partial(self.column_header_context_handler,
action='descending', column=col)) action='descending', column=col))
if self._model.sorted_on[0] == col: if self._model.sorted_on[0] == col:
ac = a if self._model.sorted_on[1] == Qt.AscendingOrder else d ac = a if self._model.sorted_on[1] else d
ac.setCheckable(True) ac.setCheckable(True)
ac.setChecked(True) ac.setChecked(True)
if col not in ('ondevice', 'rating', 'inlibrary') and \ if col not in ('ondevice', 'rating', 'inlibrary') and \
@ -282,9 +282,12 @@ class BooksView(QTableView): # {{{
def cleanup_sort_history(self, sort_history): def cleanup_sort_history(self, sort_history):
history = [] history = []
for col, order in sort_history: for col, order in sort_history:
if not isinstance(order, bool):
continue
if col == 'date': if col == 'date':
col = 'timestamp' col = 'timestamp'
if col in self.column_map and (not history or history[0][0] != col): if col in self.column_map:
if (not history or history[-1][0] != col):
history.append([col, order]) history.append([col, order])
return history return history
@ -292,7 +295,8 @@ class BooksView(QTableView): # {{{
if not saved_history: if not saved_history:
return return
for col, order in reversed(self.cleanup_sort_history(saved_history)[:3]): for col, order in reversed(self.cleanup_sort_history(saved_history)[:3]):
self.sortByColumn(self.column_map.index(col), order) self.sortByColumn(self.column_map.index(col),
Qt.AscendingOrder if order else Qt.DescendingOrder)
def apply_state(self, state): def apply_state(self, state):
h = self.column_header h = self.column_header

View File

@ -10,22 +10,25 @@ Browsing book collection by tags.
from itertools import izip from itertools import izip
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\
QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\
QPushButton, QWidget, QItemDelegate QPushButton, QWidget, QItemDelegate, QString, QLabel, \
QShortcut, QKeySequence, SIGNAL
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.library.field_metadata import TagsIcons, category_icon_map
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key, upper, lower, strcmp
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.formatter import eval_formatter
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
from calibre.gui2.widgets import HistoryLineEdit
class TagDelegate(QItemDelegate): # {{{ class TagDelegate(QItemDelegate): # {{{
@ -52,6 +55,8 @@ class TagDelegate(QItemDelegate): # {{{
painter.setClipRect(r) painter.setClipRect(r)
# Paint the text # Paint the text
if item.boxed:
painter.drawRoundedRect(r.adjusted(1,1,-1,-1), 5, 5)
r.setLeft(r.left()+r.height()+3) r.setLeft(r.left()+r.height()+3)
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
model.data(index, Qt.DisplayRole).toString()) model.data(index, Qt.DisplayRole).toString())
@ -322,21 +327,18 @@ class TagsView(QTreeView): # {{{
path = None path = None
except: #Database connection could be closed if an integrity check is happening except: #Database connection could be closed if an integrity check is happening
pass pass
if path: self._model.show_item_at_path(path)
idx = self.model().index_for_path(path)
if idx.isValid():
self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter)
# If the number of user categories changed, if custom columns have come or # If the number of user categories changed, if custom columns have come or
# gone, or if columns have been hidden or restored, we must rebuild the # gone, or if columns have been hidden or restored, we must rebuild the
# model. Reason: it is much easier than reconstructing the browser tree. # model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self): def set_new_model(self, filter_categories_by=None):
try: try:
self._model = TagsModel(self.db, parent=self, self._model = TagsModel(self.db, parent=self,
hidden_categories=self.hidden_categories, hidden_categories=self.hidden_categories,
search_restriction=self.search_restriction, search_restriction=self.search_restriction,
drag_drop_finished=self.drag_drop_finished) drag_drop_finished=self.drag_drop_finished,
filter_categories_by=filter_categories_by)
self.setModel(self._model) self.setModel(self._model)
except: except:
# The DB must be gone. Set the model to None and hope that someone # The DB must be gone. Set the model to None and hope that someone
@ -355,6 +357,7 @@ class TagTreeItem(object): # {{{
parent=None, tooltip=None, category_key=None): parent=None, tooltip=None, category_key=None):
self.parent = parent self.parent = parent
self.children = [] self.children = []
self.boxed = False
if self.parent is not None: if self.parent is not None:
self.parent.append(self) self.parent.append(self)
if data is None: if data is None:
@ -371,7 +374,13 @@ class TagTreeItem(object): # {{{
elif self.type == self.TAG: elif self.type == self.TAG:
icon_map[0] = data.icon icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
self.tooltip = tooltip if tooltip:
if tooltip.endswith(':'):
self.tooltip = tooltip + ' '
else:
self.tooltip = tooltip + ': '
else:
self.tooltip = ''
def __str__(self): def __str__(self):
if self.type == self.ROOT: if self.type == self.ROOT:
@ -400,7 +409,7 @@ class TagTreeItem(object): # {{{
def category_data(self, role): def category_data(self, role):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
return QVariant(self.py_name + ' [%d]'%len(self.children)) return QVariant(self.py_name + ' [%d]'%len(self.child_tags()))
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon return self.icon
if role == Qt.FontRole: if role == Qt.FontRole:
@ -433,20 +442,32 @@ class TagTreeItem(object): # {{{
return QVariant('(%s) %s'%(tag.name, tag.tooltip)) return QVariant('(%s) %s'%(tag.name, tag.tooltip))
else: else:
return QVariant(tag.name) return QVariant(tag.name)
if tag.tooltip is not None: if tag.tooltip:
return QVariant(tag.tooltip) return QVariant(self.tooltip + tag.tooltip)
else:
return QVariant(self.tooltip)
return NONE return NONE
def toggle(self): def toggle(self):
if self.type == self.TAG: if self.type == self.TAG:
self.tag.state = (self.tag.state + 1)%3 self.tag.state = (self.tag.state + 1)%3
def child_tags(self):
res = []
for t in self.children:
if t.type == TagTreeItem.CATEGORY:
for c in t.children:
res.append(c)
else:
res.append(t)
return res
# }}} # }}}
class TagsModel(QAbstractItemModel): # {{{ class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent, hidden_categories=None, def __init__(self, db, parent, hidden_categories=None,
search_restriction=None, drag_drop_finished=None): search_restriction=None, drag_drop_finished=None,
filter_categories_by=None):
QAbstractItemModel.__init__(self, parent) QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication # must do this here because 'QPixmap: Must construct a QApplication
@ -466,6 +487,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.hidden_categories = hidden_categories self.hidden_categories = hidden_categories
self.search_restriction = search_restriction self.search_restriction = search_restriction
self.row_map = [] self.row_map = []
self.filter_categories_by = filter_categories_by
# get_node_tree cannot return None here, because row_map is empty # get_node_tree cannot return None here, because row_map is empty
data = self.get_node_tree(config['sort_tags_by']) data = self.get_node_tree(config['sort_tags_by'])
@ -477,19 +499,11 @@ class TagsModel(QAbstractItemModel): # {{{
tt = _('The lookup/search name is "{0}"').format(r) tt = _('The lookup/search name is "{0}"').format(r)
else: else:
tt = '' tt = ''
c = TagTreeItem(parent=self.root_item, TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r) tooltip=tt, category_key=r)
# This duplicates code in refresh(). Having it here as well self.refresh(data=data)
# can save seconds during startup, because we avoid a second
# call to get_node_tree.
for tag in data[r]:
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user':
tag.avg_rating = None
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def mimeTypes(self): def mimeTypes(self):
return ["application/calibre+from_library"] return ["application/calibre+from_library"]
@ -641,6 +655,11 @@ class TagsModel(QAbstractItemModel): # {{{
else: else:
data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
if self.filter_categories_by:
for category in data.keys():
data[category] = [t for t in data[category]
if lower(t.name).find(self.filter_categories_by) >= 0]
tb_categories = self.db.field_metadata tb_categories = self.db.field_metadata
for category in tb_categories: for category in tb_categories:
if category in data: # The search category can come and go if category in data: # The search category can come and go
@ -652,34 +671,84 @@ class TagsModel(QAbstractItemModel): # {{{
return None return None
return data return data
def refresh(self): def refresh(self, data=None):
data = self.get_node_tree(config['sort_tags_by']) # get category data sort_by = config['sort_tags_by']
if data is None:
data = self.get_node_tree(sort_by) # get category data
if data is None: if data is None:
return False return False
row_index = -1 row_index = -1
collapse = tweaks['categories_collapse_more_than']
collapse_model = tweaks['categories_collapse_model']
if sort_by == 'name':
collapse_template = tweaks['categories_collapsed_name_template']
elif sort_by == 'rating':
collapse_model = 'partition'
collapse_template = tweaks['categories_collapsed_rating_template']
else:
collapse_model = 'partition'
collapse_template = tweaks['categories_collapsed_popularity_template']
collapse_letter = None
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
row_index += 1 row_index += 1
category = self.root_item.children[row_index] category = self.root_item.children[row_index]
names = [t.tag.name for t in category.children] names = []
states = [t.tag.state for t in category.children] states = []
children = category.child_tags()
states = [t.tag.state for t in children]
names = [t.tag.name for names in children]
state_map = dict(izip(names, states)) state_map = dict(izip(names, states))
category_index = self.index(row_index, 0, QModelIndex()) category_index = self.index(row_index, 0, QModelIndex())
category_node = category_index.internalPointer()
if len(category.children) > 0: if len(category.children) > 0:
self.beginRemoveRows(category_index, 0, self.beginRemoveRows(category_index, 0,
len(category.children)-1) len(category.children)-1)
category.children = [] category.children = []
self.endRemoveRows() self.endRemoveRows()
if len(data[r]) > 0: cat_len = len(data[r])
if cat_len <= 0:
continue
self.beginInsertRows(category_index, 0, len(data[r])-1) self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]: clear_rating = True if r not in self.categories_with_ratings and \
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \ not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user': not self.db.field_metadata[r]['kind'] == 'user' \
else False
for idx,tag in enumerate(data[r]):
if clear_rating:
tag.avg_rating = None tag.avg_rating = None
tag.state = state_map.get(tag.name, 0) tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
if collapse > 0 and cat_len > collapse:
if collapse_model == 'partition':
if (idx % collapse) == 0:
d = {'first': tag}
if cat_len > idx + collapse:
d['last'] = data[r][idx+collapse-1]
else:
d['last'] = data[r][cat_len-1]
name = eval_formatter.safe_format(collapse_template,
d, 'TAG_VIEW', None)
sub_cat = TagTreeItem(parent=category,
data = name, tooltip = None,
category_icon = category_node.icon,
category_key=category_node.category_key)
else:
if upper(tag.sort[0]) != collapse_letter:
collapse_letter = upper(tag.name[0])
sub_cat = TagTreeItem(parent=category,
data = collapse_letter,
category_icon = category_node.icon,
tooltip = None,
category_key=category_node.category_key)
t = TagTreeItem(parent=sub_cat, data=tag, tooltip=r,
icon_map=self.icon_state_map)
else:
t = TagTreeItem(parent=category, data=tag, tooltip=r,
icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()
return True return True
@ -737,11 +806,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.tags_view.tag_item_renamed.emit() self.tags_view.tag_item_renamed.emit()
item.tag.name = val item.tag.name = val
self.refresh() # Should work, because no categories can have disappeared self.refresh() # Should work, because no categories can have disappeared
if path: self.show_item_at_path(path)
idx = self.index_for_path(path)
if idx.isValid():
self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
return True return True
def headerData(self, *args): def headerData(self, *args):
@ -824,20 +889,28 @@ class TagsModel(QAbstractItemModel): # {{{
def reset_all_states(self, except_=None): def reset_all_states(self, except_=None):
update_list = [] update_list = []
for i in xrange(self.rowCount(QModelIndex())): def process_tag(tag_index, tag_item):
category_index = self.index(i, 0, QModelIndex())
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer()
tag = tag_item.tag tag = tag_item.tag
if tag is except_: if tag is except_:
self.dataChanged.emit(tag_index, tag_index) self.dataChanged.emit(tag_index, tag_index)
continue return
if tag.state != 0 or tag in update_list: if tag.state != 0 or tag in update_list:
tag.state = 0 tag.state = 0
update_list.append(tag) update_list.append(tag)
self.dataChanged.emit(tag_index, tag_index) self.dataChanged.emit(tag_index, tag_index)
def process_level(category_index):
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer()
if tag_item.type == TagTreeItem.CATEGORY:
process_level(tag_index)
else:
process_tag(tag_index, tag_item)
for i in xrange(self.rowCount(QModelIndex())):
process_level(self.index(i, 0, QModelIndex()))
def clear_state(self): def clear_state(self):
self.reset_all_states() self.reset_all_states()
@ -856,14 +929,16 @@ class TagsModel(QAbstractItemModel): # {{{
ans = [] ans = []
tags_seen = set() tags_seen = set()
row_index = -1 row_index = -1
for i, key in enumerate(self.row_map): for i, key in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories: if self.hidden_categories and self.categories[i] in self.hidden_categories:
continue continue
row_index += 1 row_index += 1
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category if key.endswith(':'):
# User category, so skip it. The tag will be marked in its real category
continue continue
category_item = self.root_item.children[row_index] category_item = self.root_item.children[row_index]
for tag_item in category_item.children: for tag_item in category_item.child_tags():
tag = tag_item.tag tag = tag_item.tag
if tag.state > 0: if tag.state > 0:
prefix = ' not ' if tag.state == 2 else '' prefix = ' not ' if tag.state == 2 else ''
@ -878,6 +953,102 @@ class TagsModel(QAbstractItemModel): # {{{
ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans return ans
def find_node(self, key, txt, start_path):
'''
Search for an item (a node) in the tags browser list that matches both
the key (exact case-insensitive match) and txt (contains case-
insensitive match). Returns the path to the node. Note that paths are to
a location (second item, fourth item, 25 item), not to a node. If
start_path is None, the search starts with the topmost node. If the tree
is changed subsequent to calling this method, the path can easily refer
to a different node or no node at all.
'''
if not txt:
return None
txt = lower(txt)
self.path_found = None
if start_path is None:
start_path = []
def process_tag(depth, tag_index, tag_item, start_path):
path = self.path_for_index(tag_index)
if depth < len(start_path) and path[depth] <= start_path[depth]:
return False
tag = tag_item.tag
if tag is None:
return False
if lower(tag.name).find(txt) >= 0:
self.path_found = path
return True
return False
def process_level(depth, category_index, start_path):
path = self.path_for_index(category_index)
if depth < len(start_path):
if path[depth] < start_path[depth]:
return False
if path[depth] > start_path[depth]:
start_path = path
if key and strcmp(category_index.internalPointer().category_key, key) != 0:
return False
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer()
if tag_item.type == TagTreeItem.CATEGORY:
if process_level(depth+1, tag_index, start_path):
return True
else:
if process_tag(depth+1, tag_index, tag_item, start_path):
return True
return False
for i in xrange(self.rowCount(QModelIndex())):
if process_level(0, self.index(i, 0, QModelIndex()), start_path):
break
return self.path_found
def show_item_at_path(self, path, box=False):
'''
Scroll the browser and open categories to show the item referenced by
path. If possible, the item is placed in the center. If box=True, a
box is drawn around the item.
'''
if path:
self.show_item_at_index(self.index_for_path(path), box)
def show_item_at_index(self, idx, box=False):
if idx.isValid():
self.tags_view.setCurrentIndex(idx)
self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter)
if box:
tag_item = idx.internalPointer()
tag_item.boxed = True
self.dataChanged.emit(idx, idx)
def clear_boxed(self):
'''
Clear all boxes around items.
'''
def process_tag(tag_index, tag_item):
if tag_item.boxed:
tag_item.boxed = False
self.dataChanged.emit(tag_index, tag_index)
def process_level(category_index):
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer()
if tag_item.type == TagTreeItem.CATEGORY:
process_level(tag_index)
else:
process_tag(tag_index, tag_item)
for i in xrange(self.rowCount(QModelIndex())):
process_level(self.index(i, 0, QModelIndex()))
def get_filter_categories_by(self):
return self.filter_categories_by
# }}} # }}}
class TagBrowserMixin(object): # {{{ class TagBrowserMixin(object): # {{{
@ -993,14 +1164,73 @@ class TagBrowserWidget(QWidget): # {{{
def __init__(self, parent): def __init__(self, parent):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
self.parent = parent
self._layout = QVBoxLayout() self._layout = QVBoxLayout()
self.setLayout(self._layout) self.setLayout(self._layout)
self._layout.setContentsMargins(0,0,0,0) self._layout.setContentsMargins(0,0,0,0)
# Set up the find box & button
search_layout = QHBoxLayout()
self._layout.addLayout(search_layout)
self.item_search = HistoryLineEdit(parent)
try:
self.item_search.lineEdit().setPlaceholderText(
_('Find item in tag browser'))
except:
pass # Using Qt < 4.7
self.item_search.setToolTip(_(
'Search for items. This is a "contains" search; items containing the\n'
'text anywhere in the name will be found. You can limit the search\n'
'to particular categories using syntax similar to search. For example,\n'
'tags:foo will find foo in any tag, but not in authors etc. Entering\n'
'*foo will filter all categories at once, showing only those items\n'
'containing the text "foo"'))
search_layout.addWidget(self.item_search)
# Not sure if the shortcut should be translatable ...
sc = QShortcut(QKeySequence(_('ALT+f')), parent)
sc.connect(sc, SIGNAL('activated()'), self.set_focus_to_find_box)
self.search_button = QPushButton()
self.search_button.setText(_('F&ind'))
self.search_button.setToolTip(_('Find the first/next matching item'))
self.search_button.setFixedWidth(40)
search_layout.addWidget(self.search_button)
self.expand_button = QPushButton()
self.expand_button.setText('-')
self.expand_button.setFixedWidth(20)
self.expand_button.setToolTip(_('Collapse all categories'))
search_layout.addWidget(self.expand_button)
self.current_find_position = None
self.search_button.clicked.connect(self.find)
self.item_search.initialize('tag_browser_search')
self.item_search.lineEdit().returnPressed.connect(self.do_find)
self.item_search.lineEdit().textEdited.connect(self.find_text_changed)
self.item_search.activated[QString].connect(self.do_find)
self.item_search.completer().setCaseSensitivity(Qt.CaseSensitive)
parent.tags_view = TagsView(parent) parent.tags_view = TagsView(parent)
self.tags_view = parent.tags_view self.tags_view = parent.tags_view
self.expand_button.clicked.connect(self.tags_view.collapseAll)
self._layout.addWidget(parent.tags_view) self._layout.addWidget(parent.tags_view)
# Now the floating 'not found' box
l = QLabel(self.tags_view)
self.not_found_label = l
l.setFrameStyle(QFrame.StyledPanel)
l.setAutoFillBackground(True)
l.setText('<p><b>'+_('No More Matches.</b><p> Click Find again to go to first match'))
l.setAlignment(Qt.AlignVCenter)
l.setWordWrap(True)
l.resize(l.sizeHint())
l.move(10,20)
l.setVisible(False)
self.not_found_label_timer = QTimer()
self.not_found_label_timer.setSingleShot(True)
self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event,
type=Qt.QueuedConnection)
parent.sort_by = QComboBox(parent) parent.sort_by = QComboBox(parent)
# Must be in the same order as db2.CATEGORY_SORTS # Must be in the same order as db2.CATEGORY_SORTS
for x in (_('Sort by name'), _('Sort by popularity'), for x in (_('Sort by name'), _('Sort by popularity'),
@ -1031,6 +1261,63 @@ class TagBrowserWidget(QWidget): # {{{
def set_pane_is_visible(self, to_what): def set_pane_is_visible(self, to_what):
self.tags_view.set_pane_is_visible(to_what) self.tags_view.set_pane_is_visible(to_what)
def find_text_changed(self, str):
self.current_find_position = None
def set_focus_to_find_box(self):
self.item_search.setFocus()
self.item_search.lineEdit().selectAll()
def do_find(self, str=None):
self.current_find_position = None
self.find()
def find(self):
model = self.tags_view.model()
model.clear_boxed()
txt = unicode(self.item_search.currentText()).strip()
if txt.startswith('*'):
self.tags_view.set_new_model(filter_categories_by=txt[1:])
self.current_find_position = None
return
if model.get_filter_categories_by():
self.tags_view.set_new_model(filter_categories_by=None)
self.current_find_position = None
model = self.tags_view.model()
if not txt:
return
self.item_search.lineEdit().blockSignals(True)
self.search_button.setFocus(True)
self.item_search.lineEdit().blockSignals(False)
colon = txt.find(':')
key = None
if colon > 0:
key = self.parent.library_view.model().db.\
field_metadata.search_term_to_field_key(txt[:colon])
txt = txt[colon+1:]
self.current_find_position = model.find_node(key, txt,
self.current_find_position)
if self.current_find_position:
model.show_item_at_path(self.current_find_position, box=True)
elif self.item_search.text():
self.not_found_label.setVisible(True)
if self.tags_view.verticalScrollBar().isVisible():
sbw = self.tags_view.verticalScrollBar().width()
else:
sbw = 0
width = self.width() - 8 - sbw
height = self.not_found_label.heightForWidth(width) + 20
self.not_found_label.resize(width, height)
self.not_found_label.move(4, 10)
self.not_found_label_timer.start(2000)
def not_found_label_timer_event(self):
self.not_found_label.setVisible(False)
# }}} # }}}

View File

@ -551,7 +551,11 @@ class HistoryLineEdit(QComboBox):
item = unicode(self.itemText(i)) item = unicode(self.itemText(i))
if item not in items: if item not in items:
items.append(item) items.append(item)
self.blockSignals(True)
self.clear()
self.addItems(items)
self.setEditText(ct)
self.blockSignals(False)
history.set(self.store_name, items) history.set(self.store_name, items)
def setText(self, t): def setText(self, t):

View File

@ -144,8 +144,10 @@ class SendEmail(QWidget, Ui_Form):
bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(d.accept) bb.accepted.connect(d.accept)
bb.rejected.connect(d.reject) bb.rejected.connect(d.reject)
d.tl = QLabel('<p>'+_('You can sign up for a free {name} email ' d.tl = QLabel(('<p>'+_('Setup sending email using') +
'account at <a href="http://{url}">http://{url}</a>. {extra}').format( ' <b>{name}</b><p>' +
_('If you don\'t have an account, you can sign up for a free {name} email '
'account at <a href="http://{url}">http://{url}</a>. {extra}')).format(
**service)) **service))
l.addWidget(d.tl, 0, 0, 3, 0) l.addWidget(d.tl, 0, 0, 3, 0)
d.tl.setWordWrap(True) d.tl.setWordWrap(True)

View File

@ -669,6 +669,9 @@ class ResultCache(SearchQueryParser): # {{{
fields = [('timestamp', False)] fields = [('timestamp', False)]
keyg = SortKeyGenerator(fields, self.field_metadata, self._data) keyg = SortKeyGenerator(fields, self.field_metadata, self._data)
# For efficiency, the key generator returns a plain value if only one
# field is in the sort field list. Because the normal cmp function will
# always assume asc, we must deal with asc/desc here.
if len(fields) == 1: if len(fields) == 1:
self._map.sort(key=keyg, reverse=not fields[0][1]) self._map.sort(key=keyg, reverse=not fields[0][1])
else: else:
@ -697,7 +700,7 @@ class SortKeyGenerator(object):
def __init__(self, fields, field_metadata, data): def __init__(self, fields, field_metadata, data):
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
self.field_metadata = field_metadata self.field_metadata = field_metadata
self.orders = [-1 if x[1] else 1 for x in fields] self.orders = [1 if x[1] else -1 for x in fields]
self.entries = [(x[0], field_metadata[x[0]]) for x in fields] self.entries = [(x[0], field_metadata[x[0]]) for x in fields]
self.library_order = tweaks['title_series_sorting'] == 'library_order' self.library_order = tweaks['title_series_sorting'] == 'library_order'
self.data = data self.data = data

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
''' '''
The database used to store ebook metadata The database used to store ebook metadata
''' '''
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json
from itertools import repeat from itertools import repeat
from math import ceil from math import ceil
from Queue import Queue from Queue import Queue
@ -32,7 +32,7 @@ from calibre.customize.ui import run_plugins_on_import
from calibre import isbytestring from calibre import isbytestring
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks, from_json, to_json
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import saved_searches, set_saved_searches from calibre.utils.search_query_parser import saved_searches, set_saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
@ -1243,7 +1243,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else: else:
icon = icon_map[':custom'] icon = icon_map[':custom']
icon_map[category] = icon icon_map[category] = icon
tooltip = self.custom_column_label_map[label]['name']
datatype = cat['datatype'] datatype = cat['datatype']
avgr = lambda x: 0.0 if x.rc == 0 else x.rt/x.rc avgr = lambda x: 0.0 if x.rc == 0 else x.rt/x.rc
@ -2700,6 +2699,38 @@ books_series_link feeds
return duplicates return duplicates
def add_custom_book_data(self, book_id, name, val):
x = self.conn.get('SELECT id FROM books WHERE ID=?', (book_id,), all=False)
if x is None:
raise ValueError('add_custom_book_data: no such book_id %d'%book_id)
# Do the json encode first, in case it throws an exception
s = json.dumps(val, default=to_json)
self.conn.execute('DELETE FROM books_plugin_data WHERE book=? AND name=?',
(book_id, name))
self.conn.execute('''INSERT INTO books_plugin_data(book, name, val)
VALUES(?, ?, ?)''', (book_id, name, s))
self.commit()
def get_custom_book_data(self, book_id, name, default=None):
try:
s = self.conn.get('''select val FROM books_plugin_data
WHERE book=? AND name=?''', (book_id, name), all=False)
if s is None:
return default
return json.loads(s, object_hook=from_json)
except:
pass
return default
def delete_custom_book_data(self, book_id, name):
self.conn.execute('DELETE FROM books_plugin_data WHERE book=? AND name=?',
(book_id, name))
self.commit()
def get_ids_for_custom_book_data(self, name):
s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,))
return [x[0] for x in s]
def get_custom_recipes(self): def get_custom_recipes(self):
for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'): for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'):
yield id, title, script yield id, title, script

View File

@ -441,3 +441,31 @@ class SchemaUpgrade(object):
WHERE id=NEW.id AND OLD.title <> NEW.title; WHERE id=NEW.id AND OLD.title <> NEW.title;
END; END;
''') ''')
def upgrade_version_17(self):
'custom book data table (for plugins)'
script = '''
DROP TABLE IF EXISTS books_plugin_data;
CREATE TABLE books_plugin_data(id INTEGER PRIMARY KEY,
book INTEGER NON NULL,
name TEXT NON NULL,
val TEXT NON NULL,
UNIQUE(book,name));
DROP TRIGGER IF EXISTS books_delete_trg;
CREATE TRIGGER books_delete_trg
AFTER DELETE ON books
BEGIN
DELETE FROM books_authors_link WHERE book=OLD.id;
DELETE FROM books_publishers_link WHERE book=OLD.id;
DELETE FROM books_ratings_link WHERE book=OLD.id;
DELETE FROM books_series_link WHERE book=OLD.id;
DELETE FROM books_tags_link WHERE book=OLD.id;
DELETE FROM data WHERE book=OLD.id;
DELETE FROM comments WHERE book=OLD.id;
DELETE FROM conversion_options WHERE book=OLD.id;
DELETE FROM books_plugin_data WHERE book=OLD.id;
END;
'''
self.conn.executescript(script)

View File

@ -21,6 +21,7 @@ Environment variables
----------------------- -----------------------
* ``CALIBRE_CONFIG_DIRECTORY`` - sets the directory where configuration files are stored/read. * ``CALIBRE_CONFIG_DIRECTORY`` - sets the directory where configuration files are stored/read.
* ``CALIBRE_TEMP_DIR`` - sets the temporary directory used by calibre
* ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking. * ``CALIBRE_OVERRIDE_DATABASE_PATH`` - allows you to specify the full path to metadata.db. Using this variable you can have metadata.db be in a location other than the library folder. Useful if your library folder is on a networked drive that does not support file locking.
* ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`. * ``CALIBRE_DEVELOP_FROM`` - Used to run from a calibre development environment. See :ref:`develop`.
* ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code) * ``CALIBRE_OVERRIDE_LANG`` - Used to force the language used by the interface (ISO 639 language code)

View File

@ -137,8 +137,8 @@ Note that you can use the prefix and suffix as well. If you want the number to a
{#myint:0>3s:ifempty(0)|[|]} {#myint:0>3s:ifempty(0)|[|]}
Using functions in templates - program mode Using functions in templates - template program mode
------------------------------------------- ----------------------------------------------------
The template language program mode differs from single-function mode in that it permits you to write template expressions that refer to other metadata fields, modify values, and do arithmetic. It is a reasonably complete programming language. The template language program mode differs from single-function mode in that it permits you to write template expressions that refer to other metadata fields, modify values, and do arithmetic. It is a reasonably complete programming language.
@ -161,10 +161,13 @@ The syntax of the language is shown by the following grammar::
constant ::= " string " | ' string ' | number constant ::= " string " | ' string ' | number
identifier ::= sequence of letters or ``_`` characters identifier ::= sequence of letters or ``_`` characters
function ::= identifier ( statement [ , statement ]* ) function ::= identifier ( statement [ , statement ]* )
expression ::= identifier | constant | function expression ::= identifier | constant | function | assignment
assignment ::= identifier '=' expression
statement ::= expression [ ; expression ]* statement ::= expression [ ; expression ]*
program ::= statement program ::= statement
Comments are lines with a '#' character at the beginning of the line.
An ``expression`` always has a value, either the value of the constant, the value contained in the identifier, or the value returned by a function. The value of a ``statement`` is the value of the last expression in the sequence of statements. As such, the value of the program (statement):: An ``expression`` always has a value, either the value of the constant, the value contained in the identifier, or the value returned by a function. The value of a ``statement`` is the value of the last expression in the sequence of statements. As such, the value of the program (statement)::
1; 2; 'foobar'; 3 1; 2; 'foobar'; 3
@ -208,13 +211,102 @@ The following functions are available in addition to those described in single-f
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
* ``field(name)`` -- returns the metadata field named by ``name``. * ``field(name)`` -- returns the metadata field named by ``name``.
* ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables.
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers. * ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
* ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value. * ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value.
Using general program mode
-----------------------------------
For more complicated template programs, it is sometimes easier to avoid template syntax (all the `{` and `}` characters), instead writing a more classical-looking program. You can do this in |app| by beginning the template with `program:`. In this case, no template processing is done. The special variable `$` is not set. It is up to your program to produce the correct results.
One advantage of `program:` mode is that the brackets are no longer special. For example, it is not necessary to use `[[` and `]]` when using the `template()` function.
The following example is a `program:` mode implementation of a recipe on the MobileRead forum: "Put series into the title, using either initials or a shortened form. Strip leading articles from the series name (any)." For example, for the book The Two Towers in the Lord of the Rings series, the recipe gives `LotR [02] The Two Towers`. Using standard templates, the recipe requires three custom columns and a plugboard, as explained in the following:
The solution requires creating three composite columns. The first column is used to remove the leading articles. The second is used to compute the 'shorten' form. The third is to compute the 'initials' form. Once you have these columns, the plugboard selects between them. You can hide any or all of the three columns on the library view.
First column:
Name: #stripped_series.
Template: {series:re(^(A|The|An)\s+,)||}
Second column (the shortened form):
Name: #shortened.
Template: {#stripped_series:shorten(4,-,4)}
Third column (the initials form):
Name: #initials.
Template: {#stripped_series:re(([^\s])[^\s]+(\s|$),\1)}
Plugboard expression:
Template:{#stripped_series:lookup(.\s,#initials,.,#shortened,series)}{series_index:0>2.0f| [|] }{title}
Destination field: title
This set of fields and plugboard produces:
Series: The Lord of the Rings
Series index: 2
Title: The Two Towers
Output: LotR [02] The Two Towers
Series: Dahak
Series index: 1
Title: Mutineers Moon
Output: Dahak [01] Mutineers Moon
Series: Berserkers
Series Index: 4
Title: Berserker Throne
Output: Bers-kers [04] Berserker Throne
Series: Meg Langslow Mysteries
Series Index: 3
Title: Revenge of the Wrought-Iron Flamingos
Output: MLM [03] Revenge of the Wrought-Iron Flamingos
The following program produces the same results as the original recipe, using only one custom column to hold the results of a program that computes the special title value::
Custom column:
Name: #special_title
Template: (the following with all leading spaces removed)
program:
# compute the equivalent of the composite fields and store them in local variables
stripped = re(field('series'), '^(A|The|An)\s+', '');
shortened = shorten(stripped, 4, '-' ,4);
initials = re(stripped, '[^\w]*(\w?)[^\s]+(\s|$)', '\1');
# Format the series index. Ends up as empty if there is no series index.
# Note that leading and trailing spaces will be removed by the formatter,
# so we cannot add them here. We will do that in the strcat below.
# Also note that because we are in 'program' mode, we can freely use
# curly brackets in strings, something we cannot do in template mode.
s_index = template('{series_index:0>2.0f}');
# print(stripped, shortened, initials, s_index);
# Now concatenate all the bits together. The switch picks between
# initials and shortened, depending on whether there is a space
# in stripped. We then add the brackets around s_index if it is
# not empty. Finally, add the title. As this is the last function in
# the program, its value will be returned.
strcat(
switch( stripped,
'.\s', initials,
'.', shortened,
field('series')),
test(s_index, strcat(' [', s_index, '] '), ''),
field('title'));
Plugboard expression:
Template:{#special_title}
Destination field: title
It would be possible to do the above with no custom columns by putting the program into the template box of the plugboard. However, to do so, all comments must be removed because the plugboard text box does not support multi-line editing. It is debatable whether the gain of not having the custom column is worth the vast increase in difficulty caused by the program being one giant line.
Special notes for save/send templates Special notes for save/send templates
------------------------------------- -------------------------------------

View File

@ -40,7 +40,7 @@ def base_dir():
_base_dir = td _base_dir = td
else: else:
_base_dir = tempfile.mkdtemp(prefix='%s_%s_tmp_'%(__appname__, _base_dir = tempfile.mkdtemp(prefix='%s_%s_tmp_'%(__appname__,
__version__)) __version__), dir=os.environ.get('CALIBRE_TEMP_DIR', None))
atexit.register(remove_dir, _base_dir) atexit.register(remove_dir, _base_dir)
return _base_dir return _base_dir

View File

@ -66,6 +66,10 @@ class _Parser(object):
template = template.replace('[[', '{').replace(']]', '}') template = template.replace('[[', '{').replace(']]', '}')
return eval_formatter.safe_format(template, self.variables, 'EVAL', None) return eval_formatter.safe_format(template, self.variables, 'EVAL', None)
def _print(self, *args):
print args
return None
local_functions = { local_functions = {
'add' : (2, partial(_math, op='+')), 'add' : (2, partial(_math, op='+')),
'assign' : (2, _assign), 'assign' : (2, _assign),
@ -74,6 +78,7 @@ class _Parser(object):
'eval' : (1, _eval), 'eval' : (1, _eval),
'field' : (1, lambda s, x: s.parent.get_value(x, [], s.parent.kwargs)), 'field' : (1, lambda s, x: s.parent.get_value(x, [], s.parent.kwargs)),
'multiply' : (2, partial(_math, op='*')), 'multiply' : (2, partial(_math, op='*')),
'print' : (-1, _print),
'strcat' : (-1, _concat), 'strcat' : (-1, _concat),
'strcmp' : (5, _strcmp), 'strcmp' : (5, _strcmp),
'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]), 'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]),
@ -143,12 +148,18 @@ class _Parser(object):
if not self.token_op_is_a(';'): if not self.token_op_is_a(';'):
return val return val
self.consume() self.consume()
if self.token_is_eof():
return val
def expr(self): def expr(self):
if self.token_is_id(): if self.token_is_id():
# We have an identifier. Determine if it is a function # We have an identifier. Determine if it is a function
id = self.token() id = self.token()
if not self.token_op_is_a('('): if not self.token_op_is_a('('):
if self.token_op_is_a('='):
# classic assignment statement
self.consume()
return self._assign(id, self.expr())
return self.variables.get(id, _('unknown id ') + id) return self.variables.get(id, _('unknown id ') + id)
# We have a function. # We have a function.
# Check if it is a known one. We do this here so error reporting is # Check if it is a known one. We do this here so error reporting is
@ -339,6 +350,7 @@ class TemplateFormatter(string.Formatter):
(r'\w+', lambda x,t: (2, t)), (r'\w+', lambda x,t: (2, t)),
(r'".*?((?<!\\)")', lambda x,t: (3, t[1:-1])), (r'".*?((?<!\\)")', lambda x,t: (3, t[1:-1])),
(r'\'.*?((?<!\\)\')', lambda x,t: (3, t[1:-1])), (r'\'.*?((?<!\\)\')', lambda x,t: (3, t[1:-1])),
(r'\n#.*?(?=\n)', None),
(r'\s', None) (r'\s', None)
]) ])
@ -359,6 +371,12 @@ class TemplateFormatter(string.Formatter):
raise Exception('get_value must be implemented in the subclass') raise Exception('get_value must be implemented in the subclass')
def format_field(self, val, fmt): def format_field(self, val, fmt):
# ensure we are dealing with a string.
if isinstance(val, (int, float)):
if val:
val = unicode(val)
else:
val = ''
# Handle conditional text # Handle conditional text
fmt, prefix, suffix = self._explode_format_string(fmt) fmt, prefix, suffix = self._explode_format_string(fmt)
@ -422,10 +440,10 @@ class TemplateFormatter(string.Formatter):
self.kwargs = kwargs self.kwargs = kwargs
self.book = book self.book = book
self.composite_values = {} self.composite_values = {}
try:
if fmt.startswith('program:'): if fmt.startswith('program:'):
ans = self._eval_program(None, fmt[8:]) ans = self._eval_program(None, fmt[8:])
else: else:
try:
ans = self.vformat(fmt, [], kwargs).strip() ans = self.vformat(fmt, [], kwargs).strip()
except Exception, e: except Exception, e:
if DEBUG: if DEBUG:

View File

@ -1087,7 +1087,9 @@ class ZipFile:
with open(targetpath, 'wb') as target: with open(targetpath, 'wb') as target:
shutil.copyfileobj(source, target) shutil.copyfileobj(source, target)
except: except:
targetpath = sanitize_file_name(targetpath) components = list(os.path.split(targetpath))
components[-1] = sanitize_file_name(components[-1])
targetpath = os.sep.join(components)
with open(targetpath, 'wb') as target: with open(targetpath, 'wb') as target:
shutil.copyfileobj(source, target) shutil.copyfileobj(source, target)
self.extract_mapping[member.filename] = targetpath self.extract_mapping[member.filename] = targetpath