Merge
111
Changelog.yaml
@ -19,6 +19,117 @@
|
||||
# new recipes:
|
||||
# - 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
|
||||
date: 2011-03-04
|
||||
|
||||
|
BIN
resources/images/news/avantaje.png
Normal file
After Width: | Height: | Size: 924 B |
BIN
resources/images/news/onemagazine.png
Normal file
After Width: | Height: | Size: 316 B |
BIN
resources/images/news/pcworldro.png
Normal file
After Width: | Height: | Size: 386 B |
BIN
resources/images/news/protvmagazin.png
Normal file
After Width: | Height: | Size: 251 B |
BIN
resources/images/news/psychologies.png
Normal file
After Width: | Height: | Size: 750 B |
BIN
resources/images/news/publika.png
Normal file
After Width: | Height: | Size: 290 B |
BIN
resources/images/news/tvmania.png
Normal file
After Width: | Height: | Size: 379 B |
BIN
resources/images/news/viva.png
Normal file
After Width: | Height: | Size: 747 B |
57
resources/recipes/avantaje.recipe
Normal 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)
|
46
resources/recipes/bay_citizen.recipe
Normal 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
|
@ -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'
|
||||
language = 'de'
|
||||
__author__ = 'Kovid Goyal'
|
||||
oldest_article = 7
|
||||
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;}
|
||||
'''
|
||||
|
@ -11,6 +11,26 @@ class AdvancedUserRecipe1259599587(BasicNewsRecipe):
|
||||
|
||||
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}
|
||||
'''
|
||||
|
@ -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.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Instapaper(BasicNewsRecipe):
|
||||
title = 'Instapaper.com'
|
||||
class AdvancedUserRecipe1299694372(BasicNewsRecipe):
|
||||
title = u'Instapaper'
|
||||
__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'
|
||||
category = 'news, custom'
|
||||
oldest_article = 7
|
||||
category = 'info, custom, Instapaper'
|
||||
oldest_article = 365
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
@ -25,16 +14,9 @@ class Instapaper(BasicNewsRecipe):
|
||||
INDEX = u'http://www.instapaper.com'
|
||||
LOGIN = INDEX + u'/user/login'
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
}
|
||||
|
||||
feeds = [
|
||||
(u'Unread articles' , INDEX + u'/u' )
|
||||
,(u'Starred articles', INDEX + u'/starred')
|
||||
]
|
||||
|
||||
feeds = [(u'Instapaper Unread', u'http://www.instapaper.com/u'), (u'Instapaper Starred', u'http://www.instapaper.com/starred')]
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
@ -70,7 +52,3 @@ class Instapaper(BasicNewsRecipe):
|
||||
})
|
||||
totalfeeds.append((feedtitle, articles))
|
||||
return totalfeeds
|
||||
|
||||
def print_version(self, url):
|
||||
return self.INDEX + '/text?u=' + urllib.quote(url)
|
||||
|
||||
|
42
resources/recipes/jbpress.recipe
Normal 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/')
|
@ -17,6 +17,7 @@ class Lanacion(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
no_stylesheets = True
|
||||
language = 'es_AR'
|
||||
delay = 14
|
||||
publication_type = 'newspaper'
|
||||
remove_empty_feeds = True
|
||||
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}
|
||||
body{font-family: Arial,sans-serif}
|
||||
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}
|
||||
.topNota h1{font-family: Arial,sans-serif}
|
||||
"""
|
||||
@ -38,7 +39,10 @@ class Lanacion(BasicNewsRecipe):
|
||||
, '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 = [
|
||||
dict(name='div' , attrs={'class':'notaComentario floatFix noprint' })
|
||||
@ -52,8 +56,7 @@ class Lanacion(BasicNewsRecipe):
|
||||
remove_attributes = ['height','width','visible','onclick','data-count','name']
|
||||
|
||||
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'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' )
|
||||
@ -81,16 +84,11 @@ 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):
|
||||
link = BasicNewsRecipe.get_article_url(self,article)
|
||||
if link.startswith('http://blogs.lanacion') and not link.endswith('/'):
|
||||
return self.browser.open_novisit(link).geturl()
|
||||
if link.rfind('galeria=') > 0:
|
||||
return None
|
||||
return link
|
||||
|
||||
|
33
resources/recipes/nbonline.recipe
Normal 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'
|
23
resources/recipes/oakland_north.recipe
Normal 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/')]
|
72
resources/recipes/onemagazine.recipe
Normal 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)
|
67
resources/recipes/pcworldro.recipe
Normal 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)
|
71
resources/recipes/protvmagazin.recipe
Normal 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)
|
59
resources/recipes/psychologies.recipe
Normal 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)
|
54
resources/recipes/publika.recipe
Normal 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)
|
56
resources/recipes/sltrib.py
Normal 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
|
||||
|
@ -3,6 +3,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class AdvancedUserRecipe1299054026(BasicNewsRecipe):
|
||||
title = u'Thai Post Daily'
|
||||
__author__ = 'Chotechai P.'
|
||||
language = 'th'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
cover_url = 'http://upload.wikimedia.org/wikipedia/th/1/10/ThaiPost_Logo.png'
|
||||
|
72
resources/recipes/tvmania.recipe
Normal 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)
|
75
resources/recipes/viva.recipe
Normal 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)
|
@ -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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.48'
|
||||
__version__ = '0.7.49'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -92,7 +92,7 @@ class TXT2TXTZ(FileTypePlugin):
|
||||
'containing Markdown or Textile references to images. The referenced '
|
||||
'images as well as the TXT file are added to the archive.')
|
||||
version = numeric_version
|
||||
file_types = set(['txt'])
|
||||
file_types = set(['txt', 'text'])
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
on_import = True
|
||||
|
||||
|
@ -35,7 +35,7 @@ class ANDROID(USBMS):
|
||||
# Motorola
|
||||
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
|
||||
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
|
||||
0x4286 : [0x216], 0x42b3 : [0x216] },
|
||||
0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216] },
|
||||
|
||||
# Sony Ericsson
|
||||
0xfce : { 0xd12e : [0x0100]},
|
||||
@ -96,7 +96,8 @@ class ANDROID(USBMS):
|
||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
||||
'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',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7']
|
||||
|
@ -224,7 +224,7 @@ class TREKSTOR(USBMS):
|
||||
FORMATS = ['epub', 'txt', 'pdf']
|
||||
|
||||
VENDOR_ID = [0x1e68]
|
||||
PRODUCT_ID = [0x0041]
|
||||
PRODUCT_ID = [0x0041, 0x0042]
|
||||
BCD = [0x0002]
|
||||
|
||||
EBOOK_DIR_MAIN = 'Ebooks'
|
||||
|
@ -72,7 +72,7 @@ class FB2MLizer(object):
|
||||
|
||||
def clean_text(self, text):
|
||||
# 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.
|
||||
text = re.sub(r'(?miu)<p>\s*</p>', '', text)
|
||||
# Clean up pargraph endings.
|
||||
@ -101,9 +101,6 @@ class FB2MLizer(object):
|
||||
|
||||
def fb2_header(self):
|
||||
metadata = {}
|
||||
metadata['author_first'] = u''
|
||||
metadata['author_middle'] = u''
|
||||
metadata['author_last'] = u''
|
||||
metadata['title'] = self.oeb_book.metadata.title[0].value
|
||||
metadata['appname'] = __appname__
|
||||
metadata['version'] = __version__
|
||||
@ -115,16 +112,36 @@ class FB2MLizer(object):
|
||||
metadata['id'] = None
|
||||
metadata['cover'] = self.get_cover()
|
||||
|
||||
author_parts = self.oeb_book.metadata.creator[0].value.split(' ')
|
||||
metadata['author'] = u''
|
||||
for auth in self.oeb_book.metadata.creator:
|
||||
author_first = u''
|
||||
author_middle = u''
|
||||
author_last = u''
|
||||
author_parts = auth.value.split(' ')
|
||||
if len(author_parts) == 1:
|
||||
metadata['author_last'] = author_parts[0]
|
||||
author_last = author_parts[0]
|
||||
elif len(author_parts) == 2:
|
||||
metadata['author_first'] = author_parts[0]
|
||||
metadata['author_last'] = author_parts[1]
|
||||
author_first = author_parts[0]
|
||||
author_last = author_parts[1]
|
||||
else:
|
||||
metadata['author_first'] = author_parts[0]
|
||||
metadata['author_middle'] = ' '.join(author_parts[1:-2])
|
||||
metadata['author_last'] = author_parts[-1]
|
||||
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']
|
||||
for x in identifiers:
|
||||
@ -136,28 +153,21 @@ class FB2MLizer(object):
|
||||
metadata['id'] = str(uuid.uuid4())
|
||||
|
||||
for key, value in metadata.items():
|
||||
if not key == 'cover':
|
||||
if key not in ('author', 'cover', 'sequence'):
|
||||
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">' \
|
||||
'<description>' \
|
||||
'<title-info>' \
|
||||
'<genre>antique</genre>' \
|
||||
'<author>' \
|
||||
'<first-name>%(author_first)s</first-name>' \
|
||||
'<middle-name>%(author_middle)s</middle-name>' \
|
||||
'<last-name>%(author_last)s</last-name>' \
|
||||
'</author>' \
|
||||
'%(author)s' \
|
||||
'<book-title>%(title)s</book-title>' \
|
||||
'%(cover)s' \
|
||||
'<lang>%(lang)s</lang>' \
|
||||
'%(sequence)s' \
|
||||
'</title-info>' \
|
||||
'<document-info>' \
|
||||
'<author>' \
|
||||
'<first-name></first-name>' \
|
||||
'<middle-name></middle-name>' \
|
||||
'<last-name></last-name>' \
|
||||
'</author>' \
|
||||
'%(author)s' \
|
||||
'<program-used>%(appname)s %(version)s</program-used>' \
|
||||
'<date>%(date)s</date>' \
|
||||
'<id>%(id)s</id>' \
|
||||
|
@ -23,8 +23,9 @@ cover_url_cache = {}
|
||||
cache_lock = RLock()
|
||||
|
||||
def find_asin(br, isbn):
|
||||
q = 'http://www.amazon.com/s?field-keywords='+isbn
|
||||
raw = br.open_novisit(q).read()
|
||||
q = 'http://www.amazon.com/s/?search-alias=aps&field-keywords='+isbn
|
||||
res = br.open_novisit(q)
|
||||
raw = res.read()
|
||||
raw = xml_to_unicode(raw, strip_encoding_pats=True,
|
||||
resolve_entities=True)[0]
|
||||
root = html.fromstring(raw)
|
||||
@ -151,6 +152,8 @@ def get_metadata(br, asin, mi):
|
||||
root = soupparser.fromstring(raw)
|
||||
except:
|
||||
return False
|
||||
if root.xpath('//*[@id="errorMessage"]'):
|
||||
return False
|
||||
ratings = root.xpath('//form[@id="handleBuy"]/descendant::*[@class="asinReviewsSummary"]')
|
||||
if ratings:
|
||||
pat = re.compile(r'([0-9.]+) out of (\d+) stars')
|
||||
@ -191,6 +194,7 @@ def main(args=sys.argv):
|
||||
tdir = tempfile.gettempdir()
|
||||
br = browser()
|
||||
for title, isbn in [
|
||||
('The Heroes', '9780316044981'), # Test find_asin
|
||||
('Learning Python', '8324616489'), # Test xisbn
|
||||
('Angels & Demons', '9781416580829'), # Test sophisticated comment formatting
|
||||
# Random tests
|
||||
@ -207,8 +211,12 @@ def main(args=sys.argv):
|
||||
|
||||
#import 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'
|
||||
|
||||
return 0
|
||||
|
||||
|
@ -92,6 +92,8 @@ class Metadata(object):
|
||||
def is_null(self, field):
|
||||
null_val = NULL_VALUES.get(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
|
||||
|
||||
def __getattribute__(self, field):
|
||||
@ -169,10 +171,13 @@ class Metadata(object):
|
||||
pass
|
||||
return default
|
||||
|
||||
def get_extra(self, field):
|
||||
def get_extra(self, field, default=None):
|
||||
_data = object.__getattribute__(self, '_data')
|
||||
if field in _data['user_metadata'].iterkeys():
|
||||
try:
|
||||
return _data['user_metadata'][field]['#extra#']
|
||||
except:
|
||||
return default
|
||||
raise AttributeError(
|
||||
'Metadata object has no attribute named: '+ repr(field))
|
||||
|
||||
|
@ -74,6 +74,8 @@ class HeadRequest(mechanize.Request):
|
||||
class OpenLibraryCovers(CoverDownload): # {{{
|
||||
'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'
|
||||
name = 'openlibrary.org covers'
|
||||
description = _('Download covers from openlibrary.org')
|
||||
@ -82,7 +84,8 @@ class OpenLibraryCovers(CoverDownload): # {{{
|
||||
def has_cover(self, mi, ans, timeout=5.):
|
||||
if not mi.isbn:
|
||||
return False
|
||||
br = browser()
|
||||
from calibre.ebooks.metadata.library_thing import get_browser
|
||||
br = get_browser()
|
||||
br.set_handle_redirect(False)
|
||||
try:
|
||||
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.):
|
||||
if not mi.isbn:
|
||||
return
|
||||
br = browser()
|
||||
from calibre.ebooks.metadata.library_thing import get_browser
|
||||
br = get_browser()
|
||||
try:
|
||||
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
|
||||
result_queue.put((True, ans, 'jpg', self.name))
|
||||
@ -137,6 +141,8 @@ class AmazonCovers(CoverDownload): # {{{
|
||||
br = browser()
|
||||
try:
|
||||
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()
|
||||
result_queue.put((True, cover_data, 'jpg', self.name))
|
||||
except Exception, e:
|
||||
|
@ -908,6 +908,19 @@ class Manifest(object):
|
||||
pass
|
||||
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
|
||||
if barename(data.tag) != 'html':
|
||||
self.oeb.log.warn('File %r does not appear to be (X)HTML'%self.href)
|
||||
|
@ -65,7 +65,6 @@ class TXTInput(InputFormatPlugin):
|
||||
txt = ''
|
||||
log.debug('Reading text from file...')
|
||||
length = 0
|
||||
# [(u'path', mime),]
|
||||
|
||||
# Extract content from zip archive.
|
||||
if file_ext == 'txtz':
|
||||
@ -73,7 +72,7 @@ class TXTInput(InputFormatPlugin):
|
||||
zf.extractall('.')
|
||||
|
||||
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:
|
||||
txt += tf.read() + '\n\n'
|
||||
else:
|
||||
|
@ -340,6 +340,7 @@ class FileIconProvider(QFileIconProvider):
|
||||
'rar' : 'rar',
|
||||
'zip' : 'zip',
|
||||
'txt' : 'txt',
|
||||
'text' : 'txt',
|
||||
'prc' : 'mobi',
|
||||
'azw' : 'mobi',
|
||||
'mobi' : 'mobi',
|
||||
|
@ -11,7 +11,6 @@ from PyQt4.Qt import QWizard, QWizardPage, QIcon, QPixmap, Qt, QThread, \
|
||||
pyqtSignal
|
||||
|
||||
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, \
|
||||
find_books_in_folder, hash_merge_format_collections
|
||||
|
||||
@ -122,20 +121,19 @@ class WelcomePage(WizardPage, WelcomeWidget):
|
||||
x = unicode(self.opt_root_folder.text()).strip()
|
||||
if not x:
|
||||
return None
|
||||
return os.path.abspath(x.encode(filesystem_encoding))
|
||||
return os.path.abspath(x)
|
||||
|
||||
def get_one_per_folder(self):
|
||||
return self.opt_one_per_folder.isChecked()
|
||||
|
||||
def validatePage(self):
|
||||
x = self.get_root_folder()
|
||||
xu = x.decode(filesystem_encoding)
|
||||
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()
|
||||
return True
|
||||
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
|
||||
|
||||
# }}}
|
||||
|
@ -6,6 +6,8 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
|
||||
from PyQt4.Qt import QLineEdit, QTextEdit
|
||||
|
||||
from calibre.gui2.convert.search_and_replace_ui import Ui_Form
|
||||
from calibre.gui2.convert import Widget
|
||||
from calibre.gui2 import error_dialog
|
||||
@ -72,3 +74,13 @@ class SearchAndReplaceWidget(Widget, Ui_Form):
|
||||
_('Invalid regular expression: %s')%err, show=True)
|
||||
return False
|
||||
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)
|
||||
|
@ -1013,11 +1013,13 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
||||
query['search_field'] = unicode(self.search_field.currentText())
|
||||
query['search_mode'] = unicode(self.search_mode.currentText())
|
||||
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['case_sensitive'] = self.case_sensitive.isChecked()
|
||||
query['replace_with'] = unicode(self.replace_with.text())
|
||||
query['replace_func'] = unicode(self.replace_func.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['comma_separated'] = self.comma_separated.isChecked()
|
||||
query['results_count'] = self.results_count.value()
|
||||
@ -1044,37 +1046,61 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
|
||||
self.s_r_reset_query_fields()
|
||||
return
|
||||
|
||||
def set_index(attr, txt):
|
||||
def set_text(attr, key):
|
||||
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:
|
||||
attr.setCurrentIndex(0)
|
||||
|
||||
set_index(self.search_mode, item['search_mode'])
|
||||
set_index(self.search_field, item['search_field'])
|
||||
self.s_r_template.setText(item['s_r_template'])
|
||||
set_index(self.search_mode, 'search_mode')
|
||||
set_index(self.search_field, 'search_field')
|
||||
set_text(self.s_r_template, 's_r_template')
|
||||
|
||||
self.s_r_template_changed() #simulate gain/loss of focus
|
||||
self.search_for.setText(item['search_for'])
|
||||
self.case_sensitive.setChecked(item['case_sensitive'])
|
||||
self.replace_with.setText(item['replace_with'])
|
||||
set_index(self.replace_func, item['replace_func'])
|
||||
set_index(self.destination_field, item['destination_field'])
|
||||
set_index(self.replace_mode, item['replace_mode'])
|
||||
self.comma_separated.setChecked(item['comma_separated'])
|
||||
self.results_count.setValue(int(item['results_count']))
|
||||
self.starting_from.setValue(int(item['starting_from']))
|
||||
self.multiple_separator.setText(item['multiple_separator'])
|
||||
|
||||
set_index(self.s_r_src_ident, 's_r_src_ident');
|
||||
set_text(self.s_r_dst_ident, 's_r_dst_ident')
|
||||
set_text(self.search_for, 'search_for')
|
||||
set_checked(self.case_sensitive, 'case_sensitive')
|
||||
set_text(self.replace_with, 'replace_with')
|
||||
set_index(self.replace_func, 'replace_func')
|
||||
set_index(self.destination_field, 'destination_field')
|
||||
set_index(self.replace_mode, 'replace_mode')
|
||||
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):
|
||||
# Don't reset the search mode. The user will probably want to use it
|
||||
# as it was
|
||||
self.search_field.setCurrentIndex(0)
|
||||
self.s_r_src_ident.setCurrentIndex(0)
|
||||
self.s_r_template.setText("")
|
||||
self.search_for.setText("")
|
||||
self.case_sensitive.setChecked(False)
|
||||
self.replace_with.setText("")
|
||||
self.replace_func.setCurrentIndex(0)
|
||||
self.destination_field.setCurrentIndex(0)
|
||||
self.s_r_dst_ident.setText('')
|
||||
self.replace_mode.setCurrentIndex(0)
|
||||
self.comma_separated.setChecked(True)
|
||||
self.results_count.setValue(999)
|
||||
|
@ -12,7 +12,7 @@ from threading import Thread
|
||||
|
||||
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
|
||||
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \
|
||||
QPushButton
|
||||
QPushButton, QKeySequence
|
||||
|
||||
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
|
||||
choose_files, choose_images, ResizableDialog, \
|
||||
@ -472,17 +472,19 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
|
||||
self)
|
||||
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.clicked.connect(partial(self.next_triggered,
|
||||
-1))
|
||||
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
|
||||
if next_:
|
||||
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
|
||||
self)
|
||||
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.clicked.connect(partial(self.next_triggered, 1))
|
||||
self.next_button.setShortcut(QKeySequence('Alt+Right'))
|
||||
|
||||
self.splitter.setStretchFactor(100, 1)
|
||||
self.read_state()
|
||||
|
@ -11,7 +11,7 @@ from functools import partial
|
||||
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \
|
||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \
|
||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \
|
||||
QSizePolicy, QPalette, QFrame, QSize
|
||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||
from calibre.gui2 import ResizableDialog, error_dialog, gprefs
|
||||
@ -45,9 +45,12 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
|
||||
self)
|
||||
self.next_button.setShortcut(QKeySequence('Alt+Right'))
|
||||
self.next_button.clicked.connect(partial(self.do_one, delta=1))
|
||||
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
|
||||
self)
|
||||
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
|
||||
|
||||
self.button_box.addButton(self.prev_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))
|
||||
@ -355,11 +358,13 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
next_ = self.db.title(self.row_list[self.current_row+1])
|
||||
|
||||
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.setVisible(next_ 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.setVisible(prev is not None)
|
||||
self(self.db.id(self.row_list[self.current_row]))
|
||||
|
@ -169,6 +169,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
col = unicode(self.column_name_box.text()).strip()
|
||||
if not col:
|
||||
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:
|
||||
return self.simple_error('', _('The lookup name must contain only '
|
||||
'lower case letters, digits and underscores, and start with a letter'))
|
||||
|
@ -55,6 +55,10 @@ class BaseModel(QAbstractListModel):
|
||||
text = _('Choose library')
|
||||
return QVariant(text)
|
||||
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]
|
||||
if ic is None:
|
||||
ic = 'blank.png'
|
||||
|
@ -92,7 +92,8 @@ class SendEmail(QWidget, Ui_Form):
|
||||
pa = self.preferred_to_address()
|
||||
to_set = pa is not None
|
||||
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'
|
||||
'. Is it OK to proceed?'), show_copy_button=False):
|
||||
TestEmail(pa, self).exec_()
|
||||
@ -204,10 +205,24 @@ class SendEmail(QWidget, Ui_Form):
|
||||
username = unicode(self.relay_username.text()).strip()
|
||||
password = unicode(self.relay_password.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'
|
||||
if self.relay_ssl.isChecked() else 'NONE')
|
||||
if host:
|
||||
# Validate input
|
||||
if ((username and not password) or (not username and password)):
|
||||
error_dialog(self, _('Bad configuration'),
|
||||
_('You must set the username and password for '
|
||||
'the mail server.')).exec_()
|
||||
_('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.set('from_', from_)
|
||||
@ -215,8 +230,7 @@ class SendEmail(QWidget, Ui_Form):
|
||||
conf.set('relay_port', self.relay_port.value())
|
||||
conf.set('relay_username', username if username else None)
|
||||
conf.set('relay_password', hexlify(password))
|
||||
conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL'
|
||||
if self.relay_ssl.isChecked() else 'NONE')
|
||||
conf.set('encryption', enc_method)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -8,7 +8,6 @@ __docformat__ = 'restructuredtext en'
|
||||
import os
|
||||
from hashlib import sha1
|
||||
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
|
||||
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
|
||||
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
|
||||
infinite recursion.
|
||||
|
||||
cancel_callback must be a no argument callable that returns True to cancel
|
||||
the search
|
||||
'''
|
||||
assert not isinstance(root, unicode) # root must be in filesystem encoding
|
||||
lp = db.library_path
|
||||
if isinstance(lp, unicode):
|
||||
try:
|
||||
lp = lp.encode(filesystem_encoding)
|
||||
except:
|
||||
lp = None
|
||||
if lp:
|
||||
lp = os.path.abspath(lp)
|
||||
|
||||
|
@ -147,6 +147,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
def __init__(self, library_path, row_factory=False, default_prefs=None,
|
||||
read_only=False):
|
||||
try:
|
||||
if isbytestring(library_path):
|
||||
library_path = library_path.decode(filesystem_encoding)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
self.field_metadata = FieldMetadata()
|
||||
self._library_id_ = None
|
||||
# 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.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH',
|
||||
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):
|
||||
# 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)
|
||||
if not authors:
|
||||
authors = _('Unknown')
|
||||
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||
author = ascii_filename(authors.split(',')[0]
|
||||
)[: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 (' ', '.'):
|
||||
author = author[:-1]
|
||||
if not author:
|
||||
author = ascii_filename(_('Unknown')).decode(filesystem_encoding, 'replace')
|
||||
author = ascii_filename(_('Unknown')).decode(
|
||||
'ascii', 'replace')
|
||||
path = author + '/' + title + ' (%d)'%id
|
||||
return path
|
||||
|
||||
@ -505,8 +511,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
authors = self.authors(id, index_is_id=True)
|
||||
if not authors:
|
||||
authors = _('Unknown')
|
||||
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||
author = ascii_filename(authors.split(',')[0]
|
||||
)[: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
|
||||
while name.endswith('.'):
|
||||
name = name[:-1]
|
||||
@ -1682,8 +1690,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.notify('metadata', [id])
|
||||
return books_to_refresh
|
||||
|
||||
def set_metadata(self, id, mi, ignore_errors=False,
|
||||
set_title=True, set_authors=True, commit=True):
|
||||
def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
|
||||
set_authors=True, commit=True, force_cover=False,
|
||||
force_tags=False):
|
||||
'''
|
||||
Set metadata for the book `id` from the `Metadata` object `mi`
|
||||
'''
|
||||
@ -1699,13 +1708,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
traceback.print_exc()
|
||||
else:
|
||||
raise
|
||||
# force_changes has no role to play in setting title or author
|
||||
path_changed = False
|
||||
if set_title and mi.title:
|
||||
if set_title and not mi.is_null('title'):
|
||||
self._set_title(id, mi.title)
|
||||
path_changed = True
|
||||
if set_authors:
|
||||
if not mi.authors:
|
||||
mi.authors = [_('Unknown')]
|
||||
if set_authors and not mi.is_null('authors'):
|
||||
authors = []
|
||||
for a in mi.authors:
|
||||
authors += string_to_authors(a)
|
||||
@ -1713,16 +1721,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
path_changed = True
|
||||
if path_changed:
|
||||
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,
|
||||
commit=False)
|
||||
if mi.publisher:
|
||||
if not mi.is_null('publisher'):
|
||||
doit(self.set_publisher, id, mi.publisher, notify=False,
|
||||
commit=False)
|
||||
if mi.rating:
|
||||
if not mi.is_null('rating'):
|
||||
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)
|
||||
|
||||
if mi.cover_data[1] is not None:
|
||||
doit(self.set_cover, id, mi.cover_data[1], commit=False)
|
||||
elif mi.cover is not None:
|
||||
@ -1731,14 +1741,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
raw = f.read()
|
||||
if raw:
|
||||
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)
|
||||
if mi.comments:
|
||||
if not mi.is_null('comments'):
|
||||
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,
|
||||
commit=False)
|
||||
if mi.pubdate:
|
||||
if not mi.is_null('pubdate'):
|
||||
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
|
||||
if getattr(mi, 'timestamp', None) is not None:
|
||||
doit(self.set_timestamp, id, mi.timestamp, notify=False,
|
||||
@ -1748,19 +1761,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
if mi_idents:
|
||||
identifiers = self.get_identifiers(id, index_is_id=True)
|
||||
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
|
||||
self.set_identifiers(id, identifiers, notify=False, commit=False)
|
||||
|
||||
|
||||
user_mi = mi.get_all_user_metadata(make_copy=False)
|
||||
for key in user_mi.iterkeys():
|
||||
if key in self.field_metadata and \
|
||||
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
|
||||
doit(self.set_custom, id,
|
||||
val=mi.get(key),
|
||||
extra=mi.get_extra(key),
|
||||
label=user_mi[key]['label'], commit=False)
|
||||
doit(self.set_custom, id, val=mi.get(key), commit=False,
|
||||
extra=mi.get_extra(key), label=user_mi[key]['label'])
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
self.notify('metadata', [id])
|
||||
@ -2350,6 +2360,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
@param tags: list of strings
|
||||
@param append: If True existing tags are not removed
|
||||
'''
|
||||
if not tags:
|
||||
tags = []
|
||||
if not append:
|
||||
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
|
||||
self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id)
|
||||
@ -2500,6 +2512,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.notify('metadata', [id])
|
||||
|
||||
def set_rating(self, id, rating, notify=True, commit=True):
|
||||
if not rating:
|
||||
rating = 0
|
||||
rating = int(rating)
|
||||
self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,))
|
||||
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):
|
||||
self.conn.execute('DELETE FROM comments WHERE book=?', (id,))
|
||||
if text:
|
||||
self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
|
||||
else:
|
||||
text = ''
|
||||
if commit:
|
||||
self.conn.commit()
|
||||
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])
|
||||
|
||||
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.dirtied([id], commit=False)
|
||||
if commit:
|
||||
@ -2594,6 +2613,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
def set_identifiers(self, id_, identifiers, notify=True, commit=True):
|
||||
cleaned = {}
|
||||
if not identifiers:
|
||||
identifiers = {}
|
||||
for typ, val in identifiers.iteritems():
|
||||
typ, val = self._clean_identifier(typ, val)
|
||||
if val:
|
||||
|
@ -55,6 +55,8 @@ Add books
|
||||
|
||||
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.
|
||||
See the :ref:`config_filename_metadata` section, to learn how to configure this.
|
||||
|
||||
@ -338,9 +340,7 @@ You can build advanced search queries easily using the :guilabel:`Advanced Searc
|
||||
clicking the button |sbi|.
|
||||
|
||||
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
|
||||
``ondevice`` field becomes available. To find the search name for a custom column, hover your mouse over the
|
||||
column header.
|
||||
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.
|
||||
|
||||
The syntax for searching for dates is::
|
||||
|
||||
@ -387,17 +387,17 @@ 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`.
|
||||
|
||||
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:false`` will find books with no identifier.
|
||||
* ``identifiers:123`` will search for books with any key having a value containing `123`.
|
||||
* ``identifiers:=123456789`` will search for books with any key 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:false`` will find books with no key equal to isbn.
|
||||
* ``identifiers:=isbn:123`` will find books with a key 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:i:1`` will find books with a key containing an `i` having a value containing a `1`.
|
||||
* ``identifiers:123`` will search for books with any type having a value containing `123`.
|
||||
* ``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 type equal to isbn having any value
|
||||
* ``identifiers:=isbn:false`` will find books with no type equal to isbn.
|
||||
* ``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 type equal to isbn having a value equal to `123456789`.
|
||||
* ``identifiers:i:1`` will find books with a type containing an `i` having a value containing a `1`.
|
||||
|
||||
|
||||
.. |sbi| image:: images/search_button.png
|
||||
@ -461,7 +461,7 @@ 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.
|
||||
|
||||
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".
|
||||
|
||||
|
BIN
src/calibre/manual/images/sg_cc.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
src/calibre/manual/images/sg_genre.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
src/calibre/manual/images/sg_pref.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
src/calibre/manual/images/sg_restrict.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/calibre/manual/images/sg_restrict2.jpg
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/calibre/manual/images/sg_search.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/calibre/manual/images/sg_tb.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/calibre/manual/images/sg_tree.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
117
src/calibre/manual/sub_groups.rst
Normal 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.
|
@ -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.
|
||||
|
||||
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:
|
||||
|
||||
* ``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.
|
||||
* ``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.
|
||||
* ``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`.
|
||||
|
||||
|
||||
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::
|
||||
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::
|
||||
|
||||
{#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)|[|]}
|
||||
|
||||
.. _template_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.
|
||||
* ``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
|
||||
-----------------------------------
|
||||
|
||||
|
@ -12,6 +12,7 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
|
||||
:maxdepth: 1
|
||||
|
||||
news
|
||||
sub_groups
|
||||
xpath
|
||||
template_lang
|
||||
regexp
|
||||
|