mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge
This commit is contained in:
commit
1b6a4837aa
@ -19,6 +19,85 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
# - title: "Launch of a new website that catalogues DRM free books. http://drmfree.calibre-ebook.com"
|
||||
# description: "A growing catalogue of DRM free books. Books that you actually own after buying instead of renting."
|
||||
# type: major
|
||||
|
||||
- version: 0.7.47
|
||||
date: 2011-02-25
|
||||
|
||||
new features:
|
||||
- title: "Tag Browser: Support the creation of nested User Categories"
|
||||
description: "See http://calibre-ebook.com/user_manual/gui.html#tag-browser for details"
|
||||
type: major
|
||||
|
||||
- title: "Disable Kent District Library plugin to download series information. The website could not handle the load calibre's 2 million users put on it. You can manually re-enable it if you really want series information, but it is very slow"
|
||||
|
||||
- title: "Drivers for the Wexler T7001, Archos 7, Wink and Xperia X10"
|
||||
|
||||
- title: "Comic Input: Add option to not add links to individual pages to the Table of Contents when converting CBC files"
|
||||
|
||||
- title: "EPUB Output: Try to ensure that the cover image always has an id='cover' to workaround Nook cover reading bug."
|
||||
tickets: [8182]
|
||||
|
||||
- title: "ODT input: Update odfpy library to latest version, adds support for bookmarks"
|
||||
|
||||
- title: "EPUB Output: Remove unnecessary CSS page breaks as they confuse the latest release of iBooks"
|
||||
|
||||
bug fixes:
|
||||
- title: "Fix regression in 0.7.46 that broke creating date and composite custom columns"
|
||||
|
||||
- title: "Linux binary build: Fix ImageMagick trying to load system modules instead of bundled modules"
|
||||
|
||||
- title: "Kobo driver: Handle missing firmware version file"
|
||||
|
||||
- title: "ODT Input: Do not force the background color to white."
|
||||
tickets: [9118]
|
||||
|
||||
- title: "MOBI Input: Do not speciy text-align for every paragraph. Fixes text-align inheritance issues for newer MOBIs with nested divs."
|
||||
tickets: [9098]
|
||||
|
||||
- title: "EPUB Output: Do not set the file-as attribute on title elements in the OPF as the current OPF spec does not support file-as. Instead use a calibre extension to OPF."
|
||||
tickets: [9109]
|
||||
|
||||
- title: "Content server: Fix regression that broke browsing User Categories via OPDS"
|
||||
tickets: [9090]
|
||||
|
||||
- title: "Update the book details panel after adding books incase automerge is turned on and the current book is affected"
|
||||
tickets: [9073]
|
||||
|
||||
- title: "FB2 Output: Fix paragraph spacing sometime incorrect."
|
||||
tickets: [8927]
|
||||
|
||||
- title: "Tag Browser: Fix generation of search query for authors with quote characters in their names"
|
||||
tickets: [9071]
|
||||
|
||||
- title: "Fix bug that could cause download of cover/social metadata from Amazon to sometimes fail"
|
||||
|
||||
- title: "LRF Input: Workaround for broken LRF files from BookDesigner that have incomplete TextStyle elements"
|
||||
|
||||
improved recipes:
|
||||
- Le Monde
|
||||
- Gizmodo
|
||||
- Lifehacker
|
||||
- ESPN
|
||||
- Adevarul
|
||||
- gsp.ro
|
||||
- Ming Pao
|
||||
|
||||
new recipes:
|
||||
- title: "Flickr Blog"
|
||||
author: Ricardo Jurado
|
||||
|
||||
- title: "Various Romanian news sources"
|
||||
author: Silviu Coatara
|
||||
|
||||
- title: "Osnews.pl and SwiatCzytnikow"
|
||||
author: Tomasz Dlugosz
|
||||
|
||||
- title: "Roger Ebert Journal"
|
||||
author: Shane Erstad
|
||||
|
||||
- version: 0.7.46
|
||||
date: 2011-02-18
|
||||
|
||||
|
@ -108,8 +108,10 @@ function init() {
|
||||
function toplevel_layout() {
|
||||
var last = $(".toplevel li").last();
|
||||
var title = $('.toplevel h3').first();
|
||||
if (title && title.position()) {
|
||||
var bottom = last.position().top + last.height() - title.position().top;
|
||||
$("#main").height(Math.max(200, bottom+75));
|
||||
}
|
||||
}
|
||||
|
||||
function toplevel() {
|
||||
|
BIN
resources/images/minusminus.png
Normal file
BIN
resources/images/minusminus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
resources/images/news/20minutos.png
Normal file
BIN
resources/images/news/20minutos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 800 B |
BIN
resources/images/news/bucataras.png
Normal file
BIN
resources/images/news/bucataras.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 765 B |
BIN
resources/images/news/historiaro.png
Normal file
BIN
resources/images/news/historiaro.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 521 B |
BIN
resources/images/plusplus.png
Normal file
BIN
resources/images/plusplus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
68
resources/recipes/20minutos.recipe
Normal file
68
resources/recipes/20minutos.recipe
Normal file
@ -0,0 +1,68 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.20minutos.es
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class t20Minutos(BasicNewsRecipe):
|
||||
title = '20 Minutos'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Diario de informacion general y local mas leido de Espania, noticias de ultima hora de Espania, el mundo, local, deportes, noticias curiosas y mas'
|
||||
publisher = '20 Minutos Online SL'
|
||||
category = 'news, politics, Spain'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'utf8'
|
||||
use_embedded_content = True
|
||||
language = 'es'
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://estaticos.20minutos.es/css4/img/ui/logo-301x54.png'
|
||||
extra_css = """
|
||||
body{font-family: Arial,Helvetica,sans-serif }
|
||||
img{margin-bottom: 0.4em; display:block}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [dict(attrs={'class':'mf-viral'})]
|
||||
remove_attributes=['border']
|
||||
|
||||
feeds = [
|
||||
(u'Principal' , u'http://20minutos.feedsportal.com/c/32489/f/478284/index.rss')
|
||||
,(u'Cine' , u'http://20minutos.feedsportal.com/c/32489/f/478285/index.rss')
|
||||
,(u'Internacional' , u'http://20minutos.feedsportal.com/c/32489/f/492689/index.rss')
|
||||
,(u'Deportes' , u'http://20minutos.feedsportal.com/c/32489/f/478286/index.rss')
|
||||
,(u'Nacional' , u'http://20minutos.feedsportal.com/c/32489/f/492688/index.rss')
|
||||
,(u'Economia' , u'http://20minutos.feedsportal.com/c/32489/f/492690/index.rss')
|
||||
,(u'Tecnologia' , u'http://20minutos.feedsportal.com/c/32489/f/478292/index.rss')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('a'):
|
||||
limg = item.find('img')
|
||||
if item.string is not None:
|
||||
str = item.string
|
||||
item.replaceWith(str)
|
||||
else:
|
||||
if limg:
|
||||
item.name = 'div'
|
||||
item.attrs = []
|
||||
else:
|
||||
str = self.tag_to_string(item)
|
||||
item.replaceWith(str)
|
||||
for item in soup.findAll('img'):
|
||||
if not item.has_key('alt'):
|
||||
item['alt'] = 'image'
|
||||
return soup
|
||||
|
@ -32,16 +32,25 @@ class Adevarul(BasicNewsRecipe):
|
||||
}
|
||||
|
||||
keep_only_tags = [ dict(name='div', attrs={'class':'article_header'})
|
||||
,dict(name='div', attrs={'class':'bd'})
|
||||
,dict(name='div', attrs={'class':'bb-tu first-t bb-article-body'})
|
||||
]
|
||||
|
||||
|
||||
remove_tags = [ dict(name='div', attrs={'class':'bb-wg-article_related_attachements'})
|
||||
remove_tags = [
|
||||
dict(name='li', attrs={'class':'author'})
|
||||
,dict(name='li', attrs={'class':'date'})
|
||||
,dict(name='li', attrs={'class':'comments'})
|
||||
,dict(name='div', attrs={'class':'bb-wg-article_related_attachements'})
|
||||
,dict(name='div', attrs={'class':'bb-md bb-md-article_comments'})
|
||||
,dict(name='form', attrs={'id':'bb-comment-create-form'})
|
||||
,dict(name='div', attrs={'id':'mediatag'})
|
||||
,dict(name='div', attrs={'id':'ft'})
|
||||
,dict(name='div', attrs={'id':'comment_wrapper'})
|
||||
]
|
||||
|
||||
remove_tags_after = [ dict(name='form', attrs={'id':'bb-comment-create-form'}) ]
|
||||
remove_tags_after = [
|
||||
dict(name='div', attrs={'id':'comment_wrapper'}),
|
||||
]
|
||||
|
||||
feeds = [ (u'\u0218tiri', u'http://www.adevarul.ro/rss/latest') ]
|
||||
|
||||
|
56
resources/recipes/bucataras.recipe
Normal file
56
resources/recipes/bucataras.recipe
Normal file
@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = u'2011, Silviu Cotoar\u0103'
|
||||
'''
|
||||
bucataras.ro
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Bucataras(BasicNewsRecipe):
|
||||
title = u'Bucataras'
|
||||
__author__ = u'Silviu Cotoar\u0103'
|
||||
description = ''
|
||||
publisher = 'Bucataras'
|
||||
oldest_article = 5
|
||||
language = 'ro'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
category = 'Ziare,Bucatarie,Retete'
|
||||
encoding = 'utf-8'
|
||||
cover_url = 'http://www.bucataras.ro/templates/default/images/pink/logo.jpg'
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='h1', attrs={'class':'titlu'})
|
||||
, dict(name='div', attrs={'class':'contentL'})
|
||||
, dict(name='div', attrs={'class':'contentBottom'})
|
||||
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['sociale']})
|
||||
, dict(name='div', attrs={'class':['contentR']})
|
||||
, dict(name='a', attrs={'target':['_self']})
|
||||
, dict(name='div', attrs={'class':['comentarii']})
|
||||
]
|
||||
|
||||
remove_tags_after = [
|
||||
dict(name='div', attrs={'class':['comentarii']})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'Feeds', u'http://www.bucataras.ro/rss/retete/')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
56
resources/recipes/buffalo_news.recipe
Normal file
56
resources/recipes/buffalo_news.recipe
Normal file
@ -0,0 +1,56 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Todd Chapman'
|
||||
__copyright__ = 'Todd Chapman'
|
||||
__version__ = 'v0.1'
|
||||
__date__ = '26 February 2011'
|
||||
|
||||
'''
|
||||
http://www.buffalonews.com/RSS/
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1298680852(BasicNewsRecipe):
|
||||
title = u'Buffalo News'
|
||||
__author__ = 'ChappyOnIce'
|
||||
language = 'en'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 20
|
||||
encoding = 'utf-8'
|
||||
remove_javascript = True
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['main-content-left']})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':['commentCount']}),
|
||||
dict(name='div', attrs={'class':['story-list-links']})
|
||||
]
|
||||
|
||||
remove_tags_after = dict(name='div', attrs={'class':['body storyContent']})
|
||||
conversion_options = {
|
||||
'base_font_size' : 14,
|
||||
}
|
||||
feeds = [(u'City of Buffalo', u'http://www.buffalonews.com/city/communities/buffalo/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Southern Erie County', u'http://www.buffalonews.com/city/communities/southern-erie/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Eastern Erie County', u'http://www.buffalonews.com/city/communities/eastern-erie/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Southern Tier', u'http://www.buffalonews.com/city/communities/southern-tier/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Niagara County', u'http://www.buffalonews.com/city/communities/niagara-county/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Business', u'http://www.buffalonews.com/business/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'MoneySmart', u'http://www.buffalonews.com/business/moneysmart/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Bills & NFL', u'http://www.buffalonews.com/sports/bills-nfl/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Sabres & NHL', u'http://www.buffalonews.com/sports/sabres-nhl/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Bob DiCesare', u'http://www.buffalonews.com/sports/columns/bob-dicesare/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Bucky Gleason', u'http://www.buffalonews.com/sports/columns/bucky-gleason/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Mark Gaughan', u'http://www.buffalonews.com/sports/bills-nfl/inside-the-nfl/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Mike Harrington', u'http://www.buffalonews.com/sports/columns/mike-harrington/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Jerry Sullivan', u'http://www.buffalonews.com/sports/columns/jerry-sullivan/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Other Sports Columns', u'http://www.buffalonews.com/sports/columns/other-sports-columns/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Life', u'http://www.buffalonews.com/life/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Bruce Andriatch', u'http://www.buffalonews.com/city/columns/bruce-andriatch/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Donn Esmonde', u'http://www.buffalonews.com/city/columns/donn-esmonde/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Rod Watson', u'http://www.buffalonews.com/city/columns/rod-watson/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Entertainment', u'http://www.buffalonews.com/entertainment/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Off Main Street', u'http://www.buffalonews.com/city/columns/off-main-street/?widget=rssfeed&view=feed&contentId=77944'),
|
||||
(u'Editorials', u'http://www.buffalonews.com/editorial-page/buffalo-news-editorials/?widget=rssfeed&view=feed&contentId=77944')
|
||||
]
|
@ -16,14 +16,9 @@ class Deadspin(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/deadspin.com/img/logo.png'
|
||||
extra_css = '''
|
||||
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
|
||||
img{margin-bottom: 1em}
|
||||
h1{font-family :Arial,Helvetica,sans-serif; font-size:large}
|
||||
'''
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
@ -31,13 +26,11 @@ class Deadspin(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/deadspin/full')]
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/deadspin/vip?format=xml')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
27
resources/recipes/dotpod.recipe
Normal file
27
resources/recipes/dotpod.recipe
Normal file
@ -0,0 +1,27 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011-2011, Federico Escalada <fedeescalada at gmail.com>'
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class Dotpod(BasicNewsRecipe):
|
||||
__author__ = 'Federico Escalada'
|
||||
description = 'Tecnologia y Comunicacion Audiovisual'
|
||||
encoding = 'utf-8'
|
||||
language = 'es'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
oldest_article = 7
|
||||
publication_type = 'blog'
|
||||
title = 'Dotpod'
|
||||
authors = 'Federico Picone'
|
||||
|
||||
conversion_options = {
|
||||
'authors' : authors
|
||||
,'comments' : description
|
||||
,'language' : language
|
||||
}
|
||||
|
||||
feeds = [('Dotpod', 'http://www.dotpod.com.ar/feed/')]
|
||||
|
||||
remove_tags = [dict(name='div', attrs={'class':'feedflare'})]
|
||||
|
@ -41,7 +41,8 @@ class ESPN(BasicNewsRecipe):
|
||||
'''
|
||||
|
||||
|
||||
feeds = [('Top Headlines', 'http://sports.espn.go.com/espn/rss/news'),
|
||||
feeds = [
|
||||
('Top Headlines', 'http://sports.espn.go.com/espn/rss/news'),
|
||||
'http://sports.espn.go.com/espn/rss/nfl/news',
|
||||
'http://sports.espn.go.com/espn/rss/nba/news',
|
||||
'http://sports.espn.go.com/espn/rss/mlb/news',
|
||||
@ -107,10 +108,11 @@ class ESPN(BasicNewsRecipe):
|
||||
if match and 'soccernet' not in url and 'bassmaster' not in url:
|
||||
return 'http://sports.espn.go.com/espn/print?'+match.group(1)+'&type=story'
|
||||
else:
|
||||
if match and 'soccernet' in url:
|
||||
splitlist = url.split("&", 5)
|
||||
newurl = 'http://soccernet.espn.go.com/print?'+match.group(1)+'&type=story' + '&' + str(splitlist[2] )
|
||||
return newurl
|
||||
if 'soccernet' in url:
|
||||
match = re.search(r'/id/(\d+)/', url)
|
||||
if match:
|
||||
return \
|
||||
'http://soccernet.espn.go.com/print?id=%s&type=story' % match.group(1)
|
||||
#else:
|
||||
# if 'bassmaster' in url:
|
||||
# return url
|
||||
|
48
resources/recipes/flickr.recipe
Normal file
48
resources/recipes/flickr.recipe
Normal file
@ -0,0 +1,48 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Ricardo Jurado'
|
||||
__copyright__ = 'Ricardo Jurado'
|
||||
__version__ = 'v0.1'
|
||||
__date__ = '22 February 2011'
|
||||
|
||||
'''
|
||||
http://blog.flickr.net/
|
||||
'''
|
||||
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1297031650(BasicNewsRecipe):
|
||||
|
||||
title = u'Flickr Blog'
|
||||
masthead_url = 'http://flickrtheblog.files.wordpress.com/2008/11/flickblog_logo.gif'
|
||||
cover_url = 'http://flickrtheblog.files.wordpress.com/2008/11/flickblog_logo.gif'
|
||||
publisher = u''
|
||||
|
||||
__author__ = 'Ricardo Jurado'
|
||||
description = 'Pictures Blog'
|
||||
category = 'Blog,Pictures'
|
||||
|
||||
oldest_article = 120
|
||||
max_articles_per_feed = 10
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'UTF-8'
|
||||
remove_javascript = True
|
||||
language = 'en'
|
||||
|
||||
extra_css = """
|
||||
p{text-align: justify; font-size: 100%}
|
||||
body{ text-align: left; font-size:100% }
|
||||
h2{font-family: sans-serif; font-size:130%; font-weight:bold; text-align: justify; }
|
||||
.published{font-family:Arial,Helvetica,sans-serif; font-size:80%; }
|
||||
.posted{font-family:Arial,Helvetica,sans-serif; font-size:80%; }
|
||||
"""
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':'entry'})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'BLOG', u'http://feeds.feedburner.com/Flickrblog'),
|
||||
#(u'BLOG', u'http://blog.flickr.net/es/feed/atom/')
|
||||
]
|
47
resources/recipes/flickr_es.recipe
Normal file
47
resources/recipes/flickr_es.recipe
Normal file
@ -0,0 +1,47 @@
|
||||
__license__ = 'GPL v3'
|
||||
__author__ = 'Ricardo Jurado'
|
||||
__copyright__ = 'Ricardo Jurado'
|
||||
__version__ = 'v0.1'
|
||||
__date__ = '22 February 2011'
|
||||
|
||||
'''
|
||||
http://blog.flickr.net/
|
||||
'''
|
||||
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1297031650(BasicNewsRecipe):
|
||||
|
||||
title = u'Flickr Blog'
|
||||
masthead_url = 'http://flickrtheblog.files.wordpress.com/2008/11/flickblog_logo.gif'
|
||||
cover_url = 'http://flickrtheblog.files.wordpress.com/2008/11/flickblog_logo.gif'
|
||||
publisher = u''
|
||||
|
||||
__author__ = 'Ricardo Jurado'
|
||||
description = 'Pictures Blog'
|
||||
category = 'Blog,Pictures'
|
||||
|
||||
oldest_article = 120
|
||||
max_articles_per_feed = 10
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'UTF-8'
|
||||
remove_javascript = True
|
||||
language = 'es'
|
||||
|
||||
extra_css = """
|
||||
p{text-align: justify; font-size: 100%}
|
||||
body{ text-align: left; font-size:100% }
|
||||
h2{font-family: sans-serif; font-size:130%; font-weight:bold; text-align: justify; }
|
||||
.published{font-family:Arial,Helvetica,sans-serif; font-size:80%; }
|
||||
.posted{font-family:Arial,Helvetica,sans-serif; font-size:80%; }
|
||||
"""
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':'entry'})
|
||||
]
|
||||
|
||||
feeds = [
|
||||
(u'BLOG', u'http://blog.flickr.net/es/feed/atom/')
|
||||
]
|
@ -16,14 +16,10 @@ class Gawker(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/gawker.com/img/logo.png'
|
||||
extra_css = '''
|
||||
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
|
||||
img{margin-bottom: 1em}
|
||||
h1{font-family :Arial,Helvetica,sans-serif; font-size:large}
|
||||
'''
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
@ -31,13 +27,11 @@ class Gawker(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gawker/full')]
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gawker/vip?format=xml')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
@ -17,10 +17,9 @@ class Gizmodo(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/gizmodo.com/img/logo.png'
|
||||
extra_css = ' body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif} img{margin-bottom: 1em} '
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
@ -29,13 +28,12 @@ class Gizmodo(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width','height']
|
||||
keep_only_tags = [dict(attrs={'class':'content permalink'})]
|
||||
remove_tags_before = dict(name='h1')
|
||||
remove_tags = [dict(attrs={'class':'contactinfo'})]
|
||||
remove_tags_after = dict(attrs={'class':'contactinfo'})
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/vip?format=xml')]
|
||||
|
||||
remove_tags = [
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/gizmodo/full')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
@ -1,20 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = u'2011, Silviu Cotoar\u0103'
|
||||
'''
|
||||
gsp.ro
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1286351181(BasicNewsRecipe):
|
||||
title = u'gsp.ro'
|
||||
__author__ = 'bucsie'
|
||||
oldest_article = 2
|
||||
class GSP(BasicNewsRecipe):
|
||||
title = u'Gazeta Sporturilor'
|
||||
language = 'ro'
|
||||
__author__ = u'Silviu Cotoar\u0103'
|
||||
description = u'Gazeta Sporturilor'
|
||||
publisher = u'Gazeta Sporturilor'
|
||||
category = 'Ziare,Sport,Stiri,Romania'
|
||||
oldest_article = 5
|
||||
max_articles_per_feed = 100
|
||||
language='ro'
|
||||
cover_url ='http://www.gsp.ro/images/sigla_rosu.jpg'
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'utf-8'
|
||||
remove_javascript = True
|
||||
cover_url = 'http://www.gsp.ro/images/logo.jpg'
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['related_articles', 'articol_noteaza straight_line dotted_line_top', 'comentarii','mai_multe_articole']}),
|
||||
dict(name='div', attrs={'id':'icons'})
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [ dict(name='h1', attrs={'class':'serif title_2'})
|
||||
,dict(name='div', attrs={'id':'only_text'})
|
||||
,dict(name='span', attrs={'class':'block poza_principala'})
|
||||
]
|
||||
remove_tags_after = dict(name='div', attrs={'id':'adoceanintactrovccmgpmnyt'})
|
||||
|
||||
feeds = [(u'toate stirile', u'http://www.gsp.ro/index.php?section=section&screen=rss')]
|
||||
feeds = [ (u'\u0218tiri', u'http://www.gsp.ro/rss.xml') ]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
||||
def print_version(self, url):
|
||||
return 'http://www1.gsp.ro/print/' + url[(url.rindex('/')+1):]
|
||||
|
51
resources/recipes/historiaro.recipe
Normal file
51
resources/recipes/historiaro.recipe
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = u'2011, Silviu Cotoar\u0103'
|
||||
'''
|
||||
historia.ro
|
||||
'''
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class HistoriaRo(BasicNewsRecipe):
|
||||
title = u'Historia'
|
||||
__author__ = u'Silviu Cotoar\u0103'
|
||||
description = ''
|
||||
publisher = 'Historia'
|
||||
oldest_article = 5
|
||||
language = 'ro'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
category = 'Ziare,Reviste,Istorie'
|
||||
encoding = 'utf-8'
|
||||
cover_url = 'http://www.historia.ro/sites/all/themes/historia/images/historia.png'
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':'c_antet_title'})
|
||||
, dict(name='a', attrs={'class':'overlaybox'})
|
||||
, dict(name='div', attrs={'class':'art_content'})
|
||||
]
|
||||
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'class':['fl_left']})
|
||||
, dict(name='div', attrs={'id':['article_toolbar']})
|
||||
, dict(name='div', attrs={'class':['zoom_cont']})
|
||||
]
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Feeds', u'http://www.historia.ro/rss.xml')
|
||||
]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
@ -1,10 +1,15 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011'
|
||||
'''
|
||||
lemonde.fr
|
||||
'''
|
||||
import re
|
||||
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||
|
||||
class LeMonde(BasicNewsRecipe):
|
||||
title = 'Le Monde'
|
||||
__author__ = 'veezh'
|
||||
description = u'Actualit\xe9s'
|
||||
description = 'Actualités'
|
||||
oldest_article = 1
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
@ -12,9 +17,23 @@ class LeMonde(BasicNewsRecipe):
|
||||
use_embedded_content = False
|
||||
encoding = 'cp1252'
|
||||
publisher = 'lemonde.fr'
|
||||
category = 'news, France, world'
|
||||
language = 'fr'
|
||||
#publication_type = 'newsportal'
|
||||
extra_css = '''
|
||||
h1{font-size:130%;}
|
||||
.ariane{font-size:xx-small;}
|
||||
.source{font-size:xx-small;}
|
||||
#.href{font-size:xx-small;}
|
||||
.LM_caption{color:#666666; font-size:x-small;}
|
||||
#.main-article-info{font-family:Arial,Helvetica,sans-serif;}
|
||||
#full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
||||
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
||||
'''
|
||||
#preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
,'tags' : category
|
||||
,'language' : language
|
||||
,'publisher' : publisher
|
||||
,'linearize_tables': True
|
||||
@ -32,15 +51,28 @@ class LeMonde(BasicNewsRecipe):
|
||||
return soup
|
||||
|
||||
preprocess_regexps = [
|
||||
(re.compile(r'([0-9])%'), lambda m: m.group(1) + ' %'),
|
||||
(re.compile(r'([0-9])([0-9])([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + m.group(2) + m.group(3) + ' ' + m.group(4) + m.group(5) + m.group(6)),
|
||||
(re.compile(r'([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + ' ' + m.group(2) + m.group(3) + m.group(4)),
|
||||
(re.compile(r'<span>'), lambda match: ' <span>'),
|
||||
(re.compile(r'\("'), lambda match: '(« '),
|
||||
(re.compile(r'"\)'), lambda match: ' »)'),
|
||||
(re.compile(r'“'), lambda match: '(« '),
|
||||
(re.compile(r'”'), lambda match: ' »)'),
|
||||
(re.compile(r'>\''), lambda match: '>‘'),
|
||||
(re.compile(r' \''), lambda match: ' ‘'),
|
||||
(re.compile(r'\''), lambda match: '’'),
|
||||
(re.compile(r'"<'), lambda match: ' »<'),
|
||||
(re.compile(r'"<em>'), lambda match: '<em>« '),
|
||||
(re.compile(r'"<em>"</em><em>'), lambda match: '<em>« '),
|
||||
(re.compile(r'"<a href='), lambda match: '« <a href='),
|
||||
(re.compile(r'</em>"'), lambda match: ' »</em>'),
|
||||
(re.compile(r'</a>"'), lambda match: ' »</a>'),
|
||||
(re.compile(r'"</'), lambda match: ' »</'),
|
||||
(re.compile(r'>"'), lambda match: '>« '),
|
||||
(re.compile(r'"<'), lambda match: ' »<'),
|
||||
(re.compile(r'’"'), lambda match: '’« '),
|
||||
(re.compile(r' "'), lambda match: ' « '),
|
||||
(re.compile(r'" '), lambda match: ' » '),
|
||||
(re.compile(r'\("'), lambda match: '(« '),
|
||||
(re.compile(r'"\)'), lambda match: ' »)'),
|
||||
(re.compile(r'"\.'), lambda match: ' ».'),
|
||||
(re.compile(r'",'), lambda match: ' »,'),
|
||||
(re.compile(r'"\?'), lambda match: ' »?'),
|
||||
@ -56,8 +88,14 @@ class LeMonde(BasicNewsRecipe):
|
||||
(re.compile(r' %'), lambda match: ' %'),
|
||||
(re.compile(r'\.jpg » border='), lambda match: '.jpg'),
|
||||
(re.compile(r'\.png » border='), lambda match: '.png'),
|
||||
(re.compile(r' – '), lambda match: ' – '),
|
||||
(re.compile(r' – '), lambda match: ' – '),
|
||||
(re.compile(r' - '), lambda match: ' – '),
|
||||
(re.compile(r' -,'), lambda match: ' –,'),
|
||||
(re.compile(r'»:'), lambda match: '» :'),
|
||||
]
|
||||
|
||||
|
||||
keep_only_tags = [
|
||||
dict(name='div', attrs={'class':['contenu']})
|
||||
]
|
||||
@ -65,11 +103,15 @@ class LeMonde(BasicNewsRecipe):
|
||||
remove_tags_after = [dict(id='appel_temoignage')]
|
||||
|
||||
def get_article_url(self, article):
|
||||
link = article.get('link')
|
||||
if 'blog' not in link:
|
||||
return link
|
||||
|
||||
url = article.get('guid', None)
|
||||
if '/chat/' in url or '.blog' in url or '/video/' in url or '/sport/' in url or '/portfolio/' in url or '/visuel/' in url :
|
||||
url = None
|
||||
return url
|
||||
|
||||
# def get_article_url(self, article):
|
||||
# link = article.get('link')
|
||||
# if 'blog' not in link and ('chat' not in link):
|
||||
# return link
|
||||
|
||||
feeds = [
|
||||
('A la une', 'http://www.lemonde.fr/rss/une.xml'),
|
||||
@ -94,3 +136,4 @@ class LeMonde(BasicNewsRecipe):
|
||||
cover_url = link_item.img['src']
|
||||
|
||||
return cover_url
|
||||
|
||||
|
@ -16,15 +16,9 @@ class Lifehacker(BasicNewsRecipe):
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
encoding = 'utf-8'
|
||||
use_embedded_content = False
|
||||
use_embedded_content = True
|
||||
language = 'en'
|
||||
masthead_url = 'http://cache.gawkerassets.com/assets/lifehacker.com/img/logo.png'
|
||||
extra_css = '''
|
||||
body{font-family: "Lucida Grande",Helvetica,Arial,sans-serif}
|
||||
img{margin-bottom: 1em}
|
||||
h1{font-family :Arial,Helvetica,sans-serif; font-size:large}
|
||||
h2{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
|
||||
'''
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
@ -32,20 +26,12 @@ class Lifehacker(BasicNewsRecipe):
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_attributes = ['width', 'height', 'style']
|
||||
remove_tags_before = dict(name='h1')
|
||||
keep_only_tags = [dict(id='container')]
|
||||
remove_tags_after = dict(attrs={'class':'post-body'})
|
||||
remove_tags = [
|
||||
dict(id="sharemenu"),
|
||||
{'class': 'related'},
|
||||
{'class': 'feedflare'},
|
||||
]
|
||||
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/full')]
|
||||
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/vip?format=xml')]
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
return self.adeify_images(soup)
|
||||
|
||||
def print_version(self, url):
|
||||
return url.replace('#!', '?_escaped_fragment_=')
|
||||
|
||||
|
@ -88,8 +88,8 @@ class NYTimes(BasicNewsRecipe):
|
||||
|
||||
if headlinesOnly:
|
||||
title='New York Times Headlines'
|
||||
description = 'Headlines from the New York Times'
|
||||
needs_subscription = False
|
||||
description = 'Headlines from the New York Times. Needs a subscription from http://www.nytimes.com'
|
||||
needs_subscription = 'optional'
|
||||
elif webEdition:
|
||||
title='New York Times (Web)'
|
||||
description = 'New York Times on the Web'
|
||||
|
@ -96,7 +96,7 @@ class NYTimes(BasicNewsRecipe):
|
||||
if headlinesOnly:
|
||||
title='New York Times Headlines'
|
||||
description = 'Headlines from the New York Times'
|
||||
needs_subscription = False
|
||||
needs_subscription = True
|
||||
elif webEdition:
|
||||
title='New York Times (Web)'
|
||||
description = 'New York Times on the Web'
|
||||
@ -107,7 +107,7 @@ class NYTimes(BasicNewsRecipe):
|
||||
needs_subscription = True
|
||||
else:
|
||||
title='New York Times'
|
||||
description = 'Today\'s New York Times'
|
||||
description = 'Today\'s New York Times. Needs subscription from http://www.nytimes.com'
|
||||
needs_subscription = True
|
||||
|
||||
|
||||
|
@ -3,22 +3,20 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = u'2011, Tomasz Dlugosz <tomek3d@gmail.com>'
|
||||
'''
|
||||
swiatkindle.pl
|
||||
swiatczytnikow.pl
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class swiatkindle(BasicNewsRecipe):
|
||||
title = u'Swiat Kindle'
|
||||
description = u'Blog o czytniku Amazon Kindle. Wersje, ksi\u0105\u017cki, kupowanie i korzystanie w Polsce'
|
||||
class swiatczytnikow(BasicNewsRecipe):
|
||||
title = u'Swiat Czytnikow'
|
||||
description = u'Czytniki e-książek w Polsce. Jak wybrać, kupić i korzystać z Amazon Kindle i innych'
|
||||
language = 'pl'
|
||||
__author__ = u'Tomasz D\u0142ugosz'
|
||||
oldest_article = 7
|
||||
max_articles_per_feed = 100
|
||||
|
||||
feeds = [(u'\u015awiat Kindle - wpisy', u'http://swiatkindle.pl/feed')]
|
||||
feeds = [(u'Świat Czytników - wpisy', u'http://swiatczytnikow.pl/feed')]
|
||||
|
||||
remove_tags = [dict(name = 'ul', attrs = {'class' : 'similar-posts'})]
|
||||
|
||||
|
@ -2,9 +2,9 @@
|
||||
# -*- coding: utf-8 mode: python -*-
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Steffen Siebert <calibre at steffensiebert.de>'
|
||||
__copyright__ = '2010-2011, Steffen Siebert <calibre at steffensiebert.de>'
|
||||
__docformat__ = 'restructuredtext de'
|
||||
__version__ = '1.1'
|
||||
__version__ = '1.2'
|
||||
|
||||
"""
|
||||
Die Zeit EPUB
|
||||
@ -13,21 +13,43 @@ Die Zeit EPUB
|
||||
import os, urllib2, zipfile, re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre import walk
|
||||
|
||||
class ZeitEPUBAbo(BasicNewsRecipe):
|
||||
|
||||
title = u'Zeit Online Premium'
|
||||
title = u'Die Zeit'
|
||||
description = u'Das EPUB Abo der Zeit (needs subscription)'
|
||||
language = 'de'
|
||||
lang = 'de-DE'
|
||||
|
||||
__author__ = 'Steffen Siebert'
|
||||
__author__ = 'Steffen Siebert and Tobias Isenberg'
|
||||
needs_subscription = True
|
||||
|
||||
conversion_options = {
|
||||
'no_default_epub_cover' : True
|
||||
'no_default_epub_cover' : True,
|
||||
# fixing the wrong left margin
|
||||
'mobi_ignore_margins' : True,
|
||||
}
|
||||
|
||||
preprocess_regexps = [
|
||||
# filtering for correct dashes
|
||||
(re.compile(r' - '), lambda match: ' – '), # regular "Gedankenstrich"
|
||||
(re.compile(r' -,'), lambda match: ' –,'), # "Gedankenstrich" before a comma
|
||||
(re.compile(r'(?<=\d)-(?=\d)'), lambda match: '–'), # number-number
|
||||
# filtering for unicode characters that are missing on the Kindle,
|
||||
# try to replace them with meaningful work-arounds
|
||||
(re.compile(u'\u2080'), lambda match: '<span style="font-size: 50%;">0</span>'), # subscript-0
|
||||
(re.compile(u'\u2081'), lambda match: '<span style="font-size: 50%;">1</span>'), # subscript-1
|
||||
(re.compile(u'\u2082'), lambda match: '<span style="font-size: 50%;">2</span>'), # subscript-2
|
||||
(re.compile(u'\u2083'), lambda match: '<span style="font-size: 50%;">3</span>'), # subscript-3
|
||||
(re.compile(u'\u2084'), lambda match: '<span style="font-size: 50%;">4</span>'), # subscript-4
|
||||
(re.compile(u'\u2085'), lambda match: '<span style="font-size: 50%;">5</span>'), # subscript-5
|
||||
(re.compile(u'\u2086'), lambda match: '<span style="font-size: 50%;">6</span>'), # subscript-6
|
||||
(re.compile(u'\u2087'), lambda match: '<span style="font-size: 50%;">7</span>'), # subscript-7
|
||||
(re.compile(u'\u2088'), lambda match: '<span style="font-size: 50%;">8</span>'), # subscript-8
|
||||
(re.compile(u'\u2089'), lambda match: '<span style="font-size: 50%;">9</span>'), # subscript-9
|
||||
]
|
||||
|
||||
def build_index(self):
|
||||
domain = "http://premium.zeit.de"
|
||||
url = domain + "/abovorteile/cgi-bin/_er_member/p4z.fpl?ER_Do=getUserData&ER_NextTemplate=login_ok"
|
||||
@ -55,9 +77,36 @@ class ZeitEPUBAbo(BasicNewsRecipe):
|
||||
zfile.extractall(self.output_dir)
|
||||
|
||||
tmp.close()
|
||||
|
||||
index = os.path.join(self.output_dir, 'content.opf')
|
||||
|
||||
self.report_progress(1,_('epub downloaded and extracted'))
|
||||
|
||||
# doing regular expression filtering
|
||||
for path in walk('.'):
|
||||
(shortname, extension) = os.path.splitext(path)
|
||||
if extension.lower() in ('.html', '.htm', '.xhtml'):
|
||||
with open(path, 'r+b') as f:
|
||||
raw = f.read()
|
||||
raw = raw.decode('utf-8')
|
||||
for pat, func in self.preprocess_regexps:
|
||||
raw = pat.sub(func, raw)
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(raw.encode('utf-8'))
|
||||
|
||||
# adding real cover
|
||||
self.report_progress(0,_('trying to download cover image (titlepage)'))
|
||||
self.download_cover()
|
||||
self.conversion_options["cover"] = self.cover_path
|
||||
|
||||
return index
|
||||
|
||||
# getting url of the cover
|
||||
def get_cover_url(self):
|
||||
try:
|
||||
inhalt = self.index_to_soup('http://www.zeit.de/inhalt')
|
||||
cover_url = inhalt.find('div', attrs={'class':'singlearchive clearfix'}).img['src'].replace('icon_','')
|
||||
except:
|
||||
cover_url = 'http://images.zeit.de/bilder/titelseiten_zeit/1946/001_001.jpg'
|
||||
return cover_url
|
||||
|
@ -15,6 +15,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",
|
||||
"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",
|
||||
"multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
|
||||
|
@ -391,11 +391,6 @@ noembed, param, link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Page breaks at body tags, to help out with LIT-generation */
|
||||
body {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
/* Explicit line-breaks are blocks, sure... */
|
||||
br {
|
||||
display: block;
|
||||
|
@ -7,7 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, os, shutil, platform, subprocess, stat, py_compile, glob, \
|
||||
textwrap, tarfile
|
||||
textwrap, tarfile, re
|
||||
|
||||
from setup import Command, modules, basenames, functions, __version__, \
|
||||
__appname__
|
||||
@ -19,7 +19,7 @@ SITE_PACKAGES = ['IPython', 'PIL', 'dateutil', 'dns', 'PyQt4', 'mechanize',
|
||||
|
||||
QTDIR = '/usr/lib/qt4'
|
||||
QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit', 'QtDBus')
|
||||
|
||||
MAGICK_PREFIX = '/usr'
|
||||
binary_includes = [
|
||||
'/usr/bin/pdftohtml',
|
||||
'/usr/lib/libwmflite-0.2.so.7',
|
||||
@ -41,8 +41,8 @@ binary_includes = [
|
||||
'/usr/lib/libgthread-2.0.so.0',
|
||||
'/usr/lib/libpng14.so.14',
|
||||
'/usr/lib/libexslt.so.0',
|
||||
'/usr/lib/libMagickWand.so.4',
|
||||
'/usr/lib/libMagickCore.so.4',
|
||||
MAGICK_PREFIX+'/lib/libMagickWand.so.4',
|
||||
MAGICK_PREFIX+'/lib/libMagickCore.so.4',
|
||||
'/usr/lib/libgcrypt.so.11',
|
||||
'/usr/lib/libgpg-error.so.0',
|
||||
'/usr/lib/libphonon.so.4',
|
||||
@ -116,9 +116,25 @@ class LinuxFreeze(Command):
|
||||
if x not in ('designer', 'sqldrivers', 'codecs'):
|
||||
shutil.copytree(y, self.j(dest, x))
|
||||
|
||||
im = glob.glob('/usr/lib/ImageMagick-*')[0]
|
||||
dest = self.j(self.lib_dir, 'ImageMagick')
|
||||
im = glob.glob(MAGICK_PREFIX + '/lib/ImageMagick-*')[-1]
|
||||
self.magick_base = os.path.basename(im)
|
||||
dest = self.j(self.lib_dir, self.magick_base)
|
||||
shutil.copytree(im, dest, ignore=shutil.ignore_patterns('*.a'))
|
||||
from calibre import walk
|
||||
for x in walk(dest):
|
||||
if x.endswith('.la'):
|
||||
raw = open(x).read()
|
||||
raw = re.sub('libdir=.*', '', raw)
|
||||
open(x, 'wb').write(raw)
|
||||
|
||||
dest = self.j(dest, 'config')
|
||||
src = self.j(MAGICK_PREFIX, 'share', self.magick_base, 'config')
|
||||
for x in glob.glob(src+'/*'):
|
||||
d = self.j(dest, os.path.basename(x))
|
||||
if os.path.isdir(x):
|
||||
shutil.copytree(x, d)
|
||||
else:
|
||||
shutil.copyfile(x, d)
|
||||
|
||||
def compile_mount_helper(self):
|
||||
self.info('Compiling mount helper...')
|
||||
@ -278,9 +294,10 @@ class LinuxFreeze(Command):
|
||||
base=`dirname $path`
|
||||
lib=$base/lib
|
||||
export LD_LIBRARY_PATH=$lib:$LD_LIBRARY_PATH
|
||||
export MAGICK_CONFIGURE_PATH=$lib/ImageMagick/config
|
||||
export MAGICK_CODER_MODULE_PATH=$lib/ImageMagick/modules-Q16/coders
|
||||
export MAGICK_CODER_FILTER_PATH=$lib/ImageMagick/modules-Q16/filters
|
||||
export MAGICK_HOME=$base
|
||||
export MAGICK_CONFIGURE_PATH=$lib/{1}/config
|
||||
export MAGICK_CODER_MODULE_PATH=$lib/{1}/modules-Q16/coders
|
||||
export MAGICK_CODER_FILTER_PATH=$lib/{1}/modules-Q16/filters
|
||||
$base/bin/{0} "$@"
|
||||
''')
|
||||
|
||||
@ -292,7 +309,7 @@ class LinuxFreeze(Command):
|
||||
exe = self.j(self.bin_dir, bname)
|
||||
sh = self.j(self.base, bname)
|
||||
with open(sh, 'wb') as f:
|
||||
f.write(launcher.format(bname))
|
||||
f.write(launcher.format(bname, self.magick_base))
|
||||
os.chmod(sh,
|
||||
stat.S_IREAD|stat.S_IEXEC|stat.S_IWRITE|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
|
||||
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.46'
|
||||
__version__ = '0.7.47'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -571,7 +571,7 @@ from calibre.devices.binatone.driver import README
|
||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||
from calibre.devices.edge.driver import EDGE
|
||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
|
||||
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH
|
||||
SOVOS, PICO, SUNSTECH_EB700, ARCHOS7O, STASH, WEXLER
|
||||
from calibre.devices.sne.driver import SNE
|
||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \
|
||||
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, LUMIREAD, ALURATEK_COLOR, \
|
||||
@ -682,7 +682,7 @@ plugins += [
|
||||
ELONEX,
|
||||
TECLAST_K3,
|
||||
NEWSMY,
|
||||
PICO, SUNSTECH_EB700, ARCHOS7O, SOVOS, STASH,
|
||||
PICO, SUNSTECH_EB700, ARCHOS7O, SOVOS, STASH, WEXLER,
|
||||
IPAPYRUS,
|
||||
EDGE,
|
||||
SNE,
|
||||
|
@ -583,7 +583,7 @@ def main(args=sys.argv):
|
||||
if remove_plugin(opts.remove_plugin):
|
||||
print 'Plugin removed'
|
||||
else:
|
||||
print 'No custom pluginnamed', opts.remove_plugin
|
||||
print 'No custom plugin named', opts.remove_plugin
|
||||
if opts.customize_plugin is not None:
|
||||
name, custom = opts.customize_plugin.split(',')
|
||||
plugin = find_plugin(name.strip())
|
||||
|
@ -60,7 +60,13 @@ class ANDROID(USBMS):
|
||||
0x1004 : { 0x61cc : [0x100] },
|
||||
|
||||
# Archos
|
||||
0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]},
|
||||
0x0e79 : {
|
||||
0x1400 : [0x0222, 0x0216],
|
||||
0x1408 : [0x0222, 0x0216],
|
||||
0x1419 : [0x0216],
|
||||
0x1420 : [0x0216],
|
||||
0x1422 : [0x0216]
|
||||
},
|
||||
|
||||
# Huawei
|
||||
# Disabled as this USB id is used by various USB flash drives
|
||||
@ -69,6 +75,9 @@ class ANDROID(USBMS):
|
||||
# T-Mobile
|
||||
0x0408 : { 0x03ba : [0x0109], },
|
||||
|
||||
# Xperia
|
||||
0x13d3 : { 0x3304 : [0x0001, 0x0002] },
|
||||
|
||||
}
|
||||
EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books']
|
||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
|
||||
@ -78,16 +87,16 @@ class ANDROID(USBMS):
|
||||
|
||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS',
|
||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', ]
|
||||
'TELECHIP', 'HUAWEI', 'T-MOBILE', 'SEMC']
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||
'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']
|
||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT']
|
||||
'A70S', 'A101IT', '7']
|
||||
|
||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||
|
||||
|
@ -78,9 +78,13 @@ class KOBO(USBMS):
|
||||
else self._main_prefix
|
||||
|
||||
# Determine the firmware version
|
||||
f = open(self.normalize_path(self._main_prefix + '.kobo/version'), 'r')
|
||||
try:
|
||||
with open(self.normalize_path(self._main_prefix + '.kobo/version'),
|
||||
'rb') as f:
|
||||
self.fwversion = f.readline().split(',')[2]
|
||||
f.close()
|
||||
except:
|
||||
self.fwversion = 'unknown'
|
||||
|
||||
if self.fwversion != '1.0' and self.fwversion != '1.4':
|
||||
self.has_kepubs = True
|
||||
debug_print('Version of firmware: ', self.fwversion, 'Has kepubs:', self.has_kepubs)
|
||||
|
@ -9,6 +9,8 @@ __docformat__ = 'restructuredtext en'
|
||||
import os
|
||||
|
||||
from calibre.devices.usbms.driver import USBMS
|
||||
from calibre import prints
|
||||
prints
|
||||
|
||||
class PALMPRE(USBMS):
|
||||
|
||||
@ -268,5 +270,36 @@ class NEXTBOOK(USBMS):
|
||||
EBOOK_DIR_MAIN = ''
|
||||
|
||||
VENDOR_NAME = 'NEXT2'
|
||||
WINDOWS_MAIN_MEM = '1.0.14'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '1.0.14'
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
'''
|
||||
def upload_cover(self, path, filename, metadata, filepath):
|
||||
if metadata.thumbnail and metadata.thumbnail[-1]:
|
||||
path = path.replace('/', os.sep)
|
||||
is_main = path.startswith(self._main_prefix)
|
||||
prefix = None
|
||||
if is_main:
|
||||
prefix = self._main_prefix
|
||||
else:
|
||||
if self._card_a_prefix and \
|
||||
path.startswith(self._card_a_prefix):
|
||||
prefix = self._card_a_prefix
|
||||
elif self._card_b_prefix and \
|
||||
path.startswith(self._card_b_prefix):
|
||||
prefix = self._card_b_prefix
|
||||
if prefix is None:
|
||||
prints('WARNING: Failed to find prefix for:', filepath)
|
||||
return
|
||||
thumbnail_dir = os.path.join(prefix, '.Cover')
|
||||
|
||||
relpath = os.path.relpath(filepath, prefix)
|
||||
if relpath.startswith('..\\'):
|
||||
relpath = relpath[3:]
|
||||
thumbnail_dir = os.path.join(thumbnail_dir, relpath)
|
||||
if not os.path.exists(thumbnail_dir):
|
||||
os.makedirs(thumbnail_dir)
|
||||
with open(os.path.join(thumbnail_dir, filename+'.jpg'), 'wb') as f:
|
||||
f.write(metadata.thumbnail[-1])
|
||||
'''
|
||||
|
||||
|
@ -104,3 +104,14 @@ class STASH(TECLAST_K3):
|
||||
VENDOR_NAME = 'STASH'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'W950'
|
||||
|
||||
class WEXLER(TECLAST_K3):
|
||||
|
||||
name = 'Wexler device interface'
|
||||
gui_name = 'Wexler'
|
||||
description = _('Communicate with the Wexler reader.')
|
||||
|
||||
FORMATS = ['epub', 'fb2', 'pdf', 'txt']
|
||||
|
||||
VENDOR_NAME = 'WEXLER'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'T7001'
|
||||
|
||||
|
@ -304,6 +304,10 @@ class ComicInput(InputFormatPlugin):
|
||||
help=_('Specify the image size as widthxheight pixels. Normally,'
|
||||
' an image size is automatically calculated from the output '
|
||||
'profile, this option overrides it.')),
|
||||
OptionRecommendation(name='dont_add_comic_pages_to_toc', recommended_value=False,
|
||||
help=_('When converting a CBC do not add links to each page to'
|
||||
' the TOC. Note this only applies if the TOC has more than one'
|
||||
' section')),
|
||||
])
|
||||
|
||||
recommendations = set([
|
||||
@ -449,6 +453,7 @@ class ComicInput(InputFormatPlugin):
|
||||
wrappers = comic[2]
|
||||
stoc = toc.add_item(href(wrappers[0]),
|
||||
None, comic[0], play_order=po)
|
||||
if not opts.dont_add_comic_pages_to_toc:
|
||||
for i, x in enumerate(wrappers):
|
||||
stoc.add_item(href(x), None,
|
||||
_('Page')+' %d'%(i+1), play_order=po)
|
||||
|
@ -984,7 +984,9 @@ OptionRecommendation(name='sr3_replace',
|
||||
flattener = CSSFlattener(fbase=fbase, fkey=fkey,
|
||||
lineh=line_height,
|
||||
untable=self.output_plugin.file_type in ('mobi','lit'),
|
||||
unfloat=self.output_plugin.file_type in ('mobi', 'lit'))
|
||||
unfloat=self.output_plugin.file_type in ('mobi', 'lit'),
|
||||
page_break_on_body=self.output_plugin.file_type in ('mobi',
|
||||
'lit'))
|
||||
flattener(self.oeb, self.opts)
|
||||
self.opts.insert_blank_line = oibl
|
||||
self.opts.remove_paragraph_spacing = orps
|
||||
|
@ -22,7 +22,8 @@ class LITOutput(OutputFormatPlugin):
|
||||
from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
|
||||
from calibre.ebooks.lit.writer import LitWriter
|
||||
from calibre.ebooks.oeb.transforms.split import Split
|
||||
split = Split(split_on_page_breaks=True, max_flow_size=0)
|
||||
split = Split(split_on_page_breaks=True, max_flow_size=0,
|
||||
remove_css_pagebreaks=False)
|
||||
split(self.oeb, self.opts)
|
||||
|
||||
|
||||
|
@ -83,6 +83,10 @@ CALIBRE_METADATA_FIELDS = frozenset([
|
||||
'application_id', # An application id, currently set to the db_id.
|
||||
'db_id', # the calibre primary key of the item.
|
||||
'formats', # list of formats (extensions) for this book
|
||||
# a dict of user category names, where the value is a list of item names
|
||||
# from the book that are in that category
|
||||
'user_categories',
|
||||
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -30,6 +30,7 @@ NULL_VALUES = {
|
||||
'author_sort_map': {},
|
||||
'authors' : [_('Unknown')],
|
||||
'title' : _('Unknown'),
|
||||
'user_categories' : {},
|
||||
'language' : 'und'
|
||||
}
|
||||
|
||||
|
@ -470,6 +470,13 @@ def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8))
|
||||
metadata_elem.append(meta)
|
||||
|
||||
|
||||
def dump_user_categories(cats):
|
||||
if not cats:
|
||||
cats = {}
|
||||
from calibre.ebooks.metadata.book.json_codec import object_to_unicode
|
||||
return json.dumps(object_to_unicode(cats), ensure_ascii=False,
|
||||
skipkeys=True)
|
||||
|
||||
class OPF(object): # {{{
|
||||
|
||||
MIMETYPE = 'application/oebps-package+xml'
|
||||
@ -524,6 +531,9 @@ class OPF(object): # {{{
|
||||
publication_type = MetadataField('publication_type', is_dc=False)
|
||||
timestamp = MetadataField('timestamp', is_dc=False,
|
||||
formatter=parse_date, renderer=isoformat)
|
||||
user_categories = MetadataField('user_categories', is_dc=False,
|
||||
formatter=json.loads,
|
||||
renderer=dump_user_categories)
|
||||
|
||||
|
||||
def __init__(self, stream, basedir=os.getcwdu(), unquote_urls=True,
|
||||
@ -994,7 +1004,7 @@ class OPF(object): # {{{
|
||||
for attr in ('title', 'authors', 'author_sort', 'title_sort',
|
||||
'publisher', 'series', 'series_index', 'rating',
|
||||
'isbn', 'tags', 'category', 'comments',
|
||||
'pubdate'):
|
||||
'pubdate', 'user_categories'):
|
||||
val = getattr(mi, attr, None)
|
||||
if val is not None and val != [] and val != (None, None):
|
||||
setattr(self, attr, val)
|
||||
@ -1175,6 +1185,10 @@ class OPFCreator(Metadata):
|
||||
a(CAL_ELEM('calibre:timestamp', self.timestamp.isoformat()))
|
||||
if self.publication_type is not None:
|
||||
a(CAL_ELEM('calibre:publication_type', self.publication_type))
|
||||
if self.user_categories:
|
||||
from calibre.ebooks.metadata.book.json_codec import object_to_unicode
|
||||
a(CAL_ELEM('calibre:user_categories',
|
||||
json.dumps(object_to_unicode(self.user_categories))))
|
||||
manifest = E.manifest()
|
||||
if self.manifest is not None:
|
||||
for ref in self.manifest:
|
||||
@ -1299,6 +1313,8 @@ def metadata_to_opf(mi, as_string=True):
|
||||
meta('publication_type', mi.publication_type)
|
||||
if mi.title_sort:
|
||||
meta('title_sort', mi.title_sort)
|
||||
if mi.user_categories:
|
||||
meta('user_categories', dump_user_categories(mi.user_categories))
|
||||
|
||||
serialize_user_metadata(metadata, mi.get_all_user_metadata(False))
|
||||
|
||||
|
@ -7,7 +7,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
import re, threading
|
||||
|
||||
from calibre.customize import Plugin
|
||||
from calibre.utils.logging import ThreadSafeLog, FileStream
|
||||
@ -30,7 +30,21 @@ class Source(Plugin):
|
||||
|
||||
touched_fields = frozenset()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Plugin.__init__(self, *args, **kwargs)
|
||||
self._isbn_to_identifier_cache = {}
|
||||
self.cache_lock = threading.RLock()
|
||||
|
||||
# Utility functions {{{
|
||||
|
||||
def cache_isbn_to_identifier(self, isbn, identifier):
|
||||
with self.cache_lock:
|
||||
self._isbn_to_identifier_cache[isbn] = identifier
|
||||
|
||||
def cached_isbn_to_identifier(self, isbn):
|
||||
with self.cache_lock:
|
||||
return self._isbn_to_identifier_cache.get(isbn, None)
|
||||
|
||||
def get_author_tokens(self, authors, only_first_author=True):
|
||||
'''
|
||||
Take a list of authors and return a list of tokens useful for an
|
||||
|
@ -13,6 +13,7 @@ from functools import partial
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from calibre.ebooks.metadata import check_isbn
|
||||
from calibre.ebooks.metadata.sources.base import Source
|
||||
from calibre.ebooks.metadata.book.base import Metadata
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
@ -69,6 +70,7 @@ def to_metadata(browser, log, entry_, timeout):
|
||||
|
||||
|
||||
id_url = entry_id(entry_)[0].text
|
||||
google_id = id_url.split('/')[-1]
|
||||
title_ = ': '.join([x.text for x in title(entry_)]).strip()
|
||||
authors = [x.text.strip() for x in creator(entry_) if x.text]
|
||||
if not authors:
|
||||
@ -78,6 +80,7 @@ def to_metadata(browser, log, entry_, timeout):
|
||||
return None
|
||||
|
||||
mi = Metadata(title_, authors)
|
||||
mi.identifiers = {'google':google_id}
|
||||
try:
|
||||
raw = get_details(browser, id_url, timeout)
|
||||
feed = etree.fromstring(xml_to_unicode(clean_ascii_chars(raw),
|
||||
@ -103,9 +106,12 @@ def to_metadata(browser, log, entry_, timeout):
|
||||
t = str(x.text).strip()
|
||||
if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'):
|
||||
if t[:5].upper() == 'ISBN:':
|
||||
isbns.append(t[5:])
|
||||
t = check_isbn(t[5:])
|
||||
if t:
|
||||
isbns.append(t)
|
||||
if isbns:
|
||||
mi.isbn = sorted(isbns, key=len)[-1]
|
||||
mi.all_isbns = isbns
|
||||
|
||||
# Tags
|
||||
try:
|
||||
@ -133,20 +139,6 @@ def to_metadata(browser, log, entry_, timeout):
|
||||
return mi
|
||||
|
||||
|
||||
def get_all_details(br, log, entries, abort, result_queue, timeout):
|
||||
for i in entries:
|
||||
try:
|
||||
ans = to_metadata(br, log, i, timeout)
|
||||
if isinstance(ans, Metadata):
|
||||
result_queue.put(ans)
|
||||
except:
|
||||
log.exception(
|
||||
'Failed to get metadata for identify entry:',
|
||||
etree.tostring(i))
|
||||
if abort.is_set():
|
||||
break
|
||||
|
||||
|
||||
class GoogleBooks(Source):
|
||||
|
||||
name = 'Google Books'
|
||||
@ -185,6 +177,36 @@ class GoogleBooks(Source):
|
||||
'min-viewability':'none',
|
||||
})
|
||||
|
||||
def cover_url_from_identifiers(self, identifiers):
|
||||
goog = identifiers.get('google', None)
|
||||
if goog is None:
|
||||
isbn = identifiers.get('isbn', None)
|
||||
goog = self.cached_isbn_to_identifier(isbn)
|
||||
if goog is not None:
|
||||
return ('http://books.google.com/books?id=%s&printsec=frontcover&img=1' %
|
||||
goog)
|
||||
|
||||
def is_cover_image_valid(self, raw):
|
||||
# When no cover is present, returns a PNG saying image not available
|
||||
# Try for example google identifier llNqPwAACAAJ
|
||||
# I have yet to see an actual cover in PNG format
|
||||
return raw and len(raw) > 17000 and raw[1:4] != 'PNG'
|
||||
|
||||
def get_all_details(self, br, log, entries, abort, result_queue, timeout):
|
||||
for i in entries:
|
||||
try:
|
||||
ans = to_metadata(br, log, i, timeout)
|
||||
if isinstance(ans, Metadata):
|
||||
result_queue.put(ans)
|
||||
for isbn in ans.all_isbns:
|
||||
self.cache_isbn_to_identifier(isbn,
|
||||
ans.identifiers['google'])
|
||||
except:
|
||||
log.exception(
|
||||
'Failed to get metadata for identify entry:',
|
||||
etree.tostring(i))
|
||||
if abort.is_set():
|
||||
break
|
||||
|
||||
def identify(self, log, result_queue, abort, title=None, authors=None,
|
||||
identifiers={}, timeout=5):
|
||||
@ -207,8 +229,8 @@ class GoogleBooks(Source):
|
||||
return as_unicode(e)
|
||||
|
||||
# There is no point running these queries in threads as google
|
||||
# throttles requests returning Forbidden errors
|
||||
get_all_details(br, log, entries, abort, result_queue, timeout)
|
||||
# throttles requests returning 403 Forbidden errors
|
||||
self.get_all_details(br, log, entries, abort, result_queue, timeout)
|
||||
|
||||
return None
|
||||
|
||||
@ -218,8 +240,14 @@ if __name__ == '__main__':
|
||||
title_test)
|
||||
test_identify_plugin(GoogleBooks.name,
|
||||
[
|
||||
|
||||
(
|
||||
{'title': 'Great Expectations', 'authors':['Charles Dickens']},
|
||||
[title_test('Great Expectations', exact=True)]
|
||||
{'identifiers':{'isbn': '0743273567'}},
|
||||
[title_test('The great gatsby', exact=True)]
|
||||
),
|
||||
|
||||
#(
|
||||
# {'title': 'Great Expectations', 'authors':['Charles Dickens']},
|
||||
# [title_test('Great Expectations', exact=True)]
|
||||
#),
|
||||
])
|
||||
|
@ -242,9 +242,11 @@ class MobiReader(object):
|
||||
self.debug = debug
|
||||
self.embedded_mi = None
|
||||
self.base_css_rules = textwrap.dedent('''
|
||||
blockquote { margin: 0em 0em 0em 2em; text-align: justify }
|
||||
body { text-align: justify }
|
||||
|
||||
p { margin: 0em; text-align: justify; text-indent: 1.5em }
|
||||
blockquote { margin: 0em 0em 0em 2em; }
|
||||
|
||||
p { margin: 0em; text-indent: 1.5em }
|
||||
|
||||
.bold { font-weight: bold }
|
||||
|
||||
|
@ -32,6 +32,12 @@ class OEBOutput(OutputFormatPlugin):
|
||||
for key in (OPF_MIME, NCX_MIME, PAGE_MAP_MIME):
|
||||
href, root = results.pop(key, [None, None])
|
||||
if root is not None:
|
||||
if key == OPF_MIME:
|
||||
try:
|
||||
self.workaround_nook_cover_bug(root)
|
||||
except:
|
||||
self.log.exception('Something went wrong while trying to'
|
||||
' workaround Nook cover bug, ignoring')
|
||||
raw = etree.tostring(root, pretty_print=True,
|
||||
encoding='utf-8', xml_declaration=True)
|
||||
if key == OPF_MIME:
|
||||
@ -49,3 +55,24 @@ class OEBOutput(OutputFormatPlugin):
|
||||
with open(path, 'wb') as f:
|
||||
f.write(str(item))
|
||||
item.unload_data_from_memory(memory=path)
|
||||
|
||||
def workaround_nook_cover_bug(self, root): # {{{
|
||||
cov = root.xpath('//*[local-name() = "meta" and @name="cover" and'
|
||||
' @content != "cover"]')
|
||||
if len(cov) == 1:
|
||||
manpath = ('//*[local-name() = "manifest"]/*[local-name() = "item" '
|
||||
' and @id="%s" and @media-type]')
|
||||
cov = cov[0]
|
||||
covid = cov.get('content')
|
||||
manifest_item = root.xpath(manpath%covid)
|
||||
has_cover = root.xpath(manpath%'cover')
|
||||
if len(manifest_item) == 1 and not has_cover and \
|
||||
manifest_item[0].get('media-type',
|
||||
'').startswith('image/'):
|
||||
self.log.warn('The cover image has an id != "cover". Renaming'
|
||||
' to work around Nook Color bug')
|
||||
manifest_item = manifest_item[0]
|
||||
manifest_item.set('id', 'cover')
|
||||
cov.set('content', 'cover')
|
||||
# }}}
|
||||
|
||||
|
@ -100,12 +100,13 @@ def FontMapper(sbase=None, dbase=None, dkey=None):
|
||||
|
||||
class CSSFlattener(object):
|
||||
def __init__(self, fbase=None, fkey=None, lineh=None, unfloat=False,
|
||||
untable=False):
|
||||
untable=False, page_break_on_body=False):
|
||||
self.fbase = fbase
|
||||
self.fkey = fkey
|
||||
self.lineh = lineh
|
||||
self.unfloat = unfloat
|
||||
self.untable = untable
|
||||
self.page_break_on_body = page_break_on_body
|
||||
|
||||
@classmethod
|
||||
def config(cls, cfg):
|
||||
@ -139,6 +140,8 @@ class CSSFlattener(object):
|
||||
bs.append('margin-right : %fpt'%\
|
||||
float(self.context.margin_right))
|
||||
bs.extend(['padding-left: 0pt', 'padding-right: 0pt'])
|
||||
if self.page_break_on_body:
|
||||
bs.extend(['page-break-before: always'])
|
||||
if self.context.change_justification != 'original':
|
||||
bs.append('text-align: '+ self.context.change_justification)
|
||||
body.set('style', '; '.join(bs))
|
||||
|
@ -38,11 +38,12 @@ class SplitError(ValueError):
|
||||
class Split(object):
|
||||
|
||||
def __init__(self, split_on_page_breaks=True, page_breaks_xpath=None,
|
||||
max_flow_size=0):
|
||||
max_flow_size=0, remove_css_pagebreaks=True):
|
||||
self.split_on_page_breaks = split_on_page_breaks
|
||||
self.page_breaks_xpath = page_breaks_xpath
|
||||
self.max_flow_size = max_flow_size
|
||||
self.page_break_selectors = None
|
||||
self.remove_css_pagebreaks = remove_css_pagebreaks
|
||||
if self.page_breaks_xpath is not None:
|
||||
self.page_break_selectors = [(XPath(self.page_breaks_xpath), False)]
|
||||
|
||||
@ -83,12 +84,16 @@ class Split(object):
|
||||
if before and before != 'avoid':
|
||||
self.page_break_selectors.add((CSSSelector(rule.selectorText),
|
||||
True))
|
||||
if self.remove_css_pagebreaks:
|
||||
rule.style.removeProperty('page-break-before')
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if after and after != 'avoid':
|
||||
self.page_break_selectors.add((CSSSelector(rule.selectorText),
|
||||
False))
|
||||
if self.remove_css_pagebreaks:
|
||||
rule.style.removeProperty('page-break-after')
|
||||
except:
|
||||
pass
|
||||
page_breaks = set([])
|
||||
|
@ -58,6 +58,20 @@ class FetchNewsAction(InterfaceAction):
|
||||
self.scheduler.recipe_download_failed(arg)
|
||||
return self.gui.job_exception(job)
|
||||
id = self.gui.library_view.model().add_news(pt.name, arg)
|
||||
|
||||
# Arg may contain a "keep_issues" variable. If it is non-zero,
|
||||
# delete all but newest x issues.
|
||||
try:
|
||||
keep_issues = int(arg['keep_issues'])
|
||||
except:
|
||||
keep_issues = 0
|
||||
if keep_issues > 0:
|
||||
ids_with_tag = list(sorted(self.gui.library_view.model().
|
||||
db.tags_older_than(arg['title'], None), reverse=True))
|
||||
ids_to_delete = ids_with_tag[keep_issues:]
|
||||
if ids_to_delete:
|
||||
self.gui.library_view.model().delete_books_by_id(ids_to_delete)
|
||||
|
||||
self.gui.library_view.model().reset()
|
||||
sync = self.gui.news_to_be_synced
|
||||
sync.add(id)
|
||||
|
@ -22,7 +22,8 @@ class PluginWidget(Widget, Ui_Form):
|
||||
['colors', 'dont_normalize', 'keep_aspect_ratio', 'right2left',
|
||||
'despeckle', 'no_sort', 'no_process', 'landscape',
|
||||
'dont_sharpen', 'disable_trim', 'wide', 'output_format',
|
||||
'dont_grayscale', 'comic_image_size']
|
||||
'dont_grayscale', 'comic_image_size',
|
||||
'dont_add_comic_pages_to_toc']
|
||||
)
|
||||
self.db, self.book_id = db, book_id
|
||||
for x in get_option('output_format').option.choices:
|
||||
|
@ -14,7 +14,7 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>&Number of Colors:</string>
|
||||
@ -24,7 +24,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="opt_colors">
|
||||
<property name="minimum">
|
||||
<number>8</number>
|
||||
@ -37,70 +37,70 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="5" column="0">
|
||||
<widget class="QCheckBox" name="opt_dont_normalize">
|
||||
<property name="text">
|
||||
<string>Disable &normalize</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QCheckBox" name="opt_keep_aspect_ratio">
|
||||
<property name="text">
|
||||
<string>Keep &aspect ratio</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QCheckBox" name="opt_dont_sharpen">
|
||||
<property name="text">
|
||||
<string>Disable &Sharpening</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QCheckBox" name="opt_disable_trim">
|
||||
<property name="text">
|
||||
<string>Disable &Trimming</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="9" column="0">
|
||||
<widget class="QCheckBox" name="opt_wide">
|
||||
<property name="text">
|
||||
<string>&Wide</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<item row="10" column="0">
|
||||
<widget class="QCheckBox" name="opt_landscape">
|
||||
<property name="text">
|
||||
<string>&Landscape</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<item row="11" column="0">
|
||||
<widget class="QCheckBox" name="opt_right2left">
|
||||
<property name="text">
|
||||
<string>&Right to left</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
<item row="12" column="0">
|
||||
<widget class="QCheckBox" name="opt_no_sort">
|
||||
<property name="text">
|
||||
<string>Don't so&rt</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<item row="13" column="0">
|
||||
<widget class="QCheckBox" name="opt_despeckle">
|
||||
<property name="text">
|
||||
<string>De&speckle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="0">
|
||||
<item row="15" column="0">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -120,7 +120,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="0">
|
||||
<item row="14" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>&Output format:</string>
|
||||
@ -130,7 +130,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<item row="14" column="1">
|
||||
<widget class="QComboBox" name="opt_output_format"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
@ -140,7 +140,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Override image &size:</string>
|
||||
@ -150,9 +150,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QLineEdit" name="opt_comic_image_size"/>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="opt_dont_add_comic_pages_to_toc">
|
||||
<property name="text">
|
||||
<string>Don't add links to &pages to the Table of Contents for CBC files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
@ -44,7 +44,8 @@
|
||||
<widget class="QLabel" name="msg">
|
||||
<property name="text">
|
||||
<string><p>This book is locked by <b>DRM</b>. To learn more about DRM and why you cannot read or convert this book in calibre,
|
||||
<a href="http://bugs.calibre-ebook.com/wiki/DRM">click here</a>.</string>
|
||||
<a href="http://drmfree.calibre-ebook.com/about#drm">click here</a>.<p>A large number of recent, DRM free releases are
|
||||
available at <a href="http://drmfree.calibre-ebook.com">Open Books</a>.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
@ -10,11 +10,9 @@ Scheduler for automated recipe downloads
|
||||
from datetime import timedelta
|
||||
|
||||
from PyQt4.Qt import QDialog, SIGNAL, Qt, QTime, QObject, QMenu, \
|
||||
QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QHBoxLayout, \
|
||||
QLabel
|
||||
QAction, QIcon, QMutex, QTimer, pyqtSignal
|
||||
|
||||
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
|
||||
from calibre.gui2.search_box import SearchBox2
|
||||
from calibre.gui2 import config as gconf, error_dialog
|
||||
from calibre.web.feeds.recipes.model import RecipeModel
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
@ -28,18 +26,12 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
self.setupUi(self)
|
||||
self.recipe_model = recipe_model
|
||||
self.recipe_model.do_refresh()
|
||||
self.count_label.setText(
|
||||
_('%s news sources') %
|
||||
self.recipe_model.showing_count)
|
||||
|
||||
self._cont = QWidget(self)
|
||||
self._cont.l = QHBoxLayout()
|
||||
self._cont.setLayout(self._cont.l)
|
||||
self._cont.la = QLabel(_('&Search:'))
|
||||
self._cont.l.addWidget(self._cont.la, 1)
|
||||
self.search = SearchBox2(self)
|
||||
self._cont.l.addWidget(self.search, 100)
|
||||
self._cont.la.setBuddy(self.search)
|
||||
self.search.setMinimumContentsLength(25)
|
||||
self.search.initialize('scheduler_search_history')
|
||||
self.recipe_box.layout().insertWidget(0, self._cont)
|
||||
self.search.setMinimumContentsLength(15)
|
||||
self.search.search.connect(self.recipe_model.search)
|
||||
self.recipe_model.searched.connect(self.search.search_done,
|
||||
type=Qt.QueuedConnection)
|
||||
@ -153,9 +145,12 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
self.recipe_model.un_schedule_recipe(urn)
|
||||
|
||||
add_title_tag = self.add_title_tag.isChecked()
|
||||
keep_issues = u'0'
|
||||
if self.keep_issues.isEnabled():
|
||||
keep_issues = unicode(self.keep_issues.value())
|
||||
custom_tags = unicode(self.custom_tags.text()).strip()
|
||||
custom_tags = [x.strip() for x in custom_tags.split(',')]
|
||||
self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags)
|
||||
self.recipe_model.customize_recipe(urn, add_title_tag, custom_tags, keep_issues)
|
||||
return True
|
||||
|
||||
def initialize_detail_box(self, urn):
|
||||
@ -215,9 +210,16 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
if d < timedelta(days=366):
|
||||
self.last_downloaded.setText(_('Last downloaded')+': '+tm)
|
||||
|
||||
add_title_tag, custom_tags = customize_info
|
||||
add_title_tag, custom_tags, keep_issues = customize_info
|
||||
self.add_title_tag.setChecked(add_title_tag)
|
||||
self.custom_tags.setText(u', '.join(custom_tags))
|
||||
try:
|
||||
keep_issues = int(keep_issues)
|
||||
except:
|
||||
keep_issues = 0
|
||||
self.keep_issues.setValue(keep_issues)
|
||||
self.keep_issues.setEnabled(self.add_title_tag.isChecked())
|
||||
|
||||
|
||||
|
||||
class Scheduler(QObject):
|
||||
@ -299,7 +301,7 @@ class Scheduler(QObject):
|
||||
un = pw = None
|
||||
if account_info is not None:
|
||||
un, pw = account_info
|
||||
add_title_tag, custom_tags = customize_info
|
||||
add_title_tag, custom_tags, keep_issues = customize_info
|
||||
script = self.recipe_model.get_recipe(urn)
|
||||
pt = PersistentTemporaryFile('_builtin.recipe')
|
||||
pt.write(script)
|
||||
@ -312,6 +314,7 @@ class Scheduler(QObject):
|
||||
'recipe':pt.name,
|
||||
'title':recipe.get('title',''),
|
||||
'urn':urn,
|
||||
'keep_issues':keep_issues
|
||||
}
|
||||
self.download_queue.add(urn)
|
||||
self.start_recipe_fetch.emit(arg)
|
||||
|
@ -14,58 +14,24 @@
|
||||
<string>Schedule news download</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/scheduler.png</normaloff>:/images/scheduler.png</iconset>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" rowspan="3">
|
||||
<widget class="QGroupBox" name="recipe_box">
|
||||
<property name="title">
|
||||
<string>Recipes</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTreeView" name="recipes">
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="download_all_button">
|
||||
<property name="toolTip">
|
||||
<string>Download all scheduled recipes at once</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1,2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>Download &all scheduled</string>
|
||||
<string>&Search:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="rnumber">
|
||||
<property name="text">
|
||||
<string/>
|
||||
<property name="buddy">
|
||||
<cstring>search</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="SearchBox2" name="search"/>
|
||||
</item>
|
||||
<item row="0" column="2" rowspan="3">
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
@ -78,8 +44,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>375</width>
|
||||
<height>502</height>
|
||||
<width>469</width>
|
||||
<height>504</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
@ -88,12 +54,6 @@
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="detail_box">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@ -107,9 +67,6 @@
|
||||
<property name="text">
|
||||
<string>blurb</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
@ -250,6 +207,9 @@
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@ -314,15 +274,15 @@
|
||||
<attribute name="title">
|
||||
<string>&Advanced</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QCheckBox" name="add_title_tag">
|
||||
<property name="text">
|
||||
<string>Add &title as tag</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>&Extra tags:</string>
|
||||
@ -332,10 +292,38 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="custom_tags"/>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="toolTip">
|
||||
<string>Maximum number of copies (issues) of this recipe to keep. Set to 0 to keep all (disable).</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&Keep at most:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>keep_issues</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QSpinBox" name="keep_issues">
|
||||
<property name="toolTip">
|
||||
<string><p>When set, this option will cause calibre to keep, at most, the specified number of issues of this periodical. Every time a new issue is downloaded, the oldest one is deleted, if the total is larger than this number.
|
||||
<p>Note that this feature only works if you have the option to add the title as tag checked, above.
|
||||
<p>Also, the setting for deleting periodicals older than a number of days, below, takes priority over this setting.</string>
|
||||
</property>
|
||||
<property name="specialValueText">
|
||||
<string>all issues</string>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> issues</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -348,6 +336,9 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QLineEdit" name="custom_tags"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
@ -363,9 +354,63 @@
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<widget class="QTreeView" name="recipes">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="animated">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>&Delete downloaded news older than:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>old_news</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="old_news">
|
||||
<property name="toolTip">
|
||||
<string><p>Delete downloaded news older than the specified number of days. Set to zero to disable.
|
||||
<p>You can also control the maximum number of issues of a specific periodical that are kept by clicking the Advanced tab for that periodical above.</string>
|
||||
</property>
|
||||
<property name="specialValueText">
|
||||
<string>never delete</string>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> days</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="4" column="2">
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -375,24 +420,35 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="old_news">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="download_all_button">
|
||||
<property name="toolTip">
|
||||
<string>Delete downloaded news older than the specified number of days. Set to zero to disable.</string>
|
||||
<string>Download all scheduled news sources at once</string>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> days</string>
|
||||
<property name="text">
|
||||
<string>Download &all scheduled</string>
|
||||
</property>
|
||||
<property name="prefix">
|
||||
<string>Delete downloaded news older than </string>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="QLabel" name="count_label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SearchBox2</class>
|
||||
<extends>QComboBox</extends>
|
||||
<header>calibre/gui2/search_box.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="../../../../resources/images.qrc"/>
|
||||
</resources>
|
||||
@ -436,12 +492,12 @@
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>456</x>
|
||||
<y>173</y>
|
||||
<x>458</x>
|
||||
<y>155</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>537</x>
|
||||
<y>176</y>
|
||||
<x>573</x>
|
||||
<y>158</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
@ -452,12 +508,12 @@
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>456</x>
|
||||
<y>173</y>
|
||||
<x>458</x>
|
||||
<y>155</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>647</x>
|
||||
<y>176</y>
|
||||
<x>684</x>
|
||||
<y>157</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
@ -468,12 +524,28 @@
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>456</x>
|
||||
<y>239</y>
|
||||
<x>458</x>
|
||||
<y>212</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>495</x>
|
||||
<y>218</y>
|
||||
<x>752</x>
|
||||
<y>215</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>add_title_tag</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>keep_issues</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>508</x>
|
||||
<y>42</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>577</x>
|
||||
<y>108</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
|
@ -73,16 +73,17 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
if idx == 0:
|
||||
continue
|
||||
for n in category_values[idx]():
|
||||
t = Item(name=n, label=label, index=len(self.all_items),icon=category_icons[idx], exists=True)
|
||||
t = Item(name=n, label=label, index=len(self.all_items),
|
||||
icon=category_icons[idx], exists=True)
|
||||
self.all_items.append(t)
|
||||
self.all_items_dict[label+':'+n] = t
|
||||
self.all_items_dict[icu_lower(label+':'+n)] = t
|
||||
|
||||
self.categories = dict.copy(db.prefs.get('user_categories', {}))
|
||||
if self.categories is None:
|
||||
self.categories = {}
|
||||
for cat in self.categories:
|
||||
for item,l in enumerate(self.categories[cat]):
|
||||
key = ':'.join([l[1], l[0]])
|
||||
key = icu_lower(':'.join([l[1], l[0]]))
|
||||
t = self.all_items_dict.get(key, None)
|
||||
if l[1] in self.category_labels:
|
||||
if t is None:
|
||||
@ -170,8 +171,17 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
cat_name = unicode(self.input_box.text()).strip()
|
||||
if cat_name == '':
|
||||
return False
|
||||
for c in self.categories:
|
||||
if strcmp(c, cat_name) == 0:
|
||||
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||
error_dialog(self, _('Invalid name'),
|
||||
_('That name contains leading or trailing periods, '
|
||||
'multiple periods in a row or spaces before '
|
||||
'or after periods.')).exec_()
|
||||
return False
|
||||
for c in sorted(self.categories.keys(), key=sort_key):
|
||||
if strcmp(c, cat_name) == 0 or \
|
||||
(icu_lower(cat_name).startswith(icu_lower(c) + '.') and\
|
||||
not cat_name.startswith(c + '.')):
|
||||
error_dialog(self, _('Name already used'),
|
||||
_('That name is already used, perhaps with different case.')).exec_()
|
||||
return False
|
||||
@ -192,6 +202,14 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
return False
|
||||
if not self.current_cat_name:
|
||||
return False
|
||||
comps = [c.strip() for c in cat_name.split('.') if c.strip()]
|
||||
if len(comps) == 0 or '.'.join(comps) != cat_name:
|
||||
error_dialog(self, _('Invalid name'),
|
||||
_('That name contains leading or trailing periods, '
|
||||
'multiple periods in a row or spaces before '
|
||||
'or after periods.')).exec_()
|
||||
return False
|
||||
|
||||
for c in self.categories:
|
||||
if strcmp(c, cat_name) == 0:
|
||||
error_dialog(self, _('Name already used'),
|
||||
@ -231,6 +249,12 @@ class TagCategories(QDialog, Ui_TagCategories):
|
||||
|
||||
def accept(self):
|
||||
self.save_category()
|
||||
for cat in sorted(self.categories.keys(), key=sort_key):
|
||||
components = cat.split('.')
|
||||
for i in range(0,len(components)):
|
||||
c = '.'.join(components[0:i+1])
|
||||
if c not in self.categories:
|
||||
self.categories[c] = []
|
||||
QDialog.accept(self)
|
||||
|
||||
def save_category(self):
|
||||
|
@ -58,10 +58,12 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
|
||||
self.to_rename = {}
|
||||
self.to_delete = set([])
|
||||
self.original_names = {}
|
||||
self.all_tags = {}
|
||||
|
||||
for k,v in data:
|
||||
self.all_tags[v] = k
|
||||
self.original_names[k] = v
|
||||
for tag in sorted(self.all_tags.keys(), key=key):
|
||||
item = ListWidgetItem(tag)
|
||||
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||
|
@ -209,7 +209,6 @@ class EmailMixin(object): # {{{
|
||||
|
||||
def __init__(self):
|
||||
self.emailer = Emailer(self.job_manager)
|
||||
self.emailer.start()
|
||||
|
||||
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None,
|
||||
do_auto_convert=True, specific_format=None):
|
||||
@ -255,6 +254,8 @@ class EmailMixin(object): # {{{
|
||||
|
||||
to_s = list(repeat(to, len(attachments)))
|
||||
if attachments:
|
||||
if not self.emailer.is_alive():
|
||||
self.emailer.start()
|
||||
self.emailer.send_mails(jobnames,
|
||||
Dispatcher(partial(self.email_sent, remove=remove)),
|
||||
attachments, to_s, subjects, texts, attachment_names)
|
||||
@ -325,6 +326,8 @@ class EmailMixin(object): # {{{
|
||||
files, auto = self.library_view.model().\
|
||||
get_preferred_formats_from_ids([id_], fmts)
|
||||
return files
|
||||
if not self.emailer.is_alive():
|
||||
self.emailer.start()
|
||||
sent_mails = self.emailer.email_news(mi, remove,
|
||||
get_fmts, self.email_sent)
|
||||
if sent_mails:
|
||||
|
@ -7,17 +7,19 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
|
||||
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
|
||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
||||
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
||||
from calibre.gui2 import config, gprefs, qt_app
|
||||
from calibre.utils.localization import available_translations, \
|
||||
get_language, get_lang
|
||||
from calibre.utils.config import prefs
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
def genesis(self, gui):
|
||||
self.gui = gui
|
||||
db = gui.library_view.model().db
|
||||
|
||||
r = self.register
|
||||
|
||||
@ -61,6 +63,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
r('tags_browser_partition_method', gprefs, choices=choices)
|
||||
r('tags_browser_collapse_at', gprefs)
|
||||
|
||||
choices = set([k for k in db.field_metadata.all_field_keys()
|
||||
if db.field_metadata[k]['is_category'] and
|
||||
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
|
||||
choices -= set(['authors', 'publisher', 'formats', 'news'])
|
||||
self.opt_categories_using_hierarchy.update_items_cache(choices)
|
||||
r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
|
||||
choices=sorted(list(choices), key=sort_key))
|
||||
|
||||
|
||||
self.current_font = None
|
||||
self.change_font_button.clicked.connect(self.change_font)
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>670</width>
|
||||
<height>392</height>
|
||||
<height>422</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -136,7 +136,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Tags browser category partitioning method:</string>
|
||||
<string>Tags browser category &partitioning method:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_tags_browser_partition_method</cstring>
|
||||
@ -157,7 +157,7 @@ if you never want subcategories</string>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Collapse when more items than:</string>
|
||||
<string>&Collapse when more items than:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_tags_browser_collapse_at</cstring>
|
||||
@ -190,6 +190,28 @@ up into sub-categories. If the partition method is set to disable, this value is
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_81">
|
||||
<property name="text">
|
||||
<string>Categories with &hierarchical items:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>opt_categories_using_hierarchy</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
|
||||
<property name="toolTip">
|
||||
<string>A comma-separated list of columns in which items containing
|
||||
periods are displayed in the tag browser trees. For example, if
|
||||
this box contains 'tags' then tags of the form 'Mystery.English'
|
||||
and 'Mystery.Thriller' will be displayed with English and Thriller
|
||||
both under 'Mystery'. If 'tags' is not in this box,
|
||||
then the tags will be displayed each on their own line.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0" colspan="2">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
@ -275,6 +297,13 @@ up into sub-categories. If the partition method is set to disable, this value is
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>MultiCompleteLineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>calibre/gui2/complete.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
@ -217,11 +217,15 @@ class SearchBox2(QComboBox): # {{{
|
||||
self.clear()
|
||||
else:
|
||||
self.normalize_state()
|
||||
self.lineEdit().setCompleter(None)
|
||||
self.setEditText(txt)
|
||||
self.line_edit.end(False)
|
||||
if emit_changed:
|
||||
self.changed.emit()
|
||||
self._do_search(store_in_history=store_in_history)
|
||||
c = QCompleter()
|
||||
self.lineEdit().setCompleter(c)
|
||||
c.setCompletionMode(c.PopupCompletion)
|
||||
self.focus_to_library.emit()
|
||||
finally:
|
||||
if not store_in_history:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -633,6 +633,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
mb.stop()
|
||||
|
||||
self.hide_windows()
|
||||
if self.emailer.is_alive():
|
||||
self.emailer.stop()
|
||||
try:
|
||||
try:
|
||||
|
@ -124,8 +124,15 @@ def _match(query, value, matchkind):
|
||||
for t in value:
|
||||
t = icu_lower(t)
|
||||
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
|
||||
if ((matchkind == EQUALS_MATCH and query == t) or
|
||||
(matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored
|
||||
if (matchkind == EQUALS_MATCH):
|
||||
if query[0] == '.':
|
||||
if t.startswith(query[1:]):
|
||||
ql = len(query) - 1
|
||||
if (len(t) == ql) or (t[ql:ql+1] == '.'):
|
||||
return True
|
||||
elif query == t:
|
||||
return True
|
||||
elif ((matchkind == REGEXP_MATCH and re.search(query, t, re.I)) or ### search unanchored
|
||||
(matchkind == CONTAINS_MATCH and query in t)):
|
||||
return True
|
||||
except re.error:
|
||||
@ -412,13 +419,20 @@ class ResultCache(SearchQueryParser): # {{{
|
||||
|
||||
def get_user_category_matches(self, location, query, candidates):
|
||||
res = set([])
|
||||
if self.db_prefs is None:
|
||||
if self.db_prefs is None or len(query) < 2:
|
||||
return res
|
||||
user_cats = self.db_prefs.get('user_categories', [])
|
||||
if location not in user_cats:
|
||||
return res
|
||||
c = set(candidates)
|
||||
for (item, category, ign) in user_cats[location]:
|
||||
|
||||
if query.startswith('.'):
|
||||
check_subcats = True
|
||||
query = query[1:]
|
||||
else:
|
||||
check_subcats = False
|
||||
|
||||
for key in user_cats:
|
||||
if key == location or (check_subcats and key.startswith(location + '.')):
|
||||
for (item, category, ign) in user_cats[key]:
|
||||
s = self.get_matches(category, '=' + item, candidates=c)
|
||||
c -= s
|
||||
res |= s
|
||||
|
@ -46,11 +46,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||
class Tag(object):
|
||||
|
||||
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
||||
tooltip=None, icon=None, category=None):
|
||||
tooltip=None, icon=None, category=None, id_set=None):
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.count = count
|
||||
self.state = state
|
||||
self.is_hierarchical = False
|
||||
self.is_editable = True
|
||||
self.id_set = id_set
|
||||
self.avg_rating = avg/2.0 if avg is not None else 0
|
||||
self.sort = sort
|
||||
if self.avg_rating > 0:
|
||||
@ -174,6 +177,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.prefs = DBPrefs(self)
|
||||
defs = self.prefs.defaults
|
||||
defs['gui_restriction'] = defs['cs_restriction'] = ''
|
||||
defs['categories_using_hierarchy'] = []
|
||||
|
||||
# Migrate saved search and user categories to db preference scheme
|
||||
def migrate_preference(key, default):
|
||||
@ -812,6 +816,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
index_is_id=index_is_id),
|
||||
extra=self.get_custom_extra(idx, label=meta['label'],
|
||||
index_is_id=index_is_id))
|
||||
|
||||
user_cats = self.prefs['user_categories']
|
||||
user_cat_vals = {}
|
||||
for ucat in user_cats:
|
||||
res = []
|
||||
for name,cat,ign in user_cats[ucat]:
|
||||
v = mi.get(cat, None)
|
||||
if isinstance(v, list):
|
||||
if name in v:
|
||||
res.append([name,cat])
|
||||
elif name == v:
|
||||
res.append([name,cat])
|
||||
user_cat_vals[ucat] = res
|
||||
mi.user_categories = user_cat_vals
|
||||
|
||||
if get_cover:
|
||||
mi.cover = self.cover(id, index_is_id=True, as_path=True)
|
||||
return mi
|
||||
@ -1144,6 +1163,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.n = name
|
||||
self.s = sort
|
||||
self.c = 0
|
||||
self.id_set = set()
|
||||
self.rt = 0
|
||||
self.rc = 0
|
||||
self.id = None
|
||||
@ -1161,6 +1181,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
return 'n=%s s=%s c=%d rt=%d rc=%d id=%s'%\
|
||||
(self.n, self.s, self.c, self.rt, self.rc, self.id)
|
||||
|
||||
def clean_user_categories(self):
|
||||
user_cats = self.prefs.get('user_categories', {})
|
||||
new_cats = {}
|
||||
for k in user_cats:
|
||||
comps = [c.strip() for c in k.split('.') if c.strip()]
|
||||
if len(comps) == 0:
|
||||
i = 1
|
||||
while True:
|
||||
if unicode(i) not in user_cats:
|
||||
new_cats[unicode(i)] = user_cats[k]
|
||||
break
|
||||
i += 1
|
||||
else:
|
||||
new_cats['.'.join(comps)] = user_cats[k]
|
||||
self.prefs.set('user_categories', new_cats)
|
||||
return new_cats
|
||||
|
||||
def get_categories(self, sort='name', ids=None, icon_map=None):
|
||||
#start = last = time.clock()
|
||||
@ -1248,6 +1284,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
item = tag_class(val, sort_val)
|
||||
tcategories[cat][val] = item
|
||||
item.c += 1
|
||||
item.id_set.add(book[0])
|
||||
item.id = item_id
|
||||
if rating > 0:
|
||||
item.rt += rating
|
||||
@ -1265,6 +1302,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
item = tag_class(val, sort_val)
|
||||
tcategories[cat][val] = item
|
||||
item.c += 1
|
||||
item.id_set.add(book[0])
|
||||
item.id = item_id
|
||||
if rating > 0:
|
||||
item.rt += rating
|
||||
@ -1352,7 +1390,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
categories[category] = [tag_class(formatter(r.n), count=r.c, id=r.id,
|
||||
avg=avgr(r), sort=r.s, icon=icon,
|
||||
tooltip=tooltip, category=category)
|
||||
tooltip=tooltip, category=category,
|
||||
id_set=r.id_set)
|
||||
for r in items]
|
||||
|
||||
#print 'end phase "tags list":', time.clock() - last, 'seconds'
|
||||
@ -1361,6 +1400,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# Needed for legacy databases that have multiple ratings that
|
||||
# map to n stars
|
||||
for r in categories['rating']:
|
||||
r.id_set = None
|
||||
for x in categories['rating']:
|
||||
if r.name == x.name and r.id != x.id:
|
||||
r.count = r.count + x.count
|
||||
@ -1397,7 +1437,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
categories['formats'].sort(key = lambda x:x.name)
|
||||
|
||||
#### Now do the user-defined categories. ####
|
||||
user_categories = dict.copy(self.prefs['user_categories'])
|
||||
user_categories = dict.copy(self.clean_user_categories())
|
||||
|
||||
# We want to use same node in the user category as in the source
|
||||
# category. To do that, we need to find the original Tag node. There is
|
||||
@ -1406,7 +1446,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# temporarily duplicating the categories lists.
|
||||
taglist = {}
|
||||
for c in categories.keys():
|
||||
taglist[c] = dict(map(lambda t:(t.name, t), categories[c]))
|
||||
taglist[c] = dict(map(lambda t:(icu_lower(t.name), t), categories[c]))
|
||||
|
||||
muc = self.prefs.get('grouped_search_make_user_categories', [])
|
||||
gst = self.prefs.get('grouped_search_terms', {})
|
||||
@ -1422,8 +1462,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
for user_cat in sorted(user_categories.keys(), key=sort_key):
|
||||
items = []
|
||||
for (name,label,ign) in user_categories[user_cat]:
|
||||
if label in taglist and name in taglist[label]:
|
||||
items.append(taglist[label][name])
|
||||
n = icu_lower(name)
|
||||
if label in taglist and n in taglist[label]:
|
||||
items.append(taglist[label][n])
|
||||
# else: do nothing, to not include nodes w zero counts
|
||||
cat_name = '@' + user_cat # add the '@' to avoid name collision
|
||||
# Not a problem if we accumulate entries in the icon map
|
||||
@ -1460,13 +1501,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
############# End get_categories
|
||||
|
||||
def tags_older_than(self, tag, delta):
|
||||
'''
|
||||
Return the ids of all books having the tag ``tag`` that are older than
|
||||
than the specified time. tag comparison is case insensitive.
|
||||
|
||||
:param delta: A timedelta object or None. If None, then all ids with
|
||||
the tag are returned.
|
||||
'''
|
||||
tag = tag.lower().strip()
|
||||
now = nowf()
|
||||
tindex = self.FIELD_MAP['timestamp']
|
||||
gindex = self.FIELD_MAP['tags']
|
||||
for r in self.data._data:
|
||||
if r is not None:
|
||||
if (now - r[tindex]) > delta:
|
||||
if delta is None or (now - r[tindex]) > delta:
|
||||
tags = r[gindex]
|
||||
if tags and tag in [x.strip() for x in
|
||||
tags.lower().split(',')]:
|
||||
@ -2434,6 +2482,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
stream.seek(0)
|
||||
mi = get_metadata(stream, format, use_libprs_metadata=False)
|
||||
stream.seek(0)
|
||||
if not mi.series_index:
|
||||
mi.series_index = 1.0
|
||||
mi.tags = [_('News')]
|
||||
if arg['add_title_tag']:
|
||||
|
@ -32,7 +32,7 @@ category_icon_map = {
|
||||
'news' : 'news.png',
|
||||
'tags' : 'tags.png',
|
||||
'custom:' : 'column.png',
|
||||
'user:' : 'drawer.png',
|
||||
'user:' : 'tb_folder.png',
|
||||
'search' : 'search.png'
|
||||
}
|
||||
|
||||
|
@ -342,6 +342,7 @@ class BrowseServer(object):
|
||||
return category_meta[x]['name'].lower()
|
||||
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
uc_displayed = set()
|
||||
for category in sorted(categories, key=lambda x: sort_key(getter(x))):
|
||||
if len(categories[category]) == 0:
|
||||
continue
|
||||
@ -361,6 +362,18 @@ class BrowseServer(object):
|
||||
icon = category_icon_map['user:']
|
||||
else:
|
||||
icon = 'blank.png'
|
||||
|
||||
if meta['kind'] == 'user':
|
||||
dot = category.find('.')
|
||||
if dot > 0:
|
||||
cat = category[:dot]
|
||||
if cat not in uc_displayed:
|
||||
cats.append((meta['name'][:dot-1], cat, icon))
|
||||
uc_displayed.add(cat)
|
||||
else:
|
||||
cats.append((meta['name'], category, icon))
|
||||
uc_displayed.add(category)
|
||||
else:
|
||||
cats.append((meta['name'], category, icon))
|
||||
|
||||
cats = [(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||
@ -394,13 +407,59 @@ class BrowseServer(object):
|
||||
category_name = category_meta[category]['name']
|
||||
datatype = category_meta[category]['datatype']
|
||||
|
||||
# See if we have any sub-categories to display. As we find them, add
|
||||
# them to the displayed set to avoid showing the same item twice
|
||||
uc_displayed = set()
|
||||
cats = []
|
||||
for ucat in sorted(categories.keys(), key=sort_key):
|
||||
if len(categories[ucat]) == 0:
|
||||
continue
|
||||
if category == 'formats':
|
||||
continue
|
||||
meta = category_meta.get(ucat, None)
|
||||
if meta is None:
|
||||
continue
|
||||
if meta['kind'] != 'user':
|
||||
continue
|
||||
cat_len = len(category)
|
||||
if not (len(ucat) > cat_len and ucat.startswith(category+'.')):
|
||||
continue
|
||||
icon = category_icon_map['user:']
|
||||
# we have a subcategory. Find any further dots (further subcats)
|
||||
cat_len += 1
|
||||
cat = ucat[cat_len:]
|
||||
dot = cat.find('.')
|
||||
if dot > 0:
|
||||
# More subcats
|
||||
cat = cat[:dot]
|
||||
if cat not in uc_displayed:
|
||||
cats.append((cat, ucat[:cat_len+dot], icon))
|
||||
uc_displayed.add(cat)
|
||||
else:
|
||||
# This is the end of the chain
|
||||
cats.append((cat, ucat, icon))
|
||||
uc_displayed.add(cat)
|
||||
|
||||
cats = u'\n\n'.join(
|
||||
[(u'<li><a title="{2} {0}" href="{3}/browse/category/{1}"> </a>'
|
||||
u'<img src="{3}{src}" alt="{0}" />'
|
||||
u'<span class="label">{0}</span>'
|
||||
u'</li>')
|
||||
.format(xml(x, True), xml(quote(y)), xml(_('Browse books by')),
|
||||
self.opts.url_prefix, src='/browse/icon/'+z)
|
||||
for x, y, z in cats])
|
||||
if cats:
|
||||
cats = (u'\n<div class="toplevel">\n'
|
||||
'{0}</div>').format(cats)
|
||||
script = 'toplevel();'
|
||||
else:
|
||||
script = 'true'
|
||||
|
||||
# Now do the category items
|
||||
items = categories[category]
|
||||
sort = self.browse_sort_categories(items, sort)
|
||||
|
||||
script = 'true'
|
||||
|
||||
if len(items) == 1:
|
||||
if not cats and len(items) == 1:
|
||||
# Only one item in category, go directly to book list
|
||||
prefix = '' if self.is_wsgi else self.opts.url_prefix
|
||||
html = get_category_items(category, items,
|
||||
@ -443,6 +502,9 @@ class BrowseServer(object):
|
||||
|
||||
|
||||
|
||||
if cats:
|
||||
script = 'toplevel();category(%s);'%script
|
||||
else:
|
||||
script = 'category(%s);'%script
|
||||
|
||||
main = u'''
|
||||
@ -453,7 +515,7 @@ class BrowseServer(object):
|
||||
{1}
|
||||
</div>
|
||||
'''.format(
|
||||
xml(_('Browsing by')+': ' + category_name), items,
|
||||
xml(_('Browsing by')+': ' + category_name), cats + items,
|
||||
xml(_('Up'), True), self.opts.url_prefix)
|
||||
|
||||
return self.browse_template(sort).format(title=category_name,
|
||||
|
@ -198,7 +198,7 @@ def cli_docs(app):
|
||||
documented_cmds = []
|
||||
undocumented_cmds = []
|
||||
|
||||
for script in entry_points['console_scripts']:
|
||||
for script in entry_points['console_scripts'] + entry_points['gui_scripts']:
|
||||
module = script[script.index('=')+1:script.index(':')].strip()
|
||||
cmd = script[:script.index('=')].strip()
|
||||
if cmd in ('calibre-complete', 'calibre-parallel'): continue
|
||||
|
@ -81,7 +81,7 @@ Device Integration
|
||||
|
||||
What devices does |app| support?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
At the moment |app| has full support for the SONY PRS line, Barnes & Noble Nook, Cybook Gen 3/Opus, Amazon Kindle line, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook 360, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3, SpringDesign Alex, Kobo Reader, various Android phones and the iPhone/iPad. In addition, using the :guilabel:`Save to disk` function you can use it with any ebook reader that exports itself as a USB disk.
|
||||
At the moment |app| has full support for the SONY PRS line, Barnes & Noble Nook line, Cybook Gen 3/Opus, Amazon Kindle line, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook line, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3 and clones, SpringDesign Alex, Kobo Reader, various Android phones and the iPhone/iPad. In addition, using the :guilabel:`Connect to folder` function you can use it with any ebook reader that exports itself as a USB disk.
|
||||
|
||||
How can I help get my device supported in |app|?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -27,6 +27,7 @@ Actions
|
||||
|
||||
.. image:: images/actions.png
|
||||
:alt: The Actions Toolbar
|
||||
:align: center
|
||||
|
||||
The actions toolbar provides convenient shortcuts to commonly used actions. Most of the action buttons have little arrows next to them. By clicking the arrows, you can perform variations on the default action. Please note that the actions toolbar will look slightly different depending on whether you have an ebook reader attached to your computer.
|
||||
|
||||
@ -39,6 +40,7 @@ The actions toolbar provides convenient shortcuts to commonly used actions. Most
|
||||
Add books
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
.. |adbi| image:: images/add_books.png
|
||||
:class: float-right-img
|
||||
|
||||
|adbi| The :guilabel:`Add books` action has five variations, accessed by the clicking the down arrow on the right side of the button.
|
||||
|
||||
@ -63,6 +65,7 @@ To add an additional format for an existing book, use the :ref:`edit_meta_inform
|
||||
Edit metadata
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. |emii| image:: images/edit_meta_information.png
|
||||
:class: float-right-img
|
||||
|
||||
|emii| The :guilabel:`Edit metadata` action has six variations, which can be accessed by clicking the down arrow on the right side of the button.
|
||||
|
||||
@ -80,6 +83,7 @@ Edit metadata
|
||||
Convert e-books
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. |cei| image:: images/convert_ebooks.png
|
||||
:class: float-right-img
|
||||
|
||||
|cei| Ebooks can be converted from a number of formats into whatever format your e-book reader prefers.
|
||||
Note that ebooks you purchase will typically have `Digital Rights Management <http://bugs.calibre-ebook.com/wiki/DRM>`_ *(DRM)*.
|
||||
@ -106,6 +110,7 @@ The :guilabel:`Convert E-books` action has three variations, accessed by the arr
|
||||
View
|
||||
~~~~~~~~~~~
|
||||
.. |vi| image:: images/view.png
|
||||
:class: float-right-img
|
||||
|
||||
|vi| The :guilabel:`View` action displays the book in an ebook viewer program. |app| has a builtin viewer for the most e-book formats.
|
||||
For other formats it uses the default operating system application. You can configure which formats should open with the internal viewer via
|
||||
@ -118,6 +123,7 @@ on the right of the :guilabel:`View` button.
|
||||
Send to device
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. |stdi| image:: images/send_to_device.png
|
||||
:class: float-right-img
|
||||
|
||||
|stdi| The :guilabel:`Send to device` action has eight variations, accessed by clicking the down arrow on the right of the button.
|
||||
|
||||
@ -138,6 +144,7 @@ You can control the file name and folder structure of files sent to the device b
|
||||
Fetch news
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. |fni| image:: images/fetch_news.png
|
||||
: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.
|
||||
|
||||
@ -155,6 +162,7 @@ The :guilabel:`Fetch news` action has three variations, accessed by clicking the
|
||||
Library
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. |lii| image:: images/library.png
|
||||
:class: float-right-img
|
||||
|
||||
|lii| The :guilabel: `Library` action allows you to create, switch between, rename or delete a Library. |app| allows you to create as many libraries as you wish. You coudl for instance create a fiction library, a non fiction library, a foreign language library a project library, basically any structure that suits your needs. Libraries are the highest organizational structure within |app|, each library has its own set of books, tags, categories and base storage location.
|
||||
|
||||
@ -169,6 +177,7 @@ Library
|
||||
Device
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. |dvi| image:: images/device.png
|
||||
: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.
|
||||
@ -179,6 +188,7 @@ This icon shows up automatically on the main |app| toolbar when you connect a su
|
||||
Save to disk
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. |svdi| image:: images/save_to_disk.png
|
||||
:class: float-right-img
|
||||
|
||||
|svdi| The :guilabel:`Save to disk` action has five variations, accessed by the arrow next to the button.
|
||||
|
||||
@ -212,6 +222,7 @@ Save to disk
|
||||
Connect/Share
|
||||
~~~~~~~~~~~~~~~~~
|
||||
.. |csi| image:: images/connect_share.png
|
||||
:class: float-right-img
|
||||
|
||||
|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.
|
||||
|
||||
@ -230,6 +241,7 @@ Connect/Share
|
||||
Remove books
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
.. |rbi| image:: images/remove_books.png
|
||||
:class: float-right-img
|
||||
|
||||
|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.
|
||||
|
||||
@ -259,6 +271,7 @@ The Preferences Action allows you to change the way various aspects of |app| wor
|
||||
Catalogs
|
||||
----------
|
||||
.. image:: images/catalogs.png
|
||||
:align: center
|
||||
|
||||
A *catalog* is a collection of books. |app| can manage two types of different catalogs:
|
||||
|
||||
@ -274,6 +287,7 @@ Many operations, like Adding books, deleting, viewing, etc. are context sensitiv
|
||||
Search & Sort
|
||||
---------------
|
||||
.. image:: images/search_sort.png
|
||||
:align: center
|
||||
|
||||
The Search & Sort section allows you to perform several powerful actions on your book collections.
|
||||
|
||||
@ -375,6 +389,7 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col
|
||||
:align: middle
|
||||
|
||||
.. figure:: images/search.png
|
||||
:align: center
|
||||
|
||||
:guilabel:`Advanced Search Dialog`
|
||||
|
||||
@ -408,14 +423,43 @@ will be interpreted to have the title: Foundation and Earth and author: Isaac As
|
||||
Book Details
|
||||
-------------
|
||||
.. image:: images/book_details.png
|
||||
:align: center
|
||||
|
||||
The Book Details display shows you extra information and the cover for the currently selected book.
|
||||
|
||||
.. _jobs:
|
||||
|
||||
.. _tag_browser:
|
||||
|
||||
Tag Browser
|
||||
-------------
|
||||
.. image:: images/tag_browser.png
|
||||
:class: float-left-img
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
The outer-level items in the tag browser such as Authors and Series are called categories. You can create your own categories, called User Categories, which are useful for organizing items. For example, you can use the User Categories Editor (push the Manage User Categories button) to create a user category called Favorite Authors, then put the items for your favorites into the category. User categories can have sub-categories. For example, the user category Favorites.Authors is a sub-category of Favorites. You might also have Favorites.Series, in which case there will be two sub-categories under Favorites. Sub-categories can be created by right-clicking on a user category, choosing "Add sub-category to ...", and entering the sub-category name; or by using the User Categories Editor by entering names like the Favorites example above.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
Jobs
|
||||
-----
|
||||
.. image:: images/jobs.png
|
||||
:class: float-left-img
|
||||
|
||||
The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully.
|
||||
|
||||
|
BIN
src/calibre/manual/images/tag_browser.png
Normal file
BIN
src/calibre/manual/images/tag_browser.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
@ -33,6 +33,7 @@ Lets pick a couple of feeds that look interesting:
|
||||
I got the URLs by clicking the little orange RSS icon next to each feed name. To make |app| download the feeds and convert them into an e-book, you should click the :guilabel:`Fetch news` button and then the :guilabel:`Add a custom news source` menu item. A dialog similar to that shown below should open up.
|
||||
|
||||
.. image:: images/custom_news.png
|
||||
:align: center
|
||||
|
||||
First enter ``Portfolio`` into the :guilabel:`Recipe title` field. This will be the title of the e-book that will be created from the articles in the above feeds.
|
||||
|
||||
@ -81,6 +82,7 @@ So it looks like to get the print version, we need to prefix every article URL w
|
||||
Now in the :guilabel:`Advanced Mode` of the Custom news sources dialog, you should see something like (remember to select *The BBC* recipe before switching to advanced mode):
|
||||
|
||||
.. image:: images/bbc_advanced.png
|
||||
:align: center
|
||||
|
||||
You can see that the fields from the :guilabel:`Basic mode` have been translated to python code in a straightforward manner. We need to add instructions to this recipe to use the print version of the articles. All that's needed is to add the following two lines:
|
||||
|
||||
@ -92,6 +94,7 @@ You can see that the fields from the :guilabel:`Basic mode` have been translated
|
||||
This is python, so indentation is important. After you've added the lines, it should look like:
|
||||
|
||||
.. image:: images/bbc_altered.png
|
||||
:align: center
|
||||
|
||||
In the above, ``def print_version(self, url)`` defines a *method* that is called by |app| for every article. ``url`` is the URL of the original article. What ``print_version`` does is take that url and replace it with the new URL that points to the print version of the article. To learn about `python <http://www.python.org>`_ see the `tutorial <http://docs.python.org/tut/>`_.
|
||||
|
||||
@ -109,6 +112,7 @@ The recipe now looks like:
|
||||
.. _bbc1:
|
||||
|
||||
.. image:: images/bbc_altered1.png
|
||||
:align: center
|
||||
|
||||
The new version looks pretty good. If you're a perfectionist, you'll want to read the next section, which deals with actually modifying the downloaded content.
|
||||
|
||||
|
@ -1,4 +1,59 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{% if not embedded %}
|
||||
<script type="text/javascript">
|
||||
|
||||
var _gaq = _gaq || [];
|
||||
_gaq.push(['_setAccount', 'UA-20736318-1']);
|
||||
_gaq.push(['_setDomainName', '.calibre-ebook.com']);
|
||||
_gaq.push(['_trackPageview']);
|
||||
|
||||
(function() {
|
||||
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
|
||||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<style type="text/css">
|
||||
.float-left-img { float: left; margin-right: 1em; margin-bottom: 1em }
|
||||
.float-right-img { float: right; margin-left: 1em; margin-bottom: 1em }
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block document %}
|
||||
<div class="documentwrapper">
|
||||
{%- if render_sidebar %}
|
||||
<div class="bodywrapper">
|
||||
{%- endif %}
|
||||
<div class="body">
|
||||
{% if not embedded %}
|
||||
<div id="ad-container" style="text-align:center">
|
||||
<script type="text/javascript"><!--
|
||||
google_ad_client = "ca-pub-2595272032872519";
|
||||
/* User Manual horizontal */
|
||||
google_ad_slot = "3131592704";
|
||||
google_ad_width = 728;
|
||||
google_ad_height = 90;
|
||||
//-->
|
||||
</script>
|
||||
<script type="text/javascript"
|
||||
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
|
||||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block body %} {% endblock %}
|
||||
</div>
|
||||
{%- if render_sidebar %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebarlogo %}
|
||||
<p class="logo">
|
||||
<a href="http://calibre-ebook.com"><img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/></a>
|
||||
@ -12,3 +67,4 @@
|
||||
<hr/>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -66,6 +66,7 @@ reference mode. You can turn it on by clicking the Reference Mode button |refmi|
|
||||
mouse over a paragraph, calibre will display a unique number made up of the section and paragraph numbers.
|
||||
|
||||
.. image:: images/ref_mode.png
|
||||
:align: center
|
||||
|
||||
You can use this number to unambiguously refer to parts of the books when discussing it with friends or referring to it
|
||||
in other works. You can enter these numbers in the box marked Go to at the top of the window to go to a particular
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user