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