From c6c9e5acf79d34cbdeff07afbf74c11855dfdf76 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Apr 2013 13:24:20 +0530 Subject: [PATCH 01/17] ... --- src/calibre/linux.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index 5f8ae31ce5..395831fa8f 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -347,7 +347,7 @@ class ZshCompleter(object): # {{{ subcommands.append(';;') f.write('\n_calibredb() {') - f.write( + f.write(( r''' local state line state_descr context typeset -A opt_args @@ -370,7 +370,7 @@ class ZshCompleter(object): # {{{ esac return ret - '''%'\n '.join(subcommands)) + '''%'\n '.join(subcommands)).encode('utf-8')) f.write('\n}\n\n') def write(self): From 6201e2a19bf47a5e8f2a61cf6bcef974bca60e55 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Apr 2013 13:34:32 +0530 Subject: [PATCH 02/17] PDF Output: Allow using templates to create arbitrary headers and footers. Look under PDF Output in the conversion dialog for this feature. --- manual/conversion.rst | 47 ++++++++++- resources/compiled_coffeescript.zip | Bin 70080 -> 71057 bytes .../ebooks/conversion/plugins/pdf_output.py | 6 +- src/calibre/ebooks/oeb/display/paged.coffee | 19 ++++- src/calibre/ebooks/pdf/render/from_html.py | 20 ++++- src/calibre/gui2/convert/pdf_output.py | 4 +- src/calibre/gui2/convert/pdf_output.ui | 73 +++++++++++++----- 7 files changed, 138 insertions(+), 31 deletions(-) diff --git a/manual/conversion.rst b/manual/conversion.rst index bf451d0980..2e0876bb7c 100644 --- a/manual/conversion.rst +++ b/manual/conversion.rst @@ -750,8 +750,51 @@ If this property is detected by |app|, the following custom properties are recog opf.series opf.seriesindex -In addition to this, you can specify the picture to use as the cover by naming it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no picture with this name is found, the 'smart' method is used. -As the cover detection might result in double covers in certain output formats, the process will remove the paragraph (only if the only content is the cover!) from the document. But this works only with the named picture! +In addition to this, you can specify the picture to use as the cover by naming +it ``opf.cover`` (right click, Picture->Options->Name) in the ODT. If no +picture with this name is found, the 'smart' method is used. As the cover +detection might result in double covers in certain output formats, the process +will remove the paragraph (only if the only content is the cover!) from the +document. But this works only with the named picture! To disable cover detection you can set the custom property ``opf.nocover`` ('Yes or No' type) to Yes in advanced mode. +Converting to PDF +~~~~~~~~~~~~~~~~~~~ + +The first, most important, setting to decide on when converting to PDF is the page +size. By default, |app| uses a page size defined by the current +:guilabel:`Output profile`. So if your output profile is set to Kindle, |app| +will create a PDF with page size suitable for viewing on the small kindle +screen. However, if you view this PDF file on a computer screen, then it will +appear to have too large fonts. To create "normal" sized PDFs, use the override +page size option under :guilabel:`PDF Output` in the conversion dialog. + +You can insert arbitrary headers and footers on each page of the PDF by +specifying header and footer templates. Templates are just snippets of HTML +code that get rendered in the header and footer locations. For example, to +display page numbers centered at the bottom of every page, in green, use the following +footer template:: + +

Page _PAGENUM_

+ +|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:: + +

_AUTHOR__TITLE_

+ +|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:: + +

_TITLE_

_AUTHOR_

+ +This will display the title at the left and the author at the right, in a font +size smaller than the main text. + +.. 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. + diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 782a26a0c243bca020c48e3cb05a301b99aa7e4a..19d15f6f13994754126630f38ad5d277026ec21b 100644 GIT binary patch delta 982 zcmbVL!Ee$~80Qr>7vcmQG6ODqE3iUFip(913t5;NnQjp$9%eL6p$}-RZEYV!+{|J; zd2@LQ7_S~Yn3y1mI~a}r1)hx;=f$hs^rU?h6lRQxFKL?Z*WdU1e&6@?y?x@^dFBe` zE;wC&2R+g2_m+>2=f>V|I2`l|5l{=XQi{_9A+E?KUnvPDt}BSNzWK=kFivty?)EEZ zW6D*n%2NB%%sL>iXM)qR#3dl7OC>=U%SfmoQ9_2m!8u5IadK-U+J3_}09g$GK`v<; z-cKf4Bt#yb7$GlaCp>U#%XVWkNrw$aEn`#}rn?0e-LQ9tUHihJrLKQ2{uc^wBAmh4 zkPB6eVjQmPh<1X}ZJaEcCP%GLOw)dNQP+_wovz4=6dNiNZ*~AU)^0>58NMbOP^{vL zW+Y%yF%uBWSV8^batRKllCNxFfr4~uYV{_J!g-iZr(r4%H-Gg6sgl(xi)u*i4wxl- zD^uPcs!380>?Hi#L(X_vRgsa+udUMb@c#X<)_(f@?lk8h3werCX)8#;-#NWC{Uhg^ zrLYMl+0+%WehR9Zir`(g-@o-&(Q#O2^twvD3%oREBV#4LbBYOG12vDNf$C0_2B# zvi*Tee>pUKqnZ&9*kH!4Hn%Tw}Z<*i~e}D`M?KKqaP0g>k|J2&jK?6 delta 245 zcmbQZn&rS^7MTEVW)=|!5O|c3>*R25-;{r$3=AMF%`iPcm{D}Pj4&g|M#~iD$#O;P zn-4Irm7P3CjcaqYq6g#TUn=sORh1`!7-o|V+Sw;7TxH#CrzXxcxmuBbvb`$jWFK{j z$y!=2lbJ4yOm3-Xe@fOcQ&j*NZT^GhLek5xgtHXu$OBD}qahm? IKO@kO02(+ diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index c473931aef..e336336939 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -105,12 +105,10 @@ class PDFOutput(OutputFormatPlugin): 'over this option.')), OptionRecommendation(name='pdf_footer_template', recommended_value=None, help=_('An HTML template used to generate footers on every page.' - ' The string _PAGENUM_ will be replaced by the current page' - ' number.')), + ' The strings _PAGENUM_, _TITLE_ and _AUTHOR_ will be replaced by their current values.')), OptionRecommendation(name='pdf_header_template', recommended_value=None, help=_('An HTML template used to generate headers on every page.' - ' The string _PAGENUM_ will be replaced by the current page' - ' number.')), + ' The strings _PAGENUM_, _TITLE_ and _AUTHOR_ will be replaced by their current values.')), ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 405b26a9f6..bb66c20efb 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -33,6 +33,7 @@ class PagedDisplay this.header_template = null this.header = null this.footer = null + this.hf_style = null read_document_margins: () -> # Read page margins from the document. First checks for an @page rule. @@ -184,15 +185,22 @@ class PagedDisplay # log('Time to layout:', new Date().getTime() - start_time) return sm - create_header_footer: () -> + create_header_footer: (uuid) -> if this.header_template != null this.header = document.createElement('div') this.header.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: 0px; height: #{ this.margin_top }px; width: #{ this.col_width }px; margin: 0; padding: 0") + this.header.setAttribute('id', 'pdf_page_header_'+uuid) document.body.appendChild(this.header) if this.footer_template != null this.footer = document.createElement('div') this.footer.setAttribute('style', "overflow:hidden; display:block; position:absolute; left:#{ this.side_margin }px; top: #{ window.innerHeight - this.margin_bottom }px; height: #{ this.margin_bottom }px; width: #{ this.col_width }px; margin: 0; padding: 0") + this.footer.setAttribute('id', 'pdf_page_footer_'+uuid) document.body.appendChild(this.footer) + if this.header != null or this.footer != null + this.hf_uuid = uuid + this.hf_style = document.createElement('style') + this.hf_style.setAttribute('type', 'text/css') + document.head.appendChild(this.hf_style) this.update_header_footer(1) position_header_footer: () -> @@ -203,10 +211,15 @@ class PagedDisplay this.footer.style.setProperty('left', left+'px') update_header_footer: (pagenum) -> + if this.hf_style != null + if pagenum%2 == 1 then cls = "even_page" else cls = "odd_page" + this.hf_style.innerHTML = "#pdf_page_header_#{ this.hf_uuid } .#{ cls }, #pdf_page_footer_#{ this.hf_uuid } .#{ cls } { display: none }" + title = py_bridge.title() + author = py_bridge.author() 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+"") 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+"") fit_images: () -> # Ensure no images are wider than the available width in a column. Note diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index 525bed16a3..e56375b7d0 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -130,6 +130,14 @@ class PDFWriter(QObject): _pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter, fset=_pass_json_value_setter) + @pyqtSlot(result=unicode) + def title(self): + return self.doc_title + + @pyqtSlot(result=unicode) + def author(self): + return self.doc_author + def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): @@ -170,9 +178,13 @@ class PDFWriter(QObject): opts.uncompressed_pdf, mark_links=opts.pdf_mark_links) self.footer = opts.pdf_footer_template - if self.footer is None and opts.pdf_page_numbers: + if self.footer: + self.footer = self.footer.strip() + if not self.footer and opts.pdf_page_numbers: self.footer = '

_PAGENUM_

' self.header = opts.pdf_header_template + if self.header: + self.header = self.header.strip() min_margin = 36 if self.footer and opts.margin_bottom < min_margin: self.log.warn('Bottom margin is too small for footer, increasing it.') @@ -192,6 +204,8 @@ class PDFWriter(QObject): self.doc.set_metadata(title=pdf_metadata.title, author=pdf_metadata.author, tags=pdf_metadata.tags) + self.doc_title = pdf_metadata.title + self.doc_author = pdf_metadata.author self.painter.save() try: if self.cover_data is not None: @@ -275,11 +289,13 @@ class PDFWriter(QObject): def do_paged_render(self): if self.paged_js is None: + import uuid from calibre.utils.resources import compiled_coffeescript as cc self.paged_js = cc('ebooks.oeb.display.utils') self.paged_js += cc('ebooks.oeb.display.indexing') self.paged_js += cc('ebooks.oeb.display.paged') self.paged_js += cc('ebooks.oeb.display.mathjax') + self.hf_uuid = str(uuid.uuid4()).replace('-', '') self.view.page().mainFrame().addToJavaScriptWindowObject("py_bridge", self) self.view.page().longjs_counter = 0 @@ -313,7 +329,7 @@ class PDFWriter(QObject): self.bridge_value = self.footer evaljs('paged_display.footer_template = py_bridge.value') if self.header or self.footer: - evaljs('paged_display.create_header_footer();') + evaljs('paged_display.create_header_footer("%s");'%self.hf_uuid) start_page = self.current_page_num diff --git a/src/calibre/gui2/convert/pdf_output.py b/src/calibre/gui2/convert/pdf_output.py index 98334d1709..889a99a66a 100644 --- a/src/calibre/gui2/convert/pdf_output.py +++ b/src/calibre/gui2/convert/pdf_output.py @@ -22,7 +22,9 @@ class PluginWidget(Widget, Ui_Form): 'override_profile_size', 'paper_size', 'custom_size', 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit', 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', - 'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers']) + 'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers', + 'pdf_footer_template', 'pdf_header_template', + ]) self.db, self.book_id = db, book_id for x in get_option('paper_size').option.choices: diff --git a/src/calibre/gui2/convert/pdf_output.ui b/src/calibre/gui2/convert/pdf_output.ui index a4d184d6bc..a3cd131ba3 100644 --- a/src/calibre/gui2/convert/pdf_output.ui +++ b/src/calibre/gui2/convert/pdf_output.ui @@ -6,8 +6,8 @@ 0 0 - 590 - 395 + 638 + 498 @@ -84,6 +84,13 @@ + + + + Add page &numbers to the bottom of every page + + + @@ -170,24 +177,52 @@ - - - - Qt::Vertical - - - - 20 - 213 - - - - - - - - Add page &numbers to the bottom of every page + + + + Page headers and footers + + + + + 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>. + + + true + + + true + + + + + + + &Header template: + + + opt_pdf_header_template + + + + + + + + + + &Footer template: + + + opt_pdf_footer_template + + + + + + + From a688a8018c31cc5fa2ce83927717c0ab7a89804d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Apr 2013 15:16:50 +0530 Subject: [PATCH 03/17] PDF Output: Allow use of _SECTION_ in header/footer templates --- manual/conversion.rst | 10 ++++++ resources/compiled_coffeescript.zip | Bin 71057 -> 71177 bytes .../ebooks/conversion/plugins/pdf_output.py | 8 ++--- src/calibre/ebooks/oeb/display/paged.coffee | 5 +-- src/calibre/ebooks/pdf/render/from_html.py | 30 ++++++++++++++++++ 5 files changed, 47 insertions(+), 6 deletions(-) diff --git a/manual/conversion.rst b/manual/conversion.rst index 2e0876bb7c..14710f3f6a 100644 --- a/manual/conversion.rst +++ b/manual/conversion.rst @@ -794,6 +794,16 @@ template:: 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:: + +

_SECTION_

+ +_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. diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 19d15f6f13994754126630f38ad5d277026ec21b..d5b7086600a8ca46754773e8dfaf9f7defe61cad 100644 GIT binary patch delta 315 zcmbQZnx%6Mi%ftwGm8iV2=G)kIvq3-b#aekU;trhhUo#qjH1(Jgc&(DTIy=B7pEqd zWaj5>PSNONlv7ZEausY93M%81iZWBuQ}v)?8j~3>i%xb>W}R%Su0B~yt4&$2D77Fb zF*#L3KR(#iImFZ7FJ3=g2X2P8f|3$c5BKErTI+~XUvH_md6l;9b3Rm;PyZ^!7|GZ$ z-Cvl|n<=UQ;)#vIj22A#Rbb9E5k?ax?G}iHw+N#VYxFQqu8icF z?4Zm#*;ZYBa+FrvWESnSo4;yjJ?BH!IXzdHF_MWR3aoqjU13I3rbqc;&gAX_zUk&7 zj9N_FDaZ1 fXR$F(*Jo!`5DV~TWdk{%69^YDF)-W~Vg&L4Roy&J diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index e336336939..dc943eec30 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -104,11 +104,11 @@ class PDFOutput(OutputFormatPlugin): 'specify a footer template, it will take precedence ' 'over this option.')), OptionRecommendation(name='pdf_footer_template', recommended_value=None, - help=_('An HTML template used to generate footers on every page.' - ' The strings _PAGENUM_, _TITLE_ and _AUTHOR_ will be replaced by their current values.')), + help=_('An HTML template used to generate %s on every page.' + ' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('footers')), OptionRecommendation(name='pdf_header_template', recommended_value=None, - help=_('An HTML template used to generate headers on every page.' - ' The strings _PAGENUM_, _TITLE_ and _AUTHOR_ will be replaced by their current values.')), + help=_('An HTML template used to generate %s on every page.' + ' The strings _PAGENUM_, _TITLE_, _AUTHOR_ and _SECTION_ will be replaced by their current values.')%_('headers')), ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index bb66c20efb..f97f1b3cf8 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -216,10 +216,11 @@ class PagedDisplay this.hf_style.innerHTML = "#pdf_page_header_#{ this.hf_uuid } .#{ cls }, #pdf_page_footer_#{ this.hf_uuid } .#{ cls } { display: none }" title = py_bridge.title() author = py_bridge.author() + section = py_bridge.section() if this.header != null - this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"") + this.header.innerHTML = this.header_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_SECTION_/g, section+"") if this.footer != null - this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"") + this.footer.innerHTML = this.footer_template.replace(/_PAGENUM_/g, pagenum+"").replace(/_TITLE_/g, title+"").replace(/_AUTHOR_/g, author+"").replace(/_SECTION_/g, section+"") fit_images: () -> # Ensure no images are wider than the available width in a column. Note diff --git a/src/calibre/ebooks/pdf/render/from_html.py b/src/calibre/ebooks/pdf/render/from_html.py index e56375b7d0..ae1083bb56 100644 --- a/src/calibre/ebooks/pdf/render/from_html.py +++ b/src/calibre/ebooks/pdf/render/from_html.py @@ -138,6 +138,10 @@ class PDFWriter(QObject): def author(self): return self.doc_author + @pyqtSlot(result=unicode) + def section(self): + return self.current_section + def __init__(self, opts, log, cover_data=None, toc=None): from calibre.gui2 import is_ok_to_use_qt if not is_ok_to_use_qt(): @@ -162,6 +166,7 @@ class PDFWriter(QObject): self.view.page().mainFrame().setScrollBarPolicy(x, Qt.ScrollBarAlwaysOff) self.report_progress = lambda x, y: x + self.current_section = '' def dump(self, items, out_stream, pdf_metadata): opts = self.opts @@ -287,6 +292,25 @@ class PDFWriter(QObject): self.loop.processEvents(self.loop.ExcludeUserInputEvents) evaljs('document.getElementById("MathJax_Message").style.display="none";') + def get_sections(self, anchor_map): + sections = {} + ci = os.path.abspath(os.path.normcase(self.current_item)) + if self.toc is not None: + for toc in self.toc.flat(): + path = toc.abspath or None + frag = toc.fragment or None + if path is None: + continue + path = os.path.abspath(os.path.normcase(path)) + if path == ci: + col = 0 + if frag and frag in anchor_map: + col = anchor_map[frag]['column'] + if col not in sections: + sections[col] = toc.text or _('Untitled') + + return sections + def do_paged_render(self): if self.paged_js is None: import uuid @@ -321,6 +345,8 @@ class PDFWriter(QObject): amap = self.bridge_value if not isinstance(amap, dict): amap = {'links':[], 'anchors':{}} # Some javascript error occurred + sections = self.get_sections(amap['anchors']) + col = 0 if self.header: self.bridge_value = self.header @@ -335,6 +361,8 @@ class PDFWriter(QObject): mf = self.view.page().mainFrame() while True: + if col in sections: + self.current_section = sections[col] self.doc.init_page() if self.header or self.footer: evaljs('paged_display.update_header_footer(%d)'%self.current_page_num) @@ -348,8 +376,10 @@ class PDFWriter(QObject): evaljs('window.scrollTo(%d, 0); paged_display.position_header_footer();'%nsl[0]) if self.doc.errors_occurred: break + col += 1 if not self.doc.errors_occurred: self.doc.add_links(self.current_item, start_page, amap['links'], amap['anchors']) + From e39825d46ddba284692b7d98c23f3a059b7b297a Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 1 Apr 2013 08:34:30 -0400 Subject: [PATCH 04/17] Store: Update Amazon plugin to handle new grid layout. --- src/calibre/gui2/store/stores/amazon_plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/store/stores/amazon_plugin.py b/src/calibre/gui2/store/stores/amazon_plugin.py index 564fbda579..33f8f9b048 100644 --- a/src/calibre/gui2/store/stores/amazon_plugin.py +++ b/src/calibre/gui2/store/stores/amazon_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (unicode_literals, division, absolute_import, print_function) -store_version = 2 # Needed for dynamic plugin loading +store_version = 3 # Needed for dynamic plugin loading __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' @@ -129,12 +129,12 @@ class AmazonKindleStore(StorePlugin): doc = html.fromstring(f.read().decode('latin-1', 'replace')) data_xpath = '//div[contains(@class, "prod")]' - format_xpath = './/ul[contains(@class, "rsltL")]//span[contains(@class, "lrg") and not(contains(@class, "bld"))]/text()' + format_xpath = './/ul[contains(@class, "rsltL") or contains(@class, "rsltGridList")]//span[contains(@class, "lrg") and not(contains(@class, "bld"))]/text()' asin_xpath = '@name' cover_xpath = './/img[@class="productImage"]/@src' title_xpath = './/h3[@class="newaps"]/a//text()' author_xpath = './/h3[@class="newaps"]//span[contains(@class, "reg")]//text()' - price_xpath = './/ul[contains(@class, "rsltL")]//span[contains(@class, "lrg") and contains(@class, "bld")]/text()' + price_xpath = './/ul[contains(@class, "rsltL") or contains(@class, "rsltGridList")]//span[contains(@class, "lrg") and contains(@class, "bld")]/text()' for data in doc.xpath(data_xpath): if counter <= 0: From 1663619ceffc4b56cff57ed82835aee3f363c53f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Apr 2013 21:04:52 +0530 Subject: [PATCH 05/17] ... --- resources/default_tweaks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index ff1a53de96..9851d76af4 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -79,7 +79,7 @@ author_name_copywords = ('Corporation', 'Company', 'Co.', 'Agency', 'Council', # By default, calibre splits a string containing multiple author names on # ampersands and the words "and" and "with". You can customize the splitting # by changing the regular expression below. Strings are split on whatever the -# specified regular expression matches. +# specified regular expression matches, in addition to ampersands. # Default: r'(?i),?\s+(and|with)\s+' authors_split_regex = r'(?i),?\s+(and|with)\s+' From 9ba0272b0c1768980c0d5cff8275dbb83d0bcf5b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Apr 2013 22:54:09 +0530 Subject: [PATCH 06/17] Metadata download: Add a plugin to download book covers from a google image search. Go to Preferences->Metadata download and enable the plugin to use it. Google Image search often finds larger and/or different covers from the other sources, however, it sometimes finds junk. Use at your discretion. --- src/calibre/customize/builtins.py | 3 +- src/calibre/customize/ui.py | 2 +- src/calibre/ebooks/metadata/sources/amazon.py | 2 +- src/calibre/ebooks/metadata/sources/base.py | 10 +- src/calibre/ebooks/metadata/sources/covers.py | 11 +- src/calibre/ebooks/metadata/sources/douban.py | 2 +- .../ebooks/metadata/sources/edelweiss.py | 2 +- src/calibre/ebooks/metadata/sources/google.py | 2 +- .../ebooks/metadata/sources/google_images.py | 148 ++++++++++++++++++ .../ebooks/metadata/sources/openlibrary.py | 2 +- .../ebooks/metadata/sources/overdrive.py | 2 +- src/calibre/ebooks/metadata/sources/ozon.py | 12 +- src/calibre/ebooks/metadata/sources/worker.py | 8 +- src/calibre/gui2/metadata/single_download.py | 117 +++++++++----- 14 files changed, 263 insertions(+), 60 deletions(-) create mode 100644 src/calibre/ebooks/metadata/sources/google_images.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index e157c36c5e..c87c8c63d0 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -757,8 +757,9 @@ from calibre.ebooks.metadata.sources.isbndb import ISBNDB from calibre.ebooks.metadata.sources.overdrive import OverDrive from calibre.ebooks.metadata.sources.douban import Douban from calibre.ebooks.metadata.sources.ozon import Ozon +from calibre.ebooks.metadata.sources.google_images import GoogleImages -plugins += [GoogleBooks, Amazon, Edelweiss, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon] +plugins += [GoogleBooks, Amazon, Edelweiss, GoogleImages, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon] # }}} diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 849d1a21f4..06fd2784e4 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -91,7 +91,7 @@ def restore_plugin_state_to_default(plugin_or_name): config['enabled_plugins'] = ep default_disabled_plugins = set([ - 'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', + 'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss', 'Google Images', ]) def is_disabled(plugin): diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index a8e15a6d94..3fefe2d886 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -858,7 +858,7 @@ class Amazon(Source): # }}} def download_cover(self, log, result_queue, abort, # {{{ - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: log.info('No cached cover found, running identify') diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index e15d11c3c1..41812af8eb 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -31,7 +31,7 @@ msprefs.defaults['find_first_edition_date'] = False # Google covers are often poor quality (scans/errors) but they have high # resolution, so they trump covers from better sources. So make sure they # are only used if no other covers are found. -msprefs.defaults['cover_priorities'] = {'Google':2} +msprefs.defaults['cover_priorities'] = {'Google':2, 'Google Images':2} def create_log(ostream=None): from calibre.utils.logging import ThreadSafeLog, FileStream @@ -222,6 +222,9 @@ class Source(Plugin): #: plugin config_help_message = None + #: If True this source can return multiple covers for a given query + can_get_multiple_covers = False + def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) @@ -522,7 +525,7 @@ class Source(Plugin): return None def download_cover(self, log, result_queue, abort, - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): ''' Download a cover and put it into result_queue. The parameters all have the same meaning as for :meth:`identify`. Put (self, cover_data) into @@ -531,6 +534,9 @@ class Source(Plugin): This method should use cached cover URLs for efficiency whenever possible. When cached data is not present, most plugins simply call identify and use its results. + + If the parameter get_best_cover is True and this plugin can get + multiple covers, it should only get the "best" one. ''' pass diff --git a/src/calibre/ebooks/metadata/sources/covers.py b/src/calibre/ebooks/metadata/sources/covers.py index d28ce146c6..0fe963e3f7 100644 --- a/src/calibre/ebooks/metadata/sources/covers.py +++ b/src/calibre/ebooks/metadata/sources/covers.py @@ -35,9 +35,14 @@ class Worker(Thread): start_time = time.time() if not self.abort.is_set(): try: - self.plugin.download_cover(self.log, self.rq, self.abort, - title=self.title, authors=self.authors, - identifiers=self.identifiers, timeout=self.timeout) + if self.plugin.can_get_multiple_covers: + self.plugin.download_cover(self.log, self.rq, self.abort, + title=self.title, authors=self.authors, get_best_cover=True, + identifiers=self.identifiers, timeout=self.timeout) + else: + self.plugin.download_cover(self.log, self.rq, self.abort, + title=self.title, authors=self.authors, + identifiers=self.identifiers, timeout=self.timeout) except: self.log.exception('Failed to download cover from', self.plugin.name) diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py index 6857d62d4d..f955fb8a79 100644 --- a/src/calibre/ebooks/metadata/sources/douban.py +++ b/src/calibre/ebooks/metadata/sources/douban.py @@ -221,7 +221,7 @@ class Douban(Source): # }}} def download_cover(self, log, result_queue, abort, # {{{ - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: log.info('No cached cover found, running identify') diff --git a/src/calibre/ebooks/metadata/sources/edelweiss.py b/src/calibre/ebooks/metadata/sources/edelweiss.py index c86f16ff0d..53ae6c6ee3 100644 --- a/src/calibre/ebooks/metadata/sources/edelweiss.py +++ b/src/calibre/ebooks/metadata/sources/edelweiss.py @@ -320,7 +320,7 @@ class Edelweiss(Source): # }}} def download_cover(self, log, result_queue, abort, # {{{ - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: log.info('No cached cover found, running identify') diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index 3962afcb5e..c03f20cd6b 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -209,7 +209,7 @@ class GoogleBooks(Source): # }}} def download_cover(self, log, result_queue, abort, # {{{ - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: log.info('No cached cover found, running identify') diff --git a/src/calibre/ebooks/metadata/sources/google_images.py b/src/calibre/ebooks/metadata/sources/google_images.py new file mode 100644 index 0000000000..c755fea192 --- /dev/null +++ b/src/calibre/ebooks/metadata/sources/google_images.py @@ -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 ' +__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() + diff --git a/src/calibre/ebooks/metadata/sources/openlibrary.py b/src/calibre/ebooks/metadata/sources/openlibrary.py index 4645d2a18a..b0eeb940a5 100644 --- a/src/calibre/ebooks/metadata/sources/openlibrary.py +++ b/src/calibre/ebooks/metadata/sources/openlibrary.py @@ -19,7 +19,7 @@ class OpenLibrary(Source): OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' def download_cover(self, log, result_queue, abort, - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): if 'isbn' not in identifiers: return isbn = identifiers['isbn'] diff --git a/src/calibre/ebooks/metadata/sources/overdrive.py b/src/calibre/ebooks/metadata/sources/overdrive.py index 6d6ebd3990..b232c7c9a4 100755 --- a/src/calibre/ebooks/metadata/sources/overdrive.py +++ b/src/calibre/ebooks/metadata/sources/overdrive.py @@ -75,7 +75,7 @@ class OverDrive(Source): # }}} def download_cover(self, log, result_queue, abort, # {{{ - title=None, authors=None, identifiers={}, timeout=30): + title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): import mechanize cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: diff --git a/src/calibre/ebooks/metadata/sources/ozon.py b/src/calibre/ebooks/metadata/sources/ozon.py index ebb104818f..0f4b0c2c53 100644 --- a/src/calibre/ebooks/metadata/sources/ozon.py +++ b/src/calibre/ebooks/metadata/sources/ozon.py @@ -55,7 +55,7 @@ class Ozon(Source): # for ozon.ru search we have to format ISBN with '-' isbn = _format_isbn(log, identifiers.get('isbn', None)) ozonid = identifiers.get('ozon', None) - + unk = unicode(_('Unknown')).upper() if (title and title != unk) or (authors and authors != [unk]) or isbn or not ozonid: qItems = set([isbn, title]) @@ -64,19 +64,19 @@ class Ozon(Source): qItems.discard(None) qItems.discard('') qItems = map(_quoteString, qItems) - + q = u' '.join(qItems).strip() log.info(u'search string: ' + q) - + if isinstance(q, unicode): q = q.encode('utf-8') if not q: return None - + search_url += quote_plus(q) else: search_url = self.ozon_url + '/webservices/OzonWebSvc.asmx/ItemDetail?ID=%s' % ozonid - + log.debug(u'search url: %r'%search_url) return search_url # }}} @@ -250,7 +250,7 @@ class Ozon(Source): return url # }}} - def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30): # {{{ + def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): # {{{ cached_url = self.get_cached_cover_url(identifiers) if cached_url is None: log.debug('No cached cover found, running identify') diff --git a/src/calibre/ebooks/metadata/sources/worker.py b/src/calibre/ebooks/metadata/sources/worker.py index 48f0f99584..51fb883e7d 100644 --- a/src/calibre/ebooks/metadata/sources/worker.py +++ b/src/calibre/ebooks/metadata/sources/worker.py @@ -11,6 +11,7 @@ import os from threading import Event, Thread from Queue import Queue, Empty from io import BytesIO +from collections import Counter from calibre.utils.date import as_utc from calibre.ebooks.metadata.sources.identify import identify, msprefs @@ -113,13 +114,18 @@ def single_covers(title, authors, identifiers, caches, tdir): kwargs=dict(title=title, authors=authors, identifiers=identifiers)) worker.daemon = True worker.start() + c = Counter() while worker.is_alive(): try: plugin, width, height, fmt, data = results.get(True, 1) except Empty: continue else: - name = '%s,,%s,,%s,,%s.cover'%(plugin.name, width, height, fmt) + name = plugin.name + if plugin.can_get_multiple_covers: + name += '{%d}'%c[plugin.name] + c[plugin.name] += 1 + name = '%s,,%s,,%s,,%s.cover'%(name, width, height, fmt) with open(name, 'wb') as f: f.write(data) os.mkdir(name+'.done') diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index e4a78b674a..ffa83b6ea8 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -16,13 +16,12 @@ from operator import attrgetter from Queue import Queue, Empty from io import BytesIO -from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, - QApplication, QDialog, QVBoxLayout, QLabel, - QDialogButtonBox, QStyle, QStackedWidget, QWidget, - QTableView, QGridLayout, QFontInfo, QPalette, QTimer, - pyqtSignal, QAbstractTableModel, QVariant, QSize, - QListView, QPixmap, QAbstractListModel, QColor, QRect, - QTextBrowser, QStringListModel) +from PyQt4.Qt import ( + QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt, QApplication, + QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget, + QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal, + QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex, + QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins @@ -654,7 +653,7 @@ class CoversModel(QAbstractListModel): # {{{ for i, plugin in enumerate(metadata_plugins(['cover'])): self.covers.append((plugin.name+'\n'+_('Searching...'), QVariant(self.blank), None, True)) - self.plugin_map[plugin] = i+1 + self.plugin_map[plugin] = [i+1] if do_reset: self.reset() @@ -685,48 +684,82 @@ class CoversModel(QAbstractListModel): # {{{ def plugin_for_index(self, index): row = index.row() if hasattr(index, 'row') else index for k, v in self.plugin_map.iteritems(): - if v == row: + if row in v: return k - def cover_keygen(self, x): - pmap = x[2] - if pmap is None: - return 1 - return pmap.width()*pmap.height() - def clear_failed(self): + # Remove entries that are still waiting good = [] pmap = {} - dcovers = sorted(self.covers[1:], key=self.cover_keygen, reverse=True) - cmap = {x:self.covers.index(x) for x in self.covers} + def keygen(x): + pmap = x[2] + if pmap is None: + return 1 + return pmap.width()*pmap.height() + dcovers = sorted(self.covers[1:], key=keygen, reverse=True) + cmap = {i:self.plugin_for_index(i) for i in xrange(len(self.covers))} for i, x in enumerate(self.covers[0:1] + dcovers): if not x[-1]: good.append(x) - if i > 0: - plugin = self.plugin_for_index(cmap[x]) - pmap[plugin] = len(good) - 1 + plugin = cmap[i] + if plugin is not None: + try: + pmap[plugin].append(len(good) - 1) + except KeyError: + pmap[plugin] = [len(good)-1] self.covers = good self.plugin_map = pmap self.reset() - def index_for_plugin(self, plugin): - idx = self.plugin_map.get(plugin, 0) - return self.index(idx) + def pointer_from_index(self, index): + row = index.row() if hasattr(index, 'row') else index + try: + return self.covers[row][2] + except IndexError: + pass + + def index_from_pointer(self, pointer): + for r, (text, scaled, pmap, waiting) in enumerate(self.covers): + if pointer == pmap: + return self.index(r) + return self.index(0) def update_result(self, plugin_name, width, height, data): - idx = None - for plugin, i in self.plugin_map.iteritems(): - if plugin.name == plugin_name: - idx = i - break - if idx is None: - return - pmap = QPixmap() - pmap.loadFromData(data) - if pmap.isNull(): - return - self.covers[idx] = self.get_item(plugin_name, pmap, waiting=False) - self.dataChanged.emit(self.index(idx), self.index(idx)) + if plugin_name.endswith('}'): + # multi cover plugin + plugin_name = plugin_name.partition('{')[0] + plugin = [plugin for plugin in self.plugin_map if plugin.name == plugin_name] + if not plugin: + return + plugin = plugin[0] + last_row = max(self.plugin_map[plugin]) + pmap = QPixmap() + pmap.loadFromData(data) + if pmap.isNull(): + return + self.beginInsertRows(QModelIndex(), last_row, last_row) + for rows in self.plugin_map.itervalues(): + for i in xrange(len(rows)): + if rows[i] >= last_row: + rows[i] += 1 + self.plugin_map[plugin].insert(-1, last_row) + self.covers.insert(last_row, self.get_item(plugin_name, pmap, waiting=False)) + self.endInsertRows() + else: + # single cover plugin + idx = None + for plugin, rows in self.plugin_map.iteritems(): + if plugin.name == plugin_name: + idx = rows[0] + break + if idx is None: + return + pmap = QPixmap() + pmap.loadFromData(data) + if pmap.isNull(): + return + self.covers[idx] = self.get_item(plugin_name, pmap, waiting=False) + self.dataChanged.emit(self.index(idx), self.index(idx)) def cover_pixmap(self, index): row = index.row() @@ -774,9 +807,12 @@ class CoversView(QListView): # {{{ self.m.reset_covers() def clear_failed(self): - plugin = self.m.plugin_for_index(self.currentIndex()) + pointer = self.m.pointer_from_index(self.currentIndex()) self.m.clear_failed() - self.select(self.m.index_for_plugin(plugin).row()) + if pointer is None: + self.select(0) + else: + self.select(self.m.index_from_pointer(pointer).row()) # }}} @@ -852,10 +888,11 @@ class CoversWidget(QWidget): # {{{ if num < 2: txt = _('Could not find any covers for %s')%self.book.title else: - txt = _('Found %(num)d covers of %(title)s. ' - 'Pick the one you like best.')%dict(num=num-1, + txt = _('Found %(num)d possible covers for %(title)s. ' + 'When the download completes, the covers will be sorted by size.')%dict(num=num-1, title=self.title) self.msg.setText(txt) + self.msg.setWordWrap(True) self.finished.emit() From 78cc12fe0e59745b89e7a3974133ca596ae39285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Tue, 2 Apr 2013 00:16:33 +0200 Subject: [PATCH 07/17] minor fixes --- recipes/adventure_zone_pl.recipe | 1 - recipes/km_blog.recipe | 1 - recipes/sport_pl.recipe | 2 +- recipes/wirtualnemedia_pl.recipe | 2 +- recipes/wprost.recipe | 13 ++++------ recipes/wprost_rss.recipe | 42 ++++++++++++++++---------------- 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/recipes/adventure_zone_pl.recipe b/recipes/adventure_zone_pl.recipe index 00b4a8753e..50a980dc92 100644 --- a/recipes/adventure_zone_pl.recipe +++ b/recipes/adventure_zone_pl.recipe @@ -66,4 +66,3 @@ class Adventure_zone(BasicNewsRecipe): if a.has_key('href') and 'http://' not in a['href'] and 'https://' not in a['href']: a['href']=self.index + a['href'] return soup - diff --git a/recipes/km_blog.recipe b/recipes/km_blog.recipe index 614dbc03e5..8910ee060a 100644 --- a/recipes/km_blog.recipe +++ b/recipes/km_blog.recipe @@ -20,7 +20,6 @@ class km_blog(BasicNewsRecipe): remove_javascript=True no_stylesheets=True remove_empty_feeds = True - feeds = [(u'blog', u'http://korwin-mikke.pl/blog/rss')] keep_only_tags =[] diff --git a/recipes/sport_pl.recipe b/recipes/sport_pl.recipe index 622a3675bd..4095817a6b 100644 --- a/recipes/sport_pl.recipe +++ b/recipes/sport_pl.recipe @@ -21,7 +21,7 @@ class sport_pl(BasicNewsRecipe): remove_javascript=True no_stylesheets=True remove_empty_feeds = True - + ignore_duplicate_articles = {'title', 'url'} keep_only_tags =[] keep_only_tags.append(dict(name = 'div', attrs = {'id' : 'article'})) diff --git a/recipes/wirtualnemedia_pl.recipe b/recipes/wirtualnemedia_pl.recipe index 28278c2e24..ed3b3787f8 100644 --- a/recipes/wirtualnemedia_pl.recipe +++ b/recipes/wirtualnemedia_pl.recipe @@ -1,7 +1,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class WirtualneMedia(BasicNewsRecipe): - title = u'wirtualnemedia.pl' + title = u'Wirtualnemedia.pl' oldest_article = 7 max_articles_per_feed = 100 no_stylesheets = True diff --git a/recipes/wprost.recipe b/recipes/wprost.recipe index 90dde251ca..d923f64a3f 100644 --- a/recipes/wprost.recipe +++ b/recipes/wprost.recipe @@ -1,10 +1,9 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2010, matek09, matek09@gmail.com' -__copyright__ = 'Modified 2011, Mariusz Wolek ' -__copyright__ = 'Modified 2012, Artur Stachecki ' - +__copyright__ = '''2010, matek09, matek09@gmail.com + Modified 2011, Mariusz Wolek + Modified 2012, Artur Stachecki ''' from calibre.web.feeds.news import BasicNewsRecipe import re @@ -16,12 +15,12 @@ class Wprost(BasicNewsRecipe): ICO_BLOCKED = 'http://www.wprost.pl/G/layout2/ico_blocked.png' title = u'Wprost' __author__ = 'matek09' - description = 'Weekly magazine' + description = u'Popularny tygodnik ogólnopolski - Wprost. Najlepszy wśród polskich tygodników - opiniotwórczy - społeczno-informacyjny - społeczno-kulturalny.' encoding = 'ISO-8859-2' no_stylesheets = True language = 'pl' remove_javascript = True - recursions = 0 + recursions = 0 remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'})) remove_tags_after = dict(dict(name = 'div', attrs = {'id' : 'print-layer'})) ''' @@ -94,5 +93,3 @@ class Wprost(BasicNewsRecipe): 'description' : '' }) return articles - - diff --git a/recipes/wprost_rss.recipe b/recipes/wprost_rss.recipe index bffbacc474..59c130fc75 100644 --- a/recipes/wprost_rss.recipe +++ b/recipes/wprost_rss.recipe @@ -1,10 +1,9 @@ -#!/usr/bin/env python - -__license__ = 'GPL v3' -__copyright__ = '2010, matek09, matek09@gmail.com' -__copyright__ = 'Modified 2011, Mariusz Wolek ' -__copyright__ = 'Modified 2012, Artur Stachecki ' +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '''2010, matek09, matek09@gmail.com + Modified 2011, Mariusz Wolek + Modified 2012, Artur Stachecki ''' from calibre.web.feeds.news import BasicNewsRecipe import re @@ -12,13 +11,14 @@ import re class Wprost(BasicNewsRecipe): title = u'Wprost (RSS)' __author__ = 'matek09' - description = 'Weekly magazine' + description = u'Portal informacyjny. Najświeższe wiadomości, najciekawsze komentarze i opinie. Blogi najlepszych publicystów.' encoding = 'ISO-8859-2' no_stylesheets = True language = 'pl' remove_javascript = True recursions = 0 use_embedded_content = False + ignore_duplicate_articles = {'title', 'url'} remove_empty_feeds = True remove_tags_before = dict(dict(name = 'div', attrs = {'id' : 'print-layer'})) remove_tags_after = dict(dict(name = 'div', attrs = {'id' : 'print-layer'})) @@ -48,20 +48,20 @@ class Wprost(BasicNewsRecipe): #h2 {font-size: x-large; font-weight: bold} feeds = [(u'Tylko u nas', u'http://www.wprost.pl/rss/rss_wprostextra.php'), - (u'Wydarzenia', u'http://www.wprost.pl/rss/rss.php'), - (u'Komentarze', u'http://www.wprost.pl/rss/rss_komentarze.php'), - (u'Wydarzenia: Kraj', u'http://www.wprost.pl/rss/rss_kraj.php'), - (u'Komentarze: Kraj', u'http://www.wprost.pl/rss/rss_komentarze_kraj.php'), - (u'Wydarzenia: Świat', u'http://www.wprost.pl/rss/rss_swiat.php'), - (u'Komentarze: Świat', u'http://www.wprost.pl/rss/rss_komentarze_swiat.php'), - (u'Wydarzenia: Gospodarka', u'http://www.wprost.pl/rss/rss_gospodarka.php'), - (u'Komentarze: Gospodarka', u'http://www.wprost.pl/rss/rss_komentarze_gospodarka.php'), - (u'Wydarzenia: Życie', u'http://www.wprost.pl/rss/rss_zycie.php'), - (u'Komentarze: Życie', u'http://www.wprost.pl/rss/rss_komentarze_zycie.php'), - (u'Wydarzenia: Sport', u'http://www.wprost.pl/rss/rss_sport.php'), - (u'Komentarze: Sport', u'http://www.wprost.pl/rss/rss_komentarze_sport.php'), - (u'Przegląd prasy', u'http://www.wprost.pl/rss/rss_prasa.php') - ] + (u'Wydarzenia', u'http://www.wprost.pl/rss/rss.php'), + (u'Komentarze', u'http://www.wprost.pl/rss/rss_komentarze.php'), + (u'Wydarzenia: Kraj', u'http://www.wprost.pl/rss/rss_kraj.php'), + (u'Komentarze: Kraj', u'http://www.wprost.pl/rss/rss_komentarze_kraj.php'), + (u'Wydarzenia: Świat', u'http://www.wprost.pl/rss/rss_swiat.php'), + (u'Komentarze: Świat', u'http://www.wprost.pl/rss/rss_komentarze_swiat.php'), + (u'Wydarzenia: Gospodarka', u'http://www.wprost.pl/rss/rss_gospodarka.php'), + (u'Komentarze: Gospodarka', u'http://www.wprost.pl/rss/rss_komentarze_gospodarka.php'), + (u'Wydarzenia: Życie', u'http://www.wprost.pl/rss/rss_zycie.php'), + (u'Komentarze: Życie', u'http://www.wprost.pl/rss/rss_komentarze_zycie.php'), + (u'Wydarzenia: Sport', u'http://www.wprost.pl/rss/rss_sport.php'), + (u'Komentarze: Sport', u'http://www.wprost.pl/rss/rss_komentarze_sport.php'), + (u'Przegląd prasy', u'http://www.wprost.pl/rss/rss_prasa.php') + ] def get_cover_url(self): soup = self.index_to_soup('http://www.wprost.pl/tygodnik') From a5c3567aaee1313700ceb57683da2c9da2b2b071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Tue, 2 Apr 2013 00:17:14 +0200 Subject: [PATCH 08/17] icon for gazeta prawna --- recipes/icons/gazeta-prawna-calibre-v1.png | Bin 0 -> 612 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 recipes/icons/gazeta-prawna-calibre-v1.png diff --git a/recipes/icons/gazeta-prawna-calibre-v1.png b/recipes/icons/gazeta-prawna-calibre-v1.png new file mode 100644 index 0000000000000000000000000000000000000000..e5c7ae965c05881d570b48288144382bfc846b69 GIT binary patch literal 612 zcmV-q0-ODbP)j(3cVl3Ow$a`%zezc7$sZK7J8vO)|gXu&3&uOGp$%?0KSpo}^6 z1T+Z%|K8064s{Ujy~fPQRnUX;x8OoWXc18foIZ+dTY!Ra9RRf5T~Hq-dA1v4XdGfu zWMma6p|p2^c?-fJ+>A}W_X5qSBH8{v+)@dEO$Xk0CiIY z{$!zGf#Z7Z6m6(BM~e1LjF3Rc~P?j|kZKj~zs5Sp%+v_3%E$&p%PtuL8@4x;o6!_n Date: Tue, 2 Apr 2013 00:23:56 +0200 Subject: [PATCH 09/17] new recipes from kalibrator project --- recipes/dzial_zagraniczny.recipe | 29 +++++++++ recipes/equipped.recipe | 28 +++++++++ recipes/icons/dzial_zagraniczny.png | Bin 0 -> 491 bytes recipes/icons/equipped.png | Bin 0 -> 929 bytes recipes/icons/ittechblog.png | Bin 0 -> 731 bytes recipes/icons/magazyn_consido.png | Bin 0 -> 982 bytes recipes/icons/media2.png | Bin 0 -> 660 bytes recipes/icons/mobilna.png | Bin 0 -> 885 bytes recipes/icons/mojegotowanie.png | Bin 0 -> 307 bytes recipes/icons/najwyzszy_czas.png | Bin 0 -> 616 bytes recipes/icons/nowiny_rybnik.png | Bin 0 -> 1179 bytes recipes/icons/osw.png | Bin 0 -> 489 bytes recipes/icons/ppe_pl.png | Bin 0 -> 3203 bytes recipes/icons/presseurop.png | Bin 0 -> 207 bytes recipes/icons/res_publica.png | Bin 0 -> 733 bytes recipes/icons/wolne_media.png | Bin 0 -> 497 bytes recipes/ittechblog.recipe | 27 +++++++++ recipes/magazyn_consido.recipe | 88 ++++++++++++++++++++++++++++ recipes/media2.recipe | 37 ++++++++++++ recipes/mobilna.recipe | 27 +++++++++ recipes/mojegotowanie.recipe | 51 ++++++++++++++++ recipes/najwyzszy_czas.recipe | 28 +++++++++ recipes/nowiny_rybnik.recipe | 33 +++++++++++ recipes/osw.recipe | 42 +++++++++++++ recipes/ppe_pl.recipe | 41 +++++++++++++ recipes/presseurop.recipe | 32 ++++++++++ recipes/res_publica.recipe | 34 +++++++++++ recipes/wolne_media.recipe | 27 +++++++++ 28 files changed, 524 insertions(+) create mode 100644 recipes/dzial_zagraniczny.recipe create mode 100644 recipes/equipped.recipe create mode 100644 recipes/icons/dzial_zagraniczny.png create mode 100644 recipes/icons/equipped.png create mode 100644 recipes/icons/ittechblog.png create mode 100644 recipes/icons/magazyn_consido.png create mode 100644 recipes/icons/media2.png create mode 100644 recipes/icons/mobilna.png create mode 100644 recipes/icons/mojegotowanie.png create mode 100644 recipes/icons/najwyzszy_czas.png create mode 100644 recipes/icons/nowiny_rybnik.png create mode 100644 recipes/icons/osw.png create mode 100644 recipes/icons/ppe_pl.png create mode 100644 recipes/icons/presseurop.png create mode 100644 recipes/icons/res_publica.png create mode 100644 recipes/icons/wolne_media.png create mode 100644 recipes/ittechblog.recipe create mode 100644 recipes/magazyn_consido.recipe create mode 100644 recipes/media2.recipe create mode 100644 recipes/mobilna.recipe create mode 100644 recipes/mojegotowanie.recipe create mode 100644 recipes/najwyzszy_czas.recipe create mode 100644 recipes/nowiny_rybnik.recipe create mode 100644 recipes/osw.recipe create mode 100644 recipes/ppe_pl.recipe create mode 100644 recipes/presseurop.recipe create mode 100644 recipes/res_publica.recipe create mode 100644 recipes/wolne_media.recipe diff --git a/recipes/dzial_zagraniczny.recipe b/recipes/dzial_zagraniczny.recipe new file mode 100644 index 0000000000..9709186d7e --- /dev/null +++ b/recipes/dzial_zagraniczny.recipe @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__author__ = 'teepel ' + +''' +dzialzagraniczny.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class dzial_zagraniczny(BasicNewsRecipe): + title = u'Dział Zagraniczny' + __author__ = 'teepel ' + 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 + simultaneous_downloads = 5 + remove_javascript = True + no_stylesheets = True + use_embedded_content = True + + feeds = [(u'Dział zagraniczny', u'http://feeds.feedburner.com/dyndns/UOfz')] \ No newline at end of file diff --git a/recipes/equipped.recipe b/recipes/equipped.recipe new file mode 100644 index 0000000000..274315c849 --- /dev/null +++ b/recipes/equipped.recipe @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__author__ = 'teepel , Artur Stachecki ' + +''' +equipped.pl +''' + +class equipped(AutomaticNewsRecipe): + title = u'Equipped' + __author__ = 'teepel ' + 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')] diff --git a/recipes/icons/dzial_zagraniczny.png b/recipes/icons/dzial_zagraniczny.png new file mode 100644 index 0000000000000000000000000000000000000000..1982db04626c0b9b7ca3e68271e5a57cc78eddfe GIT binary patch literal 491 zcmV}+am zBqAit%FL9OlpGoyR9aL-LPQJ-419Wgyt=$RIy9V~ob2-SBq}7#($qvsL~L?&6cQBN z+T5(FtTZw-R8myr=Hz5zWE>(K#LCP(LOhI)j06G%uiq9j00001bW%=J06^y0W&i*H z*-1n}R2Y>tk6m+uFc3x4H5B=11iO?35l5kojn$478X2|w|Nm8kjQ8ozdDy*&k&B6R zpd&d{Vnmnc%nG#FL?dX|zvzEvr?Z9j4lobO*WP6s4ulb4*?s3qttOQPNz4EoHXjWt zC7>uYu~gu7xyP`s!;Gs^vULCSer@<_=hfl10>fJTnI8Qtj^md#uTZ)h#j;bGxAW#l zdjrX2AZGocs!!fqcM*YmD}m3GnHT?UdJSfn-Xi}>7RjQ|^?jHlV3D71&To`+w%Ygo hCN=Z`MADeC&_AA+BlfHJf+qj~002ovPDHLkV1f&8*r5Ob literal 0 HcmV?d00001 diff --git a/recipes/icons/equipped.png b/recipes/icons/equipped.png new file mode 100644 index 0000000000000000000000000000000000000000..a532b6f6ac06623911045c44876e41b21628d96e GIT binary patch literal 929 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5)Mp*w z6XN>s-@pI=|KGfEbH{cN*s*Q8X=rHV z=jR_ea^%~$Z_!avj0_C+wswi}3GuOU@o{kp@d+O8?pBtT9UUE4uU`H2>sL%vG&2*^ z^5x5)J$rWV-aTeUriS|ZqeqS$I(YEVp+gfUOmKB^t*Wd9+V|+uqaQzhaC35Tb8)e= zu>+mW&c>#trBzo~_weCECI&_`Q?uZppuoUDAT~EQXJKX*7ZdaI^_?+e#*G^{e*XNq zWXY00fBqale!Q!z>&)rX(b3UNOiT+GF8uoS>w^am?%uuo_wQd#4GnucJ3CujAfc3`b2nh=b3W^8|3kwOWDyziB#BSQS>EnkFFP^`+eEISf zAii|z{Q2|OuU-4}>C@K_rAa`CGA4PuyDe`W}n%Z&llh&%NS5i_^O|znIS?#KFU=)9Jg>$tGSA)sNZAj47;OH!?pi&B9UgOP!uk*=YkuAzB|p^25Tg_WV1 zwt=CQfkDi+hnrC}}D}`9;MAiOE1)bV@_# x10@yUlEsO+1vx-R>!u|pm*f{Q7*!bQ7*-g9u;;I3TYzdAJYD@<);T3K0RWQ-R$Bl7 literal 0 HcmV?d00001 diff --git a/recipes/icons/ittechblog.png b/recipes/icons/ittechblog.png new file mode 100644 index 0000000000000000000000000000000000000000..825e0255109370f9e9e555c29f58145411096f57 GIT binary patch literal 731 zcmV<10wn#3P)E4R$CTGm?Ys@0Y`yKv}@ zxh}fRTzSz4>tm#;F>MHbfRP3W7|xU+5Z*2fheTV?-T$2D{C3Xy?+^^bAX55ShoWC& zY}Og=HFnpk5CAgh6ibPWuW6{2_Q*u(x=23}26()Fc6*<;Yw-PSBLR?ztaBGEAjzf23}#n(tX(+WYdItlH6ESDwsF#uY}1yd zVgQil(jWCEM_DWYfDoBUr&uB*G7&vuURW;j2ZHPk0C&t_c9q7e&g3RrNTrOgi44l~ zctZxWQwac69O)DAQcd&r6-kLa>qU8yU@+3FEX>l7$M(t8079F9{PbbtoU04~7=|JJ zfgtOyYJYvl)dL6bU8y_FN~G2RfFKa6ua)#r(p-A{XmvsFz?@6)OQ zAQ)m2&fR)Cl%K;HH<(?ef%PEe{k6^}05sG{yT#dDJp}-&#)_7&iz{NEFTiF(4Ee>n zED3jHT5uPe7Tm?*HvwRmjIU`uU9JKmj>+o_Bk1U6Xj@i%4}zE0MIYg-x=o8>q@6RR0wFkW?`m$r>mX z3N3y3rw;;ceSxGu3fgYzuL^BMQ1BJoTC>TY)Iw>Iw5~}sZnF2@Gt)Vt_j2x;Gc)JA zGv7BScJ0X3N0U(isLyZjF36rRE28B4>1S_llMM$7xlSnk+&CqNCH>p?4gkcKnT2rb z%qnq)i}{zb;mJs>F15ZtSNc|5nPPTNvFk`*-~Piz=qhA0yLT2|aC_g_aDq=?gF@Q}9LPWlW$mp}j>5dH_zHO~* zVH;7isyv)LF;pEcH9+7g$pN^lR*z%z7&hN_n*KWTH8X3o0?h%FJm&OpNNcJQKbqGIeoZ_&VBdf6-A}*3vNAN;0EEDGue=<8aVs%l z4aSciuU+`wt32RHw7xUlGW5P&_@i$yhN2B)85?U{TXTD4Y1=cP)RlZ;`NdI#A~39k zil8O=cws?ZkHNAbkc=eB_6e}A|Xzj#xl5(4J>Bs-rpfaLiQOg1|@7J z)^8Byv;}jwekH{=NT-HDDL_kHUX~Mj3Jk3>AuMPnD#>WGgmIB%V*tuc`5ju4BG>?; zlvSPlebM4bdrD{#TYwjlNlG%X!assq3<)TNXJ;yRCd9L{r%R{XJs?H4giS6guWre_ zKp<(UUwV)tW8hRiIY~;1NZWoaIj`-lHiTwjVa~GsYhN$xh@-F( zw2V#C-~Yl2wV-RD?0B5QVGO)rdNw4DO7BKV^K3l-`qWnwLvzN?XLod$I`_T%A7%BG AtN;K2 literal 0 HcmV?d00001 diff --git a/recipes/icons/media2.png b/recipes/icons/media2.png new file mode 100644 index 0000000000000000000000000000000000000000..8e98c4df4eac0e7d071056862077e3a26a157654 GIT binary patch literal 660 zcmV;F0&D$=P)|#TY}iO{ zI9SPIPXW_SBkvaKEH@*EcMWw;@6K9Y{}8MQV^z($@w8JDz&v{Yi|5~Sezy8p$~2cJ z20MUzvn`%{4Gi~dQW0J(dXA1JY_BA30ReO8qh?com4R}?Xjz#064;wrwv`iRYM#MV zp(K)fL5na{=(ZL`Fk31mgoubo1l}$QGmDNifnuT|5i%=$TaO$cHIxgg+rL)mmbF;x zvCiYXr%we2j9`@DgJ&)C%&%AuY}1?_ALx)D&i)1jllhJA3ZMjn2&;Ldo(X5hOPtZ9M>|Bx|I&{T_>rVk`4;Z@CIT0YXW0us4(YJ?rtA^SwU!jPz5N}-T@M&DE4U{e{NzDA@?5BvXNB7 utye3oyGZ=^ls!X1qM%YRY%Xc6jr;{@lrN5sz~U+Z0000l4 literal 0 HcmV?d00001 diff --git a/recipes/icons/mobilna.png b/recipes/icons/mobilna.png new file mode 100644 index 0000000000000000000000000000000000000000..30db9287be6eac66714fffea4e67b9dcea415b08 GIT binary patch literal 885 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5)aMxB z6XFU~aN*+>|ITNz7v5glx9sURpaW`degfJE1l2b_aqoC$cJx)^#rHsEPrv;E+HP_5HRq0J zO}9S>op}pnn;m*(e(06k8g5=-AZC{Y`GH~w3*g=H`Tm0k&$jT%UXPcSl`%UdD|cLu z(FjCIo_{36qgq3_%3eOVt8v%4%W_9!+vc{#%E^eIl94@a zt~Js8v@GA2XSZ+vd3x_Z&@RR#Z+91-#?67rKn`btM`SUO_5fqIli7Aa#%fO&#}J9j z$q7IpB0PPtI;jwoFWV_qM)ua_lY*!S@}ydrx0D zaic~0%yUCi~q(=xo&z*NBpo#FA92{lt BmFoZi literal 0 HcmV?d00001 diff --git a/recipes/icons/mojegotowanie.png b/recipes/icons/mojegotowanie.png new file mode 100644 index 0000000000000000000000000000000000000000..b9df6dc6d00f148aca415db0bed7467262851e83 GIT binary patch literal 307 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`zdT(WLo9leYvvyOJ-?CZK$Cw^ z$}jajv+k#SGBh|SvMI~T&;STFWLYI<{He@RJ$$>SuuZ=GnZX|22?-U^3XIc}o2*^$ z3ADPg#ZP-?_~-xU*$&HfLZ@P+HT%!F&(s zpoGPNjLNBcKmLC=KRo|;`U&F(SGWF$|G)S1$gBUHoZjH@Rgg{Zyb;^KM{j4e2`4i! z1Htro-Z^Jaob;c?_@eRD(_qh8j6aM-m6zU>WZ2m04#Z~MTny|MTjGa5D*Be)M4~0 zRa;-VIK7$d{~UB4uYcT#BO!?*qDY7YA`wIYKmk=HD1^|E1cbGFOG`;mrI%VF0cnSX zzr&+Y0M(WZ5J43Z4a#t68yt+8c)@Oq&Yg9qv2vsFQ74IC* zI$Z9Uos4;Of0?xh*C?GO&n-5$JiK$A$waVugZ0*f(h!u+Q#wcK9EEipbWb?$TW(#R z8~2BU0fn{Wy7CTGi3m#7HX8!VaFFq2{T+9g=Sk{@Jk7E*1RhjLLQqnZ5mW&%NsTdv z?e_<~UYupMGsE-ELrlL2gkotC`$lRQ}S~AJ1dJb*r9{60wAD`kWeL* z>iy_rnVcXqE$5}_}>VvKN3-xJQp`gZhgS1mlnv#1MTV( z$B2UTO+K}^e{xkuX5zG|{u)WOAsRVT6*>zS-u?mnq^_@X3saT=0000XmXtJzMM?%0hwA2}&<<|?41%*$ixfAv ztOdbM1SyDvKWGI-9NG*<8XMBQyd2ND$$fdx*NS)__nvdU`<fbvS(IjUbq*`A zDVi&{(fRNRbXZy@?IUe!7ig~CmC`zv7D`7$g_bD6mv_?N9>Oh=;!J74fBO=__ss~E z(fzfFjB92e=zO}K?|uvQ+ca}Xz4 zL4QyAyeAp$A0EIw0PaUIN8U?KsY8Rb5$*2*B1Cxq9Uyk`q9Kj`?-g|AJog#SpLcs7 zj)Aznm)KI@WChxFa|FHS8AH{MEb(__0?MsXoG84Jfe-ct^koqrwzlf6UE^eZj0};! y;%~lTZtTdbCwLQ62)=y;?>5Y?H$Dj`n~!yM`a$*m#*4~PC6s69i`ALM`@aDN!qLY7 literal 0 HcmV?d00001 diff --git a/recipes/icons/osw.png b/recipes/icons/osw.png new file mode 100644 index 0000000000000000000000000000000000000000..0693aee762bc1d096d00031294a76ac2685a7cbf GIT binary patch literal 489 zcmVDY3%JJ*h+KPqjWEz(E@){ z+vx;VJi_k)R#VOj+C{S7b$sr9skag(?R>Y= z+?@pOC#Ed$J;ePOcQQA-_MhN=Aai&d+j5~ItPi7VK8mKYe${zQJx-2BYYf+;U?NF6 f;5E(qLbv-rrqF>J7WGWp00000NkvXXu0mjf5R2hA literal 0 HcmV?d00001 diff --git a/recipes/icons/ppe_pl.png b/recipes/icons/ppe_pl.png new file mode 100644 index 0000000000000000000000000000000000000000..42c9b42fa5092fa27c3e6ea8c36b6a142234dacd GIT binary patch literal 3203 zcmV-}41Dv6P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}0004=NklBM1qE5NnXY?I4aq zkf@9hM3CSpYEUDz2FJjq*+p4x^lpGva(bl8A<3|HG}s z3K>W_3g+h3>gmx0t)p(T^H0IcvqlmhN6aMdBz&*)9qi+;8-6V6AGXOJY4+)4tizWL zi|oy2-Ztp;cW*r<%UF+U#n%nbt72Zvif%D1N<%R$ir1q@}qdTa*Ulk?0cV#HiSrj|GtzAMVAZ7|?9LCdD=_w0GP3Bz@q% zo0S)`w`G=%&)HykEQ!J;+ndU(E@Z8hRT#)CEJ@LucX!GCw3ZW!6+^wsg(Tiw^sK0p p3a^-;#yY2X%{J)aG^@P#YXBvlyOY4b$R_{*002ovPDHLkV1f>&`ZfRn literal 0 HcmV?d00001 diff --git a/recipes/icons/presseurop.png b/recipes/icons/presseurop.png new file mode 100644 index 0000000000000000000000000000000000000000..9967aac1fbe87108d8b86da4cf983ed27f00bec6 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!7%)r1n>(-?sK#oL!PlzjP-bUAnhyVQjx9-Bz zi%&i>#4fMga8;#lr^w|aCO|bso-U3d95Zcuy!o09cwE?5b>vFv1z2MXU>T`R4;m?c5O=UqKQT~ z*f;9`sQqGlF}9;$Z@$9jU(!;|bwB$TEz6D!vp$z0yjEsY8E^V=@ymaJwljFT`njxg HN@xNAOX^Zp literal 0 HcmV?d00001 diff --git a/recipes/icons/res_publica.png b/recipes/icons/res_publica.png new file mode 100644 index 0000000000000000000000000000000000000000..7c21e9d96e04836f6881267d90f20653017f3945 GIT binary patch literal 733 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%9|WRD45bDP46hOx7_4S6Fo+k-*%fF5)TbQa z6XN>+|Nq}#zJGi1?%T_E-(S7|`}6nLr*H1A+JCWU#kHB6-d%n0@87?_zkZ)>TDaCY zb)8xIdh?7MbGE)Xc4f0;!8~5i*{sfUI9yljCO+MF?$@WU+x;u1GMG5? z_1Rk+tg>b>*v@3I+n>;Wb?W;4@$IX05*CU2?}==h#p*bp-)nbh{U(R}namE;8LV~$ zR{j3^{qOHT7kgLEWOSIt>^O(h^=#vUKRay>FX7$(Ocb1etrD<?c6kSqI)8osw!^n_27}F^^qzlz z{vJx}Ih5YJ)xGrY>H|;rpP$9zG>6?~qfO3_w;z9h`>`*kWgfT3TyFRG*B(6Bc=W-h zqp#21(hCoC0flptx4R2N2dk_Hki%Kv5n0T@z;^_M8K-LVNdpB}c)B=-NL)@%NJvRa zOH55jNPhm{$)mJ{ga;3wJ{A{95fBm+6cwJ%z~$lPdHTeuliDXvX=t6)VxIbG#fnv$ ztFkk)GGD)V^-`K)szi~rA3BXzX1(chO$v=EcI3ja?2L4Gp|_b}l2M zjg4sPo{9-9dunS^|NObg!gPU^=^~3rbCdI*i7Y8ooDLsSe!w&_FzKRU(jgAUwi`Qs z{Mf;g5?)FK#IZ0z|ct7&`{UFGQ`lx%GlV- q#7x`3(8|Ezb@?|@6b-rgDVb@NxHZhu@>>biz~JfX=d#Wzp$P!TjXop* literal 0 HcmV?d00001 diff --git a/recipes/icons/wolne_media.png b/recipes/icons/wolne_media.png new file mode 100644 index 0000000000000000000000000000000000000000..78d72713ab1d0d9e420527dd02205e3878d643ad GIT binary patch literal 497 zcmV(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-cS%G+RCwB)lQC}FKo9_Db&EivLLF(k%>(iRH!jlo0|H$58Tmv0kRL>VTmQgK z>bxMmY4Idlbbvc;3Q@AD7%`x&<`&q+Vs}nHef|C$@SGMw%WPZ$(-Rx0thzsqbLs(7 zia4hXSO7aJ>vJi^p!xU%-DV9yp5+66-n49OiyYM(AR^>hj<)TJtEQDbFQB(Vdf}H&)wZG)XZ2eYgCo(HqkUSK+lgfO~CsJ>vapjbUGn~ z7KoS`BLG!JRRI|=7*#dn_Vy><&+vZ7RW*Cr;(Kcr5xhS`WVeae4D2S@$NmDxoKk#~ z2Mx@O5FRnJ0lzE49_o6YGDk79+1t=nkqVR`v z6@{lL&KQjxMN#cW?m4B1h#+Rxbepv|vp0nq$%t|G@gUgbm28tN4=*bJU)g^Zr&sy% n+YjaRP+e}GYapKbo&OF1K5)1Xmq^9e00000NkvXXu0mjfImF8g literal 0 HcmV?d00001 diff --git a/recipes/ittechblog.recipe b/recipes/ittechblog.recipe new file mode 100644 index 0000000000..ba2bc8e045 --- /dev/null +++ b/recipes/ittechblog.recipe @@ -0,0 +1,27 @@ +__license__ = 'GPL v3' +__copyright__ = 'MrStefan' + +''' +www.ittechblog.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class ittechblog(BasicNewsRecipe): + title = u'IT techblog' + __author__ = 'MrStefan ' + 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')] diff --git a/recipes/magazyn_consido.recipe b/recipes/magazyn_consido.recipe new file mode 100644 index 0000000000..d24c66d6a4 --- /dev/null +++ b/recipes/magazyn_consido.recipe @@ -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 ,teepel ' + 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 diff --git a/recipes/media2.recipe b/recipes/media2.recipe new file mode 100644 index 0000000000..3c9ef3231e --- /dev/null +++ b/recipes/media2.recipe @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = 'teepel' + +''' +media2.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class media2_pl(BasicNewsRecipe): + title = u'Media2' + __author__ = 'teepel ' + 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')] diff --git a/recipes/mobilna.recipe b/recipes/mobilna.recipe new file mode 100644 index 0000000000..624a431935 --- /dev/null +++ b/recipes/mobilna.recipe @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = 'MrStefan' + +''' +www.mobilna.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class mobilna(BasicNewsRecipe): + title = u'Mobilna.pl' + __author__ = 'MrStefan ' + 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/')] diff --git a/recipes/mojegotowanie.recipe b/recipes/mojegotowanie.recipe new file mode 100644 index 0000000000..e4d514b2a8 --- /dev/null +++ b/recipes/mojegotowanie.recipe @@ -0,0 +1,51 @@ +#!usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = 'MrStefan, teepel' + +''' +www.mojegotowanie.pl +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class mojegotowanie(BasicNewsRecipe): + title = u'Moje Gotowanie' + __author__ = 'MrStefan , teepel ' + 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 diff --git a/recipes/najwyzszy_czas.recipe b/recipes/najwyzszy_czas.recipe new file mode 100644 index 0000000000..6d8420d216 --- /dev/null +++ b/recipes/najwyzszy_czas.recipe @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__author__ = 'teepel ' + +''' +nczas.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class nczas(BasicNewsRecipe): + title = u'Najwy\u017cszy Czas' + __author__ = 'teepel ' + 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/')] diff --git a/recipes/nowiny_rybnik.recipe b/recipes/nowiny_rybnik.recipe new file mode 100644 index 0000000000..11337d49af --- /dev/null +++ b/recipes/nowiny_rybnik.recipe @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + + +class NowinyRybnik(BasicNewsRecipe): + title = u'Nowiny - Rybnik' + __author__ = 'Artur Stachecki ' + 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 diff --git a/recipes/osw.recipe b/recipes/osw.recipe new file mode 100644 index 0000000000..5e5d7c6ef0 --- /dev/null +++ b/recipes/osw.recipe @@ -0,0 +1,42 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__author__ = 'teepel ' + +''' +http://www.osw.waw.pl - Osrodek studiow wschodnich +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class OSW_Recipe(BasicNewsRecipe): + + language = 'pl' + title = u'Ośrodek Studiów Wschodnich' + __author__ = 'teepel ' + 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/') diff --git a/recipes/ppe_pl.recipe b/recipes/ppe_pl.recipe new file mode 100644 index 0000000000..d1d01c2961 --- /dev/null +++ b/recipes/ppe_pl.recipe @@ -0,0 +1,41 @@ +import re + +from calibre.web.feeds.news import BasicNewsRecipe + +class ppeRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = u'Artur Stachecki ' + language = 'pl' + version = 1 + + 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'), + ] diff --git a/recipes/presseurop.recipe b/recipes/presseurop.recipe new file mode 100644 index 0000000000..3a2f3209cf --- /dev/null +++ b/recipes/presseurop.recipe @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +''' +www.presseurop.eu/pl +''' + +__license__ = 'GPL v3' +__author__ = 'teepel ' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class presseurop(BasicNewsRecipe): + title = u'Presseurop' + description = u'Najlepsze artykuły z prasy europejskiej' + 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'\|.*', re.DOTALL|re.IGNORECASE), + lambda match: ''), +] diff --git a/recipes/res_publica.recipe b/recipes/res_publica.recipe new file mode 100644 index 0000000000..29d7c558e7 --- /dev/null +++ b/recipes/res_publica.recipe @@ -0,0 +1,34 @@ +import re + +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.utils.magick import Image + +class ResPublicaNowaRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = u'intromatyk ' + 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 diff --git a/recipes/wolne_media.recipe b/recipes/wolne_media.recipe new file mode 100644 index 0000000000..4dde5b22b5 --- /dev/null +++ b/recipes/wolne_media.recipe @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__author__ = 'teepel ' + +''' +wolnemedia.net +''' + +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class wolne_media(AutomaticNewsRecipe): + title = u'Wolne Media' + __author__ = 'teepel ' + 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/')] From dc568659e7df7a04dce3683d4b14aa1dfbefb593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Tue, 2 Apr 2013 00:37:23 +0200 Subject: [PATCH 10/17] changes by fenuks --- recipes/gazeta-prawna-calibre-v1.recipe | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/recipes/gazeta-prawna-calibre-v1.recipe b/recipes/gazeta-prawna-calibre-v1.recipe index 293aa05b0d..f7d2c4935b 100644 --- a/recipes/gazeta-prawna-calibre-v1.recipe +++ b/recipes/gazeta-prawna-calibre-v1.recipe @@ -14,13 +14,14 @@ class gazetaprawna(BasicNewsRecipe): title = u'Gazeta Prawna' __author__ = u'Vroo' publisher = u'Infor Biznes' - oldest_article = 7 + oldest_article = 1 max_articles_per_feed = 20 no_stylesheets = True remove_javascript = True description = 'Polski dziennik gospodarczy' language = 'pl' encoding = 'utf-8' + ignore_duplicate_articles = {'title', 'url'} remove_tags_after = [ dict(name='div', attrs={'class':['data-art']}) @@ -30,7 +31,7 @@ class gazetaprawna(BasicNewsRecipe): ] feeds = [ - (u'Wiadomo\u015bci - najwa\u017cniejsze', u'http://www.gazetaprawna.pl/wiadomosci/najwazniejsze/rss.xml'), + (u'Z ostatniej chwili', u'http://rss.gazetaprawna.pl/GazetaPrawna'), (u'Biznes i prawo gospodarcze', u'http://biznes.gazetaprawna.pl/rss.xml'), (u'Prawo i wymiar sprawiedliwo\u015bci', u'http://prawo.gazetaprawna.pl/rss.xml'), (u'Praca i ubezpieczenia', u'http://praca.gazetaprawna.pl/rss.xml'), @@ -51,3 +52,8 @@ class gazetaprawna(BasicNewsRecipe): url = url.replace('prawo.gazetaprawna', 'www.gazetaprawna') url = url.replace('praca.gazetaprawna', 'www.gazetaprawna') return url + + def get_cover_url(self): + soup = self.index_to_soup('http://www.egazety.pl/infor/e-wydanie-dziennik-gazeta-prawna.html') + self.cover_url = soup.find('p', attrs={'class':'covr'}).a['href'] + return getattr(self, 'cover_url', self.cover_url) From 1dfea2ab04fbe2ec3a0087871a8b69e8a0dc7fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?= Date: Tue, 2 Apr 2013 00:57:49 +0200 Subject: [PATCH 11/17] remove obsolete re ; update change Artur's nickname into real name --- recipes/dzial_zagraniczny.recipe | 6 ++---- recipes/focus_pl.recipe | 7 +++---- recipes/ittechblog.recipe | 1 - recipes/km_blog.recipe | 1 + recipes/kp.recipe | 3 +-- recipes/media2.recipe | 2 -- recipes/mobilna.recipe | 1 - recipes/mojegotowanie.recipe | 1 - recipes/najwyzszy_czas.recipe | 1 - recipes/nowiny_rybnik.recipe | 2 -- recipes/osw.recipe | 1 - recipes/ppe_pl.recipe | 6 +++--- recipes/res_publica.recipe | 6 ++++-- recipes/wolne_media.recipe | 1 - 14 files changed, 14 insertions(+), 25 deletions(-) diff --git a/recipes/dzial_zagraniczny.recipe b/recipes/dzial_zagraniczny.recipe index 9709186d7e..1b8453dd40 100644 --- a/recipes/dzial_zagraniczny.recipe +++ b/recipes/dzial_zagraniczny.recipe @@ -8,7 +8,6 @@ dzialzagraniczny.pl ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class dzial_zagraniczny(BasicNewsRecipe): title = u'Dział Zagraniczny' @@ -21,9 +20,8 @@ class dzial_zagraniczny(BasicNewsRecipe): 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 - simultaneous_downloads = 5 remove_javascript = True no_stylesheets = True use_embedded_content = True - - feeds = [(u'Dział zagraniczny', u'http://feeds.feedburner.com/dyndns/UOfz')] \ No newline at end of file + + feeds = [(u'Dział zagraniczny', u'http://feeds.feedburner.com/dyndns/UOfz')] diff --git a/recipes/focus_pl.recipe b/recipes/focus_pl.recipe index 66864b8561..e13e51a15a 100644 --- a/recipes/focus_pl.recipe +++ b/recipes/focus_pl.recipe @@ -1,12 +1,11 @@ -import re +#!/usr/bin/env python +__license__ = 'GPL v3' from calibre.web.feeds.news import BasicNewsRecipe - class FocusRecipe(BasicNewsRecipe): - __license__ = 'GPL v3' - __author__ = u'intromatyk ' + __author__ = u'Artur Stachecki ' language = 'pl' version = 1 diff --git a/recipes/ittechblog.recipe b/recipes/ittechblog.recipe index ba2bc8e045..3fa557d11e 100644 --- a/recipes/ittechblog.recipe +++ b/recipes/ittechblog.recipe @@ -6,7 +6,6 @@ www.ittechblog.pl ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class ittechblog(BasicNewsRecipe): title = u'IT techblog' diff --git a/recipes/km_blog.recipe b/recipes/km_blog.recipe index 8910ee060a..614dbc03e5 100644 --- a/recipes/km_blog.recipe +++ b/recipes/km_blog.recipe @@ -20,6 +20,7 @@ class km_blog(BasicNewsRecipe): remove_javascript=True no_stylesheets=True remove_empty_feeds = True + feeds = [(u'blog', u'http://korwin-mikke.pl/blog/rss')] keep_only_tags =[] diff --git a/recipes/kp.recipe b/recipes/kp.recipe index 85bf356b4d..3a2bc62eb0 100644 --- a/recipes/kp.recipe +++ b/recipes/kp.recipe @@ -2,8 +2,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class KrytykaPolitycznaRecipe(BasicNewsRecipe): - __license__ = 'GPL v3' - __author__ = u'intromatyk ' + __author__ = u'Artur Stachecki ' language = 'pl' version = 1 diff --git a/recipes/media2.recipe b/recipes/media2.recipe index 3c9ef3231e..135740a62e 100644 --- a/recipes/media2.recipe +++ b/recipes/media2.recipe @@ -8,7 +8,6 @@ media2.pl ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class media2_pl(BasicNewsRecipe): title = u'Media2' @@ -33,5 +32,4 @@ class media2_pl(BasicNewsRecipe): 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')] diff --git a/recipes/mobilna.recipe b/recipes/mobilna.recipe index 624a431935..68ae011438 100644 --- a/recipes/mobilna.recipe +++ b/recipes/mobilna.recipe @@ -8,7 +8,6 @@ www.mobilna.pl ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class mobilna(BasicNewsRecipe): title = u'Mobilna.pl' diff --git a/recipes/mojegotowanie.recipe b/recipes/mojegotowanie.recipe index e4d514b2a8..4b0de4a0e1 100644 --- a/recipes/mojegotowanie.recipe +++ b/recipes/mojegotowanie.recipe @@ -8,7 +8,6 @@ www.mojegotowanie.pl ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class mojegotowanie(BasicNewsRecipe): title = u'Moje Gotowanie' diff --git a/recipes/najwyzszy_czas.recipe b/recipes/najwyzszy_czas.recipe index 6d8420d216..9c4a82c4ea 100644 --- a/recipes/najwyzszy_czas.recipe +++ b/recipes/najwyzszy_czas.recipe @@ -8,7 +8,6 @@ nczas.com ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class nczas(BasicNewsRecipe): title = u'Najwy\u017cszy Czas' diff --git a/recipes/nowiny_rybnik.recipe b/recipes/nowiny_rybnik.recipe index 11337d49af..e00a72e09b 100644 --- a/recipes/nowiny_rybnik.recipe +++ b/recipes/nowiny_rybnik.recipe @@ -3,8 +3,6 @@ __license__ = 'GPL v3' from calibre.web.feeds.news import BasicNewsRecipe -import re - class NowinyRybnik(BasicNewsRecipe): title = u'Nowiny - Rybnik' diff --git a/recipes/osw.recipe b/recipes/osw.recipe index 5e5d7c6ef0..8022f3e346 100644 --- a/recipes/osw.recipe +++ b/recipes/osw.recipe @@ -8,7 +8,6 @@ http://www.osw.waw.pl - Osrodek studiow wschodnich ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class OSW_Recipe(BasicNewsRecipe): diff --git a/recipes/ppe_pl.recipe b/recipes/ppe_pl.recipe index d1d01c2961..2edc611ad7 100644 --- a/recipes/ppe_pl.recipe +++ b/recipes/ppe_pl.recipe @@ -1,12 +1,12 @@ -import re +#!/usr/bin/env python + +__license__ = 'GPL v3' from calibre.web.feeds.news import BasicNewsRecipe class ppeRecipe(BasicNewsRecipe): - __license__ = 'GPL v3' __author__ = u'Artur Stachecki ' language = 'pl' - version = 1 title = u'ppe.pl' category = u'News' diff --git a/recipes/res_publica.recipe b/recipes/res_publica.recipe index 29d7c558e7..1c806d4a85 100644 --- a/recipes/res_publica.recipe +++ b/recipes/res_publica.recipe @@ -1,11 +1,13 @@ -import re +#!/usr/bin/env python + +__license__ = 'GPL v3' from calibre.web.feeds.news import BasicNewsRecipe from calibre.utils.magick import Image class ResPublicaNowaRecipe(BasicNewsRecipe): __license__ = 'GPL v3' - __author__ = u'intromatyk ' + __author__ = u'Artur Stachecki ' language = 'pl' version = 1 diff --git a/recipes/wolne_media.recipe b/recipes/wolne_media.recipe index 4dde5b22b5..b0e34c8bdf 100644 --- a/recipes/wolne_media.recipe +++ b/recipes/wolne_media.recipe @@ -8,7 +8,6 @@ wolnemedia.net ''' from calibre.web.feeds.news import BasicNewsRecipe -import re class wolne_media(AutomaticNewsRecipe): title = u'Wolne Media' From 354da83e7feac36a3bd33ce63da629b35af53dd5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 09:24:58 +0530 Subject: [PATCH 12/17] ToC Editor: Add buttons to indent/unindent teh current entry --- src/calibre/gui2/toc/main.py | 180 ++++++++++++++++++++++++----------- 1 file changed, 127 insertions(+), 53 deletions(-) diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index de5ed91bcd..4e8d1f3424 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -339,7 +339,7 @@ class ItemView(QFrame): # {{{ # }}} -class TreeWidget(QTreeWidget): +class TreeWidget(QTreeWidget): # {{{ def __init__(self, parent): QTreeWidget.__init__(self, parent) @@ -357,6 +357,7 @@ class TreeWidget(QTreeWidget): self.setAnimated(True) self.setMouseTracking(True) self.in_drop_event = False + self.root = self.invisibleRootItem() def iteritems(self, parent=None): if parent is None: @@ -384,6 +385,104 @@ class TreeWidget(QTreeWidget): ans = sorted(ans, key=lambda x:sort_map.get(x, -1), reverse=True) return ans + def highlight_item(self, item): + self.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect) + self.scrollToItem(item) + + def move_left(self): + item = self.currentItem() + if item is not None: + parent = item.parent() + if parent is not None: + is_expanded = item.isExpanded() or item.childCount() == 0 + gp = parent.parent() or self.invisibleRootItem() + idx = gp.indexOfChild(parent) + for gc in [parent.child(i) for i in xrange(parent.indexOfChild(item)+1, parent.childCount())]: + parent.removeChild(gc) + item.addChild(gc) + parent.removeChild(item) + gp.insertChild(idx+1, item) + if is_expanded: + self.expandItem(item) + self.highlight_item(item) + + def move_right(self): + item = self.currentItem() + if item is not None: + parent = item.parent() or self.invisibleRootItem() + idx = parent.indexOfChild(item) + if idx > 0: + is_expanded = item.isExpanded() + np = parent.child(idx-1) + parent.removeChild(item) + np.addChild(item) + if is_expanded: + self.expandItem(item) + self.highlight_item(item) + + def move_down(self): + item = self.currentItem() + if item is None: + if self.root.childCount() == 0: + return + item = self.root.child(0) + self.highlight_item(item) + return + parent = item.parent() or self.root + idx = parent.indexOfChild(item) + if idx == parent.childCount() - 1: + # At end of parent, need to become sibling of parent + if parent is self.root: + return + gp = parent.parent() or self.root + parent.removeChild(item) + gp.insertChild(gp.indexOfChild(parent)+1, item) + else: + sibling = parent.child(idx+1) + parent.removeChild(item) + sibling.insertChild(0, item) + self.highlight_item(item) + + def move_up(self): + item = self.currentItem() + if item is None: + if self.root.childCount() == 0: + return + item = self.root.child(self.root.childCount()-1) + self.highlight_item(item) + return + parent = item.parent() or self.root + idx = parent.indexOfChild(item) + if idx == 0: + # At end of parent, need to become sibling of parent + if parent is self.root: + return + gp = parent.parent() or self.root + parent.removeChild(item) + gp.insertChild(gp.indexOfChild(parent), item) + else: + sibling = parent.child(idx-1) + parent.removeChild(item) + sibling.addChild(item) + self.highlight_item(item) + + def 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() + else: + return super(TreeWidget, self).keyPressEvent(ev) +# }}} + class TOCView(QWidget): # {{{ add_new_item = pyqtSignal(object, object) @@ -393,27 +492,43 @@ class TOCView(QWidget): # {{{ l = self.l = QGridLayout() self.setLayout(l) self.tocw = t = TreeWidget(self) - l.addWidget(t, 0, 0, 5, 3) + l.addWidget(t, 0, 0, 7, 3) self.up_button = b = QToolButton(self) b.setIcon(QIcon(I('arrow-up.png'))) b.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) l.addWidget(b, 0, 3) - b.setToolTip(_('Move current entry up')) + b.setToolTip(_('Move current entry up [Ctrl+Up]')) b.clicked.connect(self.move_up) + + self.left_button = b = QToolButton(self) + b.setIcon(QIcon(I('back.png'))) + b.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) + l.addWidget(b, 2, 3) + b.setToolTip(_('Unindent the current entry [Ctrl+Left]')) + b.clicked.connect(self.tocw.move_left) + self.del_button = b = QToolButton(self) b.setIcon(QIcon(I('trash.png'))) b.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) - l.addWidget(b, 2, 3) + l.addWidget(b, 3, 3) b.setToolTip(_('Remove all selected entries')) b.clicked.connect(self.del_items) + + self.left_button = b = QToolButton(self) + b.setIcon(QIcon(I('forward.png'))) + b.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) + l.addWidget(b, 4, 3) + b.setToolTip(_('Unindent the current entry [Ctrl+Left]')) + b.clicked.connect(self.tocw.move_right) + self.down_button = b = QToolButton(self) b.setIcon(QIcon(I('arrow-down.png'))) b.setIconSize(QSize(ICON_SIZE, ICON_SIZE)) - l.addWidget(b, 4, 3) - b.setToolTip(_('Move current entry down')) + l.addWidget(b, 6, 3) + b.setToolTip(_('Move current entry down [Ctrl+Down]')) b.clicked.connect(self.move_down) self.expand_all_button = b = QPushButton(_('&Expand all')) - col = 5 + col = 7 l.addWidget(b, col, 0) b.clicked.connect(self.tocw.expandAll) self.collapse_all_button = b = QPushButton(_('&Collapse all')) @@ -484,54 +599,13 @@ class TOCView(QWidget): # {{{ self.tocw.setCurrentItem(None) def highlight_item(self, item): - self.tocw.setCurrentItem(item, 0, QItemSelectionModel.ClearAndSelect) - self.tocw.scrollToItem(item) - - def move_down(self): - item = self.tocw.currentItem() - if item is None: - if self.root.childCount() == 0: - return - item = self.root.child(0) - self.highlight_item(item) - return - parent = item.parent() or self.root - idx = parent.indexOfChild(item) - if idx == parent.childCount() - 1: - # At end of parent, need to become sibling of parent - if parent is self.root: - return - gp = parent.parent() or self.root - parent.removeChild(item) - gp.insertChild(gp.indexOfChild(parent)+1, item) - else: - sibling = parent.child(idx+1) - parent.removeChild(item) - sibling.insertChild(0, item) - self.highlight_item(item) + self.tocw.highlight_item(item) def move_up(self): - item = self.tocw.currentItem() - if item is None: - if self.root.childCount() == 0: - return - item = self.root.child(self.root.childCount()-1) - self.highlight_item(item) - return - parent = item.parent() or self.root - idx = parent.indexOfChild(item) - if idx == 0: - # At end of parent, need to become sibling of parent - if parent is self.root: - return - gp = parent.parent() or self.root - parent.removeChild(item) - gp.insertChild(gp.indexOfChild(parent), item) - else: - sibling = parent.child(idx-1) - parent.removeChild(item) - sibling.addChild(item) - self.highlight_item(item) + self.tocw.move_up() + + def move_down(self): + self.tocw.move_down() def update_status_tip(self, item): c = item.data(0, Qt.UserRole).toPyObject() From 2eceaeb2aba9d3bde0d934da1066125e908574e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 09:28:38 +0530 Subject: [PATCH 13/17] ... --- src/calibre/gui2/toc/main.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index 4e8d1f3424..c54629b862 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -190,7 +190,7 @@ class ItemView(QFrame): # {{{ ))) l.addWidget(b) - self.fal = b = QPushButton(_('Flatten the ToC')) + self.fal = b = QPushButton(_('&Flatten the ToC')) b.clicked.connect(self.flatten_toc) b.setToolTip(textwrap.fill(_( 'Flatten the Table of Contents, putting all entries at the top level' @@ -466,6 +466,11 @@ class TreeWidget(QTreeWidget): # {{{ 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 keyPressEvent(self, ev): if ev.key() == Qt.Key_Left and ev.modifiers() & Qt.CTRL: self.move_left() @@ -479,6 +484,9 @@ class TreeWidget(QTreeWidget): # {{{ 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) # }}} @@ -559,9 +567,7 @@ class TOCView(QWidget): # {{{ return unicode(item.data(0, Qt.DisplayRole).toString()) def del_items(self): - for item in self.tocw.selectedItems(): - p = item.parent() or self.root - p.removeChild(item) + self.tocw.del_items() def delete_current_item(self): item = self.tocw.currentItem() From 800f0c19ff9e4a3c1cebd1e4bc0f575b8171a8eb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 10:02:39 +0530 Subject: [PATCH 14/17] ToC Editor: Right-click menu to perform various useful actions on entries in the ToC --- src/calibre/gui2/toc/main.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index c54629b862..74886bbf63 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -14,7 +14,7 @@ from functools import partial from PyQt4.Qt import (QPushButton, QFrame, QVariant, QMenu, QInputDialog, QDialog, QVBoxLayout, QDialogButtonBox, QSize, QStackedWidget, QWidget, QLabel, Qt, pyqtSignal, QIcon, QTreeWidget, QGridLayout, QTreeWidgetItem, - QToolButton, QItemSelectionModel) + QToolButton, QItemSelectionModel, QCursor) from calibre.ebooks.oeb.polish.container import get_container, AZW3Container from calibre.ebooks.oeb.polish.toc import ( @@ -358,6 +358,8 @@ class TreeWidget(QTreeWidget): # {{{ self.setMouseTracking(True) self.in_drop_event = False self.root = self.invisibleRootItem() + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) def iteritems(self, parent=None): if parent is None: @@ -471,6 +473,12 @@ class TreeWidget(QTreeWidget): # {{{ 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() @@ -489,6 +497,25 @@ class TreeWidget(QTreeWidget): # {{{ 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): # {{{ From 17743799bce332091ce2f4cc1be777aa13023090 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 10:13:14 +0530 Subject: [PATCH 15/17] Remove the google images plugin for now, will probably release it next week, after more testing --- src/calibre/customize/builtins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index c87c8c63d0..474617c911 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -757,9 +757,9 @@ from calibre.ebooks.metadata.sources.isbndb import ISBNDB from calibre.ebooks.metadata.sources.overdrive import OverDrive from calibre.ebooks.metadata.sources.douban import Douban from calibre.ebooks.metadata.sources.ozon import Ozon -from calibre.ebooks.metadata.sources.google_images import GoogleImages +# from calibre.ebooks.metadata.sources.google_images import GoogleImages -plugins += [GoogleBooks, Amazon, Edelweiss, GoogleImages, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon] +plugins += [GoogleBooks, Amazon, Edelweiss, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon] # }}} From 01dedbd3574c88e0ccc68477f38bbc8fc36b8304 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 11:25:25 +0530 Subject: [PATCH 16/17] Fix #1163115 (samsung android phone not recognized) --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 95a00a315c..36ab076417 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -239,7 +239,7 @@ class ANDROID(USBMS): 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E', 'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS', - 'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1'] + 'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', From 42561905d7ec9e13431cac0d26e3749afd70cb06 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Apr 2013 12:51:51 +0530 Subject: [PATCH 17/17] Work on the view interface --- src/calibre/db/view.py | 127 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 9 deletions(-) diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index e9de69e320..e0f99eede0 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -7,7 +7,9 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import weakref from functools import partial +from itertools import izip, imap def sanitize_sort_field_name(field_metadata, field): field = field_metadata.search_term_to_field_key(field.lower().strip()) @@ -15,11 +17,39 @@ def sanitize_sort_field_name(field_metadata, field): field = {'title': 'sort', 'authors':'author_sort'}.get(field, field) return field +class MarkedVirtualField(object): + + def __init__(self, marked_ids): + self.marked_ids = marked_ids + + def iter_searchable_values(self, get_metadata, candidates, default_value=None): + for book_id in candidates: + yield self.marked_ids.get(book_id, default_value), {book_id} + +class TableRow(list): + + def __init__(self, book_id, view): + self.book_id = book_id + self.view = weakref.ref(view) + + def __getitem__(self, obj): + view = self.view() + if isinstance(obj, slice): + return [view._field_getters[c](self.book_id) + for c in xrange(*obj.indices(len(view._field_getters)))] + else: + return view._field_getters[obj](self.book_id) + class View(object): + ''' A table view of the database, with rows and columns. Also supports + filtering and sorting. ''' + def __init__(self, cache): self.cache = cache self.marked_ids = {} + self.search_restriction_book_count = 0 + self.search_restriction = '' self._field_getters = {} for col, idx in cache.backend.FIELD_MAP.iteritems(): if isinstance(col, int): @@ -38,16 +68,33 @@ class View(object): except KeyError: self._field_getters[idx] = partial(self.get, col) - self._map = list(self.cache.all_book_ids()) - self._map_filtered = list(self._map) + self._map = tuple(self.cache.all_book_ids()) + self._map_filtered = tuple(self._map) @property def field_metadata(self): return self.cache.field_metadata def _get_id(self, idx, index_is_id=True): - ans = idx if index_is_id else self.index_to_id(idx) - return ans + return idx if index_is_id else self.index_to_id(idx) + + def __getitem__(self, row): + return TableRow(self._map_filtered[row], self.cache) + + def __len__(self): + return len(self._map_filtered) + + def __iter__(self): + for book_id in self._map_filtered: + yield self._data[book_id] + + def iterall(self): + for book_id in self._map: + yield self[book_id] + + def iterallids(self): + for book_id in self._map: + yield book_id def get_field_map_field(self, row, col, index_is_id=True): ''' @@ -66,7 +113,7 @@ class View(object): def get_ondevice(self, idx, index_is_id=True, default_value=''): id_ = idx if index_is_id else self.index_to_id(idx) - self.cache.field_for('ondevice', id_, default_value=default_value) + return self.cache.field_for('ondevice', id_, default_value=default_value) def get_marked(self, idx, index_is_id=True, default_value=None): id_ = idx if index_is_id else self.index_to_id(idx) @@ -93,7 +140,7 @@ class View(object): ans.append(self.cache._author_data(id_)) return tuple(ans) - def multisort(self, fields=[], subsort=False): + def multisort(self, fields=[], subsort=False, only_ids=None): fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields] keys = self.field_metadata.sortable_field_keys() fields = [x for x in fields if x[0] in keys] @@ -102,8 +149,70 @@ class View(object): if not fields: fields = [('timestamp', False)] - sorted_book_ids = self.cache.multisort(fields) - sorted_book_ids - # TODO: change maps + sorted_book_ids = self.cache.multisort(fields, ids_to_sort=only_ids) + if only_ids is None: + self._map = tuple(sorted_book_ids) + if len(self._map_filtered) == len(self._map): + self._map_filtered = tuple(self._map) + else: + fids = frozenset(self._map_filtered) + self._map_filtered = tuple(i for i in self._map if i in fids) + else: + smap = {book_id:i for i, book_id in enumerate(sorted_book_ids)} + only_ids.sort(key=smap.get) + def search(self, query, return_matches=False): + ans = self.search_getting_ids(query, self.search_restriction, + set_restriction_count=True) + if return_matches: + return ans + self._map_filtered = tuple(ans) + + def search_getting_ids(self, query, search_restriction, + set_restriction_count=False): + q = '' + if not query or not query.strip(): + q = search_restriction + else: + q = query + if search_restriction: + q = u'(%s) and (%s)' % (search_restriction, query) + if not q: + if set_restriction_count: + self.search_restriction_book_count = len(self._map) + return list(self._map) + matches = self.cache.search( + query, search_restriction, virtual_fields={'marked':MarkedVirtualField(self.marked_ids)}) + rv = [x for x in self._map if x in matches] + if set_restriction_count and q == search_restriction: + self.search_restriction_book_count = len(rv) + return rv + + def set_search_restriction(self, s): + self.search_restriction = s + + def search_restriction_applied(self): + return bool(self.search_restriction) + + def get_search_restriction_book_count(self): + return self.search_restriction_book_count + + def set_marked_ids(self, id_dict): + ''' + ids in id_dict are "marked". They can be searched for by + using the search term ``marked:true``. Pass in an empty dictionary or + set to clear marked ids. + + :param id_dict: Either a dictionary mapping ids to values or a set + of ids. In the latter case, the value is set to 'true' for all ids. If + a mapping is provided, then the search can be used to search for + particular values: ``marked:value`` + ''' + if not hasattr(id_dict, 'items'): + # Simple list. Make it a dict of string 'true' + self.marked_ids = dict.fromkeys(id_dict, u'true') + else: + # Ensure that all the items in the dict are text + self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode, + id_dict.itervalues())))