This commit is contained in:
Sengian 2011-03-13 08:48:08 +01:00
commit e1c316cd51
126 changed files with 58962 additions and 45566 deletions

View File

@ -19,6 +19,117 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.49
date: 2011-03-11
new features:
- title: "News download: More flexible news downlaod scheduling. You can now schedule by days of the week, days of the month and an interval, which can be as small as an hour for news sources that change rapidly"
- title: "Improved support for dragging and dropping cover images directly from web browsers into calibre."
description: >
"You can drop the images onto the cover in calibre and it will be replaced. Tested on a number of OS/browser combinations, but I am sure there a still a few for which it wont work."
- title: "Add shortcuts of Alt+Left and Alt+Right for the next and previous buttons in the edit metadata dialog."
tickets: [9360]
- title: "When adding a GUI plugin, prompt the user for where the plugin should be displayed"
- title: "Conversion: When using the Level x Table of Contents options, support the case when the level 1,2,3 items are spread over multiple HTML files."
- title: "Support for the Optimus V"
- title: "FB2 Input: Support for tables"
tickets: [9302]
- title: "Display a checkmark/cross next to 'true' and 'false' items in custom columns. Controlled via Preferences->Add a custom column"
- title: "Catalog generation: Reuse cover from existing catalog, allows the use of a custom cover for catalogs"
- title: "When setting covers in calibre, resize to fit within a maximum size of (1200, 1600), to prevent slowdowns due to extra large covers. This size can be controlled via Preferences->Tweaks."
tickets: [9277]
bug fixes:
- title: "Fix long standing bug that caused errors when saving books to disk if the book metadata has certain chinese/russian characters on windows. The fix required some changes to how unicode paths are handled in calibre, so it might have broken something else. If so, please open a ticket."
tickets: [7250]
- title: "Custom recipes: Store custom recipes in the calibre config directory instead of the library database. This allows scheduling of custom recipes to work with multiple libraries. Note that you may have to re-schedule any existing custom recipes."
- title: "Restore the ability to do search and replace on ISBN. Use the 'identifiers' field with type isbn to do this"
- title: "Fix amazon metadata download plugin not working with ISBN-13 and social metadata not downloading if the supplied ISBN 10 is not for an edition available on Amazon"
- title: "Workaround for openlibrary blocking the user agent used by calibre, preventing cover downloads from that site"
- title: "FB2 Output: Add sequence to metadata. Fix bugs with author names. Fix bug where <empty-line/> elements were put inside <p> tags."
- title: "Conversion pipeline: If the input HTML document uses uppercase tag and attribute names, convert them to lowercase"
- title: "RTF Input: Fix space after unicode quote character being incorrectly removed"
tickets: [9343]
- title: "Fix regression that broke the ebook-device command line program in the previous release"
- title: "Fix custom columns with numbers not allowing entry of positive numbers of 64-bit machines"
tickets: [9283]
- title: "Fix regression that caused focus to be lost when editing metadata in the device view"
tickets: [9323]
- title: "CHM Input: If an input encoding is specified, use it rather than trying to detect the encoding of the text in the CHM file."
tickets: [9173]
- title: "Fix regression that caused the viewer to forget its window size and other attributes when launched from within calibre, after calibre is restarted."
tickets: [9326]
- title: "News download: Fix regression that caused the delay parameter in recipes to not actually delay downloads."
tickets: [9332]
- title: "Conversion pipeline: When converting the :first-letter pseudo CSS selector to a <span> follow W3C rules for handling leading punctuation characters."
tickets: [9319]
- title: "Fix regression that caused clicking saved searches in the Tag Browser to not work"
- title: "Comic Input: Fix conversion failing when output profile is set to Tablet Output"
- title: "Replace leading periods in all path components generated by calibre with underscores"
- title: "Search and replace preferences: Prevent very long strings from causing the wizard button to get pushed off the screen"
- title: "Content server: Fix regression that caused various metadata to be missing in the book details view."
ticckets: [8929]
- title: "Apple driver: Ignore invalid EPUBs when sending to iTunes"
improved recipes:
- golem.de
- gulli.de
- La Nacion
- Ming Pao
- evz.ro
- Kompiuterra
- NRC Handelsblad (EPUB)
- The Leduc - Wetaskiwin Pipestone Flyer
new recipes:
- title: "Various Romanian news sources"
author: Silviu Cotoara
- title: "Salt Lake City Tribune"
author: Charles Holbert
- title: "Bay Citizen and Oakland North"
author: noah
- title: "Nikkei Business and JB Press"
author: Ado Nishimura
- title: "El Pais Babelia"
author: oneillpt
- title: "Komchadluek"
author: ballsai
- version: 0.7.48 - version: 0.7.48
date: 2011-03-04 date: 2011-03-04

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
avantaje.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Avantaje(BasicNewsRecipe):
title = u'Avantaje'
__author__ = u'Silviu Cotoar\u0103'
description = u''
publisher = u'Avantaje'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Stiri'
encoding = 'utf-8'
cover_url = 'http://www.avantaje.ro/images/default/logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'id':'articol'})
, dict(name='div', attrs={'class':'gallery clearfix'})
, dict(name='div', attrs={'align':'justify'})
]
remove_tags = [
dict(name='div', attrs={'id':['color_sanatate_box']})
, dict(name='div', attrs={'class':['nav']})
, dict(name='div', attrs={'class':['voteaza_art']})
, dict(name='div', attrs={'class':['bookmark']})
, dict(name='div', attrs={'class':['links clearfix']})
, dict(name='div', attrs={'class':['title']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['title']})
]
feeds = [
(u'Feeds', u'http://feeds.feedburner.com/Avantaje')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,46 @@
from calibre.web.feeds.news import BasicNewsRecipe
class TheBayCitizen(BasicNewsRecipe):
title = 'The Bay Citizen'
language = 'en'
__author__ = 'noah'
description = 'The Bay Citizen'
publisher = 'The Bay Citizen'
INDEX = u'http://www.baycitizen.org'
category = 'news'
oldest_article = 2
max_articles_per_feed = 20
no_stylesheets = True
masthead_url = 'http://media.baycitizen.org/images/layout/logo1.png'
feeds = [('Main Feed', 'http://www.baycitizen.org/feeds/stories/')]
keep_only_tags = [dict(name='div', attrs={'class':'story'})]
remove_tags = [
dict(name='div', attrs={'class':'socialBar'}),
dict(name='div', attrs={'id':'text-resize'}),
dict(name='div', attrs={'class':'story relatedContent'}),
dict(name='div', attrs={'id':'comment_status_loading'}),
]
def append_page(self, soup, appendtag, position):
pager = soup.find('a',attrs={'class':'stry-next'})
if pager:
nexturl = self.INDEX + pager['href']
soup2 = self.index_to_soup(nexturl)
texttag = soup2.find('div', attrs={'class':'body'})
for it in texttag.findAll(style=True):
del it['style']
newpos = len(texttag.contents)
self.append_page(soup2,texttag,newpos)
texttag.extract()
appendtag.insert(position,texttag)
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
self.append_page(soup, soup.body, 3)
garbage = soup.findAll(id='story-pagination')
[trash.extract() for trash in garbage]
garbage = soup.findAll('em', 'cont-from-prev')
[trash.extract() for trash in garbage]
return soup

View File

@ -1,17 +1,83 @@
from calibre.web.feeds.news import BasicNewsRecipe #!/usr/bin/env python
class AdvancedUserRecipe1257093338(BasicNewsRecipe): from calibre.web.feeds.news import BasicNewsRecipe
class golem_ger(BasicNewsRecipe):
title = u'Golem.de' title = u'Golem.de'
language = 'de' language = 'de'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
language = 'de'
lang = 'de-DE'
no_stylesheets = True
encoding = 'iso-8859-1'
recursions = 1
match_regexps = [r'http://www.golem.de/.*.html']
feeds = [(u'Golem.de', u'http://rss.golem.de/rss.php?feed=ATOM1.0')] keep_only_tags = [
dict(name='h1', attrs={'class':'artikelhead'}),
dict(name='p', attrs={'class':'teaser'}),
dict(name='div', attrs={'class':'artikeltext'}),
dict(name='h2', attrs={'id':'artikelhead'}),
]
def print_version(self, url):
murxb = url.rfind('/') + 1
murxc = url[murxb :-5]
murxa = 'http://www.golem.de/' + 'print.php?a=' + murxc
return murxa
remove_tags = [
dict(name='div', attrs={'id':['similarContent','topContentWrapper','storycarousel','aboveFootPromo','comments','toolbar','breadcrumbs','commentlink','sidebar','rightColumn']}),
dict(name='div', attrs={'class':['gg_embeddedSubText','gg_embeddedIndex gg_solid','gg_toOldGallery','golemGallery']}),
dict(name='img', attrs={'class':['gg_embedded','gg_embeddedIconRight gg_embeddedIconFS gg_cursorpointer']}),
dict(name='td', attrs={'class':['xsmall']}),
]
# remove_tags_after = [
# dict(name='div', attrs={'id':['contentad2']})
# ]
feeds = [
(u'Golem.de', u'http://rss.golem.de/rss.php?feed=ATOM1.0'),
(u'Audio/Video', u'http://rss.golem.de/rss.php?tp=av&feed=RSS2.0'),
(u'Foto', u'http://rss.golem.de/rss.php?tp=foto&feed=RSS2.0'),
(u'Games', u'http://rss.golem.de/rss.php?tp=games&feed=RSS2.0'),
(u'Internet', u'http://rss.golem.de/rss.php?tp=inet&feed=RSS1.0'),
(u'Mobil', u'http://rss.golem.de/rss.php?tp=mc&feed=ATOM1.0'),
(u'Internet', u'http://rss.golem.de/rss.php?tp=inet&feed=RSS1.0'),
(u'Politik/Recht', u'http://rss.golem.de/rss.php?tp=pol&feed=ATOM1.0'),
(u'Desktop-Applikationen', u'http://rss.golem.de/rss.php?tp=apps&feed=RSS2.0'),
(u'Software-Entwicklung', u'http://rss.golem.de/rss.php?tp=dev&feed=RSS2.0'),
(u'Wirtschaft', u'http://rss.golem.de/rss.php?tp=wirtschaft&feed=RSS2.0'),
(u'Hardware', u'http://rss.golem.de/rss.php?r=hw&feed=RSS2.0'),
(u'Software', u'http://rss.golem.de/rss.php?r=sw&feed=RSS2.0'),
(u'Networld', u'http://rss.golem.de/rss.php?r=nw&feed=RSS2.0'),
(u'Entertainment', u'http://rss.golem.de/rss.php?r=et&feed=RSS2.0'),
(u'TK', u'http://rss.golem.de/rss.php?r=tk&feed=RSS2.0'),
(u'E-Commerce', u'http://rss.golem.de/rss.php?r=ec&feed=RSS2.0'),
(u'Unternehmen/Maerkte', u'http://rss.golem.de/rss.php?r=wi&feed=RSS2.0')
]
feeds = [
(u'Golem.de', u'http://rss.golem.de/rss.php?feed=ATOM1.0'),
(u'Mobil', u'http://rss.golem.de/rss.php?tp=mc&feed=feed=RSS2.0'),
(u'OSS', u'http://rss.golem.de/rss.php?tp=oss&feed=RSS2.0'),
(u'Politik/Recht', u'http://rss.golem.de/rss.php?tp=pol&feed=RSS2.0'),
(u'Desktop-Applikationen', u'http://rss.golem.de/rss.php?tp=apps&feed=RSS2.0'),
(u'Software-Entwicklung', u'http://rss.golem.de/rss.php?tp=dev&feed=RSS2.0'),
]
extra_css = '''
h1 {color:#0066CC;font-family:Arial,Helvetica,sans-serif; font-size:30px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:20px;margin-bottom:2 em;}
h2 {color:#4D4D4D;font-family:Arial,Helvetica,sans-serif; font-size:22px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:16px; }
h3 {color:#4D4D4D;font-family:Arial,Helvetica,sans-serif; font-size:x-small; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal; line-height:5px;}
h4 {color:#333333; font-family:Arial,Helvetica,sans-serif;font-size:13px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:13px; }
h5 {color:#333333; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:11px; text-transform:uppercase;}
.teaser {font-style:italic;font-size:12pt;margin-bottom:15pt;}
.xsmall{font-style:italic;font-size:x-small;}
.td{font-style:italic;font-size:x-small;}
img {align:left;}
'''

View File

@ -11,6 +11,26 @@ class AdvancedUserRecipe1259599587(BasicNewsRecipe):
feeds = [(u'gulli:news', u'http://ticker.gulli.com/rss/')] feeds = [(u'gulli:news', u'http://ticker.gulli.com/rss/')]
remove_tags = [{'class' : ['addthis_button', 'BreadCrumb']}, {'id' : ['plista0']}] remove_tags = [dict(name='div', attrs={'class':['FloatL','_forumBox']})]
keep_only_tags = [dict(name='div', attrs={'class':'inside'})] keep_only_tags = [dict(name='div', attrs={'id':['_contentLeft']})]
remove_tags_after = [dict(name='div', attrs={'class':['_bookmark']})]
extra_css = '''
h1 {color:#008852;font-family:Arial,Helvetica,sans-serif; font-size:25px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:22px; }
h2 {color:#4D4D4D;font-family:Arial,Helvetica,sans-serif; font-size:18px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:16px; }
h3 {color:#4D4D4D;font-family:Arial,Helvetica,sans-serif; font-size:15px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:14px;}
h4 {color:#333333; font-family:Arial,Helvetica,sans-serif;font-size:12px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:14px; }
h5 {color:#333333; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; line-height:14px; text-transform:uppercase;}
.newsdate {color:#333333;font-family:Arial,Helvetica,sans-serif;font-size:10px; font-size-adjust:none; font-stretch:normal; font-style:italic; font-variant:normal; font-weight:bold; line-height:10px; text-decoration:none;}
.articleInfo {color:#4D4D4D;font-family:Arial,Helvetica,sans-serif;font-size:10px; font-size-adjust:none; font-stretch:normal; font-style:bold; font-variant:normal; font-weight:bold; line-height:10px; text-decoration:none;}
.byline {color:#666;margin-bottom:0;font-size:12px}
.blockquote {color:#030303;font-style:italic;padding-left:15px;}
img {align:center;}
.li {list-style-type: none}
'''

View File

@ -1,23 +1,12 @@
__license__ = 'GPL v3'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.instapaper.com
'''
import urllib
from calibre import strftime from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Instapaper(BasicNewsRecipe): class AdvancedUserRecipe1299694372(BasicNewsRecipe):
title = 'Instapaper.com' title = u'Instapaper'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = '''Personalized news feeds. Go to instapaper.com to
setup up your news. Fill in your instapaper
username, and leave the password field
below blank.'''
publisher = 'Instapaper.com' publisher = 'Instapaper.com'
category = 'news, custom' category = 'info, custom, Instapaper'
oldest_article = 7 oldest_article = 365
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
@ -25,16 +14,9 @@ class Instapaper(BasicNewsRecipe):
INDEX = u'http://www.instapaper.com' INDEX = u'http://www.instapaper.com'
LOGIN = INDEX + u'/user/login' LOGIN = INDEX + u'/user/login'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
}
feeds = [
(u'Unread articles' , INDEX + u'/u' ) feeds = [(u'Instapaper Unread', u'http://www.instapaper.com/u'), (u'Instapaper Starred', u'http://www.instapaper.com/starred')]
,(u'Starred articles', INDEX + u'/starred')
]
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
@ -70,7 +52,3 @@ class Instapaper(BasicNewsRecipe):
}) })
totalfeeds.append((feedtitle, articles)) totalfeeds.append((feedtitle, articles))
return totalfeeds return totalfeeds
def print_version(self, url):
return self.INDEX + '/text?u=' + urllib.quote(url)

View File

@ -0,0 +1,42 @@
import urllib2
from calibre.web.feeds.news import BasicNewsRecipe
class JBPress(BasicNewsRecipe):
title = u'JBPress'
language = 'ja'
description = u'Japan Business Press New articles (using small print version)'
__author__ = 'Ado Nishimura'
needs_subscription = True
oldest_article = 7
max_articles_per_feed = 100
remove_tags_before = dict(id='wrapper')
no_stylesheets = True
feeds = [('JBPress new article', 'http://feed.ismedia.jp/rss/jbpress/all.rdf')]
def get_cover_url(self):
return 'http://www.jbpress.co.jp/common/images/v1/jpn/common/logo.gif'
def get_browser(self):
html = '''<form action="https://jbpress.ismedia.jp/auth/dologin/http://jbpress.ismedia.jp/articles/print/5549" method="post">
<input id="login" name="login" type="text"/>
<input id="password" name="password" type="password"/>
<input id="rememberme" name="rememberme" type="checkbox"/>
</form>
'''
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://jbpress.ismedia.jp/articles/print/5549')
response = br.response()
response.set_data(html)
br.set_response(response)
br.select_form(nr=0)
br["login"] = self.username
br['password'] = self.password
br.submit()
return br
def print_version(self, url):
url = urllib2.urlopen(url).geturl() # resolve redirect.
return url.replace('/-/', '/print/')

View File

@ -17,6 +17,7 @@ class Lanacion(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
no_stylesheets = True no_stylesheets = True
language = 'es_AR' language = 'es_AR'
delay = 14
publication_type = 'newspaper' publication_type = 'newspaper'
remove_empty_feeds = True remove_empty_feeds = True
masthead_url = 'http://www.lanacion.com.ar/_ui/desktop/imgs/layout/logos/ln341x47.gif' masthead_url = 'http://www.lanacion.com.ar/_ui/desktop/imgs/layout/logos/ln341x47.gif'
@ -25,7 +26,7 @@ class Lanacion(BasicNewsRecipe):
h2{color: #626262; font-weight: normal; font-size: 1.1em} h2{color: #626262; font-weight: normal; font-size: 1.1em}
body{font-family: Arial,sans-serif} body{font-family: Arial,sans-serif}
img{margin-top: 0.5em; margin-bottom: 0.2em; display: block} img{margin-top: 0.5em; margin-bottom: 0.2em; display: block}
.notaFecha{color: #808080} .notaFecha{color: #808080; font-size: small}
.notaEpigrafe{font-size: x-small} .notaEpigrafe{font-size: x-small}
.topNota h1{font-family: Arial,sans-serif} .topNota h1{font-family: Arial,sans-serif}
""" """
@ -38,7 +39,10 @@ class Lanacion(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
keep_only_tags = [dict(name='div', attrs={'id':'content'})] keep_only_tags = [
dict(name='div', attrs={'class':['topNota','itemHeader','nota','itemBody']})
,dict(name='div', attrs={'id':'content'})
]
remove_tags = [ remove_tags = [
dict(name='div' , attrs={'class':'notaComentario floatFix noprint' }) dict(name='div' , attrs={'class':'notaComentario floatFix noprint' })
@ -52,8 +56,7 @@ class Lanacion(BasicNewsRecipe):
remove_attributes = ['height','width','visible','onclick','data-count','name'] remove_attributes = ['height','width','visible','onclick','data-count','name']
feeds = [ feeds = [
(u'Ultimas Noticias' , u'http://servicios.lanacion.com.ar/herramientas/rss/origen=2' ) (u'Politica' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=30' )
,(u'Politica' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=30' )
,(u'Deportes' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=131' ) ,(u'Deportes' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=131' )
,(u'Economia' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=272' ) ,(u'Economia' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=272' )
,(u'Informacion General' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=21' ) ,(u'Informacion General' , u'http://servicios.lanacion.com.ar/herramientas/rss/categoria_id=21' )
@ -81,17 +84,12 @@ class Lanacion(BasicNewsRecipe):
] ]
def get_browser(self):
br = BasicNewsRecipe.get_browser()
br.set_debug_redirects(True)
br.set_debug_responses(True)
br.set_debug_http(True)
return br
def get_article_url(self, article): def get_article_url(self, article):
link = BasicNewsRecipe.get_article_url(self,article) link = BasicNewsRecipe.get_article_url(self,article)
if link.startswith('http://blogs.lanacion') and not link.endswith('/'): if link.startswith('http://blogs.lanacion') and not link.endswith('/'):
return None return self.browser.open_novisit(link).geturl()
if link.rfind('galeria=') > 0:
return None
return link return link
def preprocess_html(self, soup): def preprocess_html(self, soup):

View File

@ -0,0 +1,33 @@
EMAILADDRESS = 'hoge@foobar.co.jp'
from calibre.web.feeds.news import BasicNewsRecipe
class NBOnline(BasicNewsRecipe):
title = u'Nikkei Business Online'
language = 'ja'
description = u'Nikkei Business Online New articles. PLEASE NOTE: You need to edit EMAILADDRESS line of this "nbonline.recipe" file to set your e-mail address which is needed when login. (file is in "Calibre2/resources/recipes" directory.)'
__author__ = 'Ado Nishimura'
needs_subscription = True
oldest_article = 7
max_articles_per_feed = 100
remove_tags_before = dict(id='kanban')
remove_tags = [dict(name='div', id='footer')]
feeds = [('Nikkei Buisiness Online', 'http://business.nikkeibp.co.jp/rss/all_nbo.rdf')]
def get_cover_url(self):
return 'http://business.nikkeibp.co.jp/images/nbo/200804/parts/logo.gif'
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('https://signon.nikkeibp.co.jp/front/login/?ct=p&ts=nbo')
br.select_form(name='loginActionForm')
br['email'] = EMAILADDRESS
br['userId'] = self.username
br['password'] = self.password
br.submit()
return br
def print_version(self, url):
return url + '?ST=print'

View File

@ -0,0 +1,23 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class AdvancedUserRecipe1299640653(BasicNewsRecipe):
title = u'Oakland North'
oldest_article = 30
max_articles_per_feed = 100
language = 'en'
__author__ = 'noah'
description = 'Oakland North'
category = 'news'
no_stylesheets = True
masthead_url = 'http://oaklandnorth.net/wp-content/themes/oaklandnorth/images/masthead.png'
keep_only_tags = [dict(name='div', attrs={'class':re.compile(r'\bpost\b(?!-)', re.IGNORECASE)})]
remove_tags_after = [dict(name='p', attrs={'class':'post-postscript'})]
remove_tags = [dict(name='p', attrs={'class':'post-postscript'})]
feeds = [(u'All Headlines', u'http://oaklandnorth.net/feed/')]

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
onemagazine.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Onemagazine(BasicNewsRecipe):
title = u'The ONE'
__author__ = u'Silviu Cotoar\u0103'
description = u'Be the ONE, not anyone ..'
publisher = u'The ONE'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Femei'
encoding = 'utf-8'
cover_url = 'http://www.onemagazine.ro/images/logo_rss.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'class':'article'})
, dict(name='div', attrs={'class':'gallery clearfix'})
, dict(name='div', attrs={'align':'justify'})
]
remove_tags = [
dict(name='p', attrs={'class':['info']})
, dict(name='table', attrs={'class':['connect_widget_interactive_area']})
, dict(name='span', attrs={'class':['photo']})
, dict(name='div', attrs={'class':['counter']})
, dict(name='div', attrs={'class':['carousel']})
, dict(name='div', attrs={'class':['jcarousel-container jcarousel-container-horizontal']})
]
remove_tags_after = [
dict(name='table', attrs={'class':['connect_widget_interactive_area']})
]
feeds = [
(u'Feeds', u'http://www.onemagazine.ro/rss')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
pcworld.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Pcworld(BasicNewsRecipe):
title = u'PC World'
__author__ = u'Silviu Cotoar\u0103'
description = u'IT'
publisher = u'PC World'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,IT'
encoding = 'utf-8'
cover_url = 'http://www.pcworld.ro/img/ui/header-logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'id':'content_page'})
, dict(name='div', attrs={'class':'box_center content_body'})
]
remove_tags = [
dict(name='h3', attrs={'class':['breadcrumb']})
, dict(name='div', attrs={'class':['box_center voteaza']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['box_center voteaza']})
]
feeds = [
(u'Feeds', u'http://www.pcworld.ro/contents/pcworld.rss')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
protvmagazin.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Protvmagazin(BasicNewsRecipe):
title = u'ProTv Magazin'
__author__ = u'Silviu Cotoar\u0103'
description = u'Ghid TV'
publisher = u'ProTv Magazin'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,TV'
encoding = 'utf-8'
cover_url = 'http://www.protvmagazin.ro/images/logo.png'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'class':'box gradient'})
]
remove_tags = [
dict(name='p', attrs={'class':['title']})
, dict(name='div', attrs={'id':['online_only']})
, dict(name='div', attrs={'class':['show_article_rating']})
, dict(name='ul', attrs={'class':['breadcrumbs']})
, dict(name='p', attrs={'class':['tags']})
]
remove_tags_after = [
dict(name='table', attrs={'class':['connect_widget_interactive_area']})
, dict(name='p', attrs={'class':['tags']})
, dict(name='dev', attrs={'class':['connect_widget_sample_connections clearfix']})
]
feeds = [
(u'Feeds', u'http://www.protvmagazin.ro/rss/articole-noi')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
psychologies.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Psychologies(BasicNewsRecipe):
title = u'Psychologies'
__author__ = u'Silviu Cotoar\u0103'
description = u'Psihologie \u015fi Dezvoltare Personal\u0103..'
publisher = u'Psychologies'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Psihologie'
encoding = 'utf-8'
cover_url = 'http://www.psychologies.ro/images/default/logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'class':'nav'})
, dict(name='div', attrs={'id':'textarticol'})
]
feeds = [
(u'Feeds', u'http://feeds.feedburner.com/Psychologies')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
publika.md
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Publika(BasicNewsRecipe):
title = u'Publika'
__author__ = u'Silviu Cotoar\u0103'
description = u'\u015etiri din Moldova'
publisher = u'Publika'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Stiri,Moldova'
encoding = 'utf-8'
cover_url = 'http://assets.publika.md/images/logo.jpg'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'id':'colLeft'})
]
remove_tags = [
dict(name='div', attrs={'class':['articleInfo']})
, dict(name='div', attrs={'class':['articleRelated']})
, dict(name='div', attrs={'class':['roundedBox socialSharing']})
, dict(name='div', attrs={'class':['comment clearfix']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['roundedBox socialSharing']})
, dict(name='div', attrs={'class':['comment clearfix']})
]
feeds = [
(u'Feeds', u'http://rss.publika.md/stiri.xml')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,56 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1278347258(BasicNewsRecipe):
title = u'Salt Lake City Tribune'
__author__ = 'Charles Holbert'
oldest_article = 7
max_articles_per_feed = 100
description = '''Utah's independent news source since 1871'''
publisher = 'http://www.sltrib.com/'
category = 'news, Utah, SLC'
language = 'en'
encoding = 'utf-8'
#delay = 1
#simultaneous_downloads = 1
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
#masthead_url = 'http://www.sltrib.com/csp/cms/sites/sltrib/assets/images/logo_main.png'
#cover_url = 'http://webmedia.newseum.org/newseum-multimedia/dfp/jpg9/lg/UT_SLT.jpg'
keep_only_tags = [dict(name='div',attrs={'id':'imageBox'})
,dict(name='div',attrs={'class':'headline'})
,dict(name='div',attrs={'class':'byline'})
,dict(name='p',attrs={'class':'TEXT_w_Indent'})]
feeds = [(u'SL Tribune Today', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rss.csp?cat=All'),
(u'Utah News', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rss.csp?cat=UtahNews'),
(u'Business News', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rss.csp?cat=Money'),
(u'Technology', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rss.csp?cat=Technology'),
(u'Most Popular', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rsspopular.csp'),
(u'Sports', u'http://www.sltrib.com/csp/cms/sites/sltrib/RSS/rss.csp?cat=Sports')]
extra_css = '''
.headline{font-family:Arial,Helvetica,sans-serif; font-size:xx-large; font-weight: bold; color:#0E5398;}
.byline{font-family:Arial,Helvetica,sans-serif; color:#333333; font-size:xx-small;}
.storytext{font-family:Arial,Helvetica,sans-serif; font-size:medium;}
'''
def print_version(self, url):
seg = url.split('/')
x = seg[5].split('-')
baseURL = 'http://www.sltrib.com/csp/cms/sites/sltrib/pages/printerfriendly.csp?id='
s = baseURL + x[0]
return s
def get_cover_url(self):
cover_url = None
href = 'http://www.newseum.org/todaysfrontpages/hr.asp?fpVname=UT_SLT&ref_pge=lst'
soup = self.index_to_soup(href)
div = soup.find('div',attrs={'class':'tfpLrgView_container'})
if div:
cover_url = div.img['src']
return cover_url

View File

@ -3,6 +3,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1299054026(BasicNewsRecipe): class AdvancedUserRecipe1299054026(BasicNewsRecipe):
title = u'Thai Post Daily' title = u'Thai Post Daily'
__author__ = 'Chotechai P.' __author__ = 'Chotechai P.'
language = 'th'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
cover_url = 'http://upload.wikimedia.org/wikipedia/th/1/10/ThaiPost_Logo.png' cover_url = 'http://upload.wikimedia.org/wikipedia/th/1/10/ThaiPost_Logo.png'

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
tvmania.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Tvmania(BasicNewsRecipe):
title = u'TVmania'
__author__ = u'Silviu Cotoar\u0103'
description = u'Programe TV'
publisher = u'TVmania'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,TV'
encoding = 'utf-8'
cover_url = 'http://www.tvmania.ro/wp-content/themes/tvmania/images/logo.png'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'class':'articol'})
, dict(name='font', attrs={'class':'mic'})
, dict(name='div', attrs={'id':'header_recomandari'})
, dict(name='div', attrs={'class':'main-image'})
, dict(name='div', attrs={'id':'articol_recomandare'})
]
remove_tags = [
dict(name='div', attrs={'class':['iLikeThis']})
, dict(name='span', attrs={'class':['tag-links']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['iLikeThis']})
, dict(name='span', attrs={'class':['tag-links']})
]
feeds = [
(u'Feeds', u'http://www.tvmania.ro/feed')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
viva.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Viva(BasicNewsRecipe):
title = u'Viva'
__author__ = u'Silviu Cotoar\u0103'
description = u'Vedete si evenimente'
publisher = u'Viva'
oldest_article = 25
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Femei'
encoding = 'utf-8'
cover_url = 'http://www.viva.ro/images/default/viva.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
.byline {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
.date {font-family:Arial,Helvetica,sans-serif; font-size:xx-small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.copyright {font-family:Arial,Helvetica,sans-serif;font-size:xx-small;text-align:center}
.story{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.entry-asset asset hentry{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.pagebody{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.maincontentcontainer{font-family:Arial,Helvetica,sans-serif;font-size:small;}
.story-body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
keep_only_tags = [
dict(name='div', attrs={'class':'articol'})
, dict(name='div', attrs={'class':'gallery clearfix'})
, dict(name='div', attrs={'align':'justify'})
]
remove_tags = [
dict(name='div', attrs={'class':['breadcrumbs']})
, dict(name='div', attrs={'class':['links clearfix']})
, dict(name='a', attrs={'id':['img_arrow_right']})
, dict(name='img', attrs={'id':['zoom']})
, dict(name='div', attrs={'class':['foto_counter']})
, dict(name='div', attrs={'class':['gal_select clearfix']})
]
remove_tags_after = [
dict(name='div', attrs={'class':['links clearfix']})
]
feeds = [
(u'Vedete', u'http://feeds.feedburner.com/viva-Vedete')
,(u'Evenimente', u'http://feeds.feedburner.com/viva-Evenimente')
,(u'Frumusete', u'http://feeds.feedburner.com/viva-Beauty-Fashion')
,(u'Noutati', u'http://feeds.feedburner.com/viva-Noutati')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -16,6 +16,7 @@
"template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n", "template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n",
"print": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n print args\n return None\n", "print": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n print args\n return None\n",
"titlecase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return titlecase(val)\n", "titlecase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return titlecase(val)\n",
"subitems": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n items = [v.strip() for v in val.split(',')]\n rv = set()\n for item in items:\n component = item.split('.')\n try:\n if ei == 0:\n rv.add('.'.join(component[si:]))\n else:\n rv.add('.'.join(component[si:ei]))\n except:\n pass\n return ', '.join(sorted(rv, key=sort_key))\n",
"sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n", "sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n",
"test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n", "test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n",
"eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n", "eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n",

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
__appname__ = 'calibre' __appname__ = 'calibre'
__version__ = '0.7.48' __version__ = '0.7.49'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re

View File

@ -92,7 +92,7 @@ class TXT2TXTZ(FileTypePlugin):
'containing Markdown or Textile references to images. The referenced ' 'containing Markdown or Textile references to images. The referenced '
'images as well as the TXT file are added to the archive.') 'images as well as the TXT file are added to the archive.')
version = numeric_version version = numeric_version
file_types = set(['txt']) file_types = set(['txt', 'text'])
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
on_import = True on_import = True

View File

@ -35,7 +35,7 @@ class ANDROID(USBMS):
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100], 0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216], 0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
0x4286 : [0x216], 0x42b3 : [0x216] }, 0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216] },
# Sony Ericsson # Sony Ericsson
0xfce : { 0xd12e : [0x0100]}, 0xfce : { 0xd12e : [0x0100]},
@ -96,7 +96,8 @@ class ANDROID(USBMS):
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7'] 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7'] 'A70S', 'A101IT', '7']

View File

@ -224,7 +224,7 @@ class TREKSTOR(USBMS):
FORMATS = ['epub', 'txt', 'pdf'] FORMATS = ['epub', 'txt', 'pdf']
VENDOR_ID = [0x1e68] VENDOR_ID = [0x1e68]
PRODUCT_ID = [0x0041] PRODUCT_ID = [0x0041, 0x0042]
BCD = [0x0002] BCD = [0x0002]
EBOOK_DIR_MAIN = 'Ebooks' EBOOK_DIR_MAIN = 'Ebooks'

View File

@ -72,7 +72,7 @@ class FB2MLizer(object):
def clean_text(self, text): def clean_text(self, text):
# Condense empty paragraphs into a line break. # Condense empty paragraphs into a line break.
text = re.sub(r'(?miu)(<p>\s*</p>\s*){3,}', '<p><empty-line /></p>', text) text = re.sub(r'(?miu)(<p>\s*</p>\s*){3,}', '<empty-line />', text)
# Remove empty paragraphs. # Remove empty paragraphs.
text = re.sub(r'(?miu)<p>\s*</p>', '', text) text = re.sub(r'(?miu)<p>\s*</p>', '', text)
# Clean up pargraph endings. # Clean up pargraph endings.
@ -101,9 +101,6 @@ class FB2MLizer(object):
def fb2_header(self): def fb2_header(self):
metadata = {} metadata = {}
metadata['author_first'] = u''
metadata['author_middle'] = u''
metadata['author_last'] = u''
metadata['title'] = self.oeb_book.metadata.title[0].value metadata['title'] = self.oeb_book.metadata.title[0].value
metadata['appname'] = __appname__ metadata['appname'] = __appname__
metadata['version'] = __version__ metadata['version'] = __version__
@ -115,16 +112,36 @@ class FB2MLizer(object):
metadata['id'] = None metadata['id'] = None
metadata['cover'] = self.get_cover() metadata['cover'] = self.get_cover()
author_parts = self.oeb_book.metadata.creator[0].value.split(' ') metadata['author'] = u''
if len(author_parts) == 1: for auth in self.oeb_book.metadata.creator:
metadata['author_last'] = author_parts[0] author_first = u''
elif len(author_parts) == 2: author_middle = u''
metadata['author_first'] = author_parts[0] author_last = u''
metadata['author_last'] = author_parts[1] author_parts = auth.value.split(' ')
else: if len(author_parts) == 1:
metadata['author_first'] = author_parts[0] author_last = author_parts[0]
metadata['author_middle'] = ' '.join(author_parts[1:-2]) elif len(author_parts) == 2:
metadata['author_last'] = author_parts[-1] author_first = author_parts[0]
author_last = author_parts[1]
else:
author_first = author_parts[0]
author_middle = ' '.join(author_parts[1:-1])
author_last = author_parts[-1]
metadata['author'] += '<author>'
metadata['author'] += '<first-name>%s</first-name>' % prepare_string_for_xml(author_first)
if author_middle:
metadata['author'] += '<middle-name>%s</middle-name>' % prepare_string_for_xml(author_middle)
metadata['author'] += '<last-name>%s</last-name>' % prepare_string_for_xml(author_last)
metadata['author'] += '</author>'
if not metadata['author']:
metadata['author'] = u'<author><first-name></first-name><last-name><last-name></author>'
metadata['sequence'] = u''
if self.oeb_book.metadata.series:
index = '1'
if self.oeb_book.metadata.series_index:
index = self.oeb_book.metadata.series_index[0]
metadata['sequence'] = u'<sequence name="%s" number="%s" />' % (prepare_string_for_xml(u'%s' % self.oeb_book.metadata.series[0]), index)
identifiers = self.oeb_book.metadata['identifier'] identifiers = self.oeb_book.metadata['identifier']
for x in identifiers: for x in identifiers:
@ -136,28 +153,21 @@ class FB2MLizer(object):
metadata['id'] = str(uuid.uuid4()) metadata['id'] = str(uuid.uuid4())
for key, value in metadata.items(): for key, value in metadata.items():
if not key == 'cover': if key not in ('author', 'cover', 'sequence'):
metadata[key] = prepare_string_for_xml(value) metadata[key] = prepare_string_for_xml(value)
return u'<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">' \ return u'<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:xlink="http://www.w3.org/1999/xlink">' \
'<description>' \ '<description>' \
'<title-info>' \ '<title-info>' \
'<genre>antique</genre>' \ '<genre>antique</genre>' \
'<author>' \ '%(author)s' \
'<first-name>%(author_first)s</first-name>' \
'<middle-name>%(author_middle)s</middle-name>' \
'<last-name>%(author_last)s</last-name>' \
'</author>' \
'<book-title>%(title)s</book-title>' \ '<book-title>%(title)s</book-title>' \
'%(cover)s' \ '%(cover)s' \
'<lang>%(lang)s</lang>' \ '<lang>%(lang)s</lang>' \
'%(sequence)s' \
'</title-info>' \ '</title-info>' \
'<document-info>' \ '<document-info>' \
'<author>' \ '%(author)s' \
'<first-name></first-name>' \
'<middle-name></middle-name>' \
'<last-name></last-name>' \
'</author>' \
'<program-used>%(appname)s %(version)s</program-used>' \ '<program-used>%(appname)s %(version)s</program-used>' \
'<date>%(date)s</date>' \ '<date>%(date)s</date>' \
'<id>%(id)s</id>' \ '<id>%(id)s</id>' \

View File

@ -23,8 +23,9 @@ cover_url_cache = {}
cache_lock = RLock() cache_lock = RLock()
def find_asin(br, isbn): def find_asin(br, isbn):
q = 'http://www.amazon.com/s?field-keywords='+isbn q = 'http://www.amazon.com/s/?search-alias=aps&field-keywords='+isbn
raw = br.open_novisit(q).read() res = br.open_novisit(q)
raw = res.read()
raw = xml_to_unicode(raw, strip_encoding_pats=True, raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0] resolve_entities=True)[0]
root = html.fromstring(raw) root = html.fromstring(raw)
@ -151,6 +152,8 @@ def get_metadata(br, asin, mi):
root = soupparser.fromstring(raw) root = soupparser.fromstring(raw)
except: except:
return False return False
if root.xpath('//*[@id="errorMessage"]'):
return False
ratings = root.xpath('//form[@id="handleBuy"]/descendant::*[@class="asinReviewsSummary"]') ratings = root.xpath('//form[@id="handleBuy"]/descendant::*[@class="asinReviewsSummary"]')
if ratings: if ratings:
pat = re.compile(r'([0-9.]+) out of (\d+) stars') pat = re.compile(r'([0-9.]+) out of (\d+) stars')
@ -191,6 +194,7 @@ def main(args=sys.argv):
tdir = tempfile.gettempdir() tdir = tempfile.gettempdir()
br = browser() br = browser()
for title, isbn in [ for title, isbn in [
('The Heroes', '9780316044981'), # Test find_asin
('Learning Python', '8324616489'), # Test xisbn ('Learning Python', '8324616489'), # Test xisbn
('Angels & Demons', '9781416580829'), # Test sophisticated comment formatting ('Angels & Demons', '9781416580829'), # Test sophisticated comment formatting
# Random tests # Random tests
@ -207,8 +211,12 @@ def main(args=sys.argv):
#import time #import time
#st = time.time() #st = time.time()
print get_social_metadata(title, None, None, isbn) mi = get_social_metadata(title, None, None, isbn)
if not mi.comments:
print 'Failed to downlaod social metadata for', title
return 1
#print '\n\n', time.time() - st, '\n\n' #print '\n\n', time.time() - st, '\n\n'
print '\n'
return 0 return 0

View File

@ -92,6 +92,8 @@ class Metadata(object):
def is_null(self, field): def is_null(self, field):
null_val = NULL_VALUES.get(field, None) null_val = NULL_VALUES.get(field, None)
val = getattr(self, field, None) val = getattr(self, field, None)
if val is False or val in (0, 0.0):
return True
return not val or val == null_val return not val or val == null_val
def __getattribute__(self, field): def __getattribute__(self, field):
@ -169,10 +171,13 @@ class Metadata(object):
pass pass
return default return default
def get_extra(self, field): def get_extra(self, field, default=None):
_data = object.__getattribute__(self, '_data') _data = object.__getattribute__(self, '_data')
if field in _data['user_metadata'].iterkeys(): if field in _data['user_metadata'].iterkeys():
return _data['user_metadata'][field]['#extra#'] try:
return _data['user_metadata'][field]['#extra#']
except:
return default
raise AttributeError( raise AttributeError(
'Metadata object has no attribute named: '+ repr(field)) 'Metadata object has no attribute named: '+ repr(field))

View File

@ -74,6 +74,8 @@ class HeadRequest(mechanize.Request):
class OpenLibraryCovers(CoverDownload): # {{{ class OpenLibraryCovers(CoverDownload): # {{{
'Download covers from openlibrary.org' 'Download covers from openlibrary.org'
# See http://openlibrary.org/dev/docs/api/covers
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
name = 'openlibrary.org covers' name = 'openlibrary.org covers'
description = _('Download covers from openlibrary.org') description = _('Download covers from openlibrary.org')
@ -82,7 +84,8 @@ class OpenLibraryCovers(CoverDownload): # {{{
def has_cover(self, mi, ans, timeout=5.): def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn: if not mi.isbn:
return False return False
br = browser() from calibre.ebooks.metadata.library_thing import get_browser
br = get_browser()
br.set_handle_redirect(False) br.set_handle_redirect(False)
try: try:
br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout) br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout)
@ -98,7 +101,8 @@ class OpenLibraryCovers(CoverDownload): # {{{
def get_covers(self, mi, result_queue, abort, timeout=5.): def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn: if not mi.isbn:
return return
br = browser() from calibre.ebooks.metadata.library_thing import get_browser
br = get_browser()
try: try:
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read() ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
result_queue.put((True, ans, 'jpg', self.name)) result_queue.put((True, ans, 'jpg', self.name))
@ -137,6 +141,8 @@ class AmazonCovers(CoverDownload): # {{{
br = browser() br = browser()
try: try:
url = get_cover_url(mi.isbn, br) url = get_cover_url(mi.isbn, br)
if url is None:
raise ValueError('No cover found for ISBN: %s'%mi.isbn)
cover_data = br.open_novisit(url).read() cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name)) result_queue.put((True, cover_data, 'jpg', self.name))
except Exception, e: except Exception, e:

View File

@ -908,6 +908,19 @@ class Manifest(object):
pass pass
data = first_pass(data) data = first_pass(data)
if data.tag == 'HTML':
# Lower case all tag and attribute names
data.tag = data.tag.lower()
for x in data.iterdescendants():
try:
x.tag = x.tag.lower()
for key, val in list(x.attrib.iteritems()):
del x.attrib[key]
key = key.lower()
x.attrib[key] = val
except:
pass
# Handle weird (non-HTML/fragment) files # Handle weird (non-HTML/fragment) files
if barename(data.tag) != 'html': if barename(data.tag) != 'html':
self.oeb.log.warn('File %r does not appear to be (X)HTML'%self.href) self.oeb.log.warn('File %r does not appear to be (X)HTML'%self.href)

View File

@ -65,7 +65,6 @@ class TXTInput(InputFormatPlugin):
txt = '' txt = ''
log.debug('Reading text from file...') log.debug('Reading text from file...')
length = 0 length = 0
# [(u'path', mime),]
# Extract content from zip archive. # Extract content from zip archive.
if file_ext == 'txtz': if file_ext == 'txtz':
@ -73,7 +72,7 @@ class TXTInput(InputFormatPlugin):
zf.extractall('.') zf.extractall('.')
for x in walk('.'): for x in walk('.'):
if os.path.splitext(x)[1].lower() == '.txt': if os.path.splitext(x)[1].lower() in ('.txt', '.text'):
with open(x, 'rb') as tf: with open(x, 'rb') as tf:
txt += tf.read() + '\n\n' txt += tf.read() + '\n\n'
else: else:

View File

@ -340,6 +340,7 @@ class FileIconProvider(QFileIconProvider):
'rar' : 'rar', 'rar' : 'rar',
'zip' : 'zip', 'zip' : 'zip',
'txt' : 'txt', 'txt' : 'txt',
'text' : 'txt',
'prc' : 'mobi', 'prc' : 'mobi',
'azw' : 'mobi', 'azw' : 'mobi',
'mobi' : 'mobi', 'mobi' : 'mobi',

View File

@ -11,7 +11,6 @@ from PyQt4.Qt import QWizard, QWizardPage, QIcon, QPixmap, Qt, QThread, \
pyqtSignal pyqtSignal
from calibre.gui2 import error_dialog, choose_dir, gprefs from calibre.gui2 import error_dialog, choose_dir, gprefs
from calibre.constants import filesystem_encoding
from calibre.library.add_to_library import find_folders_under, \ from calibre.library.add_to_library import find_folders_under, \
find_books_in_folder, hash_merge_format_collections find_books_in_folder, hash_merge_format_collections
@ -122,20 +121,19 @@ class WelcomePage(WizardPage, WelcomeWidget):
x = unicode(self.opt_root_folder.text()).strip() x = unicode(self.opt_root_folder.text()).strip()
if not x: if not x:
return None return None
return os.path.abspath(x.encode(filesystem_encoding)) return os.path.abspath(x)
def get_one_per_folder(self): def get_one_per_folder(self):
return self.opt_one_per_folder.isChecked() return self.opt_one_per_folder.isChecked()
def validatePage(self): def validatePage(self):
x = self.get_root_folder() x = self.get_root_folder()
xu = x.decode(filesystem_encoding)
if x and os.access(x, os.R_OK) and os.path.isdir(x): if x and os.access(x, os.R_OK) and os.path.isdir(x):
gprefs['add wizard root folder'] = xu gprefs['add wizard root folder'] = x
gprefs['add wizard one per folder'] = self.get_one_per_folder() gprefs['add wizard one per folder'] = self.get_one_per_folder()
return True return True
error_dialog(self, _('Invalid root folder'), error_dialog(self, _('Invalid root folder'),
xu + _('is not a valid root folder'), show=True) x + _('is not a valid root folder'), show=True)
return False return False
# }}} # }}}

View File

@ -6,6 +6,8 @@ __docformat__ = 'restructuredtext en'
import re import re
from PyQt4.Qt import QLineEdit, QTextEdit
from calibre.gui2.convert.search_and_replace_ui import Ui_Form from calibre.gui2.convert.search_and_replace_ui import Ui_Form
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
@ -72,3 +74,13 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
_('Invalid regular expression: %s')%err, show=True) _('Invalid regular expression: %s')%err, show=True)
return False return False
return True return True
def get_vaule(self, g):
if isinstance(g, (QLineEdit, QTextEdit)):
func = getattr(g, 'toPlainText', getattr(g, 'text', None))()
ans = unicode(func)
if not ans:
ans = None
return ans
else:
return Widget.get_value(self, g)

View File

@ -1013,11 +1013,13 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
query['search_field'] = unicode(self.search_field.currentText()) query['search_field'] = unicode(self.search_field.currentText())
query['search_mode'] = unicode(self.search_mode.currentText()) query['search_mode'] = unicode(self.search_mode.currentText())
query['s_r_template'] = unicode(self.s_r_template.text()) query['s_r_template'] = unicode(self.s_r_template.text())
query['s_r_src_ident'] = unicode(self.s_r_src_ident.currentText())
query['search_for'] = unicode(self.search_for.text()) query['search_for'] = unicode(self.search_for.text())
query['case_sensitive'] = self.case_sensitive.isChecked() query['case_sensitive'] = self.case_sensitive.isChecked()
query['replace_with'] = unicode(self.replace_with.text()) query['replace_with'] = unicode(self.replace_with.text())
query['replace_func'] = unicode(self.replace_func.currentText()) query['replace_func'] = unicode(self.replace_func.currentText())
query['destination_field'] = unicode(self.destination_field.currentText()) query['destination_field'] = unicode(self.destination_field.currentText())
query['s_r_dst_ident'] = unicode(self.s_r_dst_ident.text())
query['replace_mode'] = unicode(self.replace_mode.currentText()) query['replace_mode'] = unicode(self.replace_mode.currentText())
query['comma_separated'] = self.comma_separated.isChecked() query['comma_separated'] = self.comma_separated.isChecked()
query['results_count'] = self.results_count.value() query['results_count'] = self.results_count.value()
@ -1044,37 +1046,61 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
self.s_r_reset_query_fields() self.s_r_reset_query_fields()
return return
def set_index(attr, txt): def set_text(attr, key):
try: try:
attr.setCurrentIndex(attr.findText(txt)) attr.setText(item[key])
except:
pass
def set_checked(attr, key):
try:
attr.setChecked(item[key])
except:
attr.setChecked(False)
def set_value(attr, key):
try:
attr.setValue(int(item[key]))
except:
attr.setValue(0)
def set_index(attr, key):
try:
attr.setCurrentIndex(attr.findText(item[key]))
except: except:
attr.setCurrentIndex(0) attr.setCurrentIndex(0)
set_index(self.search_mode, item['search_mode']) set_index(self.search_mode, 'search_mode')
set_index(self.search_field, item['search_field']) set_index(self.search_field, 'search_field')
self.s_r_template.setText(item['s_r_template']) set_text(self.s_r_template, 's_r_template')
self.s_r_template_changed() #simulate gain/loss of focus self.s_r_template_changed() #simulate gain/loss of focus
self.search_for.setText(item['search_for'])
self.case_sensitive.setChecked(item['case_sensitive']) set_index(self.s_r_src_ident, 's_r_src_ident');
self.replace_with.setText(item['replace_with']) set_text(self.s_r_dst_ident, 's_r_dst_ident')
set_index(self.replace_func, item['replace_func']) set_text(self.search_for, 'search_for')
set_index(self.destination_field, item['destination_field']) set_checked(self.case_sensitive, 'case_sensitive')
set_index(self.replace_mode, item['replace_mode']) set_text(self.replace_with, 'replace_with')
self.comma_separated.setChecked(item['comma_separated']) set_index(self.replace_func, 'replace_func')
self.results_count.setValue(int(item['results_count'])) set_index(self.destination_field, 'destination_field')
self.starting_from.setValue(int(item['starting_from'])) set_index(self.replace_mode, 'replace_mode')
self.multiple_separator.setText(item['multiple_separator']) set_checked(self.comma_separated, 'comma_separated')
set_value(self.results_count, 'results_count')
set_value(self.starting_from, 'starting_from')
set_text(self.multiple_separator, 'multiple_separator')
def s_r_reset_query_fields(self): def s_r_reset_query_fields(self):
# Don't reset the search mode. The user will probably want to use it # Don't reset the search mode. The user will probably want to use it
# as it was # as it was
self.search_field.setCurrentIndex(0) self.search_field.setCurrentIndex(0)
self.s_r_src_ident.setCurrentIndex(0)
self.s_r_template.setText("") self.s_r_template.setText("")
self.search_for.setText("") self.search_for.setText("")
self.case_sensitive.setChecked(False) self.case_sensitive.setChecked(False)
self.replace_with.setText("") self.replace_with.setText("")
self.replace_func.setCurrentIndex(0) self.replace_func.setCurrentIndex(0)
self.destination_field.setCurrentIndex(0) self.destination_field.setCurrentIndex(0)
self.s_r_dst_ident.setText('')
self.replace_mode.setCurrentIndex(0) self.replace_mode.setCurrentIndex(0)
self.comma_separated.setChecked(True) self.comma_separated.setChecked(True)
self.results_count.setValue(999) self.results_count.setValue(999)

View File

@ -12,7 +12,7 @@ from threading import Thread
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \ from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \ QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \
QPushButton QPushButton, QKeySequence
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \ from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
choose_files, choose_images, ResizableDialog, \ choose_files, choose_images, ResizableDialog, \
@ -472,17 +472,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
self) self)
self.button_box.addButton(self.prev_button, self.button_box.ActionRole) self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
tip = _('Save changes and edit the metadata of %s')%prev tip = (_('Save changes and edit the metadata of %s')+' [Alt+Left]')%prev
self.prev_button.setToolTip(tip) self.prev_button.setToolTip(tip)
self.prev_button.clicked.connect(partial(self.next_triggered, self.prev_button.clicked.connect(partial(self.next_triggered,
-1)) -1))
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
if next_: if next_:
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
self) self)
self.button_box.addButton(self.next_button, self.button_box.ActionRole) self.button_box.addButton(self.next_button, self.button_box.ActionRole)
tip = _('Save changes and edit the metadata of %s')%next_ tip = (_('Save changes and edit the metadata of %s')+' [Alt+Right]')%next_
self.next_button.setToolTip(tip) self.next_button.setToolTip(tip)
self.next_button.clicked.connect(partial(self.next_triggered, 1)) self.next_button.clicked.connect(partial(self.next_triggered, 1))
self.next_button.setShortcut(QKeySequence('Alt+Right'))
self.splitter.setStretchFactor(100, 1) self.splitter.setStretchFactor(100, 1)
self.read_state() self.read_state()

View File

@ -11,7 +11,7 @@ from functools import partial
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
QSizePolicy, QPalette, QFrame, QSize QSizePolicy, QPalette, QFrame, QSize, QKeySequence
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs from calibre.gui2 import ResizableDialog, error_dialog, gprefs
@ -45,9 +45,12 @@ class MetadataSingleDialogBase(ResizableDialog):
self.button_box.rejected.connect(self.reject) self.button_box.rejected.connect(self.reject)
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
self) self)
self.next_button.setShortcut(QKeySequence('Alt+Right'))
self.next_button.clicked.connect(partial(self.do_one, delta=1)) self.next_button.clicked.connect(partial(self.do_one, delta=1))
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
self) self)
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
self.button_box.addButton(self.prev_button, self.button_box.ActionRole) self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
self.button_box.addButton(self.next_button, self.button_box.ActionRole) self.button_box.addButton(self.next_button, self.button_box.ActionRole)
self.prev_button.clicked.connect(partial(self.do_one, delta=-1)) self.prev_button.clicked.connect(partial(self.do_one, delta=-1))
@ -355,11 +358,13 @@ class MetadataSingleDialogBase(ResizableDialog):
next_ = self.db.title(self.row_list[self.current_row+1]) next_ = self.db.title(self.row_list[self.current_row+1])
if next_ is not None: if next_ is not None:
tip = _('Save changes and edit the metadata of %s')%next_ tip = (_('Save changes and edit the metadata of %s')+
' [Alt+Right]')%next_
self.next_button.setToolTip(tip) self.next_button.setToolTip(tip)
self.next_button.setVisible(next_ is not None) self.next_button.setVisible(next_ is not None)
if prev is not None: if prev is not None:
tip = _('Save changes and edit the metadata of %s')%prev tip = (_('Save changes and edit the metadata of %s')+
' [Alt+Left]')%prev
self.prev_button.setToolTip(tip) self.prev_button.setToolTip(tip)
self.prev_button.setVisible(prev is not None) self.prev_button.setVisible(prev is not None)
self(self.db.id(self.row_list[self.current_row])) self(self.db.id(self.row_list[self.current_row]))

View File

@ -169,6 +169,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
col = unicode(self.column_name_box.text()).strip() col = unicode(self.column_name_box.text()).strip()
if not col: if not col:
return self.simple_error('', _('No lookup name was provided')) return self.simple_error('', _('No lookup name was provided'))
if col.startswith('#'):
col = col[1:]
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col: if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only ' return self.simple_error('', _('The lookup name must contain only '
'lower case letters, digits and underscores, and start with a letter')) 'lower case letters, digits and underscores, and start with a letter'))

View File

@ -55,6 +55,10 @@ class BaseModel(QAbstractListModel):
text = _('Choose library') text = _('Choose library')
return QVariant(text) return QVariant(text)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
if hasattr(self._data[row], 'qaction'):
icon = self._data[row].qaction.icon()
if not icon.isNull():
return QVariant(icon)
ic = action[1] ic = action[1]
if ic is None: if ic is None:
ic = 'blank.png' ic = 'blank.png'

View File

@ -92,7 +92,8 @@ class SendEmail(QWidget, Ui_Form):
pa = self.preferred_to_address() pa = self.preferred_to_address()
to_set = pa is not None to_set = pa is not None
if self.set_email_settings(to_set): if self.set_email_settings(to_set):
if question_dialog(self, _('OK to proceed?'), opts = smtp_prefs().parse()
if not opts.relay_password or question_dialog(self, _('OK to proceed?'),
_('This will display your email password on the screen' _('This will display your email password on the screen'
'. Is it OK to proceed?'), show_copy_button=False): '. Is it OK to proceed?'), show_copy_button=False):
TestEmail(pa, self).exec_() TestEmail(pa, self).exec_()
@ -204,19 +205,32 @@ class SendEmail(QWidget, Ui_Form):
username = unicode(self.relay_username.text()).strip() username = unicode(self.relay_username.text()).strip()
password = unicode(self.relay_password.text()).strip() password = unicode(self.relay_password.text()).strip()
host = unicode(self.relay_host.text()).strip() host = unicode(self.relay_host.text()).strip()
if host and not (username and password): enc_method = ('TLS' if self.relay_tls.isChecked() else 'SSL'
error_dialog(self, _('Bad configuration'), if self.relay_ssl.isChecked() else 'NONE')
_('You must set the username and password for ' if host:
'the mail server.')).exec_() # Validate input
return False if ((username and not password) or (not username and password)):
error_dialog(self, _('Bad configuration'),
_('You must either set both the username <b>and</b> password for '
'the mail server or no username and no password at all.')).exec_()
return False
if not username and not password and enc_method != 'NONE':
error_dialog(self, _('Bad configuration'),
_('Please enter a username and password or set'
' encryption to None ')).exec_()
return False
if not (username and password) and not question_dialog(self,
_('Are you sure?'),
_('No username and password set for mailserver. Most '
' mailservers need a username and password. Are you sure?')):
return False
conf = smtp_prefs() conf = smtp_prefs()
conf.set('from_', from_) conf.set('from_', from_)
conf.set('relay_host', host if host else None) conf.set('relay_host', host if host else None)
conf.set('relay_port', self.relay_port.value()) conf.set('relay_port', self.relay_port.value())
conf.set('relay_username', username if username else None) conf.set('relay_username', username if username else None)
conf.set('relay_password', hexlify(password)) conf.set('relay_password', hexlify(password))
conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL' conf.set('encryption', enc_method)
if self.relay_ssl.isChecked() else 'NONE')
return True return True

View File

@ -8,7 +8,6 @@ __docformat__ = 'restructuredtext en'
import os import os
from hashlib import sha1 from hashlib import sha1
from calibre.constants import filesystem_encoding
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
def find_folders_under(root, db, add_root=True, # {{{ def find_folders_under(root, db, add_root=True, # {{{
@ -17,21 +16,13 @@ def find_folders_under(root, db, add_root=True, # {{{
Find all folders under the specified root path, ignoring any folders under Find all folders under the specified root path, ignoring any folders under
the library path of db the library path of db
root must be a bytestring in filesystem_encoding
If follow_links is True, follow symbolic links. WARNING; this can lead to If follow_links is True, follow symbolic links. WARNING; this can lead to
infinite recursion. infinite recursion.
cancel_callback must be a no argument callable that returns True to cancel cancel_callback must be a no argument callable that returns True to cancel
the search the search
''' '''
assert not isinstance(root, unicode) # root must be in filesystem encoding
lp = db.library_path lp = db.library_path
if isinstance(lp, unicode):
try:
lp = lp.encode(filesystem_encoding)
except:
lp = None
if lp: if lp:
lp = os.path.abspath(lp) lp = os.path.abspath(lp)

View File

@ -147,6 +147,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def __init__(self, library_path, row_factory=False, default_prefs=None, def __init__(self, library_path, row_factory=False, default_prefs=None,
read_only=False): read_only=False):
try:
if isbytestring(library_path):
library_path = library_path.decode(filesystem_encoding)
except:
traceback.print_exc()
self.field_metadata = FieldMetadata() self.field_metadata = FieldMetadata()
self._library_id_ = None self._library_id_ = None
# Create the lock to be used to guard access to the metadata writer # Create the lock to be used to guard access to the metadata writer
@ -160,8 +165,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.dbpath = os.path.join(library_path, 'metadata.db') self.dbpath = os.path.join(library_path, 'metadata.db')
self.dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', self.dbpath = os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH',
self.dbpath) self.dbpath)
if isinstance(self.dbpath, unicode) and not iswindows:
self.dbpath = self.dbpath.encode(filesystem_encoding)
if read_only and os.path.exists(self.dbpath): if read_only and os.path.exists(self.dbpath):
# Work on only a copy of metadata.db to ensure that # Work on only a copy of metadata.db to ensure that
@ -489,12 +492,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True) authors = self.authors(id, index_is_id=True)
if not authors: if not authors:
authors = _('Unknown') authors = _('Unknown')
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') author = ascii_filename(authors.split(',')[0]
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') )[:self.PATH_LIMIT].decode('ascii', 'replace')
title = ascii_filename(self.title(id, index_is_id=True)
)[:self.PATH_LIMIT].decode('ascii', 'replace')
while author[-1] in (' ', '.'): while author[-1] in (' ', '.'):
author = author[:-1] author = author[:-1]
if not author: if not author:
author = ascii_filename(_('Unknown')).decode(filesystem_encoding, 'replace') author = ascii_filename(_('Unknown')).decode(
'ascii', 'replace')
path = author + '/' + title + ' (%d)'%id path = author + '/' + title + ' (%d)'%id
return path return path
@ -505,8 +511,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
authors = self.authors(id, index_is_id=True) authors = self.authors(id, index_is_id=True)
if not authors: if not authors:
authors = _('Unknown') authors = _('Unknown')
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') author = ascii_filename(authors.split(',')[0]
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace') )[:self.PATH_LIMIT].decode('ascii', 'replace')
title = ascii_filename(self.title(id, index_is_id=True)
)[:self.PATH_LIMIT].decode('ascii', 'replace')
name = title + ' - ' + author name = title + ' - ' + author
while name.endswith('.'): while name.endswith('.'):
name = name[:-1] name = name[:-1]
@ -1682,8 +1690,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id]) self.notify('metadata', [id])
return books_to_refresh return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
set_title=True, set_authors=True, commit=True): set_authors=True, commit=True, force_cover=False,
force_tags=False):
''' '''
Set metadata for the book `id` from the `Metadata` object `mi` Set metadata for the book `id` from the `Metadata` object `mi`
''' '''
@ -1699,13 +1708,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc() traceback.print_exc()
else: else:
raise raise
# force_changes has no role to play in setting title or author
path_changed = False path_changed = False
if set_title and mi.title: if set_title and not mi.is_null('title'):
self._set_title(id, mi.title) self._set_title(id, mi.title)
path_changed = True path_changed = True
if set_authors: if set_authors and not mi.is_null('authors'):
if not mi.authors:
mi.authors = [_('Unknown')]
authors = [] authors = []
for a in mi.authors: for a in mi.authors:
authors += string_to_authors(a) authors += string_to_authors(a)
@ -1713,16 +1721,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path_changed = True path_changed = True
if path_changed: if path_changed:
self.set_path(id, index_is_id=True) self.set_path(id, index_is_id=True)
if mi.author_sort:
if not mi.is_null('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False, doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False) commit=False)
if mi.publisher: if not mi.is_null('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False, doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False) commit=False)
if mi.rating: if not mi.is_null('rating'):
doit(self.set_rating, id, mi.rating, notify=False, commit=False) doit(self.set_rating, id, mi.rating, notify=False, commit=False)
if mi.series: if not mi.is_null('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False) doit(self.set_series, id, mi.series, notify=False, commit=False)
if mi.cover_data[1] is not None: if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False) doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None: elif mi.cover is not None:
@ -1731,14 +1741,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read() raw = f.read()
if raw: if raw:
doit(self.set_cover, id, raw, commit=False) doit(self.set_cover, id, raw, commit=False)
if mi.tags: elif force_cover:
doit(self.remove_cover, id, notify=False, commit=False)
if force_tags or not mi.is_null('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False) doit(self.set_tags, id, mi.tags, notify=False, commit=False)
if mi.comments: if not mi.is_null('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False) doit(self.set_comment, id, mi.comments, notify=False, commit=False)
if mi.series_index: if not mi.is_null('series_index'):
doit(self.set_series_index, id, mi.series_index, notify=False, doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False) commit=False)
if mi.pubdate: if not mi.is_null('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False) doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None: if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False, doit(self.set_timestamp, id, mi.timestamp, notify=False,
@ -1748,19 +1761,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if mi_idents: if mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True) identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems(): for key, val in mi_idents.iteritems():
if val and val.strip(): # Don't delete an existing identifier if val and val.strip():
identifiers[icu_lower(key)] = val identifiers[icu_lower(key)] = val
self.set_identifiers(id, identifiers, notify=False, commit=False) self.set_identifiers(id, identifiers, notify=False, commit=False)
user_mi = mi.get_all_user_metadata(make_copy=False) user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys(): for key in user_mi.iterkeys():
if key in self.field_metadata and \ if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id, doit(self.set_custom, id, val=mi.get(key), commit=False,
val=mi.get(key), extra=mi.get_extra(key), label=user_mi[key]['label'])
extra=mi.get_extra(key),
label=user_mi[key]['label'], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
self.notify('metadata', [id]) self.notify('metadata', [id])
@ -2350,6 +2360,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
@param tags: list of strings @param tags: list of strings
@param append: If True existing tags are not removed @param append: If True existing tags are not removed
''' '''
if not tags:
tags = []
if not append: if not append:
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id) self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id)
@ -2500,6 +2512,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id]) self.notify('metadata', [id])
def set_rating(self, id, rating, notify=True, commit=True): def set_rating(self, id, rating, notify=True, commit=True):
if not rating:
rating = 0
rating = int(rating) rating = int(rating)
self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,))
rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False) rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False)
@ -2514,7 +2528,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_comment(self, id, text, notify=True, commit=True): def set_comment(self, id, text, notify=True, commit=True):
self.conn.execute('DELETE FROM comments WHERE book=?', (id,)) self.conn.execute('DELETE FROM comments WHERE book=?', (id,))
self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text)) if text:
self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
else:
text = ''
if commit: if commit:
self.conn.commit() self.conn.commit()
self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True) self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True)
@ -2523,6 +2540,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id]) self.notify('metadata', [id])
def set_author_sort(self, id, sort, notify=True, commit=True): def set_author_sort(self, id, sort, notify=True, commit=True):
if not sort:
sort = ''
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id)) self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id))
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
@ -2594,6 +2613,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_identifiers(self, id_, identifiers, notify=True, commit=True): def set_identifiers(self, id_, identifiers, notify=True, commit=True):
cleaned = {} cleaned = {}
if not identifiers:
identifiers = {}
for typ, val in identifiers.iteritems(): for typ, val in identifiers.iteritems():
typ, val = self._clean_identifier(typ, val) typ, val = self._clean_identifier(typ, val)
if val: if val:

View File

@ -12,7 +12,7 @@ for using |app| is to first add books to the library from your hard disk.
to its internal database. Once they are in the database, you can perform a various to its internal database. Once they are in the database, you can perform a various
:ref:`actions` on them that include conversion from one format to another, :ref:`actions` on them that include conversion from one format to another,
transfer to the reading device, viewing on your computer, editing metadata, including covers, etc. transfer to the reading device, viewing on your computer, editing metadata, including covers, etc.
Note that |app| creates copies of the files you add to it, your original files are left untouched. Note that |app| creates copies of the files you add to it, your original files are left untouched.
The interface is divided into various sections: The interface is divided into various sections:
@ -51,10 +51,12 @@ Add books
3. **Add books directories, including sub-directories (Multiple books per directory, assumes every ebook file is a different book)**: Allows you to choose a directory. The directory and all its sub-directories are scanned recursively and any ebooks found are added to the library. The algorithm assumes that each directory contains many books. All ebook files with the same name in a directory are assumed to be the same book in different formats. Ebooks with different names are added as different books. This action is the inverse of the :ref:`Save to disk <save_to_disk_single>` action, i.e. you can :guilabel:`Save to disk`, delete the books and re-add them with no lost information (except date). 3. **Add books directories, including sub-directories (Multiple books per directory, assumes every ebook file is a different book)**: Allows you to choose a directory. The directory and all its sub-directories are scanned recursively and any ebooks found are added to the library. The algorithm assumes that each directory contains many books. All ebook files with the same name in a directory are assumed to be the same book in different formats. Ebooks with different names are added as different books. This action is the inverse of the :ref:`Save to disk <save_to_disk_single>` action, i.e. you can :guilabel:`Save to disk`, delete the books and re-add them with no lost information (except date).
4. **Add empty book. (Book Entry with blank formats)**: Allows you to create a blank book record. This can be used to then manually fill out the information about a book that you may not have yet in your collection. 4. **Add empty book. (Book Entry with blank formats)**: Allows you to create a blank book record. This can be used to then manually fill out the information about a book that you may not have yet in your collection.
5. **Add by ISBN**: Allows you to add one or more books by entering just their ISBN into a list or pasting the list of ISBNs from your clipboard. 5. **Add by ISBN**: Allows you to add one or more books by entering just their ISBN into a list or pasting the list of ISBNs from your clipboard.
6. **Add files to selected book records**: Allows you to add or update the files associated with an existing book in your library.
The :guilabel:`Add books` action can read metadata from a wide variety of e-book formats. In addition it tries to guess metadata from the filename. The :guilabel:`Add books` action can read metadata from a wide variety of e-book formats. In addition it tries to guess metadata from the filename.
See the :ref:`config_filename_metadata` section, to learn how to configure this. See the :ref:`config_filename_metadata` section, to learn how to configure this.
@ -77,7 +79,7 @@ Edit metadata
6. **Download only social metadata**: Downloads only social metadata such as tags and reviews (if available), for the books that are selected in the book list. 6. **Download only social metadata**: Downloads only social metadata such as tags and reviews (if available), for the books that are selected in the book list.
7. **Merge Book Records**: Gives you the capability of merging the metadata and formats of two or more book records together. You can choose to either delete or keep the records that were not clicked first. 7. **Merge Book Records**: Gives you the capability of merging the metadata and formats of two or more book records together. You can choose to either delete or keep the records that were not clicked first.
.. _convert_ebooks: .. _convert_ebooks:
Convert e-books Convert e-books
@ -91,13 +93,13 @@ Note that ebooks you purchase will typically have `Digital Rights Management <ht
you have to find tools to liberate your books yourself and then use |app| to convert them. you have to find tools to liberate your books yourself and then use |app| to convert them.
For most people, conversion should be a simple 1-click affair. But if you want to learn more about the conversion process, see :ref:`conversion`. For most people, conversion should be a simple 1-click affair. But if you want to learn more about the conversion process, see :ref:`conversion`.
The :guilabel:`Convert E-books` action has three variations, accessed by the arrow next to the button. The :guilabel:`Convert E-books` action has three variations, accessed by the arrow next to the button.
1. **Convert individually**: This will allow you to specify conversion options to customize the conversion of each selected ebook. 1. **Convert individually**: This will allow you to specify conversion options to customize the conversion of each selected ebook.
2. **Bulk convert**: This allows you to specify options only once to convert a number of ebooks in bulk. 2. **Bulk convert**: This allows you to specify options only once to convert a number of ebooks in bulk.
3. **Create catalog**: This action allows you to generate a complete listing with all metadata of the books in your library, 3. **Create catalog**: This action allows you to generate a complete listing with all metadata of the books in your library,
in several formats, like XML, CSV, BiBTeX, EPUB and MOBI. The catalog will contain all the books showing in the library view currently, in several formats, like XML, CSV, BiBTeX, EPUB and MOBI. The catalog will contain all the books showing in the library view currently,
so you can use the search features to limit the books to be catalogued. In addition, if you select multiple books using the mouse, so you can use the search features to limit the books to be catalogued. In addition, if you select multiple books using the mouse,
@ -117,7 +119,7 @@ For other formats it uses the default operating system application. You can conf
Preferences->Behavior. If a book has more than one format, you can view a particular format by clicking the down arrow Preferences->Behavior. If a book has more than one format, you can view a particular format by clicking the down arrow
on the right of the :guilabel:`View` button. on the right of the :guilabel:`View` button.
.. _send_to_device: .. _send_to_device:
Send to device Send to device
@ -138,7 +140,7 @@ Send to device
You can control the file name and folder structure of files sent to the device by setting up a template in You can control the file name and folder structure of files sent to the device by setting up a template in
:guilabel:`Preferences->Import/Export->Sending books to devices`. Also see :ref:`templatelangcalibre`. :guilabel:`Preferences->Import/Export->Sending books to devices`. Also see :ref:`templatelangcalibre`.
.. _fetch_news: .. _fetch_news:
Fetch news Fetch news
@ -147,11 +149,11 @@ Fetch news
:class: float-right-img :class: float-right-img
|fni| The :guilabel:`Fetch news` action downloads news from various websites and converts it into an ebook that can be read on your ebook reader. Normally, the newly created ebook is added to your ebook library, but if an ebook reader is connected at the time the download finishes, the news is also uploaded to the reader automatically. |fni| The :guilabel:`Fetch news` action downloads news from various websites and converts it into an ebook that can be read on your ebook reader. Normally, the newly created ebook is added to your ebook library, but if an ebook reader is connected at the time the download finishes, the news is also uploaded to the reader automatically.
The :guilabel:`Fetch news` action uses simple recipes (10-15 lines of code) for each news site. To learn how to create recipes for your own news sources, see :ref:`news`. The :guilabel:`Fetch news` action uses simple recipes (10-15 lines of code) for each news site. To learn how to create recipes for your own news sources, see :ref:`news`.
The :guilabel:`Fetch news` action has three variations, accessed by clicking the down arrow on the right of the button. The :guilabel:`Fetch news` action has three variations, accessed by clicking the down arrow on the right of the button.
1. **Schedule news download**: This action allows you to schedule the download of of your selected news sources from a list of hundreds of available. Scheduling can be set individually for each news source you select and the scheduling is flexible allowing you to select specific days of the week or a frequency of days between downloads. 1. **Schedule news download**: This action allows you to schedule the download of of your selected news sources from a list of hundreds of available. Scheduling can be set individually for each news source you select and the scheduling is flexible allowing you to select specific days of the week or a frequency of days between downloads.
2. **Add a custom news service**: This action allows you to create a simple recipe for downloading news from a custom news site that you wish to access. Creating the recipe can be as simple as specifying an RSS news feed URL, or you can be more prescriptive by creating python based code for the task, see :ref:`news`. 2. **Add a custom news service**: This action allows you to create a simple recipe for downloading news from a custom news site that you wish to access. Creating the recipe can be as simple as specifying an RSS news feed URL, or you can be more prescriptive by creating python based code for the task, see :ref:`news`.
3. **Download all scheduled news sources**: This action causes |app| to immediately begin to download all news sources that you have previously scheduled. 3. **Download all scheduled news sources**: This action causes |app| to immediately begin to download all news sources that you have previously scheduled.
@ -180,9 +182,9 @@ Device
:class: float-right-img :class: float-right-img
|dvi| The :guilabel:`Device` action allows you to view the books in the main memory or storage cards of your device, or to eject the device (detach it from |app|). |dvi| The :guilabel:`Device` action allows you to view the books in the main memory or storage cards of your device, or to eject the device (detach it from |app|).
This icon shows up automatically on the main |app| toolbar when you connect a supported device. You can click on it to see the books on your device. You can also drag and drop books from your |app| library onto the icon to transfer them to your device. Conversely, you can drag and drop books from your device onto the |app| icon on the toolbar to transfer books from your device to the |app| library. This icon shows up automatically on the main |app| toolbar when you connect a supported device. You can click on it to see the books on your device. You can also drag and drop books from your |app| library onto the icon to transfer them to your device. Conversely, you can drag and drop books from your device onto the |app| icon on the toolbar to transfer books from your device to the |app| library.
.. _save_to_disk: .. _save_to_disk:
Save to disk Save to disk
@ -199,14 +201,14 @@ Save to disk
Author_(sort) Author_(sort)
Title Title
Book Files Book Files
You can control the file name and folder structure of files saved to disk by setting up a template in You can control the file name and folder structure of files saved to disk by setting up a template in
:guilabel:`Preferences->Import/Export->Saving books to disk`. Also see :ref:`templatelangcalibre`. :guilabel:`Preferences->Import/Export->Saving books to disk`. Also see :ref:`templatelangcalibre`.
.. _save_to_disk_single: .. _save_to_disk_single:
2. **Save to disk in a single directory**: The selected books are saved to disk in a single directory. 2. **Save to disk in a single directory**: The selected books are saved to disk in a single directory.
For 1. and 2. All available formats as well as metadata is stored to disk for each selected book. Metadata is stored in an OPF file. For 1. and 2. All available formats as well as metadata is stored to disk for each selected book. Metadata is stored in an OPF file.
Saved books can be re-imported to the library without any loss of information by using the :ref:`Add books <add_books>` action. Saved books can be re-imported to the library without any loss of information by using the :ref:`Add books <add_books>` action.
@ -227,14 +229,14 @@ Connect/Share
|csi| The :guilabel:`Connect/Share` action allows you to manually connect to a device or folder on your computer, it also allows you to set up you |app| library for access via a web browser, or email. |csi| The :guilabel:`Connect/Share` action allows you to manually connect to a device or folder on your computer, it also allows you to set up you |app| library for access via a web browser, or email.
The :guilabel:`Connect/Share` action has four variations, accessed by clicking the down arrow on the right of the button. The :guilabel:`Connect/Share` action has four variations, accessed by clicking the down arrow on the right of the button.
1. **Connect to folder**: This action allows you to connect to any folder on your computer as though it were a device and use all the facilities |app| has for devices with that folder. Useful if your device cannot be supported by |app| but is available as a USB disk. 1. **Connect to folder**: This action allows you to connect to any folder on your computer as though it were a device and use all the facilities |app| has for devices with that folder. Useful if your device cannot be supported by |app| but is available as a USB disk.
2. **Connect to iTunes**: Allows you to connect to your iTunes books database as though it were a device. Once the books are sent to iTunes, you can then use iTunes to make them available on your various iDevices. Useful if you would rather not have |app| send books to your iDevice directly. 2. **Connect to iTunes**: Allows you to connect to your iTunes books database as though it were a device. Once the books are sent to iTunes, you can then use iTunes to make them available on your various iDevices. Useful if you would rather not have |app| send books to your iDevice directly.
3. **Start Content Server**: This action causes |app| to start up its built-in web server. When this is started, your |app| library will be accessible via a web browser from the internet (if you choose). You can configure how the web server is accessed by setting preferences at :guilabel:`Preferences->Sharing->Sharing over the net` 3. **Start Content Server**: This action causes |app| to start up its built-in web server. When this is started, your |app| library will be accessible via a web browser from the internet (if you choose). You can configure how the web server is accessed by setting preferences at :guilabel:`Preferences->Sharing->Sharing over the net`
4. **Setup email based sharing of books**: This action allows you to setup |app| to share books (and news feeds) by email. After setting up email addresses for this option |app| will send news updates and book updates to the entered email addresses. You can configure how the |app| sends email by setting preferences at :guilabel:`Preferences->Sharing->Sharing books by email`. Once you have setup one or more email addresses, this menu entry get replaced by menu entries to send books to the setup email addresses. 4. **Setup email based sharing of books**: This action allows you to setup |app| to share books (and news feeds) by email. After setting up email addresses for this option |app| will send news updates and book updates to the entered email addresses. You can configure how the |app| sends email by setting preferences at :guilabel:`Preferences->Sharing->Sharing books by email`. Once you have setup one or more email addresses, this menu entry get replaced by menu entries to send books to the setup email addresses.
.. _remove_books: .. _remove_books:
@ -245,14 +247,14 @@ Remove books
|rbi| The :guilabel:`Remove books` action **deletes books permanently**, so use it with care. It is *context sensitive*, i.e. it depends on which :ref:`catalog <catalogs>` you have selected. If you have selected the :guilabel:`Library`, books will be removed from the library. If you have selected the ebook reader device, the books will be removed from the device. To remove only a particular format for a given book use the :ref:`edit_meta_information` action. Remove books also has five variations which can be accessed by clicking the down arrow on the right side of the button. |rbi| The :guilabel:`Remove books` action **deletes books permanently**, so use it with care. It is *context sensitive*, i.e. it depends on which :ref:`catalog <catalogs>` you have selected. If you have selected the :guilabel:`Library`, books will be removed from the library. If you have selected the ebook reader device, the books will be removed from the device. To remove only a particular format for a given book use the :ref:`edit_meta_information` action. Remove books also has five variations which can be accessed by clicking the down arrow on the right side of the button.
1. **Remove Selected Books**: Allows you to **permanently** remove all books that are selected in the book list. 1. **Remove Selected Books**: Allows you to **permanently** remove all books that are selected in the book list.
2. **Remove files of a specified format from selected books..**: Allows you to **permanently** remove ebook files of a specified format, from books that are selected in the book list. 2. **Remove files of a specified format from selected books..**: Allows you to **permanently** remove ebook files of a specified format, from books that are selected in the book list.
3. **Remove all files of a specified format, except..**: Allows you to **permanently** remove ebook files of a multiple formats except a given format, from books that are selected in the book list. 3. **Remove all files of a specified format, except..**: Allows you to **permanently** remove ebook files of a multiple formats except a given format, from books that are selected in the book list.
4. **Remove covers from selected books**: Allows you to **permanently** remove cover images files, from books that are selected in the book list. 4. **Remove covers from selected books**: Allows you to **permanently** remove cover images files, from books that are selected in the book list.
5. **Remove matching books from device**: Allows you to remove ebook files from a connected device, that match the books that are selected in the book list. 5. **Remove matching books from device**: Allows you to remove ebook files from a connected device, that match the books that are selected in the book list.
.. note:: .. note::
@ -265,7 +267,7 @@ Preferences
.. |cbi| image:: images/preferences.png .. |cbi| image:: images/preferences.png
The Preferences Action allows you to change the way various aspects of |app| work. To access it, click the |cbi|. The Preferences Action allows you to change the way various aspects of |app| work. To access it, click the |cbi|.
.. _catalogs: .. _catalogs:
Catalogs Catalogs
@ -274,9 +276,9 @@ Catalogs
:align: center :align: center
A *catalog* is a collection of books. |app| can manage two types of different catalogs: A *catalog* is a collection of books. |app| can manage two types of different catalogs:
1. **Library**: This is a collection of books stored in your |app| library on your computer 1. **Library**: This is a collection of books stored in your |app| library on your computer
2. **Device**: This is a collection of books stored in the main memory of your ebook reader. It will be available when you connect the reader to your computer. 2. **Device**: This is a collection of books stored in the main memory of your ebook reader. It will be available when you connect the reader to your computer.
- In addition, you can see the books on the storage card (if any) in your reader device. - In addition, you can see the books on the storage card (if any) in your reader device.
@ -292,17 +294,17 @@ Search & Sort
The Search & Sort section allows you to perform several powerful actions on your book collections. The Search & Sort section allows you to perform several powerful actions on your book collections.
* You can sort them by title, author, date, rating etc. by clicking on the column titles. You can also sub-sort (i.e. sort on multiple columns). For example, if you click on the title column and then the author column, the book will be sorted by author and then all the entries for the same author will be sorted by title. * You can sort them by title, author, date, rating etc. by clicking on the column titles. You can also sub-sort (i.e. sort on multiple columns). For example, if you click on the title column and then the author column, the book will be sorted by author and then all the entries for the same author will be sorted by title.
* You can search for a particular book or set of books using the search bar. More on that below. * You can search for a particular book or set of books using the search bar. More on that below.
* You can quickly and conveniently edit metadata by double-clicking the entry you want changed in the list. * You can quickly and conveniently edit metadata by double-clicking the entry you want changed in the list.
* You can perform :ref:`actions` on sets to books. To select multiple books you can either: * You can perform :ref:`actions` on sets to books. To select multiple books you can either:
- Keep the :kbd:`Ctrl` key pressed and click on the books you want selected. - Keep the :kbd:`Ctrl` key pressed and click on the books you want selected.
- Keep the :kbd:`Shift` key pressed and click on the starting and ending book of arange of books you want selected. - Keep the :kbd:`Shift` key pressed and click on the starting and ending book of arange of books you want selected.
* You can configure which fields you want displayed by using the :ref:`configuration` dialog. * You can configure which fields you want displayed by using the :ref:`configuration` dialog.
.. _search_interface: .. _search_interface:
@ -310,10 +312,10 @@ The Search & Sort section allows you to perform several powerful actions on your
The Search Interface The Search Interface
--------------------- ---------------------
You can search all the metadata by entering search terms in the search bar. Searches are case insensitive. For example:: You can search all the metadata by entering search terms in the search bar. Searches are case insensitive. For example::
Asimov Foundation format:lrf Asimov Foundation format:lrf
This will match all books in your library that have ``Asimov`` and ``Foundation`` in their metadata and This will match all books in your library that have ``Asimov`` and ``Foundation`` in their metadata and
are available in the LRF format. Some more examples:: are available in the LRF format. Some more examples::
author:Asimov and not series:Foundation author:Asimov and not series:Foundation
@ -327,20 +329,18 @@ Equality searches are indicated by prefixing the search string with an equals si
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are ``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
be used. Regular expression searches are contains searches unless the expression contains anchors. be used. Regular expression searches are contains searches unless the expression contains anchors.
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash. Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
for the tag ``Science Fiction``, you would need to search for ``tag:"=science fiction"``. If you search for for the tag ``Science Fiction``, you would need to search for ``tag:"=science fiction"``. If you search for
``tag:=science fiction``, you will find all books with the tag 'science' and containing the word 'fiction' in any ``tag:=science fiction``, you will find all books with the tag 'science' and containing the word 'fiction' in any
metadata. metadata.
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
clicking the button |sbi|. clicking the button |sbi|.
Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover, Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover,
comments, format, identifiers, date, pubdate, search, size`` and custom columns. If a device is plugged in, the comments, format, identifiers, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field becomes available. To find the search name (actually called the `lookup name`) for a custom column, hover your mouse over the column header in the library view.
``ondevice`` field becomes available. To find the search name for a custom column, hover your mouse over the
column header.
The syntax for searching for dates is:: The syntax for searching for dates is::
@ -387,31 +387,31 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col
Hierarchical items (e.g. A.B.C) use an extended syntax to match initial parts of the hierarchy. This is done by adding a period between the exact match indicator (=) and the text. For example, the query ``tags:=.A`` will find the tags `A` and `A.B`, but will not find the tags `AA` or `AA.B`. The query ``tags:=.A.B`` will find the tags `A.B` and `A.C`, but not the tag `A`. Hierarchical items (e.g. A.B.C) use an extended syntax to match initial parts of the hierarchy. This is done by adding a period between the exact match indicator (=) and the text. For example, the query ``tags:=.A`` will find the tags `A` and `A.B`, but will not find the tags `AA` or `AA.B`. The query ``tags:=.A.B`` will find the tags `A.B` and `A.C`, but not the tag `A`.
Identifiers (e.g., isbn, doi, lccn etc) also use an extended syntax. First, note that an identifier has the form ``key:value``, as in ``isbn:123456789``. The extended syntax permits you to specify independently which key and value to search for. Both the key and the value parts of the query can use `equality`, `contains`, or `regular expression` matches. Examples: Identifiers (e.g., isbn, doi, lccn etc) also use an extended syntax. First, note that an identifier has the form ``type:value``, as in ``isbn:123456789``. The extended syntax permits you to specify independently which type and value to search for. Both the type and the value parts of the query can use `equality`, `contains`, or `regular expression` matches. Examples:
* ``identifiers:true`` will find books with any identifier. * ``identifiers:true`` will find books with any identifier.
* ``identifiers:false`` will find books with no identifier. * ``identifiers:false`` will find books with no identifier.
* ``identifiers:123`` will search for books with any key having a value containing `123`. * ``identifiers:123`` will search for books with any type having a value containing `123`.
* ``identifiers:=123456789`` will search for books with any key having a value equal to `123456789`. * ``identifiers:=123456789`` will search for books with any type having a value equal to `123456789`.
* ``identifiers:=isbn:`` and ``identifiers:isbn:true`` will find books with a key equal to isbn having any value * ``identifiers:=isbn:`` and ``identifiers:isbn:true`` will find books with a type equal to isbn having any value
* ``identifiers:=isbn:false`` will find books with no key equal to isbn. * ``identifiers:=isbn:false`` will find books with no type equal to isbn.
* ``identifiers:=isbn:123`` will find books with a key equal to isbn having a value containing `123`. * ``identifiers:=isbn:123`` will find books with a type equal to isbn having a value containing `123`.
* ``identifiers:=isbn:=123456789`` will find books with a key equal to isbn having a value equal to `123456789`. * ``identifiers:=isbn:=123456789`` will find books with a type equal to isbn having a value equal to `123456789`.
* ``identifiers:i:1`` will find books with a key containing an `i` having a value containing a `1`. * ``identifiers:i:1`` will find books with a type containing an `i` having a value containing a `1`.
.. |sbi| image:: images/search_button.png .. |sbi| image:: images/search_button.png
:align: middle :align: middle
.. figure:: images/search.png .. figure:: images/search.png
:align: center :align: center
:guilabel:`Advanced Search Dialog` :guilabel:`Advanced Search Dialog`
Saving searches Saving searches
----------------- -----------------
|app| has a useful feature, it allows you to save a search you use frequently under a special name and then re-use that search with a single click. To do this, create your search, either by typing it in the search bar, or using the Tag Browser. Then, type the name you would like to give to the search in the Saved Searches box next to the search bar and click the plus icon next to the saved searches box to save the search. |app| has a useful feature, it allows you to save a search you use frequently under a special name and then re-use that search with a single click. To do this, create your search, either by typing it in the search bar, or using the Tag Browser. Then, type the name you would like to give to the search in the Saved Searches box next to the search bar and click the plus icon next to the saved searches box to save the search.
Now, you can access your saved search in the Tag Browser under "Searches". A single click will allow you to re-use any arbitrarily complex search easily, without needing to re-create it. Now, you can access your saved search in the Tag Browser under "Searches". A single click will allow you to re-use any arbitrarily complex search easily, without needing to re-create it.
@ -453,7 +453,7 @@ Tag Browser
The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any item in the Tag Browser, for example the author name Isaac Asimov, then the list of books to the right is restricted to showing books by that author. You can click on category names as well. For example, clicking on "Series" will show you all books in any series. The Tag Browser allows you to easily browse your collection by Author/Tags/Series/etc. If you click on any item in the Tag Browser, for example the author name Isaac Asimov, then the list of books to the right is restricted to showing books by that author. You can click on category names as well. For example, clicking on "Series" will show you all books in any series.
The first click on an item will restrict the list of books to those that contain/match the item. Continuing the above example, clicking on Isaac Asimov will show books by that author. Clicking again on the item will change what is shown, depending on whether the item has children (see sub-categories and hierarchical items below). Continuing the Isaac Asimov example, clicking again on Isaac Asimov will restrict the list of books to those not by Isaac Asimov. A third click will remove the restriction, showing all books. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. Looking at what the Tag Browser generates is a good way to learn how to construct basic search expressions. The first click on an item will restrict the list of books to those that contain/match the item. Continuing the above example, clicking on Isaac Asimov will show books by that author. Clicking again on the item will change what is shown, depending on whether the item has children (see sub-categories and hierarchical items below). Continuing the Isaac Asimov example, clicking again on Isaac Asimov will restrict the list of books to those not by Isaac Asimov. A third click will remove the restriction, showing all books. If you hold down the Ctrl or Shift keys and click on multiple items, then restrictions based on multiple items are created. For example you could hold Ctrl and click on the tags History and Europe for find books on European history. The Tag Browser works by constructing search expressions that are automatically entered into the Search bar. Looking at what the Tag Browser generates is a good way to learn how to construct basic search expressions.
Items in the Tag browser have their icons partially colored. The amount of color depends on the average rating of the books in that category. So for example if the books by Isaac Asimov have an average of four stars, the icon for Isaac Asimov in the Tag Browser will be 4/5th colored. You can hover your mouse over the icon to see the average rating. Items in the Tag browser have their icons partially colored. The amount of color depends on the average rating of the books in that category. So for example if the books by Isaac Asimov have an average of four stars, the icon for Isaac Asimov in the Tag Browser will be 4/5th colored. You can hover your mouse over the icon to see the average rating.
@ -461,13 +461,13 @@ The outer-level items in the tag browser such as Authors and Series are called c
You can search user categories in the same way as built-in categories, by clicking on them. There are four different searches cycled through by clicking: "everything matching an item in the category" indicated by a single green plus sign, "everything matching an item in the category or its sub-categories" indicated by two green plus signs, "everything not matching an item in the category" shown by a single red minus sign, and "everything not matching an item in the category or its sub-categories" shown by two red minus signs. You can search user categories in the same way as built-in categories, by clicking on them. There are four different searches cycled through by clicking: "everything matching an item in the category" indicated by a single green plus sign, "everything matching an item in the category or its sub-categories" indicated by two green plus signs, "everything not matching an item in the category" shown by a single red minus sign, and "everything not matching an item in the category or its sub-categories" shown by two red minus signs.
It is also possible to create hierarchies inside some of the text categories such as tags, series, and custom columns. These hierarchies show with the small triangle, permitting the sub-items to be hidden. To use hierarchies of items in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items. It is also possible to create hierarchies inside some of the text categories such as tags, series, and custom columns. These hierarchies show with the small triangle, permitting the sub-items to be hidden. To use hierarchies of items in a category, you must first go to Preferences / Look & Feel and enter the category name(s) into the "Categories with hierarchical items" box. Once this is done, items in that category that contain periods will be shown using the small triangle. For example, assume you create a custom column called "Genre" and indicate that it contains hierarchical items. Once done, items such as Mystery.Thriller and Mystery.English will display as Mystery with the small triangle next to it. Clicking on the triangle will show Thriller and English as sub-items. See :ref:`Managing subgroups of books, for example "genre" <subgroups-tutorial>` for more information.
Hierarchical items (items with children) use the same four 'click-on' searches as user categories. Items that do not have children use two of the searches: "everything matching" and "everything not matching". Hierarchical items (items with children) use the same four 'click-on' searches as user categories. Items that do not have children use two of the searches: "everything matching" and "everything not matching".
You can drag and drop items in the Tag browser onto user categories to add them to that category. If the source is a user category, holding the shift key while dragging will move the item to the new category. You can also drag and drop books from the book list onto items in the Tag Browser; dropping a book on an item causes that item to be automatically applied to the dropped books. For example, dragging a book onto Isaac Asimov will set the author of that book to Isaac Asimov. Dropping it onto the tag History will add the tag History to the book's tags. You can drag and drop items in the Tag browser onto user categories to add them to that category. If the source is a user category, holding the shift key while dragging will move the item to the new category. You can also drag and drop books from the book list onto items in the Tag Browser; dropping a book on an item causes that item to be automatically applied to the dropped books. For example, dragging a book onto Isaac Asimov will set the author of that book to Isaac Asimov. Dropping it onto the tag History will add the tag History to the book's tags.
There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose one of several operations. Some examples are to hide the it, rename it, or open a "Manage x" dialog that allows you to manage items of that kind. For example, the "Manage Authors" dialog allows you to rename authors and control how their names are sorted. There is a search bar at the top of the Tag Browser that allows you to easily find any item in the Tag Browser. In addition, you can right click on any item and choose one of several operations. Some examples are to hide the it, rename it, or open a "Manage x" dialog that allows you to manage items of that kind. For example, the "Manage Authors" dialog allows you to rename authors and control how their names are sorted.
You can control how items are sorted in the Tag browser via the box at the bottom of the Tag Browser. You can choose to sort by name, average rating or popularity (popularity is the number of books with an item in your library; for example; the popularity of Isaac Asimov is the number of book sin your library by Isaac Asimov). You can control how items are sorted in the Tag browser via the box at the bottom of the Tag Browser. You can choose to sort by name, average rating or popularity (popularity is the number of books with an item in your library; for example; the popularity of Isaac Asimov is the number of book sin your library by Isaac Asimov).
@ -495,47 +495,47 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Action - Action
* - :kbd:`F2 (Enter in OS X)` * - :kbd:`F2 (Enter in OS X)`
- Edit the metadata of the currently selected field in the book list. - Edit the metadata of the currently selected field in the book list.
* - :kbd:`A` * - :kbd:`A`
- Add Books - Add Books
* - :kbd:`Shift+A` * - :kbd:`Shift+A`
- Add Formats to the selected books - Add Formats to the selected books
* - :kbd:`C` * - :kbd:`C`
- Convert selected Books - Convert selected Books
* - :kbd:`D` * - :kbd:`D`
- Send to device - Send to device
* - :kbd:`Del` * - :kbd:`Del`
- Remove selected Books - Remove selected Books
* - :kbd:`E` * - :kbd:`E`
- Edit metadata of selected books - Edit metadata of selected books
* - :kbd:`I` * - :kbd:`I`
- Show book details - Show book details
* - :kbd:`M` * - :kbd:`M`
- Merge selected records - Merge selected records
* - :kbd:`Alt+M` * - :kbd:`Alt+M`
- Merge selected records, keeping originals - Merge selected records, keeping originals
* - :kbd:`O` * - :kbd:`O`
- Open containing folder - Open containing folder
* - :kbd:`S` * - :kbd:`S`
- Save to Disk - Save to Disk
* - :kbd:`V` * - :kbd:`V`
- View - View
* - :kbd:`Alt+V/Cmd+V in OS X` * - :kbd:`Alt+V/Cmd+V in OS X`
- View specific format - View specific format
* - :kbd:`Alt+Shift+J` * - :kbd:`Alt+Shift+J`
- Toggle jobs list - Toggle jobs list
* - :kbd:`Alt+Shift+B` * - :kbd:`Alt+Shift+B`
- Toggle Cover Browser - Toggle Cover Browser
* - :kbd:`Alt+Shift+T` * - :kbd:`Alt+Shift+T`
- Toggle Tag Browser - Toggle Tag Browser
* - :kbd:`Alt+A` * - :kbd:`Alt+A`
- Show books by the Same author as the current book - Show books by the Same author as the current book
* - :kbd:`Alt+T` * - :kbd:`Alt+T`
- Show books with the same tags as current book - Show books with the same tags as current book
* - :kbd:`Alt+P` * - :kbd:`Alt+P`
- Show books by the same publisher as current book - Show books by the same publisher as current book
* - :kbd:`Alt+Shift+S` * - :kbd:`Alt+Shift+S`
- Show books in the same series as current book - Show books in the same series as current book
* - :kbd:`/, Ctrl+F` * - :kbd:`/, Ctrl+F`
- Focus the search bar - Focus the search bar
* - :kbd:`Shift+Ctrl+F` * - :kbd:`Shift+Ctrl+F`
- Open the advanced search dialog - Open the advanced search dialog
@ -545,13 +545,13 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked) - Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
* - :kbd:`Shift+N or Shift+F3` * - :kbd:`Shift+N or Shift+F3`
- Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked) - Find the next book that matches the current search (only works if the highlight checkbox next to the search bar is checked)
* - :kbd:`Ctrl+D` * - :kbd:`Ctrl+D`
- Download metadata and shortcuts - Download metadata and shortcuts
* - :kbd:`Ctrl+R` * - :kbd:`Ctrl+R`
- Restart calibre - Restart calibre
* - :kbd:`Shift+Ctrl+E` * - :kbd:`Shift+Ctrl+E`
- Add empty books to calibre - Add empty books to calibre
* - :kbd:`Ctrl+Q` * - :kbd:`Ctrl+Q`
- Quit calibre - Quit calibre

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,117 @@
.. include:: global.rst
.. _subgroups-tutorial:
Managing subgroups of books, for example "genre"
==================================================
Some people wish to organize the books in their library into subgroups, similar to subfolders. The most commonly provided reason is to create genre hierarchies, but there are many others. One user asked for a way to organize textbooks by subject and course number. Another wanted to keep track of gifts by subject and recipient. This tutorial will use the genre example for the rest of this post.
Before going on, please note that we are not talking about folders on the hard disk. Subgroups are not file folders. Books will not be copied anywhere. Calibre's library file structure is not affected. Instead, we are presenting a way to organize and display subgroups of books within a |app| library.
.. contents::
:depth: 1
:local:
.. |sgtree| image:: images/sg_tree.jpg
:class: float-right-img
The commonly-provided requirements for subgroups such as genres are:
* A subgroup (e.g., a genre) must contain (point to) books, not categories of books. This is what distinguishes subgroups from |app| user categories.
* A book can be in multiple subgroups (genres). This distinguishes subgroups from physical file folders.
* Subgroups (genres) must form a hierarchy; subgroups can contain subgroups.
Tags give you the first two. If you tag a book with the genre then you can use the tag browser (or search) for find the books with that genre, giving you the first. Many books can have the same tag(s), giving you the second. The problem is that tags don't satisfy the third requirement. They don't provide a hierarchy.
|sgtree| Calibre's hierarchy feature gives you the third, the ability to see the genres in a 'tree' and the ability to easily search for books in genre or sub-genre. For example, assume that your genre structure is similar to the following::
Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.
By using the hierarchy feature, you can see these genres in the tag browser in tree form, as shown in the screen image. In this example the outermost level (Genre) is a custom column that contains the genres. Genres containing sub-genres appear with a small triangle next to them. Clicking on that triangle will open the item and show the sub-genres, as you can see with History and Science Fiction.
Clicking on a genre can search for all books with that genre or children of that genre. For example, clicking on Science Fiction can give all three of the child genres, Alternate History, Military, and Space Opera. Clicking on Alternate History will give books in that genre, ignoring those in Military and Space Opera. Of course, a book can have multiple genres. If a book has both Space Opera and Military genres, then you will see that book if you click on either genre. Searching is discussed in more detail below.
Another thing you can see from the image is that the genre Military appears twice, once under History and once under Science Fiction. Because the genres are in a hierarchy, these are two separate genres. A book can be in one, the other, or (doubtfully in this case) both. For example, the books in Winston Churchill's "The Second World War" could be in "History.Military". David Weber's Honor Harrington books could be in "Science Fiction.Military", and for that matter also in "Science Fiction.Space Opera."
Once a genre exists, that is at least one book has that genre, you can easily apply it to other books by dragging the books from the library view onto the genre you want the books to have. You can also apply genres in the metadata editors; more on this below.
Setup
----------------------------------------
By now, your question might be "How was all of this up?" There are three steps: 1) create the custom column, 2) tell |app| that the new column is to be treated as a hierarchy, and 3) add genres.
You create the custom column in the usual way, using Preferences -> Add your own columns. This example uses "#genre" as the lookup name and "Genre" as the column heading. The column type is "Comma-separated text, like tags, shown in the tag browser."
.. image:: images/sg_cc.jpg
:align: center
Then after restarting |app|, you must tell |app| that the column is to be treated as a hierarchy. Go to Preferences -> Look and Feel and enter the lookup name "#genre" into the "Categories with hierarchical items" box. Press Apply, and you are done with setting up.
.. image:: images/sg_pref.jpg
:align: center
At the point there are no genres in the column. We are left with the last step: how to apply a genre to a book. A genre does not exist in |app| until it appears on at least one book. To learn how to apply a genre for the first time, we must go into some detail about what a genre looks like in the metadata for a book.
A hierarchy of 'things' is built by creating an item consisting of phrases separated by periods. Continuing the genre example, these items would "History.Military", "Mysteries.Vampire", "Science Fiction.Space Opera", etc. Thus to create a new genre, you pick a book that should have that genre, edit its metadata, and enter the new genre into the column you created. Continuing our example, if you want to assign a new genre "Comics" with a sub-genre "Superheros" to a book, you would 'edit metadata' for that (comic) book, choose the Custom metadata tab, and then enter "Comics.Superheros" as shown in the following (ignore the other custom columns):
.. image:: images/sg_genre.jpg
:align: center
After doing the above, you see in the tag browser:
.. image:: images/sg_tb.jpg
:align: center
From here on, to apply this new genre to a book (a comic book, presumably), you can either drag the book onto the genre, or add it to the book using edit metadata in exactly the same way as done above.
Searching
---------------
.. image:: images/sg_search.jpg
:align: center
The easiest way to search for genres is using the tag browser, clicking on the genre you wish to see. Clicking on a genre with children will show you books with that genre and all child genres. However, this might bring up a question. Just because a genre has children doesn't mean that it isn't a genre in its own right. For example, a book can have the genre "History" but not "History.Military". How do you search for books with only "History"?
The tag browser search mechanism knows if an item has children. If it does, clicking on the item cycles through 5 searches instead of the normal three. The first is the normal green plus, which shows you books with that genre only (e.g., History). The second is a doubled plus (shown above), which shows you books with that genre and all sub-genres (e.g., History and History.Military). The third is the normal red minus, which shows you books without that exact genre. The fourth is a doubled minus, which shows you books without that genre or sub-genres. The fifth is back to the beginning, no mark, meaning no search.
Restrictions
---------------
If you search for a genre then create a saved search for it, you can use the 'restrict to' box to create a virtual library of books with that genre. This is useful if you want to do other searches within the genre or to manage/update metadata for books in the genre. Continuing our example, you can create a saved search named 'History.Japanese' by first clicking on the genre Japanese in the tag browser to get a search into the search box, entering History.Japanese into the saved search box, then pushing the "save search" button (the green box with the white plus, on the right-hand side).
.. image:: images/sg_restrict.jpg
:align: center
After creating the saved search, you can use it as a restriction.
.. image:: images/sg_restrict2.jpg
:align: center
Useful Template Functions
-------------------------
You might want to use the genre information in a template, such as with save to disk or send to device. The question might then be "How do I get the outermost genre name or names?" An |app| template function, subitems, is provided to make doing this easier.
For example, assume you want to add the outermost genre level to the save-to-disk template to make genre folders, as in "History/The Gathering Storm - Churchill, Winston". To do this, you must extract the first level of the hierarchy and add it to the front along with a slash to indicate that it should make a folder. The template below accomplishes this::
{#genre:subitems(0,1)||/}{title} - {authors}
See :ref:`The |app| template language <templatelangcalibre>` for more information templates and the subitem function.

View File

@ -112,6 +112,8 @@ Functions are always applied before format specifications. See further down for
The syntax for using functions is ``{field:function(arguments)}``, or ``{field:function(arguments)|prefix|suffix}``. Arguments are separated by commas. Commas inside arguments must be preceeded by a backslash ( '\\' ). The last (or only) argument cannot contain a closing parenthesis ( ')' ). Functions return the value of the field used in the template, suitably modified. The syntax for using functions is ``{field:function(arguments)}``, or ``{field:function(arguments)|prefix|suffix}``. Arguments are separated by commas. Commas inside arguments must be preceeded by a backslash ( '\\' ). The last (or only) argument cannot contain a closing parenthesis ( ')' ). Functions return the value of the field used in the template, suitably modified.
If you have programming experience, please note that the syntax in this mode (single function) is not what you might expect. Strings are not quoted. Spaces are significant. All arguments must be constants; there is no sub-evaluation. Use :ref:`template program mode <template_mode>` and :ref:`general program mode <general_mode>` to avoid these differences.
The functions available are: The functions available are:
* ``lowercase()`` -- return value of the field in lower case. * ``lowercase()`` -- return value of the field in lower case.
@ -127,10 +129,25 @@ The functions available are:
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want. * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
* ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later). * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``select(key)`` -- interpret the field as a comma-separated list of items, with the items being of the form "id:value". Find the pair with the id equal to key, and return the corresponding value. This function is particularly useful for extracting a value such as an isbn from the set of identifiers for a book. * ``select(key)`` -- interpret the field as a comma-separated list of items, with the items being of the form "id:value". Find the pair with the id equal to key, and return the corresponding value. This function is particularly useful for extracting a value such as an isbn from the set of identifiers for a book.
* ``subitems(val, start_index, end_index)`` -- This function is used to break apart lists of tag-like hierarchical items such as genres. It interprets the value as a comma-separated list of tag-like items, where each item is a period-separated list. Returns a new list made by first finding all the period-separated tag-like items, then for each such item extracting the components from `start_index` to `end_index`, then combining the results back together. The first component in a period-separated list has an index of zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples::
Assuming a #genre column containing "A.B.C":
{#genre:subitems(0,1)} returns "A"
{#genre:subitems(0,2)} returns "A.B"
{#genre:subitems(1,0)} returns "B.C"
Assuming a #genre column containing "A.B.C, D.E":
{#genre:subitems(0,1)} returns "A, D"
{#genre:subitems(0,2)} returns "A.B, D.E"
* ``sublist(val, start_index, end_index, separator)`` -- interpret the value as a list of items separated by `separator`, returning a new list made from the items from `start_index`to `end_index`. The first item is number zero. If an index is negative, then it counts from the end of the list. As a special case, an end_index of zero is assumed to be the length of the list. Examples assuming that the tags column (which is comma-separated) contains "A, B ,C"::
{tags:sublist(0,1,\,)} returns "A"
{tags:sublist(-1,0,\,)} returns "C"
{tags:sublist(0,-1,\,)} returns "A, B"
* ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`. * ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`.
Now, what about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
{#myint:0>3s:ifempty(0)} {#myint:0>3s:ifempty(0)}
@ -138,6 +155,7 @@ 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)|[|]}
.. _template_mode:
Using functions in templates - template program mode Using functions in templates - template program mode
---------------------------------------------------- ----------------------------------------------------
@ -238,6 +256,8 @@ The following functions are available in addition to those described in single-f
* ``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.
.. _general_mode:
Using general program mode Using general program mode
----------------------------------- -----------------------------------

View File

@ -12,6 +12,7 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
:maxdepth: 1 :maxdepth: 1
news news
sub_groups
xpath xpath
template_lang template_lang
regexp regexp

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More