Merge from trunk

This commit is contained in:
Charles Haley 2013-04-02 10:20:36 +02:00
commit 2ec281b179
65 changed files with 1290 additions and 207 deletions

View File

@ -750,8 +750,61 @@ If this property is detected by |app|, the following custom properties are recog
opf.series
opf.seriesindex
In addition to this, you can specify the picture to use as the cover by naming it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the 'smart' method is used.
As the cover detection might result in double covers in certain output formats, the process will remove the paragraph (only if the only content is the cover!) from the document. But this works only with the named picture!
In addition to this, you can specify the picture to use as the cover by naming
it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no
picture with this name is found, the 'smart' method is used. As the cover
detection might result in double covers in certain output formats, the process
will remove the paragraph (only if the only content is the cover!) from the
document. But this works only with the named picture!
To disable cover detection you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes in advanced mode.
Converting to PDF
~~~~~~~~~~~~~~~~~~~
The first, most important, setting to decide on when converting to PDF is the page
size. By default, |app| uses a page size defined by the current
:guilabel:`Output profile`. So if your output profile is set to Kindle, |app|
will create a PDF with page size suitable for viewing on the small kindle
screen. However, if you view this PDF file on a computer screen, then it will
appear to have too large fonts. To create "normal" sized PDFs, use the override
page size option under :guilabel:`PDF Output` in the conversion dialog.
You can insert arbitrary headers and footers on each page of the PDF by
specifying header and footer templates. Templates are just snippets of HTML
code that get rendered in the header and footer locations. For example, to
display page numbers centered at the bottom of every page, in green, use the following
footer template::
<p style="text-align:center; color:green">Page _PAGENUM_</p>
|app| will automatically replace _PAGENUM_ with the current page number. You
can even put different content on even and odd pages, for example the following
header template will show the title on odd pages and the author on even pages::
<p style="text-align:right"><span class="even_page">_AUTHOR_</span><span class="odd_page"><i>_TITLE_</i></span></p>
|app| will automatically replace _TITLE_ and _AUTHOR_ with the title and author
of the document being converted. You can also display text at the left and
right edges and change the font size, as demonstrated with this header
template::
<div style="font-size:x-small"><p style="float:left">_TITLE_</p><p style="float:right;"><i>_AUTHOR_</i></p></div>
This will display the title at the left and the author at the right, in a font
size smaller than the main text.
Finally, you can also use the current section in templates, as shown below::
<p style="text-align:right">_SECTION_</p>
_SECTION_ is replaced by whatever the name of the current section is. These
names are taken from the metadata Table of Contents in the document (the PDF
Outline). If the document has no table of contents then it will be replaced by
empty text. If a single PDF page has multiple sections, the first section on
the page will be used.
.. note:: When adding headers and footers make sure you set the page top and
bottom margins to large enough values, under the Page Setup section of the
conversion dialog.

View File

@ -66,4 +66,3 @@ class Adventure_zone(BasicNewsRecipe):
if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']:
a['href']=self.index + a['href']
return soup

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>'
'''
dzialzagraniczny.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class dzial_zagraniczny(BasicNewsRecipe):
title = u'Dział Zagraniczny'
__author__ = 'teepel <teepel44@gmail.com>'
language = 'pl'
description = u'Polskiego czytelnika to nie interesuje'
INDEX = 'http://dzialzagraniczny.pl'
extra_css = 'img {display: block;}'
oldest_article = 7
cover_url = 'https://fbcdn-profile-a.akamaihd.net/hprofile-ak-prn1/c145.5.160.160/559442_415653975115959_2126205128_n.jpg'
max_articles_per_feed = 100
remove_empty_feeds = True
remove_javascript = True
no_stylesheets = True
use_embedded_content = True
feeds = [(u'Dział zagraniczny', u'http://feeds.feedburner.com/dyndns/UOfz')]

View File

@ -26,7 +26,7 @@ class ElDiplo_Recipe(BasicNewsRecipe):
title = u'El Diplo'
__author__ = 'Tomas Di Domenico'
description = 'Publicacion mensual de Le Monde Diplomatique, edicion Argentina'
langauge = 'es_AR'
language = 'es_AR'
needs_subscription = True
auto_cleanup = True

29
recipes/equipped.recipe Normal file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>, Artur Stachecki <artur.stachecki@gmail.com>'
'''
equipped.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class equipped(BasicNewsRecipe):
title = u'Equipped'
__author__ = 'teepel <teepel44@gmail.com>'
language = 'pl'
description = u'Wiadomości z equipped.pl'
INDEX = 'http://equipped.pl'
extra_css = '.alignleft {float:left; margin-right:5px;}'
oldest_article = 7
max_articles_per_feed = 100
remove_empty_feeds = True
simultaneous_downloads = 5
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
#keep_only_tags = [dict(name='article')]
#remove_tags = [dict(id='disqus_thread')]
#remove_tags_after = [dict(id='disqus_thread')]
feeds = [(u'Equipped', u'http://feeds.feedburner.com/Equippedpl?format=xml')]

View File

@ -1,12 +1,12 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
import re
from calibre.web.feeds.news import BasicNewsRecipe
class FocusRecipe(BasicNewsRecipe):
__license__ = 'GPL v3'
__author__ = u'intromatyk <intromatyk@gmail.com>'
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
language = 'pl'
version = 1

View File

@ -14,13 +14,14 @@ class gazetaprawna(BasicNewsRecipe):
title = u'Gazeta Prawna'
__author__ = u'Vroo'
publisher = u'Infor Biznes'
oldest_article = 7
oldest_article = 1
max_articles_per_feed = 20
no_stylesheets = True
remove_javascript = True
description = 'Polski dziennik gospodarczy'
language = 'pl'
encoding = 'utf-8'
ignore_duplicate_articles = {'title', 'url'}
remove_tags_after = [
dict(name='div', attrs={'class':['data-art']})
@ -30,7 +31,7 @@ class gazetaprawna(BasicNewsRecipe):
]
feeds = [
(u'Wiadomo\u015bci - najwa\u017cniejsze', u'http://www.gazetaprawna.pl/wiadomosci/najwazniejsze/rss.xml'),
(u'Z ostatniej chwili', u'http://rss.gazetaprawna.pl/GazetaPrawna'),
(u'Biznes i prawo gospodarcze', u'http://biznes.gazetaprawna.pl/rss.xml'),
(u'Prawo i wymiar sprawiedliwo\u015bci', u'http://prawo.gazetaprawna.pl/rss.xml'),
(u'Praca i ubezpieczenia', u'http://praca.gazetaprawna.pl/rss.xml'),
@ -51,3 +52,8 @@ class gazetaprawna(BasicNewsRecipe):
url = url.replace('prawo.gazetaprawna', 'www.gazetaprawna')
url = url.replace('praca.gazetaprawna', 'www.gazetaprawna')
return url
def get_cover_url(self):
soup = self.index_to_soup('http://www.egazety.pl/infor/e-wydanie-dziennik-gazeta-prawna.html')
self.cover_url = soup.find('p', attrs={'class':'covr'}).a['href']
return getattr(self, 'cover_url', self.cover_url)

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

BIN
recipes/icons/equipped.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

BIN
recipes/icons/media2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

BIN
recipes/icons/mobilna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
recipes/icons/osw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

BIN
recipes/icons/ppe_pl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

26
recipes/ittechblog.recipe Normal file
View File

@ -0,0 +1,26 @@
__license__ = 'GPL v3'
__copyright__ = 'MrStefan'
'''
www.ittechblog.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ittechblog(BasicNewsRecipe):
title = u'IT techblog'
__author__ = 'MrStefan <mrstefaan@gmail.com>'
language = 'pl'
description =u'Na naszym blogu technologicznym znajdziesz między innymi: testy sprzętu, najnowsze startupy, technologiczne nowinki, felietony tematyczne.'
extra_css = '.cover > img {display:block;}'
remove_empty_feeds = True
oldest_article = 7
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
keep_only_tags =[dict(attrs={'class':'box'})]
remove_tags =[dict(name='aside'), dict(attrs={'class':['tags', 'counter', 'twitter-share-button']})]
feeds = [(u'Artykuły', u'http://feeds.feedburner.com/ITTechBlog?format=xml')]

View File

@ -2,8 +2,7 @@
from calibre.web.feeds.news import BasicNewsRecipe
class KrytykaPolitycznaRecipe(BasicNewsRecipe):
__license__ = 'GPL v3'
__author__ = u'intromatyk <intromatyk@gmail.com>'
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
language = 'pl'
version = 1

View File

@ -0,0 +1,88 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
'''
magazynconsido.pl/
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.utils.magick import Image
class magazynconsido(BasicNewsRecipe):
title = u'Magazyn Consido'
__author__ = 'Artur Stachecki <artur.stachecki@gmail.com> ,teepel <teepel44@gmail.com>'
language = 'pl'
description =u'Portal dla architektów i projektantów'
masthead_url='http://qualitypixels.pl/wp-content/themes/airlock/advance/inc/timthumb.php?src=http://qualitypixels.pl/wp-content/uploads/2012/01/logotyp-magazynconsido-11.png&w=455&zc=1'
oldest_article = 7
max_articles_per_feed = 100
remove_javascript=True
no_stylesheets = True
use_embedded_content = False
keep_only_tags =[]
keep_only_tags.append(dict(name = 'h1'))
keep_only_tags.append(dict(name = 'p'))
keep_only_tags.append(dict(attrs = {'class' : 'navigation'}))
remove_tags =[dict(attrs = {'style' : 'font-size: x-small;' })]
remove_tags_after =[dict(attrs = {'class' : 'navigation' })]
extra_css=''' img {max-width:30%; max-height:30%; display: block; margin-left: auto; margin-right: auto;}
h1 {text-align: center;}'''
def parse_index(self): #(kk)
soup = self.index_to_soup('http://feeds.feedburner.com/magazynconsido?format=xml')
feeds = []
articles = {}
sections = []
section = ''
for item in soup.findAll('item') :
section = self.tag_to_string(item.category)
if not articles.has_key(section) :
sections.append(section)
articles[section] = []
article_url = self.tag_to_string(item.guid)
article_title = self.tag_to_string(item.title)
article_date = self.tag_to_string(item.pubDate)
article_description = self.tag_to_string(item.description)
articles[section].append( { 'title' : article_title, 'url' : article_url, 'date' : article_date, 'description' : article_description })
for section in sections :
if section == 'Video':
feeds.append((section, articles[section]))
feeds.pop()
else:
feeds.append((section, articles[section]))
return feeds
def append_page(self, soup, appendtag):
apage = soup.find('div', attrs={'class':'wp-pagenavi'})
if apage is not None:
nexturl = soup.find('a', attrs={'class':'nextpostslink'})
soup2 = self.index_to_soup(nexturl['href'])
pagetext = soup2.findAll('p')
for tag in pagetext:
pos = len(appendtag.contents)
appendtag.insert(pos, tag)
while appendtag.find('div', attrs={'class': ['height: 35px;', 'post-meta', 'addthis_toolbox addthis_default_style addthis_', 'post-meta-bottom', 'block_recently_post', 'fbcomments', 'pin-it-button', 'pages', 'navigation']}) is not None:
appendtag.find('div', attrs={'class': ['height: 35px;', 'post-meta', 'addthis_toolbox addthis_default_style addthis_', 'post-meta-bottom', 'block_recently_post', 'fbcomments', 'pin-it-button', 'pages', 'navigation']}).replaceWith('')
def preprocess_html(self, soup): #(kk)
self.append_page(soup, soup.body)
return self.adeify_images(soup)
def postprocess_html(self, soup, first):
#process all the images
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
iurl = tag['src']
img = Image()
img.open(iurl)
if img < 0:
raise RuntimeError('Out of memory')
img.type = "GrayscaleType"
img.save(iurl)
return soup

35
recipes/media2.recipe Normal file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = 'teepel'
'''
media2.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class media2_pl(BasicNewsRecipe):
title = u'Media2'
__author__ = 'teepel <teepel44@gmail.com>'
language = 'pl'
description =u'Media2.pl to jeden z najczęściej odwiedzanych serwisów dla profesjonalistów z branży medialnej, telekomunikacyjnej, public relations oraz nowych technologii.'
masthead_url='http://media2.pl/res/logo/www.png'
remove_empty_feeds= True
oldest_article = 1
max_articles_per_feed = 100
remove_javascript=True
no_stylesheets=True
simultaneous_downloads = 5
extra_css = '''.news-lead{font-weight: bold; }'''
keep_only_tags =[]
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'news-item tpl-big'}))
remove_tags =[]
remove_tags.append(dict(name = 'span', attrs = {'class' : 'news-comments'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'item-sidebar'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'news-tags'}))
feeds = [(u'Media2', u'http://feeds.feedburner.com/media2')]

26
recipes/mobilna.recipe Normal file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = 'MrStefan'
'''
www.mobilna.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class mobilna(BasicNewsRecipe):
title = u'Mobilna.pl'
__author__ = 'MrStefan <mrstefaan@gmail.com>'
language = 'pl'
description =u'twoja mobilna strona'
#masthead_url=''
remove_empty_feeds= True
oldest_article = 7
max_articles_per_feed = 100
remove_javascript=True
no_stylesheets=True
use_embedded_content = True
#keep_only_tags =[dict(attrs={'class':'Post'})]
feeds = [(u'Artykuły', u'http://mobilna.pl/feed/')]

View File

@ -0,0 +1,50 @@
#!usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = 'MrStefan, teepel'
'''
www.mojegotowanie.pl
'''
from calibre.web.feeds.news import BasicNewsRecipe
class mojegotowanie(BasicNewsRecipe):
title = u'Moje Gotowanie'
__author__ = 'MrStefan <mrstefaan@gmail.com>, teepel <teepel44@gmail.com>'
language = 'pl'
description =u'Gotowanie to Twoja pasja? Uwielbiasz sałatki? Lubisz grillować? Przepisy kulinarne doskonałe na wszystkie okazje znajdziesz na www.mojegotowanie.pl.'
masthead_url='http://www.mojegotowanie.pl/extension/selfstart/design/self/images/top_c2.gif'
cover_url = 'http://www.mojegotowanie.pl/extension/selfstart/design/self/images/mgpl/mojegotowanie.gif'
remove_empty_feeds= True
oldest_article = 7
max_articles_per_feed = 100
remove_javascript=True
no_stylesheets=True
keep_only_tags =[]
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'content'}))
feeds = [(u'Artykuły', u'http://mojegotowanie.pl/rss/feed/artykuly'),
(u'Przepisy', u'http://mojegotowanie.pl/rss/feed/przepisy')]
def parse_feeds(self):
feeds = BasicNewsRecipe.parse_feeds(self)
for feed in feeds:
for article in feed.articles[:]:
if 'film' in article.title:
feed.articles.remove(article)
return feeds
def get_article_url(self, article):
link = article.get('link')
if 'Clayout0Cset0Cprint0' in link:
return link
def print_version(self, url):
segment = url.split('/')
URLPart = segment[-2]
URLPart = URLPart.replace('0L0Smojegotowanie0Bpl0Clayout0Cset0Cprint0C', '/')
URLPart = URLPart.replace('0I', '_')
URLPart = URLPart.replace('0C', '/')
return 'http://www.mojegotowanie.pl/layout/set/print' + URLPart

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>'
'''
nczas.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class nczas(BasicNewsRecipe):
title = u'Najwy\u017cszy Czas'
__author__ = 'teepel <teepel44@gmail.com>'
language = 'pl'
description ='Wiadomości z nczas.com'
INDEX='http://nczas.com'
oldest_article = 7
max_articles_per_feed = 100
use_embedded_content = True
remove_empty_feeds= True
simultaneous_downloads = 5
remove_javascript=True
remove_attributes = ['style']
no_stylesheets=True
feeds = [(u'Najwyższy Czas', u'http://nczas.com/feed/')]

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
from calibre.web.feeds.news import BasicNewsRecipe
class NowinyRybnik(BasicNewsRecipe):
title = u'Nowiny - Rybnik'
__author__ = 'Artur Stachecki <artur.stachecki@gmail.com>'
language = 'pl'
description = u'Tygodnik Regionalny NOWINY. Ogłoszenia drobne, wiadomości i wydarzenia z regionu Rybnika i okolic'
oldest_article = 7
masthead_url = 'http://www.nowiny.rybnik.pl/logo/logo.jpg'
max_articles_per_feed = 100
simultaneous_downloads = 5
remove_javascript = True
no_stylesheets = True
keep_only_tags = [(dict(name='div', attrs={'id': 'drukuj'}))]
remove_tags = []
remove_tags.append(dict(name='div', attrs={'id': 'footer'}))
feeds = [(u'Wszystkie artykuły', u'http://www.nowiny.rybnik.pl/rss,artykuly,dzial,0,miasto,0,ile,25.xml')]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

41
recipes/osw.recipe Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>'
'''
http://www.osw.waw.pl - Osrodek studiow wschodnich
'''
from calibre.web.feeds.news import BasicNewsRecipe
class OSW_Recipe(BasicNewsRecipe):
language = 'pl'
title = u'Ośrodek Studiów Wschodnich'
__author__ = 'teepel <teepel44@gmail.com>'
INDEX='http://www.osw.waw.pl'
description = u'Ośrodek Studiów Wschodnich im. Marka Karpia. Centre for Eastern Studies.'
category = u'News'
oldest_article = 7
max_articles_per_feed = 100
cover_url=''
remove_empty_feeds= True
no_stylesheets=True
remove_javascript = True
simultaneous_downloads = 5
keep_only_tags =[]
#this line should show title of the article, but it doesnt work
keep_only_tags.append(dict(name = 'h1', attrs = {'class' : 'print-title'}))
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'print-submitted'}))
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'print-content'}))
remove_tags =[]
remove_tags.append(dict(name = 'table', attrs = {'id' : 'attachments'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'print-submitted'}))
feeds = [(u'OSW', u'http://www.osw.waw.pl/pl/rss.xml')]
def print_version(self, url):
return url.replace('http://www.osw.waw.pl/pl/', 'http://www.osw.waw.pl/pl/print/')

41
recipes/ppe_pl.recipe Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
from calibre.web.feeds.news import BasicNewsRecipe
class ppeRecipe(BasicNewsRecipe):
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
language = 'pl'
title = u'ppe.pl'
category = u'News'
description = u'Portal o konsolach i grach wideo.'
cover_url=''
remove_empty_feeds= True
no_stylesheets=True
oldest_article = 1
max_articles_per_feed = 100000
recursions = 0
no_stylesheets = True
remove_javascript = True
simultaneous_downloads = 2
keep_only_tags =[]
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'news-heading'}))
keep_only_tags.append(dict(name = 'div', attrs = {'class' : 'tresc-poziom'}))
remove_tags =[]
remove_tags.append(dict(name = 'div', attrs = {'class' : 'bateria1'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'bateria2'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'bateria3'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'news-photo'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'fbl'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'info'}))
remove_tags.append(dict(name = 'div', attrs = {'class' : 'links'}))
remove_tags.append(dict(name = 'div', attrs = {'style' : 'padding: 4px'}))
feeds = [
('Newsy', 'feed://ppe.pl/rss/rss.xml'),
]

33
recipes/presseurop.recipe Normal file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python
'''
www.presseurop.eu/pl
'''
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>'
from calibre.web.feeds.news import BasicNewsRecipe
import re
class presseurop(BasicNewsRecipe):
title = u'Presseurop'
description = u'Najlepsze artykuły z prasy europejskiej'
language = 'pl'
oldest_article = 7
max_articles_per_feed = 100
auto_cleanup = True
feeds = [
(u'Polityka', u'http://www.presseurop.eu/pl/taxonomy/term/1/%2A/feed'),
(u'Społeczeństwo', u'http://www.presseurop.eu/pl/taxonomy/term/2/%2A/feed'),
(u'Gospodarka', u'http://www.presseurop.eu/pl/taxonomy/term/3/%2A/feed'),
(u'Kultura i debaty', u'http://www.presseurop.eu/pl/taxonomy/term/4/%2A/feed'),
(u'UE i Świat', u'http://www.presseurop.eu/pl/taxonomy/term/5/%2A/feed')
]
preprocess_regexps = [
(re.compile(r'\|.*</title>', re.DOTALL|re.IGNORECASE),
lambda match: '</title>'),
]

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
from calibre.web.feeds.news import BasicNewsRecipe
class ResPublicaNowaRecipe(BasicNewsRecipe):
__license__ = 'GPL v3'
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
language = 'pl'
version = 1
title = u'Res Publica Nowa'
category = u'News'
description = u'Portal kulturalno-społecznego kwartalnika o profilu liberalnym, wydawany przez Fundację Res Publica'
cover_url=''
remove_empty_feeds= True
no_stylesheets=True
oldest_article = 7
max_articles_per_feed = 100000
recursions = 0
no_stylesheets = True
remove_javascript = True
simultaneous_downloads = 5
feeds = [
('Artykuly', 'feed://publica.pl/feed'),
]
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -20,7 +20,7 @@ class sport_pl(BasicNewsRecipe):
remove_javascript=True
no_stylesheets=True
remove_empty_feeds = True
ignore_duplicate_articles = {'title', 'url'}
keep_only_tags =[]
keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'article'}))

View File

@ -1,7 +1,7 @@
from calibre.web.feeds.news import BasicNewsRecipe
class WirtualneMedia(BasicNewsRecipe):
title = u'wirtualnemedia.pl'
title = u'Wirtualnemedia.pl'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'teepel <teepel44@gmail.com>'
'''
wolnemedia.net
'''
from calibre.web.feeds.news import BasicNewsRecipe
class wolne_media(BasicNewsRecipe):
title = u'Wolne Media'
__author__ = 'teepel <teepel44@gmail.com>'
language = 'pl'
description ='Wiadomości z wolnemedia.net'
INDEX='http://wolnemedia.net'
oldest_article = 1
max_articles_per_feed = 100
remove_empty_feeds= True
simultaneous_downloads = 5
remove_javascript=True
no_stylesheets=True
auto_cleanup = True
feeds = [(u'Wiadomości z kraju', u'http://wolnemedia.net/category/wiadomosci-z-kraju/feed/'),(u'Wiadomości ze świata', u'http://wolnemedia.net/category/wiadomosci-ze-swiata/feed/'),(u'Edukacja', u'http://wolnemedia.net/category/edukacja/feed/'),(u'Ekologia', u'http://wolnemedia.net/category/ekologia/feed/'),(u'Gospodarka', u'http://wolnemedia.net/category/gospodarka/feed/'),(u'Historia', u'http://wolnemedia.net/category/historia/feed/'),(u'Kultura', u'http://wolnemedia.net/category/kultura/feed/'),(u'Kulturoznawstwo', u'http://wolnemedia.net/category/kulturoznawstwo/feed/'),(u'Media', u'http://wolnemedia.net/category/media/feed/'),(u'Nauka', u'http://wolnemedia.net/category/nauka/feed/'),(u'Opowiadania', u'http://wolnemedia.net/category/opowiadania/feed/'),(u'Paranauka i ezoteryka', u'http://wolnemedia.net/category/ezoteryka/feed/'),(u'Polityka', u'http://wolnemedia.net/category/polityka/feed/'),(u'Prawo', u'http://wolnemedia.net/category/prawo/feed/'),(u'Publicystyka', u'http://wolnemedia.net/category/publicystyka/feed/'),(u'Reportaż', u'http://wolnemedia.net/category/reportaz/feed/'),(u'Seks', u'http://wolnemedia.net/category/seks/feed/'),(u'Społeczeństwo', u'http://wolnemedia.net/category/spoleczenstwo/feed/'),(u'Świat komputerów', u'http://wolnemedia.net/category/swiat-komputerow/feed/'),(u'Wierzenia', u'http://wolnemedia.net/category/wierzenia/feed/'),(u'Zdrowie', u'http://wolnemedia.net/category/zdrowie/feed/')]

View File

@ -1,10 +1,9 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, matek09, matek09@gmail.com'
__copyright__ = 'Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>'
__copyright__ = 'Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'
__copyright__ = '''2010, matek09, matek09@gmail.com
Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>
Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'''
from calibre.web.feeds.news import BasicNewsRecipe
import re
@ -16,12 +15,12 @@ class Wprost(BasicNewsRecipe):
ICO_BLOCKED = 'http://www.wprost.pl/G/layout2/ico_blocked.png'
title = u'Wprost'
__author__ = 'matek09'
description = 'Weekly magazine'
description = u'Popularny tygodnik ogólnopolski - Wprost. Najlepszy wśród polskich tygodników - opiniotwórczy - społeczno-informacyjny - społeczno-kulturalny.'
encoding = 'ISO-8859-2'
no_stylesheets = True
language = 'pl'
remove_javascript = True
recursions = 0
recursions = 0
remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
remove_tags_after = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
'''
@ -94,5 +93,3 @@ class Wprost(BasicNewsRecipe):
'description' : ''
})
return articles

View File

@ -1,10 +1,9 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, matek09, matek09@gmail.com'
__copyright__ = 'Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>'
__copyright__ = 'Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '''2010, matek09, matek09@gmail.com
Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>
Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'''
from calibre.web.feeds.news import BasicNewsRecipe
import re
@ -12,13 +11,14 @@ import re
class Wprost(BasicNewsRecipe):
title = u'Wprost (RSS)'
__author__ = 'matek09'
description = 'Weekly magazine'
description = u'Portal informacyjny. Najświeższe wiadomości, najciekawsze komentarze i opinie. Blogi najlepszych publicystów.'
encoding = 'ISO-8859-2'
no_stylesheets = True
language = 'pl'
remove_javascript = True
recursions = 0
use_embedded_content = False
ignore_duplicate_articles = {'title', 'url'}
remove_empty_feeds = True
remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
remove_tags_after = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
@ -48,20 +48,20 @@ class Wprost(BasicNewsRecipe):
#h2 {font-size: x-large; font-weight: bold}
feeds = [(u'Tylko u nas', u'http://www.wprost.pl/rss/rss_wprostextra.php'),
(u'Wydarzenia', u'http://www.wprost.pl/rss/rss.php'),
(u'Komentarze', u'http://www.wprost.pl/rss/rss_komentarze.php'),
(u'Wydarzenia: Kraj', u'http://www.wprost.pl/rss/rss_kraj.php'),
(u'Komentarze: Kraj', u'http://www.wprost.pl/rss/rss_komentarze_kraj.php'),
(u'Wydarzenia: Świat', u'http://www.wprost.pl/rss/rss_swiat.php'),
(u'Komentarze: Świat', u'http://www.wprost.pl/rss/rss_komentarze_swiat.php'),
(u'Wydarzenia: Gospodarka', u'http://www.wprost.pl/rss/rss_gospodarka.php'),
(u'Komentarze: Gospodarka', u'http://www.wprost.pl/rss/rss_komentarze_gospodarka.php'),
(u'Wydarzenia: Życie', u'http://www.wprost.pl/rss/rss_zycie.php'),
(u'Komentarze: Życie', u'http://www.wprost.pl/rss/rss_komentarze_zycie.php'),
(u'Wydarzenia: Sport', u'http://www.wprost.pl/rss/rss_sport.php'),
(u'Komentarze: Sport', u'http://www.wprost.pl/rss/rss_komentarze_sport.php'),
(u'Przegląd prasy', u'http://www.wprost.pl/rss/rss_prasa.php')
]
(u'Wydarzenia', u'http://www.wprost.pl/rss/rss.php'),
(u'Komentarze', u'http://www.wprost.pl/rss/rss_komentarze.php'),
(u'Wydarzenia: Kraj', u'http://www.wprost.pl/rss/rss_kraj.php'),
(u'Komentarze: Kraj', u'http://www.wprost.pl/rss/rss_komentarze_kraj.php'),
(u'Wydarzenia: Świat', u'http://www.wprost.pl/rss/rss_swiat.php'),
(u'Komentarze: Świat', u'http://www.wprost.pl/rss/rss_komentarze_swiat.php'),
(u'Wydarzenia: Gospodarka', u'http://www.wprost.pl/rss/rss_gospodarka.php'),
(u'Komentarze: Gospodarka', u'http://www.wprost.pl/rss/rss_komentarze_gospodarka.php'),
(u'Wydarzenia: Życie', u'http://www.wprost.pl/rss/rss_zycie.php'),
(u'Komentarze: Życie', u'http://www.wprost.pl/rss/rss_komentarze_zycie.php'),
(u'Wydarzenia: Sport', u'http://www.wprost.pl/rss/rss_sport.php'),
(u'Komentarze: Sport', u'http://www.wprost.pl/rss/rss_komentarze_sport.php'),
(u'Przegląd prasy', u'http://www.wprost.pl/rss/rss_prasa.php')
]
def get_cover_url(self):
soup = self.index_to_soup('http://www.wprost.pl/tygodnik')

Binary file not shown.

View File

@ -79,7 +79,7 @@ author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council',
# By default, calibre splits a string containing multiple author names on
# ampersands and the words "and" and "with". You can customize the splitting
# by changing the regular expression below. Strings are split on whatever the
# specified regular expression matches.
# specified regular expression matches, in addition to ampersands.
# Default: r'(?i),?\s+(and|with)\s+'
authors_split_regex = r'(?i),?\s+(and|with)\s+'

View File

@ -757,6 +757,7 @@ from calibre.ebooks.metadata.sources.isbndb import ISBNDB
from calibre.ebooks.metadata.sources.overdrive import OverDrive
from calibre.ebooks.metadata.sources.douban import Douban
from calibre.ebooks.metadata.sources.ozon import Ozon
# from calibre.ebooks.metadata.sources.google_images import GoogleImages
plugins += [GoogleBooks, Amazon, Edelweiss, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon]

View File

@ -91,7 +91,7 @@ def restore_plugin_state_to_default(plugin_or_name):
config['enabled_plugins'] = ep
default_disabled_plugins = set([
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss',
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images',
])
def is_disabled(plugin):

View File

@ -7,7 +7,9 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import weakref
from functools import partial
from itertools import izip, imap
def sanitize_sort_field_name(field_metadata, field):
field = field_metadata.search_term_to_field_key(field.lower().strip())
@ -15,11 +17,39 @@ def sanitize_sort_field_name(field_metadata, field):
field = {'title': 'sort', 'authors':'author_sort'}.get(field, field)
return field
class MarkedVirtualField(object):
def __init__(self, marked_ids):
self.marked_ids = marked_ids
def iter_searchable_values(self, get_metadata, candidates, default_value=None):
for book_id in candidates:
yield self.marked_ids.get(book_id, default_value), {book_id}
class TableRow(list):
def __init__(self, book_id, view):
self.book_id = book_id
self.view = weakref.ref(view)
def __getitem__(self, obj):
view = self.view()
if isinstance(obj, slice):
return [view._field_getters[c](self.book_id)
for c in xrange(*obj.indices(len(view._field_getters)))]
else:
return view._field_getters[obj](self.book_id)
class View(object):
''' A table view of the database, with rows and columns. Also supports
filtering and sorting. '''
def __init__(self, cache):
self.cache = cache
self.marked_ids = {}
self.search_restriction_book_count = 0
self.search_restriction = ''
self._field_getters = {}
for col, idx in cache.backend.FIELD_MAP.iteritems():
if isinstance(col, int):
@ -38,16 +68,33 @@ class View(object):
except KeyError:
self._field_getters[idx] = partial(self.get, col)
self._map = list(self.cache.all_book_ids())
self._map_filtered = list(self._map)
self._map = tuple(self.cache.all_book_ids())
self._map_filtered = tuple(self._map)
@property
def field_metadata(self):
return self.cache.field_metadata
def _get_id(self, idx, index_is_id=True):
ans = idx if index_is_id else self.index_to_id(idx)
return ans
return idx if index_is_id else self.index_to_id(idx)
def __getitem__(self, row):
return TableRow(self._map_filtered[row], self.cache)
def __len__(self):
return len(self._map_filtered)
def __iter__(self):
for book_id in self._map_filtered:
yield self._data[book_id]
def iterall(self):
for book_id in self._map:
yield self[book_id]
def iterallids(self):
for book_id in self._map:
yield book_id
def get_field_map_field(self, row, col, index_is_id=True):
'''
@ -66,7 +113,7 @@ class View(object):
def get_ondevice(self, idx, index_is_id=True, default_value=''):
id_ = idx if index_is_id else self.index_to_id(idx)
self.cache.field_for('ondevice', id_, default_value=default_value)
return self.cache.field_for('ondevice', id_, default_value=default_value)
def get_marked(self, idx, index_is_id=True, default_value=None):
id_ = idx if index_is_id else self.index_to_id(idx)
@ -93,7 +140,7 @@ class View(object):
ans.append(self.cache._author_data(id_))
return tuple(ans)
def multisort(self, fields=[], subsort=False):
def multisort(self, fields=[], subsort=False, only_ids=None):
fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields]
keys = self.field_metadata.sortable_field_keys()
fields = [x for x in fields if x[0] in keys]
@ -102,8 +149,70 @@ class View(object):
if not fields:
fields = [('timestamp', False)]
sorted_book_ids = self.cache.multisort(fields)
sorted_book_ids
# TODO: change maps
sorted_book_ids = self.cache.multisort(fields, ids_to_sort=only_ids)
if only_ids is None:
self._map = tuple(sorted_book_ids)
if len(self._map_filtered) == len(self._map):
self._map_filtered = tuple(self._map)
else:
fids = frozenset(self._map_filtered)
self._map_filtered = tuple(i for i in self._map if i in fids)
else:
smap = {book_id:i for i, book_id in enumerate(sorted_book_ids)}
only_ids.sort(key=smap.get)
def search(self, query, return_matches=False):
ans = self.search_getting_ids(query, self.search_restriction,
set_restriction_count=True)
if return_matches:
return ans
self._map_filtered = tuple(ans)
def search_getting_ids(self, query, search_restriction,
set_restriction_count=False):
q = ''
if not query or not query.strip():
q = search_restriction
else:
q = query
if search_restriction:
q = u'(%s) and (%s)' % (search_restriction, query)
if not q:
if set_restriction_count:
self.search_restriction_book_count = len(self._map)
return list(self._map)
matches = self.cache.search(
query, search_restriction, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)})
rv = [x for x in self._map if x in matches]
if set_restriction_count and q == search_restriction:
self.search_restriction_book_count = len(rv)
return rv
def set_search_restriction(self, s):
self.search_restriction = s
def search_restriction_applied(self):
return bool(self.search_restriction)
def get_search_restriction_book_count(self):
return self.search_restriction_book_count
def set_marked_ids(self, id_dict):
'''
ids in id_dict are "marked". They can be searched for by
using the search term ``marked:true``. Pass in an empty dictionary or
set to clear marked ids.
:param id_dict: Either a dictionary mapping ids to values or a set
of ids. In the latter case, the value is set to 'true' for all ids. If
a mapping is provided, then the search can be used to search for
particular values: ``marked:value``
'''
if not hasattr(id_dict, 'items'):
# Simple list. Make it a dict of string 'true'
self.marked_ids = dict.fromkeys(id_dict, u'true')
else:
# Ensure that all the items in the dict are text
self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode,
id_dict.itervalues())))

View File

@ -239,7 +239,7 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1']
'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',

View File

@ -104,13 +104,11 @@ class PDFOutput(OutputFormatPlugin):
'specify a footer template, it will take precedence '
'over this option.')),
OptionRecommendation(name='pdf_footer_template', recommended_value=None,
help=_('An HTML template used to generate footers on every page.'
' The string _PAGENUM_ will be replaced by the current page'
' number.')),
help=_('An HTML template used to generate %s on every page.'
' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('footers')),
OptionRecommendation(name='pdf_header_template', recommended_value=None,
help=_('An HTML template used to generate headers on every page.'
' The string _PAGENUM_ will be replaced by the current page'
' number.')),
help=_('An HTML template used to generate %s on every page.'
' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('headers')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):

View File

@ -858,7 +858,7 @@ class Amazon(Source):
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.info('No cached cover found, running identify')

View File

@ -31,7 +31,7 @@ msprefs.defaults['find_first_edition_date'] = False
# Google covers are often poor quality (scans/errors) but they have high
# resolution, so they trump covers from better sources. So make sure they
# are only used if no other covers are found.
msprefs.defaults['cover_priorities'] = {'Google':2}
msprefs.defaults['cover_priorities'] = {'Google':2, 'Google Images':2}
def create_log(ostream=None):
from calibre.utils.logging import ThreadSafeLog, FileStream
@ -222,6 +222,9 @@ class Source(Plugin):
#: plugin
config_help_message = None
#: If True this source can return multiple covers for a given query
can_get_multiple_covers = False
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
@ -522,7 +525,7 @@ class Source(Plugin):
return None
def download_cover(self, log, result_queue, abort,
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
'''
Download a cover and put it into result_queue. The parameters all have
the same meaning as for :meth:`identify`. Put (self, cover_data) into
@ -531,6 +534,9 @@ class Source(Plugin):
This method should use cached cover URLs for efficiency whenever
possible. When cached data is not present, most plugins simply call
identify and use its results.
If the parameter get_best_cover is True and this plugin can get
multiple covers, it should only get the "best" one.
'''
pass

View File

@ -35,9 +35,14 @@ class Worker(Thread):
start_time = time.time()
if not self.abort.is_set():
try:
self.plugin.download_cover(self.log, self.rq, self.abort,
title=self.title, authors=self.authors,
identifiers=self.identifiers, timeout=self.timeout)
if self.plugin.can_get_multiple_covers:
self.plugin.download_cover(self.log, self.rq, self.abort,
title=self.title, authors=self.authors, get_best_cover=True,
identifiers=self.identifiers, timeout=self.timeout)
else:
self.plugin.download_cover(self.log, self.rq, self.abort,
title=self.title, authors=self.authors,
identifiers=self.identifiers, timeout=self.timeout)
except:
self.log.exception('Failed to download cover from',
self.plugin.name)

View File

@ -221,7 +221,7 @@ class Douban(Source):
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.info('No cached cover found, running identify')

View File

@ -320,7 +320,7 @@ class Edelweiss(Source):
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.info('No cached cover found, running identify')

View File

@ -209,7 +209,7 @@ class GoogleBooks(Source):
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.info('No cached cover found, running identify')

View File

@ -0,0 +1,148 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from collections import OrderedDict
from calibre import as_unicode
from calibre.ebooks.metadata.sources.base import Source, Option
class GoogleImages(Source):
name = 'Google Images'
description = _('Downloads covers from a Google Image search. Useful to find larger/alternate covers.')
capabilities = frozenset(['cover'])
config_help_message = _('Configure the Google Image Search plugin')
can_get_multiple_covers = True
options = (Option('max_covers', 'number', 5, _('Maximum number of covers to get'),
_('The maximum number of covers to process from the google search result')),
Option('size', 'choices', 'svga', _('Cover size'),
_('Search for covers larger than the specified size'),
choices=OrderedDict((
('any', _('Any size'),),
('l', _('Large'),),
('qsvga', _('Larger than %s')%'400x300',),
('vga', _('Larger than %s')%'640x480',),
('svga', _('Larger than %s')%'600x800',),
('xga', _('Larger than %s')%'1024x768',),
('2mp', _('Larger than %s')%'2 MP',),
('4mp', _('Larger than %s')%'4 MP',),
))),
)
def download_cover(self, log, result_queue, abort,
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
if not title:
return
from threading import Thread
import time
timeout = max(60, timeout) # Needs at least a minute
title = ' '.join(self.get_title_tokens(title))
author = ' '.join(self.get_author_tokens(authors))
urls = self.get_image_urls(title, author, log, abort, timeout)
if not urls:
log('No images found in Google for, title: %r and authors: %r'%(title, author))
return
urls = urls[:self.prefs['max_covers']]
if get_best_cover:
urls = urls[:1]
workers = [Thread(target=self.download_image, args=(url, timeout, log, result_queue)) for url in urls]
for w in workers:
w.daemon = True
w.start()
alive = True
start_time = time.time()
while alive and not abort.is_set() and time.time() - start_time < timeout:
alive = False
for w in workers:
if w.is_alive():
alive = True
break
abort.wait(0.1)
def download_image(self, url, timeout, log, result_queue):
try:
ans = self.browser.open_novisit(url, timeout=timeout).read()
result_queue.put((self, ans))
log('Downloaded cover from: %s'%url)
except Exception:
self.log.exception('Failed to download cover from: %r'%url)
def get_image_urls(self, title, author, log, abort, timeout):
from calibre.utils.ipc.simple_worker import fork_job, WorkerError
try:
return fork_job('calibre.ebooks.metadata.sources.google_images',
'search', args=(title, author, self.prefs['size'], timeout), no_output=True, abort=abort, timeout=timeout)['result']
except WorkerError as e:
if e.orig_tb:
log.error(e.orig_tb)
log.exception('Searching google failed:' + as_unicode(e))
except Exception as e:
log.exception('Searching google failed:' + as_unicode(e))
return []
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Firefox/3.6.13'
def find_image_urls(br, ans):
import urlparse
for w in br.page.mainFrame().documentElement().findAll('.images_table a[href]'):
try:
imgurl = urlparse.parse_qs(urlparse.urlparse(unicode(w.attribute('href'))).query)['imgurl'][0]
except:
continue
if imgurl not in ans:
ans.append(imgurl)
def search(title, author, size, timeout, debug=False):
import time
from calibre.web.jsbrowser.browser import Browser, LoadWatcher, Timeout
ans = []
start_time = time.time()
br = Browser(user_agent=USER_AGENT, enable_developer_tools=debug)
br.visit('https://www.google.com/advanced_image_search')
f = br.select_form('form[action="/search"]')
f['as_q'] = '%s %s'%(title, author)
if size != 'any':
f['imgsz'] = size
f['imgar'] = 't|xt'
f['as_filetype'] = 'jpg'
br.submit(wait_for_load=False)
# Loop until the page finishes loading or at least five image urls are
# found
lw = LoadWatcher(br.page, br)
while lw.is_loading and len(ans) < 5:
br.run_for_a_time(0.2)
find_image_urls(br, ans)
if time.time() - start_time > timeout:
raise Timeout('Timed out trying to load google image search page')
find_image_urls(br, ans)
if debug:
br.show_browser()
br.close()
del br # Needed to prevent PyQt from segfaulting
return ans
def test_google():
import pprint
pprint.pprint(search('heroes', 'abercrombie', 'svga', 60, debug=True))
def test():
from Queue import Queue
from threading import Event
from calibre.utils.logging import default_log
p = GoogleImages(None)
rq = Queue()
p.download_cover(default_log, rq, Event(), title='The Heroes',
authors=('Joe Abercrombie',))
print ('Downloaded', rq.qsize(), 'covers')
if __name__ == '__main__':
test()

View File

@ -19,7 +19,7 @@ class OpenLibrary(Source):
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
def download_cover(self, log, result_queue, abort,
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
if 'isbn' not in identifiers:
return
isbn = identifiers['isbn']

View File

@ -75,7 +75,7 @@ class OverDrive(Source):
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False):
import mechanize
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:

View File

@ -55,7 +55,7 @@ class Ozon(Source):
# for ozon.ru search we have to format ISBN with '-'
isbn = _format_isbn(log, identifiers.get('isbn', None))
ozonid = identifiers.get('ozon', None)
unk = unicode(_('Unknown')).upper()
if (title and title != unk) or (authors and authors != [unk]) or isbn or not ozonid:
qItems = set([isbn, title])
@ -64,19 +64,19 @@ class Ozon(Source):
qItems.discard(None)
qItems.discard('')
qItems = map(_quoteString, qItems)
q = u' '.join(qItems).strip()
log.info(u'search string: ' + q)
if isinstance(q, unicode):
q = q.encode('utf-8')
if not q:
return None
search_url += quote_plus(q)
else:
search_url = self.ozon_url + '/webservices/OzonWebSvc.asmx/ItemDetail?ID=%s' % ozonid
log.debug(u'search url: %r'%search_url)
return search_url
# }}}
@ -250,7 +250,7 @@ class Ozon(Source):
return url
# }}}
def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30): # {{{
def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): # {{{
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.debug('No cached cover found, running identify')

View File

@ -11,6 +11,7 @@ import os
from threading import Event, Thread
from Queue import Queue, Empty
from io import BytesIO
from collections import Counter
from calibre.utils.date import as_utc
from calibre.ebooks.metadata.sources.identify import identify, msprefs
@ -113,13 +114,18 @@ def single_covers(title, authors, identifiers, caches, tdir):
kwargs=dict(title=title, authors=authors, identifiers=identifiers))
worker.daemon = True
worker.start()
c = Counter()
while worker.is_alive():
try:
plugin, width, height, fmt, data = results.get(True, 1)
except Empty:
continue
else:
name = '%s,,%s,,%s,,%s.cover'%(plugin.name, width, height, fmt)
name = plugin.name
if plugin.can_get_multiple_covers:
name += '{%d}'%c[plugin.name]
c[plugin.name] += 1
name = '%s,,%s,,%s,,%s.cover'%(name, width, height, fmt)
with open(name, 'wb') as f:
f.write(data)
os.mkdir(name+'.done')

View File

@ -33,6 +33,7 @@ class PagedDisplay
this.header_template = null
this.header = null
this.footer = null
this.hf_style = null
read_document_margins: () ->
# Read page margins from the document. First checks for an @page rule.
@ -184,15 +185,22 @@ class PagedDisplay
# log('Time to layout:', new Date().getTime() - start_time)
return sm
create_header_footer: () ->
create_header_footer: (uuid) ->
if this.header_template != null
this.header = document.createElement('div')
this.header.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: 0px; height: #{ this.margin_top }px; width: #{ this.col_width }px; margin: 0; padding: 0")
this.header.setAttribute('id', 'pdf_page_header_'+uuid)
document.body.appendChild(this.header)
if this.footer_template != null
this.footer = document.createElement('div')
this.footer.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: #{ window.innerHeight - this.margin_bottom }px; height: #{ this.margin_bottom }px; width: #{ this.col_width }px; margin: 0; padding: 0")
this.footer.setAttribute('id', 'pdf_page_footer_'+uuid)
document.body.appendChild(this.footer)
if this.header != null or this.footer != null
this.hf_uuid = uuid
this.hf_style = document.createElement('style')
this.hf_style.setAttribute('type', 'text/css')
document.head.appendChild(this.hf_style)
this.update_header_footer(1)
position_header_footer: () ->
@ -203,10 +211,16 @@ class PagedDisplay
this.footer.style.setProperty('left', left+'px')
update_header_footer: (pagenum) ->
if this.hf_style != null
if pagenum%2 == 1 then cls = "even_page" else cls = "odd_page"
this.hf_style.innerHTML = "#pdf_page_header_#{ this.hf_uuid } .#{ cls }, #pdf_page_footer_#{ this.hf_uuid } .#{ cls } { display: none }"
title = py_bridge.title()
author = py_bridge.author()
section = py_bridge.section()
if this.header != null
this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"")
this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_SECTION_/g, section+"")
if this.footer != null
this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"")
this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_SECTION_/g, section+"")
fit_images: () ->
# Ensure no images are wider than the available width in a column. Note

View File

@ -130,6 +130,18 @@ class PDFWriter(QObject):
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
fset=_pass_json_value_setter)
@pyqtSlot(result=unicode)
def title(self):
return self.doc_title
@pyqtSlot(result=unicode)
def author(self):
return self.doc_author
@pyqtSlot(result=unicode)
def section(self):
return self.current_section
def __init__(self, opts, log, cover_data=None, toc=None):
from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt():
@ -154,6 +166,7 @@ class PDFWriter(QObject):
self.view.page().mainFrame().setScrollBarPolicy(x,
Qt.ScrollBarAlwaysOff)
self.report_progress = lambda x, y: x
self.current_section = ''
def dump(self, items, out_stream, pdf_metadata):
opts = self.opts
@ -170,9 +183,13 @@ class PDFWriter(QObject):
opts.uncompressed_pdf,
mark_links=opts.pdf_mark_links)
self.footer = opts.pdf_footer_template
if self.footer is None and opts.pdf_page_numbers:
if self.footer:
self.footer = self.footer.strip()
if not self.footer and opts.pdf_page_numbers:
self.footer = '<p style="text-align:center; text-indent: 0">_PAGENUM_</p>'
self.header = opts.pdf_header_template
if self.header:
self.header = self.header.strip()
min_margin = 36
if self.footer and opts.margin_bottom < min_margin:
self.log.warn('Bottom margin is too small for footer, increasing it.')
@ -192,6 +209,8 @@ class PDFWriter(QObject):
self.doc.set_metadata(title=pdf_metadata.title,
author=pdf_metadata.author,
tags=pdf_metadata.tags)
self.doc_title = pdf_metadata.title
self.doc_author = pdf_metadata.author
self.painter.save()
try:
if self.cover_data is not None:
@ -273,13 +292,34 @@ class PDFWriter(QObject):
self.loop.processEvents(self.loop.ExcludeUserInputEvents)
evaljs('document.getElementById("MathJax_Message").style.display="none";')
def get_sections(self, anchor_map):
sections = {}
ci = os.path.abspath(os.path.normcase(self.current_item))
if self.toc is not None:
for toc in self.toc.flat():
path = toc.abspath or None
frag = toc.fragment or None
if path is None:
continue
path = os.path.abspath(os.path.normcase(path))
if path == ci:
col = 0
if frag and frag in anchor_map:
col = anchor_map[frag]['column']
if col not in sections:
sections[col] = toc.text or _('Untitled')
return sections
def do_paged_render(self):
if self.paged_js is None:
import uuid
from calibre.utils.resources import compiled_coffeescript as cc
self.paged_js = cc('ebooks.oeb.display.utils')
self.paged_js += cc('ebooks.oeb.display.indexing')
self.paged_js += cc('ebooks.oeb.display.paged')
self.paged_js += cc('ebooks.oeb.display.mathjax')
self.hf_uuid = str(uuid.uuid4()).replace('-', '')
self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
self.view.page().longjs_counter = 0
@ -305,6 +345,8 @@ class PDFWriter(QObject):
amap = self.bridge_value
if not isinstance(amap, dict):
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
sections = self.get_sections(amap['anchors'])
col = 0
if self.header:
self.bridge_value = self.header
@ -313,12 +355,14 @@ class PDFWriter(QObject):
self.bridge_value = self.footer
evaljs('paged_display.footer_template = py_bridge.value')
if self.header or self.footer:
evaljs('paged_display.create_header_footer();')
evaljs('paged_display.create_header_footer("%s");'%self.hf_uuid)
start_page = self.current_page_num
mf = self.view.page().mainFrame()
while True:
if col in sections:
self.current_section = sections[col]
self.doc.init_page()
if self.header or self.footer:
evaljs('paged_display.update_header_footer(%d)'%self.current_page_num)
@ -332,8 +376,10 @@ class PDFWriter(QObject):
evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl[0])
if self.doc.errors_occurred:
break
col += 1
if not self.doc.errors_occurred:
self.doc.add_links(self.current_item, start_page, amap['links'],
amap['anchors'])

View File

@ -22,7 +22,9 @@ class PluginWidget(Widget, Ui_Form):
'override_profile_size', 'paper_size', 'custom_size',
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers'])
'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers',
'pdf_footer_template', 'pdf_header_template',
])
self.db, self.book_id = db, book_id
for x in get_option('paper_size').option.choices:

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>590</width>
<height>395</height>
<width>638</width>
<height>498</height>
</rect>
</property>
<property name="windowTitle">
@ -84,6 +84,13 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_pdf_page_numbers">
<property name="text">
<string>Add page &amp;numbers to the bottom of every page</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
@ -170,24 +177,52 @@
</property>
</widget>
</item>
<item row="12" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>213</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_pdf_page_numbers">
<property name="text">
<string>Add page &amp;numbers to the bottom of every page</string>
<item row="12" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Page headers and footers</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>You can insert headers and footers into every page of the produced PDF file by using header and footer templates. For examples, see the &lt;a href=&quot;http://manual.calibre-ebook.com/conversion.html#converting-to-pdf&quot;&gt;documentation&lt;/a&gt;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>&amp;Header template:</string>
</property>
<property name="buddy">
<cstring>opt_pdf_header_template</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="opt_pdf_header_template"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>&amp;Footer template:</string>
</property>
<property name="buddy">
<cstring>opt_pdf_footer_template</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="opt_pdf_footer_template"/>
</item>
</layout>
</widget>
</item>
</layout>

View File

@ -16,13 +16,12 @@ from operator import attrgetter
from Queue import Queue, Empty
from io import BytesIO
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QApplication, QDialog, QVBoxLayout, QLabel,
QDialogButtonBox, QStyle, QStackedWidget, QWidget,
QTableView, QGridLayout, QFontInfo, QPalette, QTimer,
pyqtSignal, QAbstractTableModel, QVariant, QSize,
QListView, QPixmap, QAbstractListModel, QColor, QRect,
QTextBrowser, QStringListModel)
from PyQt4.Qt import (
QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QApplication,
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
@ -654,7 +653,7 @@ class CoversModel(QAbstractListModel): # {{{
for i, plugin in enumerate(metadata_plugins(['cover'])):
self.covers.append((plugin.name+'\n'+_('Searching...'),
QVariant(self.blank), None, True))
self.plugin_map[plugin] = i+1
self.plugin_map[plugin] = [i+1]
if do_reset:
self.reset()
@ -685,48 +684,82 @@ class CoversModel(QAbstractListModel): # {{{
def plugin_for_index(self, index):
row = index.row() if hasattr(index, 'row') else index
for k, v in self.plugin_map.iteritems():
if v == row:
if row in v:
return k
def cover_keygen(self, x):
pmap = x[2]
if pmap is None:
return 1
return pmap.width()*pmap.height()
def clear_failed(self):
# Remove entries that are still waiting
good = []
pmap = {}
dcovers = sorted(self.covers[1:], key=self.cover_keygen, reverse=True)
cmap = {x:self.covers.index(x) for x in self.covers}
def keygen(x):
pmap = x[2]
if pmap is None:
return 1
return pmap.width()*pmap.height()
dcovers = sorted(self.covers[1:], key=keygen, reverse=True)
cmap = {i:self.plugin_for_index(i) for i in xrange(len(self.covers))}
for i, x in enumerate(self.covers[0:1] + dcovers):
if not x[-1]:
good.append(x)
if i > 0:
plugin = self.plugin_for_index(cmap[x])
pmap[plugin] = len(good) - 1
plugin = cmap[i]
if plugin is not None:
try:
pmap[plugin].append(len(good) - 1)
except KeyError:
pmap[plugin] = [len(good)-1]
self.covers = good
self.plugin_map = pmap
self.reset()
def index_for_plugin(self, plugin):
idx = self.plugin_map.get(plugin, 0)
return self.index(idx)
def pointer_from_index(self, index):
row = index.row() if hasattr(index, 'row') else index
try:
return self.covers[row][2]
except IndexError:
pass
def index_from_pointer(self, pointer):
for r, (text, scaled, pmap, waiting) in enumerate(self.covers):
if pointer == pmap:
return self.index(r)
return self.index(0)
def update_result(self, plugin_name, width, height, data):
idx = None
for plugin, i in self.plugin_map.iteritems():
if plugin.name == plugin_name:
idx = i
break
if idx is None:
return
pmap = QPixmap()
pmap.loadFromData(data)
if pmap.isNull():
return
self.covers[idx] = self.get_item(plugin_name, pmap, waiting=False)
self.dataChanged.emit(self.index(idx), self.index(idx))
if plugin_name.endswith('}'):
# multi cover plugin
plugin_name = plugin_name.partition('{')[0]
plugin = [plugin for plugin in self.plugin_map if plugin.name == plugin_name]
if not plugin:
return
plugin = plugin[0]
last_row = max(self.plugin_map[plugin])
pmap = QPixmap()
pmap.loadFromData(data)
if pmap.isNull():
return
self.beginInsertRows(QModelIndex(), last_row, last_row)
for rows in self.plugin_map.itervalues():
for i in xrange(len(rows)):
if rows[i] >= last_row:
rows[i] += 1
self.plugin_map[plugin].insert(-1, last_row)
self.covers.insert(last_row, self.get_item(plugin_name, pmap, waiting=False))
self.endInsertRows()
else:
# single cover plugin
idx = None
for plugin, rows in self.plugin_map.iteritems():
if plugin.name == plugin_name:
idx = rows[0]
break
if idx is None:
return
pmap = QPixmap()
pmap.loadFromData(data)
if pmap.isNull():
return
self.covers[idx] = self.get_item(plugin_name, pmap, waiting=False)
self.dataChanged.emit(self.index(idx), self.index(idx))
def cover_pixmap(self, index):
row = index.row()
@ -774,9 +807,12 @@ class CoversView(QListView): # {{{
self.m.reset_covers()
def clear_failed(self):
plugin = self.m.plugin_for_index(self.currentIndex())
pointer = self.m.pointer_from_index(self.currentIndex())
self.m.clear_failed()
self.select(self.m.index_for_plugin(plugin).row())
if pointer is None:
self.select(0)
else:
self.select(self.m.index_from_pointer(pointer).row())
# }}}
@ -852,10 +888,11 @@ class CoversWidget(QWidget): # {{{
if num < 2:
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
else:
txt = _('Found <b>%(num)d</b> covers of %(title)s. '
'Pick the one you like best.')%dict(num=num-1,
txt = _('Found <b>%(num)d</b> possible covers for %(title)s. '
'When the download completes, the covers will be sorted by size.')%dict(num=num-1,
title=self.title)
self.msg.setText(txt)
self.msg.setWordWrap(True)
self.finished.emit()

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
store_version = 2 # Needed for dynamic plugin loading
store_version = 3 # Needed for dynamic plugin loading
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
@ -129,12 +129,12 @@ class AmazonKindleStore(StorePlugin):
doc = html.fromstring(f.read().decode('latin-1', 'replace'))
data_xpath = '//div[contains(@class, "prod")]'
format_xpath = './/ul[contains(@class, "rsltL")]//span[contains(@class, "lrg") and not(contains(@class, "bld"))]/text()'
format_xpath = './/ul[contains(@class, "rsltL") or contains(@class, "rsltGridList")]//span[contains(@class, "lrg") and not(contains(@class, "bld"))]/text()'
asin_xpath = '@name'
cover_xpath = './/img[@class="productImage"]/@src'
title_xpath = './/h3[@class="newaps"]/a//text()'
author_xpath = './/h3[@class="newaps"]//span[contains(@class, "reg")]//text()'
price_xpath = './/ul[contains(@class, "rsltL")]//span[contains(@class, "lrg") and contains(@class, "bld")]/text()'
price_xpath = './/ul[contains(@class, "rsltL") or contains(@class, "rsltGridList")]//span[contains(@class, "lrg") and contains(@class, "bld")]/text()'
for data in doc.xpath(data_xpath):
if counter <= 0:

View File

@ -14,7 +14,7 @@ from functools import partial
from PyQt4.Qt import (QPushButton, QFrame, QVariant, QMenu, QInputDialog,
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
QToolButton, QItemSelectionModel)
QToolButton, QItemSelectionModel, QCursor)
from calibre.ebooks.oeb.polish.container import get_container, AZW3Container
from calibre.ebooks.oeb.polish.toc import (
@ -190,7 +190,7 @@ class ItemView(QFrame): # {{{
)))
l.addWidget(b)
self.fal = b = QPushButton(_('Flatten the ToC'))
self.fal = b = QPushButton(_('&Flatten the ToC'))
b.clicked.connect(self.flatten_toc)
b.setToolTip(textwrap.fill(_(
'Flatten the Table of Contents, putting all entries at the top level'
@ -339,7 +339,7 @@ class ItemView(QFrame): # {{{
# }}}
class TreeWidget(QTreeWidget):
class TreeWidget(QTreeWidget): # {{{
def __init__(self, parent):
QTreeWidget.__init__(self, parent)
@ -357,6 +357,9 @@ class TreeWidget(QTreeWidget):
self.setAnimated(True)
self.setMouseTracking(True)
self.in_drop_event = False
self.root = self.invisibleRootItem()
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
def iteritems(self, parent=None):
if parent is None:
@ -384,6 +387,137 @@ class TreeWidget(QTreeWidget):
ans = sorted(ans, key=lambda x:sort_map.get(x, -1), reverse=True)
return ans
def highlight_item(self, item):
self.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect)
self.scrollToItem(item)
def move_left(self):
item = self.currentItem()
if item is not None:
parent = item.parent()
if parent is not None:
is_expanded = item.isExpanded() or item.childCount() == 0
gp = parent.parent() or self.invisibleRootItem()
idx = gp.indexOfChild(parent)
for gc in [parent.child(i) for i in xrange(parent.indexOfChild(item)+1, parent.childCount())]:
parent.removeChild(gc)
item.addChild(gc)
parent.removeChild(item)
gp.insertChild(idx+1, item)
if is_expanded:
self.expandItem(item)
self.highlight_item(item)
def move_right(self):
item = self.currentItem()
if item is not None:
parent = item.parent() or self.invisibleRootItem()
idx = parent.indexOfChild(item)
if idx > 0:
is_expanded = item.isExpanded()
np = parent.child(idx-1)
parent.removeChild(item)
np.addChild(item)
if is_expanded:
self.expandItem(item)
self.highlight_item(item)
def move_down(self):
item = self.currentItem()
if item is None:
if self.root.childCount() == 0:
return
item = self.root.child(0)
self.highlight_item(item)
return
parent = item.parent() or self.root
idx = parent.indexOfChild(item)
if idx == parent.childCount() - 1:
# At end of parent, need to become sibling of parent
if parent is self.root:
return
gp = parent.parent() or self.root
parent.removeChild(item)
gp.insertChild(gp.indexOfChild(parent)+1, item)
else:
sibling = parent.child(idx+1)
parent.removeChild(item)
sibling.insertChild(0, item)
self.highlight_item(item)
def move_up(self):
item = self.currentItem()
if item is None:
if self.root.childCount() == 0:
return
item = self.root.child(self.root.childCount()-1)
self.highlight_item(item)
return
parent = item.parent() or self.root
idx = parent.indexOfChild(item)
if idx == 0:
# At end of parent, need to become sibling of parent
if parent is self.root:
return
gp = parent.parent() or self.root
parent.removeChild(item)
gp.insertChild(gp.indexOfChild(parent), item)
else:
sibling = parent.child(idx-1)
parent.removeChild(item)
sibling.addChild(item)
self.highlight_item(item)
def del_items(self):
for item in self.selectedItems():
p = item.parent() or self.root
p.removeChild(item)
def title_case(self):
from calibre.utils.titlecase import titlecase
for item in self.selectedItems():
t = unicode(item.data(0, Qt.DisplayRole).toString())
item.setData(0, Qt.DisplayRole, titlecase(t))
def keyPressEvent(self, ev):
if ev.key() == Qt.Key_Left and ev.modifiers() & Qt.CTRL:
self.move_left()
ev.accept()
elif ev.key() == Qt.Key_Right and ev.modifiers() & Qt.CTRL:
self.move_right()
ev.accept()
elif ev.key() == Qt.Key_Up and ev.modifiers() & Qt.CTRL:
self.move_up()
ev.accept()
elif ev.key() == Qt.Key_Down and ev.modifiers() & Qt.CTRL:
self.move_down()
ev.accept()
elif ev.key() in (Qt.Key_Delete, Qt.Key_Backspace):
self.del_items()
ev.accept()
else:
return super(TreeWidget, self).keyPressEvent(ev)
def show_context_menu(self, point):
item = self.currentItem()
if item is not None:
m = QMenu()
ci = unicode(item.data(0, Qt.DisplayRole).toString())
p = item.parent() or self.invisibleRootItem()
idx = p.indexOfChild(item)
if idx > 0:
m.addAction(QIcon(I('arrow-up.png')), _('Move "%s" up')%ci, self.move_up)
if idx + 1 < p.childCount():
m.addAction(QIcon(I('arrow-down.png')), _('Move "%s" down')%ci, self.move_down)
m.addAction(QIcon(I('trash.png')), _('Remove all selected items'), self.del_items)
if item.parent() is not None:
m.addAction(QIcon(I('back.png')), _('Unindent "%s"')%ci, self.move_left)
if idx > 0:
m.addAction(QIcon(I('forward.png')), _('Indent "%s"')%ci, self.move_right)
m.addAction(_('Change all selected items to title case'), self.title_case)
m.exec_(QCursor.pos())
# }}}
class TOCView(QWidget): # {{{
add_new_item = pyqtSignal(object, object)
@ -393,27 +527,43 @@ class TOCView(QWidget): # {{{
l = self.l = QGridLayout()
self.setLayout(l)
self.tocw = t = TreeWidget(self)
l.addWidget(t, 0, 0, 5, 3)
l.addWidget(t, 0, 0, 7, 3)
self.up_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-up.png')))
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
l.addWidget(b, 0, 3)
b.setToolTip(_('Move current entry up'))
b.setToolTip(_('Move current entry up [Ctrl+Up]'))
b.clicked.connect(self.move_up)
self.left_button = b = QToolButton(self)
b.setIcon(QIcon(I('back.png')))
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
l.addWidget(b, 2, 3)
b.setToolTip(_('Unindent the current entry [Ctrl+Left]'))
b.clicked.connect(self.tocw.move_left)
self.del_button = b = QToolButton(self)
b.setIcon(QIcon(I('trash.png')))
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
l.addWidget(b, 2, 3)
l.addWidget(b, 3, 3)
b.setToolTip(_('Remove all selected entries'))
b.clicked.connect(self.del_items)
self.left_button = b = QToolButton(self)
b.setIcon(QIcon(I('forward.png')))
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
l.addWidget(b, 4, 3)
b.setToolTip(_('Unindent the current entry [Ctrl+Left]'))
b.clicked.connect(self.tocw.move_right)
self.down_button = b = QToolButton(self)
b.setIcon(QIcon(I('arrow-down.png')))
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
l.addWidget(b, 4, 3)
b.setToolTip(_('Move current entry down'))
l.addWidget(b, 6, 3)
b.setToolTip(_('Move current entry down [Ctrl+Down]'))
b.clicked.connect(self.move_down)
self.expand_all_button = b = QPushButton(_('&Expand all'))
col = 5
col = 7
l.addWidget(b, col, 0)
b.clicked.connect(self.tocw.expandAll)
self.collapse_all_button = b = QPushButton(_('&Collapse all'))
@ -444,9 +594,7 @@ class TOCView(QWidget): # {{{
return unicode(item.data(0, Qt.DisplayRole).toString())
def del_items(self):
for item in self.tocw.selectedItems():
p = item.parent() or self.root
p.removeChild(item)
self.tocw.del_items()
def delete_current_item(self):
item = self.tocw.currentItem()
@ -484,54 +632,13 @@ class TOCView(QWidget): # {{{
self.tocw.setCurrentItem(None)
def highlight_item(self, item):
self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect)
self.tocw.scrollToItem(item)
def move_down(self):
item = self.tocw.currentItem()
if item is None:
if self.root.childCount() == 0:
return
item = self.root.child(0)
self.highlight_item(item)
return
parent = item.parent() or self.root
idx = parent.indexOfChild(item)
if idx == parent.childCount() - 1:
# At end of parent, need to become sibling of parent
if parent is self.root:
return
gp = parent.parent() or self.root
parent.removeChild(item)
gp.insertChild(gp.indexOfChild(parent)+1, item)
else:
sibling = parent.child(idx+1)
parent.removeChild(item)
sibling.insertChild(0, item)
self.highlight_item(item)
self.tocw.highlight_item(item)
def move_up(self):
item = self.tocw.currentItem()
if item is None:
if self.root.childCount() == 0:
return
item = self.root.child(self.root.childCount()-1)
self.highlight_item(item)
return
parent = item.parent() or self.root
idx = parent.indexOfChild(item)
if idx == 0:
# At end of parent, need to become sibling of parent
if parent is self.root:
return
gp = parent.parent() or self.root
parent.removeChild(item)
gp.insertChild(gp.indexOfChild(parent), item)
else:
sibling = parent.child(idx-1)
parent.removeChild(item)
sibling.addChild(item)
self.highlight_item(item)
self.tocw.move_up()
def move_down(self):
self.tocw.move_down()
def update_status_tip(self, item):
c = item.data(0, Qt.UserRole).toPyObject()

View File

@ -347,7 +347,7 @@ class ZshCompleter(object): # {{{
subcommands.append(';;')
f.write('\n_calibredb() {')
f.write(
f.write((
r'''
local state line state_descr context
typeset -A opt_args
@ -370,7 +370,7 @@ class ZshCompleter(object): # {{{
esac
return ret
'''%'\n '.join(subcommands))
'''%'\n '.join(subcommands)).encode('utf-8'))
f.write('\n}\n\n')
def write(self):