Merge from trunk
@ -750,8 +750,61 @@ If this property is detected by |app|, the following custom properties are recog
|
|||||||
opf.series
|
opf.series
|
||||||
opf.seriesindex
|
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.
|
In addition to this, you can specify the picture to use as the cover by naming
|
||||||
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!
|
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.
|
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.
|
||||||
|
|
||||||
|
@ -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']:
|
if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']:
|
||||||
a['href']=self.index + a['href']
|
a['href']=self.index + a['href']
|
||||||
return soup
|
return soup
|
||||||
|
|
||||||
|
27
recipes/dzial_zagraniczny.recipe
Normal 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')]
|
@ -26,7 +26,7 @@ class ElDiplo_Recipe(BasicNewsRecipe):
|
|||||||
title = u'El Diplo'
|
title = u'El Diplo'
|
||||||
__author__ = 'Tomas Di Domenico'
|
__author__ = 'Tomas Di Domenico'
|
||||||
description = 'Publicacion mensual de Le Monde Diplomatique, edicion Argentina'
|
description = 'Publicacion mensual de Le Monde Diplomatique, edicion Argentina'
|
||||||
langauge = 'es_AR'
|
language = 'es_AR'
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
auto_cleanup = True
|
auto_cleanup = True
|
||||||
|
|
||||||
|
29
recipes/equipped.recipe
Normal 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')]
|
@ -1,12 +1,12 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
|
||||||
class FocusRecipe(BasicNewsRecipe):
|
class FocusRecipe(BasicNewsRecipe):
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
|
||||||
__author__ = u'intromatyk <intromatyk@gmail.com>'
|
|
||||||
language = 'pl'
|
language = 'pl'
|
||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
|
@ -14,13 +14,14 @@ class gazetaprawna(BasicNewsRecipe):
|
|||||||
title = u'Gazeta Prawna'
|
title = u'Gazeta Prawna'
|
||||||
__author__ = u'Vroo'
|
__author__ = u'Vroo'
|
||||||
publisher = u'Infor Biznes'
|
publisher = u'Infor Biznes'
|
||||||
oldest_article = 7
|
oldest_article = 1
|
||||||
max_articles_per_feed = 20
|
max_articles_per_feed = 20
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
description = 'Polski dziennik gospodarczy'
|
description = 'Polski dziennik gospodarczy'
|
||||||
language = 'pl'
|
language = 'pl'
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
|
ignore_duplicate_articles = {'title', 'url'}
|
||||||
|
|
||||||
remove_tags_after = [
|
remove_tags_after = [
|
||||||
dict(name='div', attrs={'class':['data-art']})
|
dict(name='div', attrs={'class':['data-art']})
|
||||||
@ -30,7 +31,7 @@ class gazetaprawna(BasicNewsRecipe):
|
|||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
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'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'Prawo i wymiar sprawiedliwo\u015bci', u'http://prawo.gazetaprawna.pl/rss.xml'),
|
||||||
(u'Praca i ubezpieczenia', u'http://praca.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('prawo.gazetaprawna', 'www.gazetaprawna')
|
||||||
url = url.replace('praca.gazetaprawna', 'www.gazetaprawna')
|
url = url.replace('praca.gazetaprawna', 'www.gazetaprawna')
|
||||||
return url
|
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)
|
||||||
|
BIN
recipes/icons/dzial_zagraniczny.png
Normal file
After Width: | Height: | Size: 491 B |
BIN
recipes/icons/equipped.png
Normal file
After Width: | Height: | Size: 929 B |
BIN
recipes/icons/gazeta-prawna-calibre-v1.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
recipes/icons/ittechblog.png
Normal file
After Width: | Height: | Size: 731 B |
BIN
recipes/icons/magazyn_consido.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
recipes/icons/media2.png
Normal file
After Width: | Height: | Size: 660 B |
BIN
recipes/icons/mobilna.png
Normal file
After Width: | Height: | Size: 885 B |
BIN
recipes/icons/mojegotowanie.png
Normal file
After Width: | Height: | Size: 307 B |
BIN
recipes/icons/najwyzszy_czas.png
Normal file
After Width: | Height: | Size: 616 B |
BIN
recipes/icons/nowiny_rybnik.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
recipes/icons/osw.png
Normal file
After Width: | Height: | Size: 489 B |
BIN
recipes/icons/ppe_pl.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
recipes/icons/presseurop.png
Normal file
After Width: | Height: | Size: 207 B |
BIN
recipes/icons/res_publica.png
Normal file
After Width: | Height: | Size: 733 B |
BIN
recipes/icons/wolne_media.png
Normal file
After Width: | Height: | Size: 497 B |
26
recipes/ittechblog.recipe
Normal 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')]
|
@ -2,8 +2,7 @@
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class KrytykaPolitycznaRecipe(BasicNewsRecipe):
|
class KrytykaPolitycznaRecipe(BasicNewsRecipe):
|
||||||
__license__ = 'GPL v3'
|
__author__ = u'Artur Stachecki <artur.stachecki@gmail.com>'
|
||||||
__author__ = u'intromatyk <intromatyk@gmail.com>'
|
|
||||||
language = 'pl'
|
language = 'pl'
|
||||||
version = 1
|
version = 1
|
||||||
|
|
||||||
|
88
recipes/magazyn_consido.recipe
Normal 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
@ -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
@ -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/')]
|
50
recipes/mojegotowanie.recipe
Normal 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
|
27
recipes/najwyzszy_czas.recipe
Normal 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/')]
|
31
recipes/nowiny_rybnik.recipe
Normal 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
@ -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
@ -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
@ -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>'),
|
||||||
|
]
|
35
recipes/res_publica.recipe
Normal 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
|
@ -20,7 +20,7 @@ class sport_pl(BasicNewsRecipe):
|
|||||||
remove_javascript=True
|
remove_javascript=True
|
||||||
no_stylesheets=True
|
no_stylesheets=True
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
|
ignore_duplicate_articles = {'title', 'url'}
|
||||||
keep_only_tags =[]
|
keep_only_tags =[]
|
||||||
keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'article'}))
|
keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'article'}))
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class WirtualneMedia(BasicNewsRecipe):
|
class WirtualneMedia(BasicNewsRecipe):
|
||||||
title = u'wirtualnemedia.pl'
|
title = u'Wirtualnemedia.pl'
|
||||||
oldest_article = 7
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
26
recipes/wolne_media.recipe
Normal 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/')]
|
@ -1,10 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, matek09, matek09@gmail.com'
|
__copyright__ = '''2010, matek09, matek09@gmail.com
|
||||||
__copyright__ = 'Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>'
|
Modified 2011, Mariusz Wolek <mariusz_dot_wolek @ gmail dot com>
|
||||||
__copyright__ = 'Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'
|
Modified 2012, Artur Stachecki <artur.stachecki@gmail.com>'''
|
||||||
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
import re
|
import re
|
||||||
@ -16,7 +15,7 @@ class Wprost(BasicNewsRecipe):
|
|||||||
ICO_BLOCKED = 'http://www.wprost.pl/G/layout2/ico_blocked.png'
|
ICO_BLOCKED = 'http://www.wprost.pl/G/layout2/ico_blocked.png'
|
||||||
title = u'Wprost'
|
title = u'Wprost'
|
||||||
__author__ = 'matek09'
|
__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'
|
encoding = 'ISO-8859-2'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
language = 'pl'
|
language = 'pl'
|
||||||
@ -94,5 +93,3 @@ class Wprost(BasicNewsRecipe):
|
|||||||
'description' : ''
|
'description' : ''
|
||||||
})
|
})
|
||||||
return articles
|
return articles
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/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>'
|
|
||||||
|
|
||||||
|
__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
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
import re
|
import re
|
||||||
@ -12,13 +11,14 @@ import re
|
|||||||
class Wprost(BasicNewsRecipe):
|
class Wprost(BasicNewsRecipe):
|
||||||
title = u'Wprost (RSS)'
|
title = u'Wprost (RSS)'
|
||||||
__author__ = 'matek09'
|
__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'
|
encoding = 'ISO-8859-2'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
language = 'pl'
|
language = 'pl'
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
recursions = 0
|
recursions = 0
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
|
ignore_duplicate_articles = {'title', 'url'}
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
|
remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'}))
|
||||||
remove_tags_after = 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}
|
#h2 {font-size: x-large; font-weight: bold}
|
||||||
|
|
||||||
feeds = [(u'Tylko u nas', u'http://www.wprost.pl/rss/rss_wprostextra.php'),
|
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'Wydarzenia', u'http://www.wprost.pl/rss/rss.php'),
|
||||||
(u'Komentarze', u'http://www.wprost.pl/rss/rss_komentarze.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'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'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'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'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'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'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'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'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'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'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'Przegląd prasy', u'http://www.wprost.pl/rss/rss_prasa.php')
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_cover_url(self):
|
def get_cover_url(self):
|
||||||
soup = self.index_to_soup('http://www.wprost.pl/tygodnik')
|
soup = self.index_to_soup('http://www.wprost.pl/tygodnik')
|
||||||
|
@ -79,7 +79,7 @@ author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council',
|
|||||||
# By default, calibre splits a string containing multiple author names on
|
# By default, calibre splits a string containing multiple author names on
|
||||||
# ampersands and the words "and" and "with". You can customize the splitting
|
# ampersands and the words "and" and "with". You can customize the splitting
|
||||||
# by changing the regular expression below. Strings are split on whatever the
|
# 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+'
|
# Default: r'(?i),?\s+(and|with)\s+'
|
||||||
authors_split_regex = r'(?i),?\s+(and|with)\s+'
|
authors_split_regex = r'(?i),?\s+(and|with)\s+'
|
||||||
|
|
||||||
|
@ -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.overdrive import OverDrive
|
||||||
from calibre.ebooks.metadata.sources.douban import Douban
|
from calibre.ebooks.metadata.sources.douban import Douban
|
||||||
from calibre.ebooks.metadata.sources.ozon import Ozon
|
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]
|
plugins += [GoogleBooks, Amazon, Edelweiss, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon]
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ def restore_plugin_state_to_default(plugin_or_name):
|
|||||||
config['enabled_plugins'] = ep
|
config['enabled_plugins'] = ep
|
||||||
|
|
||||||
default_disabled_plugins = set([
|
default_disabled_plugins = set([
|
||||||
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss',
|
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images',
|
||||||
])
|
])
|
||||||
|
|
||||||
def is_disabled(plugin):
|
def is_disabled(plugin):
|
||||||
|
@ -7,7 +7,9 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import weakref
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from itertools import izip, imap
|
||||||
|
|
||||||
def sanitize_sort_field_name(field_metadata, field):
|
def sanitize_sort_field_name(field_metadata, field):
|
||||||
field = field_metadata.search_term_to_field_key(field.lower().strip())
|
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)
|
field = {'title': 'sort', 'authors':'author_sort'}.get(field, field)
|
||||||
return 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):
|
class View(object):
|
||||||
|
|
||||||
|
''' A table view of the database, with rows and columns. Also supports
|
||||||
|
filtering and sorting. '''
|
||||||
|
|
||||||
def __init__(self, cache):
|
def __init__(self, cache):
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
self.marked_ids = {}
|
self.marked_ids = {}
|
||||||
|
self.search_restriction_book_count = 0
|
||||||
|
self.search_restriction = ''
|
||||||
self._field_getters = {}
|
self._field_getters = {}
|
||||||
for col, idx in cache.backend.FIELD_MAP.iteritems():
|
for col, idx in cache.backend.FIELD_MAP.iteritems():
|
||||||
if isinstance(col, int):
|
if isinstance(col, int):
|
||||||
@ -38,16 +68,33 @@ class View(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
self._field_getters[idx] = partial(self.get, col)
|
self._field_getters[idx] = partial(self.get, col)
|
||||||
|
|
||||||
self._map = list(self.cache.all_book_ids())
|
self._map = tuple(self.cache.all_book_ids())
|
||||||
self._map_filtered = list(self._map)
|
self._map_filtered = tuple(self._map)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def field_metadata(self):
|
def field_metadata(self):
|
||||||
return self.cache.field_metadata
|
return self.cache.field_metadata
|
||||||
|
|
||||||
def _get_id(self, idx, index_is_id=True):
|
def _get_id(self, idx, index_is_id=True):
|
||||||
ans = idx if index_is_id else self.index_to_id(idx)
|
return idx if index_is_id else self.index_to_id(idx)
|
||||||
return ans
|
|
||||||
|
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):
|
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=''):
|
def get_ondevice(self, idx, index_is_id=True, default_value=''):
|
||||||
id_ = idx if index_is_id else self.index_to_id(idx)
|
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):
|
def get_marked(self, idx, index_is_id=True, default_value=None):
|
||||||
id_ = idx if index_is_id else self.index_to_id(idx)
|
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_))
|
ans.append(self.cache._author_data(id_))
|
||||||
return tuple(ans)
|
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]
|
fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields]
|
||||||
keys = self.field_metadata.sortable_field_keys()
|
keys = self.field_metadata.sortable_field_keys()
|
||||||
fields = [x for x in fields if x[0] in keys]
|
fields = [x for x in fields if x[0] in keys]
|
||||||
@ -102,8 +149,70 @@ class View(object):
|
|||||||
if not fields:
|
if not fields:
|
||||||
fields = [('timestamp', False)]
|
fields = [('timestamp', False)]
|
||||||
|
|
||||||
sorted_book_ids = self.cache.multisort(fields)
|
sorted_book_ids = self.cache.multisort(fields, ids_to_sort=only_ids)
|
||||||
sorted_book_ids
|
if only_ids is None:
|
||||||
# TODO: change maps
|
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())))
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ class ANDROID(USBMS):
|
|||||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
||||||
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
||||||
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
|
'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',
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||||
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||||
|
@ -104,13 +104,11 @@ class PDFOutput(OutputFormatPlugin):
|
|||||||
'specify a footer template, it will take precedence '
|
'specify a footer template, it will take precedence '
|
||||||
'over this option.')),
|
'over this option.')),
|
||||||
OptionRecommendation(name='pdf_footer_template', recommended_value=None,
|
OptionRecommendation(name='pdf_footer_template', recommended_value=None,
|
||||||
help=_('An HTML template used to generate footers on every page.'
|
help=_('An HTML template used to generate %s on every page.'
|
||||||
' The string _PAGENUM_ will be replaced by the current page'
|
' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('footers')),
|
||||||
' number.')),
|
|
||||||
OptionRecommendation(name='pdf_header_template', recommended_value=None,
|
OptionRecommendation(name='pdf_header_template', recommended_value=None,
|
||||||
help=_('An HTML template used to generate headers on every page.'
|
help=_('An HTML template used to generate %s on every page.'
|
||||||
' The string _PAGENUM_ will be replaced by the current page'
|
' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('headers')),
|
||||||
' number.')),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
def convert(self, oeb_book, output_path, input_plugin, opts, log):
|
||||||
|
@ -858,7 +858,7 @@ class Amazon(Source):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort, # {{{
|
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)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
log.info('No cached cover found, running identify')
|
log.info('No cached cover found, running identify')
|
||||||
|
@ -31,7 +31,7 @@ msprefs.defaults['find_first_edition_date'] = False
|
|||||||
# Google covers are often poor quality (scans/errors) but they have high
|
# Google covers are often poor quality (scans/errors) but they have high
|
||||||
# resolution, so they trump covers from better sources. So make sure they
|
# resolution, so they trump covers from better sources. So make sure they
|
||||||
# are only used if no other covers are found.
|
# 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):
|
def create_log(ostream=None):
|
||||||
from calibre.utils.logging import ThreadSafeLog, FileStream
|
from calibre.utils.logging import ThreadSafeLog, FileStream
|
||||||
@ -222,6 +222,9 @@ class Source(Plugin):
|
|||||||
#: plugin
|
#: plugin
|
||||||
config_help_message = None
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
Plugin.__init__(self, *args, **kwargs)
|
Plugin.__init__(self, *args, **kwargs)
|
||||||
@ -522,7 +525,7 @@ class Source(Plugin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort,
|
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
|
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
|
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
|
This method should use cached cover URLs for efficiency whenever
|
||||||
possible. When cached data is not present, most plugins simply call
|
possible. When cached data is not present, most plugins simply call
|
||||||
identify and use its results.
|
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
|
pass
|
||||||
|
|
||||||
|
@ -35,9 +35,14 @@ class Worker(Thread):
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
if not self.abort.is_set():
|
if not self.abort.is_set():
|
||||||
try:
|
try:
|
||||||
self.plugin.download_cover(self.log, self.rq, self.abort,
|
if self.plugin.can_get_multiple_covers:
|
||||||
title=self.title, authors=self.authors,
|
self.plugin.download_cover(self.log, self.rq, self.abort,
|
||||||
identifiers=self.identifiers, timeout=self.timeout)
|
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:
|
except:
|
||||||
self.log.exception('Failed to download cover from',
|
self.log.exception('Failed to download cover from',
|
||||||
self.plugin.name)
|
self.plugin.name)
|
||||||
|
@ -221,7 +221,7 @@ class Douban(Source):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort, # {{{
|
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)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
log.info('No cached cover found, running identify')
|
log.info('No cached cover found, running identify')
|
||||||
|
@ -320,7 +320,7 @@ class Edelweiss(Source):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort, # {{{
|
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)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
log.info('No cached cover found, running identify')
|
log.info('No cached cover found, running identify')
|
||||||
|
@ -209,7 +209,7 @@ class GoogleBooks(Source):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort, # {{{
|
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)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
log.info('No cached cover found, running identify')
|
log.info('No cached cover found, running identify')
|
||||||
|
148
src/calibre/ebooks/metadata/sources/google_images.py
Normal 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()
|
||||||
|
|
@ -19,7 +19,7 @@ class OpenLibrary(Source):
|
|||||||
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
|
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort,
|
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:
|
if 'isbn' not in identifiers:
|
||||||
return
|
return
|
||||||
isbn = identifiers['isbn']
|
isbn = identifiers['isbn']
|
||||||
|
@ -75,7 +75,7 @@ class OverDrive(Source):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def download_cover(self, log, result_queue, abort, # {{{
|
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
|
import mechanize
|
||||||
cached_url = self.get_cached_cover_url(identifiers)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
|
@ -250,7 +250,7 @@ class Ozon(Source):
|
|||||||
return url
|
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)
|
cached_url = self.get_cached_cover_url(identifiers)
|
||||||
if cached_url is None:
|
if cached_url is None:
|
||||||
log.debug('No cached cover found, running identify')
|
log.debug('No cached cover found, running identify')
|
||||||
|
@ -11,6 +11,7 @@ import os
|
|||||||
from threading import Event, Thread
|
from threading import Event, Thread
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from calibre.utils.date import as_utc
|
from calibre.utils.date import as_utc
|
||||||
from calibre.ebooks.metadata.sources.identify import identify, msprefs
|
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))
|
kwargs=dict(title=title, authors=authors, identifiers=identifiers))
|
||||||
worker.daemon = True
|
worker.daemon = True
|
||||||
worker.start()
|
worker.start()
|
||||||
|
c = Counter()
|
||||||
while worker.is_alive():
|
while worker.is_alive():
|
||||||
try:
|
try:
|
||||||
plugin, width, height, fmt, data = results.get(True, 1)
|
plugin, width, height, fmt, data = results.get(True, 1)
|
||||||
except Empty:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
else:
|
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:
|
with open(name, 'wb') as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
os.mkdir(name+'.done')
|
os.mkdir(name+'.done')
|
||||||
|
@ -33,6 +33,7 @@ class PagedDisplay
|
|||||||
this.header_template = null
|
this.header_template = null
|
||||||
this.header = null
|
this.header = null
|
||||||
this.footer = null
|
this.footer = null
|
||||||
|
this.hf_style = null
|
||||||
|
|
||||||
read_document_margins: () ->
|
read_document_margins: () ->
|
||||||
# Read page margins from the document. First checks for an @page rule.
|
# 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)
|
# log('Time to layout:', new Date().getTime() - start_time)
|
||||||
return sm
|
return sm
|
||||||
|
|
||||||
create_header_footer: () ->
|
create_header_footer: (uuid) ->
|
||||||
if this.header_template != null
|
if this.header_template != null
|
||||||
this.header = document.createElement('div')
|
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('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)
|
document.body.appendChild(this.header)
|
||||||
if this.footer_template != null
|
if this.footer_template != null
|
||||||
this.footer = document.createElement('div')
|
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('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)
|
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)
|
this.update_header_footer(1)
|
||||||
|
|
||||||
position_header_footer: () ->
|
position_header_footer: () ->
|
||||||
@ -203,10 +211,16 @@ class PagedDisplay
|
|||||||
this.footer.style.setProperty('left', left+'px')
|
this.footer.style.setProperty('left', left+'px')
|
||||||
|
|
||||||
update_header_footer: (pagenum) ->
|
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
|
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
|
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: () ->
|
fit_images: () ->
|
||||||
# Ensure no images are wider than the available width in a column. Note
|
# Ensure no images are wider than the available width in a column. Note
|
||||||
|
@ -130,6 +130,18 @@ class PDFWriter(QObject):
|
|||||||
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
|
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
|
||||||
fset=_pass_json_value_setter)
|
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):
|
def __init__(self, opts, log, cover_data=None, toc=None):
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
from calibre.gui2 import is_ok_to_use_qt
|
||||||
if not 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,
|
self.view.page().mainFrame().setScrollBarPolicy(x,
|
||||||
Qt.ScrollBarAlwaysOff)
|
Qt.ScrollBarAlwaysOff)
|
||||||
self.report_progress = lambda x, y: x
|
self.report_progress = lambda x, y: x
|
||||||
|
self.current_section = ''
|
||||||
|
|
||||||
def dump(self, items, out_stream, pdf_metadata):
|
def dump(self, items, out_stream, pdf_metadata):
|
||||||
opts = self.opts
|
opts = self.opts
|
||||||
@ -170,9 +183,13 @@ class PDFWriter(QObject):
|
|||||||
opts.uncompressed_pdf,
|
opts.uncompressed_pdf,
|
||||||
mark_links=opts.pdf_mark_links)
|
mark_links=opts.pdf_mark_links)
|
||||||
self.footer = opts.pdf_footer_template
|
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.footer = '<p style="text-align:center; text-indent: 0">_PAGENUM_</p>'
|
||||||
self.header = opts.pdf_header_template
|
self.header = opts.pdf_header_template
|
||||||
|
if self.header:
|
||||||
|
self.header = self.header.strip()
|
||||||
min_margin = 36
|
min_margin = 36
|
||||||
if self.footer and opts.margin_bottom < min_margin:
|
if self.footer and opts.margin_bottom < min_margin:
|
||||||
self.log.warn('Bottom margin is too small for footer, increasing it.')
|
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,
|
self.doc.set_metadata(title=pdf_metadata.title,
|
||||||
author=pdf_metadata.author,
|
author=pdf_metadata.author,
|
||||||
tags=pdf_metadata.tags)
|
tags=pdf_metadata.tags)
|
||||||
|
self.doc_title = pdf_metadata.title
|
||||||
|
self.doc_author = pdf_metadata.author
|
||||||
self.painter.save()
|
self.painter.save()
|
||||||
try:
|
try:
|
||||||
if self.cover_data is not None:
|
if self.cover_data is not None:
|
||||||
@ -273,13 +292,34 @@ class PDFWriter(QObject):
|
|||||||
self.loop.processEvents(self.loop.ExcludeUserInputEvents)
|
self.loop.processEvents(self.loop.ExcludeUserInputEvents)
|
||||||
evaljs('document.getElementById("MathJax_Message").style.display="none";')
|
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):
|
def do_paged_render(self):
|
||||||
if self.paged_js is None:
|
if self.paged_js is None:
|
||||||
|
import uuid
|
||||||
from calibre.utils.resources import compiled_coffeescript as cc
|
from calibre.utils.resources import compiled_coffeescript as cc
|
||||||
self.paged_js = cc('ebooks.oeb.display.utils')
|
self.paged_js = cc('ebooks.oeb.display.utils')
|
||||||
self.paged_js += cc('ebooks.oeb.display.indexing')
|
self.paged_js += cc('ebooks.oeb.display.indexing')
|
||||||
self.paged_js += cc('ebooks.oeb.display.paged')
|
self.paged_js += cc('ebooks.oeb.display.paged')
|
||||||
self.paged_js += cc('ebooks.oeb.display.mathjax')
|
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().mainFrame().addToJavaScriptWindowObject("py_bridge", self)
|
||||||
self.view.page().longjs_counter = 0
|
self.view.page().longjs_counter = 0
|
||||||
@ -305,6 +345,8 @@ class PDFWriter(QObject):
|
|||||||
amap = self.bridge_value
|
amap = self.bridge_value
|
||||||
if not isinstance(amap, dict):
|
if not isinstance(amap, dict):
|
||||||
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
|
amap = {'links':[], 'anchors':{}} # Some javascript error occurred
|
||||||
|
sections = self.get_sections(amap['anchors'])
|
||||||
|
col = 0
|
||||||
|
|
||||||
if self.header:
|
if self.header:
|
||||||
self.bridge_value = self.header
|
self.bridge_value = self.header
|
||||||
@ -313,12 +355,14 @@ class PDFWriter(QObject):
|
|||||||
self.bridge_value = self.footer
|
self.bridge_value = self.footer
|
||||||
evaljs('paged_display.footer_template = py_bridge.value')
|
evaljs('paged_display.footer_template = py_bridge.value')
|
||||||
if self.header or self.footer:
|
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
|
start_page = self.current_page_num
|
||||||
|
|
||||||
mf = self.view.page().mainFrame()
|
mf = self.view.page().mainFrame()
|
||||||
while True:
|
while True:
|
||||||
|
if col in sections:
|
||||||
|
self.current_section = sections[col]
|
||||||
self.doc.init_page()
|
self.doc.init_page()
|
||||||
if self.header or self.footer:
|
if self.header or self.footer:
|
||||||
evaljs('paged_display.update_header_footer(%d)'%self.current_page_num)
|
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])
|
evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl[0])
|
||||||
if self.doc.errors_occurred:
|
if self.doc.errors_occurred:
|
||||||
break
|
break
|
||||||
|
col += 1
|
||||||
|
|
||||||
if not self.doc.errors_occurred:
|
if not self.doc.errors_occurred:
|
||||||
self.doc.add_links(self.current_item, start_page, amap['links'],
|
self.doc.add_links(self.current_item, start_page, amap['links'],
|
||||||
amap['anchors'])
|
amap['anchors'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +22,9 @@ class PluginWidget(Widget, Ui_Form):
|
|||||||
'override_profile_size', 'paper_size', 'custom_size',
|
'override_profile_size', 'paper_size', 'custom_size',
|
||||||
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
|
'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit',
|
||||||
'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font',
|
'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
|
self.db, self.book_id = db, book_id
|
||||||
|
|
||||||
for x in get_option('paper_size').option.choices:
|
for x in get_option('paper_size').option.choices:
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>590</width>
|
<width>638</width>
|
||||||
<height>395</height>
|
<height>498</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -84,6 +84,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="5" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_pdf_page_numbers">
|
||||||
|
<property name="text">
|
||||||
|
<string>Add page &numbers to the bottom of every page</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -170,24 +177,52 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="12" column="0">
|
<item row="12" column="0" colspan="2">
|
||||||
<spacer name="verticalSpacer">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="orientation">
|
<property name="title">
|
||||||
<enum>Qt::Vertical</enum>
|
<string>Page headers and footers</string>
|
||||||
</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 &numbers to the bottom of every page</string>
|
|
||||||
</property>
|
</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 <a href="http://manual.calibre-ebook.com/conversion.html#converting-to-pdf">documentation</a>.</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>&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>&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>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -16,13 +16,12 @@ from operator import attrgetter
|
|||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
|
from PyQt4.Qt import (
|
||||||
QApplication, QDialog, QVBoxLayout, QLabel,
|
QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QApplication,
|
||||||
QDialogButtonBox, QStyle, QStackedWidget, QWidget,
|
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
|
||||||
QTableView, QGridLayout, QFontInfo, QPalette, QTimer,
|
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
|
||||||
pyqtSignal, QAbstractTableModel, QVariant, QSize,
|
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
|
||||||
QListView, QPixmap, QAbstractListModel, QColor, QRect,
|
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel)
|
||||||
QTextBrowser, QStringListModel)
|
|
||||||
from PyQt4.QtWebKit import QWebView
|
from PyQt4.QtWebKit import QWebView
|
||||||
|
|
||||||
from calibre.customize.ui import metadata_plugins
|
from calibre.customize.ui import metadata_plugins
|
||||||
@ -654,7 +653,7 @@ class CoversModel(QAbstractListModel): # {{{
|
|||||||
for i, plugin in enumerate(metadata_plugins(['cover'])):
|
for i, plugin in enumerate(metadata_plugins(['cover'])):
|
||||||
self.covers.append((plugin.name+'\n'+_('Searching...'),
|
self.covers.append((plugin.name+'\n'+_('Searching...'),
|
||||||
QVariant(self.blank), None, True))
|
QVariant(self.blank), None, True))
|
||||||
self.plugin_map[plugin] = i+1
|
self.plugin_map[plugin] = [i+1]
|
||||||
|
|
||||||
if do_reset:
|
if do_reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
@ -685,48 +684,82 @@ class CoversModel(QAbstractListModel): # {{{
|
|||||||
def plugin_for_index(self, index):
|
def plugin_for_index(self, index):
|
||||||
row = index.row() if hasattr(index, 'row') else index
|
row = index.row() if hasattr(index, 'row') else index
|
||||||
for k, v in self.plugin_map.iteritems():
|
for k, v in self.plugin_map.iteritems():
|
||||||
if v == row:
|
if row in v:
|
||||||
return k
|
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):
|
def clear_failed(self):
|
||||||
|
# Remove entries that are still waiting
|
||||||
good = []
|
good = []
|
||||||
pmap = {}
|
pmap = {}
|
||||||
dcovers = sorted(self.covers[1:], key=self.cover_keygen, reverse=True)
|
def keygen(x):
|
||||||
cmap = {x:self.covers.index(x) for x in self.covers}
|
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):
|
for i, x in enumerate(self.covers[0:1] + dcovers):
|
||||||
if not x[-1]:
|
if not x[-1]:
|
||||||
good.append(x)
|
good.append(x)
|
||||||
if i > 0:
|
plugin = cmap[i]
|
||||||
plugin = self.plugin_for_index(cmap[x])
|
if plugin is not None:
|
||||||
pmap[plugin] = len(good) - 1
|
try:
|
||||||
|
pmap[plugin].append(len(good) - 1)
|
||||||
|
except KeyError:
|
||||||
|
pmap[plugin] = [len(good)-1]
|
||||||
self.covers = good
|
self.covers = good
|
||||||
self.plugin_map = pmap
|
self.plugin_map = pmap
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def index_for_plugin(self, plugin):
|
def pointer_from_index(self, index):
|
||||||
idx = self.plugin_map.get(plugin, 0)
|
row = index.row() if hasattr(index, 'row') else index
|
||||||
return self.index(idx)
|
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):
|
def update_result(self, plugin_name, width, height, data):
|
||||||
idx = None
|
if plugin_name.endswith('}'):
|
||||||
for plugin, i in self.plugin_map.iteritems():
|
# multi cover plugin
|
||||||
if plugin.name == plugin_name:
|
plugin_name = plugin_name.partition('{')[0]
|
||||||
idx = i
|
plugin = [plugin for plugin in self.plugin_map if plugin.name == plugin_name]
|
||||||
break
|
if not plugin:
|
||||||
if idx is None:
|
return
|
||||||
return
|
plugin = plugin[0]
|
||||||
pmap = QPixmap()
|
last_row = max(self.plugin_map[plugin])
|
||||||
pmap.loadFromData(data)
|
pmap = QPixmap()
|
||||||
if pmap.isNull():
|
pmap.loadFromData(data)
|
||||||
return
|
if pmap.isNull():
|
||||||
self.covers[idx] = self.get_item(plugin_name, pmap, waiting=False)
|
return
|
||||||
self.dataChanged.emit(self.index(idx), self.index(idx))
|
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):
|
def cover_pixmap(self, index):
|
||||||
row = index.row()
|
row = index.row()
|
||||||
@ -774,9 +807,12 @@ class CoversView(QListView): # {{{
|
|||||||
self.m.reset_covers()
|
self.m.reset_covers()
|
||||||
|
|
||||||
def clear_failed(self):
|
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.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:
|
if num < 2:
|
||||||
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
|
txt = _('Could not find any covers for <b>%s</b>')%self.book.title
|
||||||
else:
|
else:
|
||||||
txt = _('Found <b>%(num)d</b> covers of %(title)s. '
|
txt = _('Found <b>%(num)d</b> possible covers for %(title)s. '
|
||||||
'Pick the one you like best.')%dict(num=num-1,
|
'When the download completes, the covers will be sorted by size.')%dict(num=num-1,
|
||||||
title=self.title)
|
title=self.title)
|
||||||
self.msg.setText(txt)
|
self.msg.setText(txt)
|
||||||
|
self.msg.setWordWrap(True)
|
||||||
|
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
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'
|
__license__ = 'GPL 3'
|
||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
@ -129,12 +129,12 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
doc = html.fromstring(f.read().decode('latin-1', 'replace'))
|
doc = html.fromstring(f.read().decode('latin-1', 'replace'))
|
||||||
|
|
||||||
data_xpath = '//div[contains(@class, "prod")]'
|
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'
|
asin_xpath = '@name'
|
||||||
cover_xpath = './/img[@class="productImage"]/@src'
|
cover_xpath = './/img[@class="productImage"]/@src'
|
||||||
title_xpath = './/h3[@class="newaps"]/a//text()'
|
title_xpath = './/h3[@class="newaps"]/a//text()'
|
||||||
author_xpath = './/h3[@class="newaps"]//span[contains(@class, "reg")]//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):
|
for data in doc.xpath(data_xpath):
|
||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
|
@ -14,7 +14,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import (QPushButton, QFrame, QVariant, QMenu, QInputDialog,
|
from PyQt4.Qt import (QPushButton, QFrame, QVariant, QMenu, QInputDialog,
|
||||||
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
|
QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget,
|
||||||
QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem,
|
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.container import get_container, AZW3Container
|
||||||
from calibre.ebooks.oeb.polish.toc import (
|
from calibre.ebooks.oeb.polish.toc import (
|
||||||
@ -190,7 +190,7 @@ class ItemView(QFrame): # {{{
|
|||||||
)))
|
)))
|
||||||
l.addWidget(b)
|
l.addWidget(b)
|
||||||
|
|
||||||
self.fal = b = QPushButton(_('Flatten the ToC'))
|
self.fal = b = QPushButton(_('&Flatten the ToC'))
|
||||||
b.clicked.connect(self.flatten_toc)
|
b.clicked.connect(self.flatten_toc)
|
||||||
b.setToolTip(textwrap.fill(_(
|
b.setToolTip(textwrap.fill(_(
|
||||||
'Flatten the Table of Contents, putting all entries at the top level'
|
'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):
|
def __init__(self, parent):
|
||||||
QTreeWidget.__init__(self, parent)
|
QTreeWidget.__init__(self, parent)
|
||||||
@ -357,6 +357,9 @@ class TreeWidget(QTreeWidget):
|
|||||||
self.setAnimated(True)
|
self.setAnimated(True)
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
self.in_drop_event = False
|
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):
|
def iteritems(self, parent=None):
|
||||||
if parent is 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)
|
ans = sorted(ans, key=lambda x:sort_map.get(x, -1), reverse=True)
|
||||||
return ans
|
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): # {{{
|
class TOCView(QWidget): # {{{
|
||||||
|
|
||||||
add_new_item = pyqtSignal(object, object)
|
add_new_item = pyqtSignal(object, object)
|
||||||
@ -393,27 +527,43 @@ class TOCView(QWidget): # {{{
|
|||||||
l = self.l = QGridLayout()
|
l = self.l = QGridLayout()
|
||||||
self.setLayout(l)
|
self.setLayout(l)
|
||||||
self.tocw = t = TreeWidget(self)
|
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)
|
self.up_button = b = QToolButton(self)
|
||||||
b.setIcon(QIcon(I('arrow-up.png')))
|
b.setIcon(QIcon(I('arrow-up.png')))
|
||||||
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||||
l.addWidget(b, 0, 3)
|
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)
|
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)
|
self.del_button = b = QToolButton(self)
|
||||||
b.setIcon(QIcon(I('trash.png')))
|
b.setIcon(QIcon(I('trash.png')))
|
||||||
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||||
l.addWidget(b, 2, 3)
|
l.addWidget(b, 3, 3)
|
||||||
b.setToolTip(_('Remove all selected entries'))
|
b.setToolTip(_('Remove all selected entries'))
|
||||||
b.clicked.connect(self.del_items)
|
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)
|
self.down_button = b = QToolButton(self)
|
||||||
b.setIcon(QIcon(I('arrow-down.png')))
|
b.setIcon(QIcon(I('arrow-down.png')))
|
||||||
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
b.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
|
||||||
l.addWidget(b, 4, 3)
|
l.addWidget(b, 6, 3)
|
||||||
b.setToolTip(_('Move current entry down'))
|
b.setToolTip(_('Move current entry down [Ctrl+Down]'))
|
||||||
b.clicked.connect(self.move_down)
|
b.clicked.connect(self.move_down)
|
||||||
self.expand_all_button = b = QPushButton(_('&Expand all'))
|
self.expand_all_button = b = QPushButton(_('&Expand all'))
|
||||||
col = 5
|
col = 7
|
||||||
l.addWidget(b, col, 0)
|
l.addWidget(b, col, 0)
|
||||||
b.clicked.connect(self.tocw.expandAll)
|
b.clicked.connect(self.tocw.expandAll)
|
||||||
self.collapse_all_button = b = QPushButton(_('&Collapse all'))
|
self.collapse_all_button = b = QPushButton(_('&Collapse all'))
|
||||||
@ -444,9 +594,7 @@ class TOCView(QWidget): # {{{
|
|||||||
return unicode(item.data(0, Qt.DisplayRole).toString())
|
return unicode(item.data(0, Qt.DisplayRole).toString())
|
||||||
|
|
||||||
def del_items(self):
|
def del_items(self):
|
||||||
for item in self.tocw.selectedItems():
|
self.tocw.del_items()
|
||||||
p = item.parent() or self.root
|
|
||||||
p.removeChild(item)
|
|
||||||
|
|
||||||
def delete_current_item(self):
|
def delete_current_item(self):
|
||||||
item = self.tocw.currentItem()
|
item = self.tocw.currentItem()
|
||||||
@ -484,54 +632,13 @@ class TOCView(QWidget): # {{{
|
|||||||
self.tocw.setCurrentItem(None)
|
self.tocw.setCurrentItem(None)
|
||||||
|
|
||||||
def highlight_item(self, item):
|
def highlight_item(self, item):
|
||||||
self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect)
|
self.tocw.highlight_item(item)
|
||||||
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)
|
|
||||||
|
|
||||||
def move_up(self):
|
def move_up(self):
|
||||||
item = self.tocw.currentItem()
|
self.tocw.move_up()
|
||||||
if item is None:
|
|
||||||
if self.root.childCount() == 0:
|
def move_down(self):
|
||||||
return
|
self.tocw.move_down()
|
||||||
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 update_status_tip(self, item):
|
def update_status_tip(self, item):
|
||||||
c = item.data(0, Qt.UserRole).toPyObject()
|
c = item.data(0, Qt.UserRole).toPyObject()
|
||||||
|
@ -347,7 +347,7 @@ class ZshCompleter(object): # {{{
|
|||||||
subcommands.append(';;')
|
subcommands.append(';;')
|
||||||
|
|
||||||
f.write('\n_calibredb() {')
|
f.write('\n_calibredb() {')
|
||||||
f.write(
|
f.write((
|
||||||
r'''
|
r'''
|
||||||
local state line state_descr context
|
local state line state_descr context
|
||||||
typeset -A opt_args
|
typeset -A opt_args
|
||||||
@ -370,7 +370,7 @@ class ZshCompleter(object): # {{{
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
'''%'\n '.join(subcommands))
|
'''%'\n '.join(subcommands)).encode('utf-8'))
|
||||||
f.write('\n}\n\n')
|
f.write('\n}\n\n')
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
|