mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-05 08:40:13 -04:00
Get Books: When a free download is available for a search result, for example, for public domain books, allow direct download of the book into your calibre library.
This commit is contained in:
commit
6747188703
@ -5,7 +5,6 @@ www.ft.com
|
||||
'''
|
||||
|
||||
import datetime
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class FinancialTimes_rss(BasicNewsRecipe):
|
||||
@ -104,4 +103,4 @@ class FinancialTimes_rss(BasicNewsRecipe):
|
||||
if cdate.isoweekday() == 7:
|
||||
cdate -= datetime.timedelta(days=1)
|
||||
return cdate.strftime('http://specials.ft.com/vtf_pdf/%d%m%y_FRONT1_USA.pdf')
|
||||
|
||||
|
||||
|
@ -1148,7 +1148,7 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
|
||||
class StoreAmazonKindleStore(StoreBase):
|
||||
name = 'Amazon Kindle'
|
||||
description = u'Kindle books from Amazon.'
|
||||
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.amazon_plugin:AmazonKindleStore'
|
||||
|
||||
headquarters = 'US'
|
||||
formats = ['KINDLE']
|
||||
@ -1158,7 +1158,7 @@ class StoreAmazonDEKindleStore(StoreBase):
|
||||
name = 'Amazon DE Kindle'
|
||||
author = 'Charles Haley'
|
||||
description = u'Kindle Bücher von Amazon.'
|
||||
actual_plugin = 'calibre.gui2.store.amazon_de_plugin:AmazonDEKindleStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.amazon_de_plugin:AmazonDEKindleStore'
|
||||
|
||||
headquarters = 'DE'
|
||||
formats = ['KINDLE']
|
||||
@ -1168,7 +1168,7 @@ class StoreAmazonUKKindleStore(StoreBase):
|
||||
name = 'Amazon UK Kindle'
|
||||
author = 'Charles Haley'
|
||||
description = u'Kindle books from Amazon\'s UK web site. Also, includes French language ebooks.'
|
||||
actual_plugin = 'calibre.gui2.store.amazon_uk_plugin:AmazonUKKindleStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.amazon_uk_plugin:AmazonUKKindleStore'
|
||||
|
||||
headquarters = 'UK'
|
||||
formats = ['KINDLE']
|
||||
@ -1177,7 +1177,7 @@ class StoreAmazonUKKindleStore(StoreBase):
|
||||
class StoreArchiveOrgStore(StoreBase):
|
||||
name = 'Archive.org'
|
||||
description = u'An Internet library offering permanent access for researchers, historians, scholars, people with disabilities, and the general public to historical collections that exist in digital format.'
|
||||
actual_plugin = 'calibre.gui2.store.archive_org_plugin:ArchiveOrgStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.archive_org_plugin:ArchiveOrgStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1186,7 +1186,7 @@ class StoreArchiveOrgStore(StoreBase):
|
||||
class StoreBaenWebScriptionStore(StoreBase):
|
||||
name = 'Baen WebScription'
|
||||
description = u'Sci-Fi & Fantasy brought to you by Jim Baen.'
|
||||
actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.baen_webscription_plugin:BaenWebScriptionStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1195,7 +1195,7 @@ class StoreBaenWebScriptionStore(StoreBase):
|
||||
class StoreBNStore(StoreBase):
|
||||
name = 'Barnes and Noble'
|
||||
description = u'The world\'s largest book seller. As the ultimate destination for book lovers, Barnes & Noble.com offers an incredible array of content.'
|
||||
actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.bn_plugin:BNStore'
|
||||
|
||||
headquarters = 'US'
|
||||
formats = ['NOOK']
|
||||
@ -1205,7 +1205,7 @@ class StoreBeamEBooksDEStore(StoreBase):
|
||||
name = 'Beam EBooks DE'
|
||||
author = 'Charles Haley'
|
||||
description = u'Bei uns finden Sie: Tausende deutschsprachige eBooks; Alle eBooks ohne hartes DRM; PDF, ePub und Mobipocket Format; Sofortige Verfügbarkeit - 24 Stunden am Tag; Günstige Preise; eBooks für viele Lesegeräte, PC,Mac und Smartphones; Viele Gratis eBooks'
|
||||
actual_plugin = 'calibre.gui2.store.beam_ebooks_de_plugin:BeamEBooksDEStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.beam_ebooks_de_plugin:BeamEBooksDEStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'DE'
|
||||
@ -1215,7 +1215,7 @@ class StoreBeamEBooksDEStore(StoreBase):
|
||||
class StoreBeWriteStore(StoreBase):
|
||||
name = 'BeWrite Books'
|
||||
description = u'Publishers of fine books. Highly selective and editorially driven. Does not offer: books for children or exclusively YA, erotica, swords-and-sorcery fantasy and space-opera-style science fiction. All other genres are represented.'
|
||||
actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.bewrite_plugin:BeWriteStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1224,7 +1224,7 @@ class StoreBeWriteStore(StoreBase):
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = u'Instant access to over 2.4 million titles from hundreds of publishers including Harlequin, HarperCollins, John Wiley & Sons, McGraw-Hill, Simon & Schuster and Random House.'
|
||||
actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.diesel_ebooks_plugin:DieselEbooksStore'
|
||||
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1233,7 +1233,7 @@ class StoreDieselEbooksStore(StoreBase):
|
||||
class StoreEbookscomStore(StoreBase):
|
||||
name = 'eBooks.com'
|
||||
description = u'Sells books in multiple electronic formats in all categories. Technical infrastructure is cutting edge, robust and scalable, with servers in the US and Europe.'
|
||||
actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.ebooks_com_plugin:EbookscomStore'
|
||||
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'LIT', 'MOBI', 'PDF']
|
||||
@ -1243,7 +1243,7 @@ class StoreEPubBuyDEStore(StoreBase):
|
||||
name = 'EPUBBuy DE'
|
||||
author = 'Charles Haley'
|
||||
description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!'
|
||||
actual_plugin = 'calibre.gui2.store.epubbuy_de_plugin:EPubBuyDEStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.epubbuy_de_plugin:EPubBuyDEStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'DE'
|
||||
@ -1254,7 +1254,7 @@ class StoreEBookShoppeUKStore(StoreBase):
|
||||
name = 'ebookShoppe UK'
|
||||
author = u'Charles Haley'
|
||||
description = u'We made this website in an attempt to offer the widest range of UK eBooks possible across and as many formats as we could manage.'
|
||||
actual_plugin = 'calibre.gui2.store.ebookshoppe_uk_plugin:EBookShoppeUKStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.ebookshoppe_uk_plugin:EBookShoppeUKStore'
|
||||
|
||||
headquarters = 'UK'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1263,7 +1263,7 @@ class StoreEBookShoppeUKStore(StoreBase):
|
||||
class StoreEHarlequinStore(StoreBase):
|
||||
name = 'eHarlequin'
|
||||
description = u'A global leader in series romance and one of the world\'s leading publishers of books for women. Offers women a broad range of reading from romance to bestseller fiction, from young adult novels to erotic literature, from nonfiction to fantasy, from African-American novels to inspirational romance, and more.'
|
||||
actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.eharlequin_plugin:EHarlequinStore'
|
||||
|
||||
headquarters = 'CA'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1272,7 +1272,7 @@ class StoreEHarlequinStore(StoreBase):
|
||||
class StoreEpubBudStore(StoreBase):
|
||||
name = 'ePub Bud'
|
||||
description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
|
||||
actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.epubbud_plugin:EpubBudStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1281,7 +1281,7 @@ class StoreEpubBudStore(StoreBase):
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.'
|
||||
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.feedbooks_plugin:FeedbooksStore'
|
||||
|
||||
headquarters = 'FR'
|
||||
formats = ['EPUB', 'MOBI', 'PDF']
|
||||
@ -1290,7 +1290,7 @@ class StoreFoylesUKStore(StoreBase):
|
||||
name = 'Foyles UK'
|
||||
author = 'Charles Haley'
|
||||
description = u'Foyles of London\'s ebook store. Provides extensive range covering all subjects.'
|
||||
actual_plugin = 'calibre.gui2.store.foyles_uk_plugin:FoylesUKStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.foyles_uk_plugin:FoylesUKStore'
|
||||
|
||||
headquarters = 'UK'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1300,7 +1300,7 @@ class StoreGandalfStore(StoreBase):
|
||||
name = 'Gandalf'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Księgarnia internetowa Gandalf.'
|
||||
actual_plugin = 'calibre.gui2.store.gandalf_plugin:GandalfStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.gandalf_plugin:GandalfStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1308,7 +1308,7 @@ class StoreGandalfStore(StoreBase):
|
||||
class StoreGoogleBooksStore(StoreBase):
|
||||
name = 'Google Books'
|
||||
description = u'Google Books'
|
||||
actual_plugin = 'calibre.gui2.store.google_books_plugin:GoogleBooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.google_books_plugin:GoogleBooksStore'
|
||||
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'PDF', 'TXT']
|
||||
@ -1316,7 +1316,7 @@ class StoreGoogleBooksStore(StoreBase):
|
||||
class StoreGutenbergStore(StoreBase):
|
||||
name = 'Project Gutenberg'
|
||||
description = u'The first producer of free ebooks. Free in the United States because their copyright has expired. They may not be free of copyright in other countries. Readers outside of the United States must check the copyright laws of their countries before downloading or redistributing our ebooks.'
|
||||
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.gutenberg_plugin:GutenbergStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1325,7 +1325,7 @@ class StoreGutenbergStore(StoreBase):
|
||||
class StoreKoboStore(StoreBase):
|
||||
name = 'Kobo'
|
||||
description = u'With over 2.3 million eBooks to browse we have engaged readers in over 200 countries in Kobo eReading. Our eBook listings include New York Times Bestsellers, award winners, classics and more!'
|
||||
actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.kobo_plugin:KoboStore'
|
||||
|
||||
headquarters = 'CA'
|
||||
formats = ['EPUB']
|
||||
@ -1335,7 +1335,7 @@ class StoreLegimiStore(StoreBase):
|
||||
name = 'Legimi'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer'
|
||||
actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.legimi_plugin:LegimiStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB']
|
||||
@ -1344,7 +1344,7 @@ class StoreLibreDEStore(StoreBase):
|
||||
name = 'Libri DE'
|
||||
author = 'Charles Haley'
|
||||
description = u'Sicher Bücher, Hörbücher und Downloads online bestellen.'
|
||||
actual_plugin = 'calibre.gui2.store.libri_de_plugin:LibreDEStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.libri_de_plugin:LibreDEStore'
|
||||
|
||||
headquarters = 'DE'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1353,7 +1353,7 @@ class StoreLibreDEStore(StoreBase):
|
||||
class StoreManyBooksStore(StoreBase):
|
||||
name = 'ManyBooks'
|
||||
description = u'Public domain and creative commons works from many sources.'
|
||||
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.manybooks_plugin:ManyBooksStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1362,7 +1362,7 @@ class StoreManyBooksStore(StoreBase):
|
||||
class StoreMobileReadStore(StoreBase):
|
||||
name = 'MobileRead'
|
||||
description = u'Ebooks handcrafted with the utmost care.'
|
||||
actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.mobileread.mobileread_plugin:MobileReadStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'CH'
|
||||
@ -1372,7 +1372,7 @@ class StoreNextoStore(StoreBase):
|
||||
name = 'Nexto'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Największy w Polsce sklep internetowy z audiobookami mp3, ebookami pdf oraz prasą do pobrania on-line.'
|
||||
actual_plugin = 'calibre.gui2.store.nexto_plugin:NextoStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.nexto_plugin:NextoStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1381,7 +1381,7 @@ class StoreNextoStore(StoreBase):
|
||||
class StoreOpenBooksStore(StoreBase):
|
||||
name = 'Open Books'
|
||||
description = u'Comprehensive listing of DRM free ebooks from a variety of sources provided by users of calibre.'
|
||||
actual_plugin = 'calibre.gui2.store.open_books_plugin:OpenBooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.open_books_plugin:OpenBooksStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1389,7 +1389,7 @@ class StoreOpenBooksStore(StoreBase):
|
||||
class StoreOpenLibraryStore(StoreBase):
|
||||
name = 'Open Library'
|
||||
description = u'One web page for every book ever published. The goal is to be a true online library. Over 20 million records from a variety of large catalogs as well as single contributions, with more on the way.'
|
||||
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.open_library_plugin:OpenLibraryStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1398,7 +1398,7 @@ class StoreOpenLibraryStore(StoreBase):
|
||||
class StoreOReillyStore(StoreBase):
|
||||
name = 'OReilly'
|
||||
description = u'Programming and tech ebooks from OReilly.'
|
||||
actual_plugin = 'calibre.gui2.store.oreilly_plugin:OReillyStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.oreilly_plugin:OReillyStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1407,7 +1407,7 @@ class StoreOReillyStore(StoreBase):
|
||||
class StorePragmaticBookshelfStore(StoreBase):
|
||||
name = 'Pragmatic Bookshelf'
|
||||
description = u'The Pragmatic Bookshelf\'s collection of programming and tech books avaliable as ebooks.'
|
||||
actual_plugin = 'calibre.gui2.store.pragmatic_bookshelf_plugin:PragmaticBookshelfStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.pragmatic_bookshelf_plugin:PragmaticBookshelfStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1416,7 +1416,7 @@ class StorePragmaticBookshelfStore(StoreBase):
|
||||
class StoreSmashwordsStore(StoreBase):
|
||||
name = 'Smashwords'
|
||||
description = u'An ebook publishing and distribution platform for ebook authors, publishers and readers. Covers many genres and formats.'
|
||||
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.smashwords_plugin:SmashwordsStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1427,7 +1427,7 @@ class StoreVirtualoStore(StoreBase):
|
||||
name = 'Virtualo'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.'
|
||||
actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.virtualo_plugin:VirtualoStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1436,7 +1436,7 @@ class StoreWaterstonesUKStore(StoreBase):
|
||||
name = 'Waterstones UK'
|
||||
author = 'Charles Haley'
|
||||
description = u'Waterstone\'s mission is to be the leading Bookseller on the High Street and online providing customers the widest choice, great value and expert advice from a team passionate about Bookselling.'
|
||||
actual_plugin = 'calibre.gui2.store.waterstones_uk_plugin:WaterstonesUKStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.waterstones_uk_plugin:WaterstonesUKStore'
|
||||
|
||||
headquarters = 'UK'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1444,7 +1444,7 @@ class StoreWaterstonesUKStore(StoreBase):
|
||||
class StoreWeightlessBooksStore(StoreBase):
|
||||
name = 'Weightless Books'
|
||||
description = u'An independent DRM-free ebooksite devoted to ebooks of all sorts.'
|
||||
actual_plugin = 'calibre.gui2.store.weightless_books_plugin:WeightlessBooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.weightless_books_plugin:WeightlessBooksStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'US'
|
||||
@ -1454,7 +1454,7 @@ class StoreWHSmithUKStore(StoreBase):
|
||||
name = 'WH Smith UK'
|
||||
author = 'Charles Haley'
|
||||
description = u"Shop for savings on Books, discounted Magazine subscriptions and great prices on Stationery, Toys & Games"
|
||||
actual_plugin = 'calibre.gui2.store.whsmith_uk_plugin:WHSmithUKStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.whsmith_uk_plugin:WHSmithUKStore'
|
||||
|
||||
headquarters = 'UK'
|
||||
formats = ['EPUB', 'PDF']
|
||||
@ -1462,7 +1462,7 @@ class StoreWHSmithUKStore(StoreBase):
|
||||
class StoreWizardsTowerBooksStore(StoreBase):
|
||||
name = 'Wizards Tower Books'
|
||||
description = u'A science fiction and fantasy publisher. Concentrates mainly on making out-of-print works available once more as e-books, and helping other small presses exploit the e-book market. Also publishes a small number of limited-print-run anthologies with a view to encouraging diversity in the science fiction and fantasy field.'
|
||||
actual_plugin = 'calibre.gui2.store.wizards_tower_books_plugin:WizardsTowerBooksStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.wizards_tower_books_plugin:WizardsTowerBooksStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'UK'
|
||||
@ -1472,7 +1472,7 @@ class StoreWoblinkStore(StoreBase):
|
||||
name = 'Woblink'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Czytanie zdarza się wszędzie!'
|
||||
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.woblink_plugin:WoblinkStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB']
|
||||
@ -1481,7 +1481,7 @@ class StoreZixoStore(StoreBase):
|
||||
name = 'Zixo'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'Księgarnia z ebookami oraz książkami audio. Aby otwierać książki w formacie Zixo należy zainstalować program dostępny na stronie księgarni. Umożliwia on m.in. dodawanie zakładek i dostosowywanie rozmiaru czcionki.'
|
||||
actual_plugin = 'calibre.gui2.store.zixo_plugin:ZixoStore'
|
||||
actual_plugin = 'calibre.gui2.store.stores.zixo_plugin:ZixoStore'
|
||||
|
||||
headquarters = 'PL'
|
||||
formats = ['PDF, ZIXO']
|
||||
|
@ -1,89 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class ArchiveOrgStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://www.archive.org/details/texts'
|
||||
|
||||
if detail_item:
|
||||
detail_item = url_slash_cleaner('http://www.archive.org' + detail_item)
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
query = query + ' AND mediatype:texts'
|
||||
url = 'http://www.archive.org/search.php?query=' + urllib.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//td[@class="hitCell"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[@class="titleLink"]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('.//a[@class="titleLink"]//text()'))
|
||||
authors = data.xpath('.//text()')
|
||||
if not authors:
|
||||
continue
|
||||
author = None
|
||||
for a in authors:
|
||||
if '-' in a:
|
||||
author = a.replace('-', ' ').strip()
|
||||
if author:
|
||||
break
|
||||
if not author:
|
||||
continue
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = '$0.00'
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
|
||||
yield s
|
||||
|
||||
def get_details(self, search_result, timeout):
|
||||
url = url_slash_cleaner('http://www.archive.org' + search_result.detail_item)
|
||||
|
||||
br = browser()
|
||||
with closing(br.open(url, timeout=timeout)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
formats = ', '.join(idata.xpath('//p[@id="dl" and @class="content"]//a/text()'))
|
||||
search_result.formats = formats.upper()
|
||||
|
||||
return True
|
@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class EpubBudStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://epubbud.com/'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
OPDS based search.
|
||||
|
||||
We really should get the catelog from http://pragprog.com/catalog.opds
|
||||
and look for the application/opensearchdescription+xml entry.
|
||||
Then get the opensearch description to get the search url and
|
||||
format. However, we are going to be lazy and hard code it.
|
||||
'''
|
||||
url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
# Use html instead of etree as html allows us
|
||||
# to ignore the namespace easily.
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//entry'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//id/text()'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href'))
|
||||
|
||||
title = u''.join(data.xpath('.//title/text()'))
|
||||
author = u''.join(data.xpath('.//author/name/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = '$0.00'
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB'
|
||||
|
||||
yield s
|
@ -1,106 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib2
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class FeedbooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://m.feedbooks.com/'
|
||||
ext_url = 'http://feedbooks.com/'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
if detail_item:
|
||||
ext_url = ext_url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(ext_url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://m.feedbooks.com/search?query=' + urllib2.quote(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//ul[@class="m-list"]//li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
data = html.fromstring(html.tostring(data))
|
||||
|
||||
id = ''
|
||||
id_a = data.xpath('//a[@class="buy"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
id = id.split('/')[-2]
|
||||
id = '/item/' + id
|
||||
else:
|
||||
id_a = data.xpath('//a[@class="download"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
id = id.split('/')[-1]
|
||||
id = id.split('.')[0]
|
||||
id = '/book/' + id
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('//h5//a/text()'))
|
||||
author = ''.join(data.xpath('//h6//a/text()'))
|
||||
price = ''.join(data.xpath('//a[@class="buy"]/text()'))
|
||||
formats = 'EPUB'
|
||||
if not price:
|
||||
price = '$0.00'
|
||||
formats = 'EPUB, MOBI, PDF'
|
||||
cover_url = ''
|
||||
cover_url_img = data.xpath('//img')
|
||||
if cover_url_img:
|
||||
cover_url = cover_url_img[0].get('src')
|
||||
cover_url.split('?')[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.replace(' ', '').strip()
|
||||
s.detail_item = id.strip()
|
||||
s.formats = formats
|
||||
|
||||
yield s
|
||||
|
||||
def get_details(self, search_result, timeout):
|
||||
url = 'http://m.feedbooks.com/'
|
||||
|
||||
br = browser()
|
||||
with closing(br.open(url_slash_cleaner(url + search_result.detail_item), timeout=timeout)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
if idata.xpath('boolean(//div[contains(@class, "m-description-long")]//p[contains(., "DRM") or contains(b, "Protection")])'):
|
||||
search_result.drm = SearchResult.DRM_LOCKED
|
||||
else:
|
||||
search_result.drm = SearchResult.DRM_UNLOCKED
|
||||
return True
|
75
src/calibre/gui2/store/opensearch_store.py
Normal file
75
src/calibre/gui2/store/opensearch_store.py
Normal file
@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import mimetypes
|
||||
import urllib
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
from calibre.utils.opensearch import Client
|
||||
|
||||
class OpenSearchStore(StorePlugin):
|
||||
|
||||
open_search_url = ''
|
||||
web_url = ''
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
if not hasattr(self, 'web_url'):
|
||||
return
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(detail_item if detail_item else self.web_url))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, self.web_url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
if not hasattr(self, 'open_search_url'):
|
||||
return
|
||||
|
||||
client = Client(self.open_search_url)
|
||||
results = client.search(urllib.quote_plus(query), max_results)
|
||||
|
||||
counter = max_results
|
||||
for r in results:
|
||||
if counter <= 0:
|
||||
break
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
|
||||
s.detail_item = r.get('id', '')
|
||||
|
||||
links = r.get('links', None)
|
||||
for l in links:
|
||||
if l.get('rel', None):
|
||||
if l['rel'] in ('http://opds-spec.org/thumbnail', 'http://opds-spec.org/image/thumbnail'):
|
||||
s.cover_url = l.get('href', '')
|
||||
elif l['rel'] == u'http://opds-spec.org/acquisition/buy':
|
||||
s.detail_item = l.get('href', s.detail_item)
|
||||
elif l['rel'] == u'http://opds-spec.org/acquisition':
|
||||
mime = l.get('type', '')
|
||||
if mime:
|
||||
ext = mimetypes.guess_extension(mime)
|
||||
if ext:
|
||||
ext = ext[1:].upper()
|
||||
s.downloads[ext] = l.get('href', '')
|
||||
|
||||
s.formats = ', '.join(s.downloads.keys())
|
||||
|
||||
s.title = r.get('title', '')
|
||||
s.author = r.get('author', '')
|
||||
s.price = r.get('price', '')
|
||||
|
||||
yield s
|
@ -1,84 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class PragmaticBookshelfStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://pragprog.com/'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
OPDS based search.
|
||||
|
||||
We really should get the catelog from http://pragprog.com/catalog.opds
|
||||
and look for the application/opensearchdescription+xml entry.
|
||||
Then get the opensearch description to get the search url and
|
||||
format. However, we are going to be lazy and hard code it.
|
||||
'''
|
||||
url = 'http://pragprog.com/catalog/search?q=' + urllib.quote_plus(query)
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
# Use html instead of etree as html allows us
|
||||
# to ignore the namespace easily.
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//entry'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/acquisition/buy"]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
price = ''.join(data.xpath('.//price/@currencycode')).strip()
|
||||
price += ' '
|
||||
price += ''.join(data.xpath('.//price/text()')).strip()
|
||||
if not price.strip():
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/cover"]/@href'))
|
||||
|
||||
title = ''.join(data.xpath('.//title/text()'))
|
||||
author = ''.join(data.xpath('.//author//text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB, PDF, MOBI'
|
||||
|
||||
yield s
|
@ -45,6 +45,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
|
||||
self.author_box.setText('')
|
||||
self.price_box.setText('')
|
||||
self.format_box.setText('')
|
||||
self.download_combo.setCurrentIndex(0)
|
||||
self.affiliate_combo.setCurrentIndex(0)
|
||||
|
||||
def tokens(self, raw):
|
||||
@ -119,6 +120,9 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
|
||||
format = unicode(self.format_box.text()).strip()
|
||||
if format:
|
||||
ans.append('format:"' + self.mc + format + '"')
|
||||
download = unicode(self.download_combo.currentText()).strip()
|
||||
if download:
|
||||
ans.append('download:' + download)
|
||||
affiliate = unicode(self.affiliate_combo.currentText()).strip()
|
||||
if affiliate:
|
||||
ans.append('affiliate:' + affiliate)
|
||||
|
@ -226,7 +226,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<item row="8" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QPushButton" name="clear_button">
|
||||
@ -244,7 +244,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="7" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@ -283,14 +283,14 @@
|
||||
<item row="3" column="1">
|
||||
<widget class="EnLineEdit" name="price_box"/>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Affiliate:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QComboBox" name="affiliate_combo">
|
||||
<item>
|
||||
<property name="text">
|
||||
@ -309,6 +309,32 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string>Download:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="download_combo">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>true</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>false</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -33,7 +33,7 @@ class Matches(QAbstractItemModel):
|
||||
|
||||
total_changed = pyqtSignal(int)
|
||||
|
||||
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), '']
|
||||
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store'), _('Download'), _('Affiliate')]
|
||||
HTML_COLS = (1, 4)
|
||||
|
||||
def __init__(self, cover_thread_count=2, detail_thread_count=4):
|
||||
@ -47,6 +47,8 @@ class Matches(QAbstractItemModel):
|
||||
Qt.SmoothTransformation)
|
||||
self.DONATE_ICON = QPixmap(I('donate.png')).scaledToHeight(16,
|
||||
Qt.SmoothTransformation)
|
||||
self.DOWNLOAD_ICON = QPixmap(I('arrow-down.png')).scaledToHeight(16,
|
||||
Qt.SmoothTransformation)
|
||||
|
||||
# All matches. Used to determine the order to display
|
||||
# self.matches because the SearchFilter returns
|
||||
@ -181,9 +183,11 @@ class Matches(QAbstractItemModel):
|
||||
elif result.drm == SearchResult.DRM_UNKNOWN:
|
||||
return QVariant(self.DRM_UNKNOWN_ICON)
|
||||
if col == 5:
|
||||
if result.downloads:
|
||||
return QVariant(self.DOWNLOAD_ICON)
|
||||
if col == 6:
|
||||
if result.affiliate:
|
||||
return QVariant(self.DONATE_ICON)
|
||||
return NONE
|
||||
elif role == Qt.ToolTipRole:
|
||||
if col == 1:
|
||||
return QVariant('<p>%s</p>' % result.title)
|
||||
@ -199,6 +203,9 @@ class Matches(QAbstractItemModel):
|
||||
elif col == 4:
|
||||
return QVariant('<p>%s</p>' % result.formats)
|
||||
elif col == 5:
|
||||
if result.downloads:
|
||||
return QVariant('<p>' + _('The following formats can be downloaded directly: %s.') % ', '.join(result.downloads.keys()) + '</p>')
|
||||
elif col == 6:
|
||||
if result.affiliate:
|
||||
return QVariant('<p>' + _('Buying from this store supports the calibre developer: %s.') % result.plugin_author + '</p>')
|
||||
elif role == Qt.SizeHintRole:
|
||||
@ -221,6 +228,11 @@ class Matches(QAbstractItemModel):
|
||||
elif col == 4:
|
||||
text = result.store_name
|
||||
elif col == 5:
|
||||
if result.downloads:
|
||||
text = 'a'
|
||||
else:
|
||||
text = 'b'
|
||||
elif col == 6:
|
||||
if result.affiliate:
|
||||
text = 'a'
|
||||
else:
|
||||
@ -257,6 +269,8 @@ class SearchFilter(SearchQueryParser):
|
||||
'author',
|
||||
'authors',
|
||||
'cover',
|
||||
'download',
|
||||
'downloads',
|
||||
'drm',
|
||||
'format',
|
||||
'formats',
|
||||
@ -282,6 +296,8 @@ class SearchFilter(SearchQueryParser):
|
||||
location = location.lower().strip()
|
||||
if location == 'authors':
|
||||
location = 'author'
|
||||
elif location == 'downloads':
|
||||
location = 'download'
|
||||
elif location == 'formats':
|
||||
location = 'format'
|
||||
|
||||
@ -308,12 +324,13 @@ class SearchFilter(SearchQueryParser):
|
||||
'author': lambda x: x.author.lower(),
|
||||
'cover': attrgetter('cover_url'),
|
||||
'drm': attrgetter('drm'),
|
||||
'download': attrgetter('downloads'),
|
||||
'format': attrgetter('formats'),
|
||||
'price': lambda x: comparable_price(x.price),
|
||||
'store': lambda x: x.store_name.lower(),
|
||||
'title': lambda x: x.title.lower(),
|
||||
}
|
||||
for x in ('author', 'format'):
|
||||
for x in ('author', 'download', 'format'):
|
||||
q[x+'s'] = q[x]
|
||||
for sr in self.srs:
|
||||
for locvalue in locations:
|
||||
@ -347,7 +364,7 @@ class SearchFilter(SearchQueryParser):
|
||||
matches.add(sr)
|
||||
continue
|
||||
# this is bool or treated as bool, so can't match below.
|
||||
if locvalue in ('affiliate', 'drm'):
|
||||
if locvalue in ('affiliate', 'drm', 'download', 'downloads'):
|
||||
continue
|
||||
try:
|
||||
### Can't separate authors because comma is used for name sep and author sep
|
||||
|
@ -6,13 +6,18 @@ __license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (QTreeView)
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import (pyqtSignal, QMenu, QTreeView)
|
||||
|
||||
from calibre.gui2.metadata.single_download import RichTextDelegate
|
||||
from calibre.gui2.store.search.models import Matches
|
||||
|
||||
class ResultsView(QTreeView):
|
||||
|
||||
download_requested = pyqtSignal(object)
|
||||
open_requested = pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args):
|
||||
QTreeView.__init__(self,*args)
|
||||
|
||||
@ -24,3 +29,18 @@ class ResultsView(QTreeView):
|
||||
for i in self._model.HTML_COLS:
|
||||
self.setItemDelegateForColumn(i, self.rt_delegate)
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
index = self.indexAt(event.pos())
|
||||
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
result = self.model().get_result(index)
|
||||
|
||||
menu = QMenu()
|
||||
da = menu.addAction(_('Download...'), partial(self.download_requested.emit, result))
|
||||
if not result.downloads:
|
||||
da.setEnabled(False)
|
||||
menu.addSeparator()
|
||||
menu.addAction(_('Goto in store...'), partial(self.open_requested.emit, result))
|
||||
menu.exec_(event.globalPos())
|
||||
|
@ -14,6 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, QLabel,
|
||||
QComboBox)
|
||||
|
||||
from calibre.gui2 import JSONConfig, info_dialog
|
||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||
from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
|
||||
from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
|
||||
@ -72,7 +73,9 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
self.search.clicked.connect(self.do_search)
|
||||
self.checker.timeout.connect(self.get_results)
|
||||
self.progress_checker.timeout.connect(self.check_progress)
|
||||
self.results_view.activated.connect(self.open_store)
|
||||
self.results_view.activated.connect(self.result_item_activated)
|
||||
self.results_view.download_requested.connect(self.download_book)
|
||||
self.results_view.open_requested.connect(self.open_store)
|
||||
self.results_view.model().total_changed.connect(self.update_book_total)
|
||||
self.select_all_stores.clicked.connect(self.stores_select_all)
|
||||
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
||||
@ -129,11 +132,15 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
# Title / Author
|
||||
self.results_view.setColumnWidth(1,int(total*.40))
|
||||
# Price
|
||||
self.results_view.setColumnWidth(2,int(total*.20))
|
||||
self.results_view.setColumnWidth(2,int(total*.12))
|
||||
# DRM
|
||||
self.results_view.setColumnWidth(3, int(total*.15))
|
||||
# Store / Formats
|
||||
self.results_view.setColumnWidth(4, int(total*.25))
|
||||
# Download
|
||||
self.results_view.setColumnWidth(5, 20)
|
||||
# Affiliate
|
||||
self.results_view.setColumnWidth(6, 20)
|
||||
|
||||
def do_search(self):
|
||||
# Stop all running threads.
|
||||
@ -183,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
query = re.sub(r'%s:"(?P<a>[^\s"]+)"' % loc, '\g<a>', query)
|
||||
query = query.replace('%s:' % loc, '')
|
||||
# Remove the prefix and search text.
|
||||
for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'):
|
||||
for loc in ('cover', 'download', 'downloads', 'drm', 'format', 'formats', 'price', 'store'):
|
||||
query = re.sub(r'%s:"[^"]"' % loc, '', query)
|
||||
query = re.sub(r'%s:[^\s]*' % loc, '', query)
|
||||
# Remove logic.
|
||||
@ -330,8 +337,21 @@ class SearchDialog(QDialog, Ui_Dialog):
|
||||
def update_book_total(self, total):
|
||||
self.total.setText('%s' % total)
|
||||
|
||||
def open_store(self, index):
|
||||
def result_item_activated(self, index):
|
||||
result = self.results_view.model().get_result(index)
|
||||
|
||||
if result.downloads:
|
||||
self.download_book(result)
|
||||
else:
|
||||
self.open_store(result)
|
||||
|
||||
def download_book(self, result):
|
||||
d = ChooseFormatDialog(self, _('Choose format to download to your library.'), result.downloads.keys())
|
||||
if d.exec_() == d.Accepted:
|
||||
ext = d.format()
|
||||
self.gui.download_ebook(result.downloads[ext])
|
||||
|
||||
def open_store(self, result):
|
||||
self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
|
||||
|
||||
def check_progress(self):
|
||||
|
@ -22,6 +22,9 @@ class SearchResult(object):
|
||||
self.detail_item = ''
|
||||
self.drm = None
|
||||
self.formats = ''
|
||||
# key = format in upper case.
|
||||
# value = url to download the file.
|
||||
self.downloads = {}
|
||||
self.affiliate = False
|
||||
self.plugin_author = ''
|
||||
|
||||
|
3
src/calibre/gui2/store/stores/__init__.py
Normal file
3
src/calibre/gui2/store/stores/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
'''
|
||||
All store plugins are placed here.
|
||||
'''
|
36
src/calibre/gui2/store/stores/archive_org_plugin.py
Normal file
36
src/calibre/gui2/store/stores/archive_org_plugin.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.opensearch_store import OpenSearchStore
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class ArchiveOrgStore(BasicStoreConfig, OpenSearchStore):
|
||||
|
||||
open_search_url = 'http://bookserver.archive.org/catalog/opensearch.xml'
|
||||
web_url = 'http://www.archive.org/details/texts'
|
||||
|
||||
# http://bookserver.archive.org/catalog/
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
for s in OpenSearchStore.search(self, query, max_results, timeout):
|
||||
s.detail_item = 'http://www.archive.org/details/' + s.detail_item.split(':')[-1]
|
||||
s.price = '$0.00'
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
yield s
|
||||
'''
|
||||
def get_details(self, search_result, timeout):
|
||||
br = browser()
|
||||
with closing(br.open(search_result.detail_item, timeout=timeout)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
formats = ', '.join(idata.xpath('//p[@id="dl" and @class="content"]//a/text()'))
|
||||
search_result.formats = formats.upper()
|
||||
|
||||
return True
|
||||
'''
|
27
src/calibre/gui2/store/stores/epubbud_plugin.py
Normal file
27
src/calibre/gui2/store/stores/epubbud_plugin.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.opensearch_store import OpenSearchStore
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class EpubBudStore(BasicStoreConfig, OpenSearchStore):
|
||||
|
||||
open_search_url = 'http://www.epubbud.com/feeds/opensearch.xml'
|
||||
web_url = 'http://www.epubbud.com/'
|
||||
|
||||
# http://www.epubbud.com/feeds/catalog.atom
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
for s in OpenSearchStore.search(self, query, max_results, timeout):
|
||||
s.price = '$0.00'
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB'
|
||||
# Download links are broken for this store.
|
||||
s.downloads = {}
|
||||
yield s
|
28
src/calibre/gui2/store/stores/feedbooks_plugin.py
Normal file
28
src/calibre/gui2/store/stores/feedbooks_plugin.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.opensearch_store import OpenSearchStore
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class FeedbooksStore(BasicStoreConfig, OpenSearchStore):
|
||||
|
||||
open_search_url = 'http://assets0.feedbooks.net/opensearch.xml?t=1253087147'
|
||||
web_url = 'http://feedbooks.com/'
|
||||
|
||||
# http://www.feedbooks.com/catalog
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
for s in OpenSearchStore.search(self, query, max_results, timeout):
|
||||
if s.downloads:
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.price = '$0.00'
|
||||
else:
|
||||
s.drm = SearchResult.DRM_LOCKED
|
||||
s.formats = 'EPUB'
|
||||
yield s
|
@ -20,7 +20,7 @@ from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
|
||||
class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
@ -29,7 +29,7 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
@ -44,16 +44,16 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
# secondary titles. Google is also faster.
|
||||
# Using a google search so we can search on both fields at once.
|
||||
url = 'http://www.google.com/xhtml?q=site:manybooks.net+' + urllib.quote_plus(query)
|
||||
|
||||
|
||||
br = browser()
|
||||
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//div[@class="edewpi"]//div[@class="r ld"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
|
||||
url = ''
|
||||
url_a = data.xpath('div[@class="jd"]/a')
|
||||
if url_a:
|
||||
@ -65,13 +65,13 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
continue
|
||||
id = url.split('/')[-1]
|
||||
id = id.strip()
|
||||
|
||||
|
||||
url_a = html.fromstring(html.tostring(url_a))
|
||||
heading = ''.join(url_a.xpath('//text()'))
|
||||
title, _, author = heading.rpartition('by ')
|
||||
author = author.split('-')[0]
|
||||
price = '$0.00'
|
||||
|
||||
|
||||
cover_url = ''
|
||||
mo = re.match('^\D+', id)
|
||||
if mo:
|
||||
@ -79,10 +79,9 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
cover_name = cover_name.replace('etext', '')
|
||||
cover_id = id.split('.')[0]
|
||||
cover_url = 'http://www.manybooks.net/images/' + id[0] + '/' + cover_name + '/' + cover_id + '-thumb.jpg'
|
||||
print(cover_url)
|
||||
|
||||
counter -= 1
|
||||
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
@ -91,5 +90,5 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
s.detail_item = '/titles/' + id
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formts = 'EPUB, PDB (eReader, PalmDoc, zTXT, Plucker, iSilo), FB2, ZIP, AZW, MOBI, PRC, LIT, PKG, PDF, TXT, RB, RTF, LRF, TCR, JAR'
|
||||
|
||||
|
||||
yield s
|
@ -10,7 +10,7 @@ import re
|
||||
|
||||
from PyQt4.Qt import (QDialog, QDialogButtonBox)
|
||||
|
||||
from calibre.gui2.store.mobileread.adv_search_builder_ui import Ui_Dialog
|
||||
from calibre.gui2.store.stores.mobileread.adv_search_builder_ui import Ui_Dialog
|
||||
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||
|
||||
class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QDialog
|
||||
|
||||
from calibre.gui2.store.mobileread.cache_progress_dialog_ui import Ui_Dialog
|
||||
from calibre.gui2.store.stores.mobileread.cache_progress_dialog_ui import Ui_Dialog
|
||||
|
||||
class CacheProgressDialog(QDialog, Ui_Dialog):
|
||||
|
@ -15,10 +15,10 @@ from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
from calibre.gui2.store.mobileread.models import SearchFilter
|
||||
from calibre.gui2.store.mobileread.cache_progress_dialog import CacheProgressDialog
|
||||
from calibre.gui2.store.mobileread.cache_update_thread import CacheUpdateThread
|
||||
from calibre.gui2.store.mobileread.store_dialog import MobileReadStoreDialog
|
||||
from calibre.gui2.store.stores.mobileread.models import SearchFilter
|
||||
from calibre.gui2.store.stores.mobileread.cache_progress_dialog import CacheProgressDialog
|
||||
from calibre.gui2.store.stores.mobileread.cache_update_thread import CacheUpdateThread
|
||||
from calibre.gui2.store.stores.mobileread.store_dialog import MobileReadStoreDialog
|
||||
|
||||
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
||||
|
@ -9,9 +9,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import (Qt, QDialog, QIcon, QComboBox)
|
||||
|
||||
from calibre.gui2.store.mobileread.adv_search_builder import AdvSearchBuilderDialog
|
||||
from calibre.gui2.store.mobileread.models import BooksModel
|
||||
from calibre.gui2.store.mobileread.store_dialog_ui import Ui_Dialog
|
||||
from calibre.gui2.store.stores.mobileread.adv_search_builder import AdvSearchBuilderDialog
|
||||
from calibre.gui2.store.stores.mobileread.models import BooksModel
|
||||
from calibre.gui2.store.stores.mobileread.store_dialog_ui import Ui_Dialog
|
||||
|
||||
class MobileReadStoreDialog(QDialog, Ui_Dialog):
|
||||
|
24
src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py
Normal file
24
src/calibre/gui2/store/stores/pragmatic_bookshelf_plugin.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.opensearch_store import OpenSearchStore
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class PragmaticBookshelfStore(BasicStoreConfig, OpenSearchStore):
|
||||
|
||||
open_search_url = 'http://pragprog.com/catalog/search-description'
|
||||
web_url = 'http://pragprog.com/'
|
||||
|
||||
# http://pragprog.com/catalog.opds
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
for s in OpenSearchStore.search(self, query, max_results, timeout):
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = 'EPUB, PDF, MOBI'
|
||||
yield s
|
6
src/calibre/utils/opensearch/__init__.py
Normal file
6
src/calibre/utils/opensearch/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from description import Description
|
||||
from query import Query
|
||||
from client import Client
|
||||
from results import Results
|
||||
|
||||
Description, Query, Client, Results
|
39
src/calibre/utils/opensearch/client.py
Normal file
39
src/calibre/utils/opensearch/client.py
Normal file
@ -0,0 +1,39 @@
|
||||
from description import Description
|
||||
from query import Query
|
||||
from results import Results
|
||||
|
||||
class Client:
|
||||
|
||||
"""This is the class you'll probably want to be using. You simply
|
||||
pass the constructor the url for the service description file and
|
||||
issue a search and get back results as an iterable Results object.
|
||||
|
||||
The neat thing about a Results object is that it will seamlessly
|
||||
handle fetching more results from the opensearch server when it can...
|
||||
so you just need to iterate and can let the paging be taken care of
|
||||
for you.
|
||||
|
||||
from opensearch import Client
|
||||
client = Client(description_url)
|
||||
results = client.search("computer")
|
||||
for result in results:
|
||||
print result.title
|
||||
"""
|
||||
|
||||
def __init__(self, url, agent="python-opensearch <https://github.com/edsu/opensearch>"):
|
||||
self.agent = agent
|
||||
self.description = Description(url, self.agent)
|
||||
|
||||
def search(self, search_terms, page_size=25):
|
||||
"""Perform a search and get back a results object
|
||||
"""
|
||||
url = self.description.get_best_template()
|
||||
query = Query(url)
|
||||
|
||||
# set up initial values
|
||||
query.searchTerms = search_terms
|
||||
query.count = page_size
|
||||
|
||||
# run the results
|
||||
return Results(query, agent=self.agent)
|
||||
|
127
src/calibre/utils/opensearch/description.py
Normal file
127
src/calibre/utils/opensearch/description.py
Normal file
@ -0,0 +1,127 @@
|
||||
from urllib2 import urlopen, Request
|
||||
from xml.dom.minidom import parse
|
||||
from url import URL
|
||||
|
||||
class Description:
|
||||
"""A class for representing OpenSearch Description files.
|
||||
"""
|
||||
|
||||
def __init__(self, url="", agent=""):
|
||||
"""The constructor which may pass an optional url to load from.
|
||||
|
||||
d = Description("http://www.example.com/description")
|
||||
"""
|
||||
self.agent = agent
|
||||
if url:
|
||||
self.load(url)
|
||||
|
||||
|
||||
def load(self, url):
|
||||
"""For loading up a description object from a url. Normally
|
||||
you'll probably just want to pass a URL into the constructor.
|
||||
"""
|
||||
req = Request(url, headers={'User-Agent':self.agent})
|
||||
self.dom = parse(urlopen(req))
|
||||
|
||||
# version 1.1 has repeating Url elements
|
||||
self.urls = self._get_urls()
|
||||
|
||||
# this is version 1.0 specific
|
||||
self.url = self._get_element_text('Url')
|
||||
self.format = self._get_element_text('Format')
|
||||
|
||||
self.shortname = self._get_element_text('ShortName')
|
||||
self.longname = self._get_element_text('LongName')
|
||||
self.description = self._get_element_text('Description')
|
||||
self.image = self._get_element_text('Image')
|
||||
self.samplesearch = self._get_element_text('SampleSearch')
|
||||
self.developer = self._get_element_text('Developer')
|
||||
self.contact = self._get_element_text('Contact')
|
||||
self.attribution = self._get_element_text('Attribution')
|
||||
self.syndicationright = self._get_element_text('SyndicationRight')
|
||||
|
||||
tag_text = self._get_element_text('Tags')
|
||||
if tag_text != None:
|
||||
self.tags = tag_text.split(" ")
|
||||
|
||||
if self._get_element_text('AdultContent') == 'true':
|
||||
self.adultcontent = True
|
||||
else:
|
||||
self.adultcontent = False
|
||||
|
||||
def get_url_by_type(self, type):
|
||||
"""Walks available urls and returns them by type. Only
|
||||
appropriate in opensearch v1.1 where there can be multiple
|
||||
query targets. Returns none if no such type is found.
|
||||
|
||||
url = description.get_url_by_type('application/rss+xml')
|
||||
"""
|
||||
for url in self.urls:
|
||||
if url.type == type:
|
||||
return url
|
||||
return None
|
||||
|
||||
def get_best_template(self):
|
||||
"""OK, best is a value judgement, but so be it. You'll get
|
||||
back either the atom, rss or first template available. This
|
||||
method handles the main difference between opensearch v1.0 and v1.1
|
||||
"""
|
||||
# version 1.0
|
||||
if self.url:
|
||||
return self.url
|
||||
|
||||
# atom
|
||||
if self.get_url_by_type('application/atom+xml'):
|
||||
return self.get_url_by_type('application/atom+xml').template
|
||||
|
||||
# rss
|
||||
if self.get_url_by_type('application/rss+xml'):
|
||||
return self.get_url_by_type('application/rss+xml').template
|
||||
|
||||
# other possible rss type
|
||||
if self.get_url_by_type('text/xml'):
|
||||
return self.get_url_by_Type('text/xml').template
|
||||
|
||||
# otherwise just the first one
|
||||
if len(self.urls) > 0:
|
||||
return self.urls[0].template
|
||||
|
||||
# out of luck
|
||||
return None
|
||||
|
||||
|
||||
# these are internal methods for querying xml
|
||||
|
||||
def _get_element_text(self, tag):
|
||||
elements = self._get_elements(tag)
|
||||
if not elements:
|
||||
return None
|
||||
return self._get_text(elements[0].childNodes)
|
||||
|
||||
def _get_attribute_text(self, tag, attribute):
|
||||
elements = self._get_elements(tag)
|
||||
if not elements:
|
||||
return ''
|
||||
return elements[0].getAttribute('template')
|
||||
|
||||
def _get_elements(self, tag):
|
||||
return self.dom.getElementsByTagName(tag)
|
||||
|
||||
def _get_text(self, nodes):
|
||||
text = ''
|
||||
for node in nodes:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
text += node.data
|
||||
return text.strip()
|
||||
|
||||
def _get_urls(self):
|
||||
urls = []
|
||||
for element in self._get_elements('Url'):
|
||||
template = element.getAttribute('template')
|
||||
type = element.getAttribute('type')
|
||||
if template and type:
|
||||
url = URL()
|
||||
url.template = template
|
||||
url.type = type
|
||||
urls.append(url)
|
||||
return urls
|
2799
src/calibre/utils/opensearch/osfeedparser.py
Normal file
2799
src/calibre/utils/opensearch/osfeedparser.py
Normal file
File diff suppressed because it is too large
Load Diff
66
src/calibre/utils/opensearch/query.py
Normal file
66
src/calibre/utils/opensearch/query.py
Normal file
@ -0,0 +1,66 @@
|
||||
from urlparse import urlparse, urlunparse
|
||||
from urllib import urlencode
|
||||
from cgi import parse_qs
|
||||
|
||||
class Query:
|
||||
"""Represents an opensearch query. Used internally by the Client to
|
||||
construct an opensearch url to request. Really this class is just a
|
||||
helper for substituting values into the macros in a format.
|
||||
|
||||
format = 'http://beta.indeed.com/opensearch?q={searchTerms}&start={startIndex}&limit={count}'
|
||||
q = Query(format)
|
||||
q.searchTerms('zx81')
|
||||
q.startIndex = 1
|
||||
q.count = 25
|
||||
print q.to_url()
|
||||
"""
|
||||
|
||||
standard_macros = ['searchTerms','count','startIndex','startPage',
|
||||
'language', 'outputEncoding', 'inputEncoding']
|
||||
|
||||
def __init__(self, format):
|
||||
"""Create a query object by passing it the url format obtained
|
||||
from the opensearch Description.
|
||||
"""
|
||||
self.format = format
|
||||
|
||||
# unpack the url to a tuple
|
||||
self.url_parts = urlparse(format)
|
||||
|
||||
# unpack the query string to a dictionary
|
||||
self.query_string = parse_qs(self.url_parts[4])
|
||||
|
||||
# look for standard macros and create a mapping of the
|
||||
# opensearch names to the service specific ones
|
||||
# so q={searchTerms} will result in a mapping between searchTerms and q
|
||||
self.macro_map = {}
|
||||
for key,values in self.query_string.items():
|
||||
# TODO eventually optional/required params should be
|
||||
# distinguished somehow (the ones with/without trailing ?
|
||||
macro = values[0].replace('{','').replace('}','').replace('?','')
|
||||
if macro in Query.standard_macros:
|
||||
self.macro_map[macro] = key
|
||||
|
||||
def url(self):
|
||||
# copy the original query string
|
||||
query_string = dict(self.query_string)
|
||||
|
||||
# iterate through macros and set the position in the querystring
|
||||
for macro, name in self.macro_map.items():
|
||||
if hasattr(self, macro):
|
||||
# set the name/value pair
|
||||
query_string[name] = [getattr(self, macro)]
|
||||
else:
|
||||
# remove the name/value pair
|
||||
del(query_string[name])
|
||||
|
||||
# copy the url parts and substitute in our new query string
|
||||
url_parts = list(self.url_parts)
|
||||
url_parts[4] = urlencode(query_string, 1)
|
||||
|
||||
# recompose and return url
|
||||
return urlunparse(tuple(url_parts))
|
||||
|
||||
def has_macro(self, macro):
|
||||
return self.macro_map.has_key(macro)
|
||||
|
131
src/calibre/utils/opensearch/results.py
Normal file
131
src/calibre/utils/opensearch/results.py
Normal file
@ -0,0 +1,131 @@
|
||||
|
||||
class Results(object):
|
||||
|
||||
def __init__(self, query, agent=None):
|
||||
self.agent = agent
|
||||
self._fetch(query)
|
||||
self._iter = 0
|
||||
|
||||
def __iter__(self):
|
||||
self._iter = 0
|
||||
return self
|
||||
|
||||
def __len__(self):
|
||||
return self.totalResults
|
||||
|
||||
def next(self):
|
||||
|
||||
# just keep going like the energizer bunny
|
||||
while True:
|
||||
|
||||
# return any item we haven't returned
|
||||
if self._iter < len(self.items):
|
||||
self._iter += 1
|
||||
return self.items[self._iter-1]
|
||||
|
||||
# if there appears to be more to fetch
|
||||
if \
|
||||
self.totalResults != 0 \
|
||||
and self.totalResults > self.startIndex + self.itemsPerPage - 1:
|
||||
|
||||
# get the next query
|
||||
next_query = self._get_next_query()
|
||||
|
||||
# if we got one executed it and go back to the beginning
|
||||
if next_query:
|
||||
self._fetch(next_query)
|
||||
# very important to reset this counter
|
||||
# or else the return will fail
|
||||
self._iter = 0
|
||||
|
||||
else:
|
||||
raise StopIteration
|
||||
|
||||
|
||||
def _fetch(self, query):
|
||||
import osfeedparser
|
||||
feed = osfeedparser.opensearch_parse(query.url(), agent=self.agent)
|
||||
self.feed = feed
|
||||
|
||||
# general channel stuff
|
||||
channel = feed['feed']
|
||||
self.title = _pick(channel,'title')
|
||||
self.link = _pick(channel,'link')
|
||||
self.description = _pick(channel,'description')
|
||||
self.language = _pick(channel,'language')
|
||||
self.copyright = _pick(channel,'copyright')
|
||||
|
||||
# get back opensearch specific values
|
||||
self.totalResults = _pick(channel,'opensearch_totalresults',0)
|
||||
self.startIndex = _pick(channel,'opensearch_startindex',1)
|
||||
self.itemsPerPage = _pick(channel,'opensearch_itemsperpage',0)
|
||||
|
||||
# alias items from the feed to our results object
|
||||
self.items = feed['items']
|
||||
|
||||
# set default values if necessary
|
||||
if self.startIndex == 0:
|
||||
self.startIndex = 1
|
||||
if self.itemsPerPage == 0 and len(self.items) > 0:
|
||||
self.itemsPerPage = len(self.items)
|
||||
|
||||
# store away query for calculating next results
|
||||
# if necessary
|
||||
self.last_query = query
|
||||
|
||||
|
||||
def _get_next_query(self):
|
||||
# update our query to get the next set of records
|
||||
query = self.last_query
|
||||
|
||||
# use start page if the query supports it
|
||||
if query.has_macro('startPage'):
|
||||
# if the query already defined the startPage
|
||||
# we just need to increment it
|
||||
if hasattr(query, 'startPage'):
|
||||
query.startPage += 1
|
||||
# to issue the first query startPage might not have
|
||||
# been specified, so set it to 2
|
||||
else:
|
||||
query.startPage = 2
|
||||
return query
|
||||
|
||||
# otherwise the query should support startIndex
|
||||
elif query.has_macro('startIndex'):
|
||||
# if startIndex was used before we just add the
|
||||
# items per page to it to get the next set
|
||||
if hasattr(query, 'startIndex'):
|
||||
query.startIndex += self.itemsPerPage
|
||||
# to issue the first query the startIndex may have
|
||||
# been left blank in that case we assume it to be
|
||||
# the item just after the last one on this page
|
||||
else:
|
||||
query.startIndex = self.itemsPerPage + 1
|
||||
return query
|
||||
|
||||
# doesn't look like there is another stage to this query
|
||||
return None
|
||||
|
||||
|
||||
# helper for pulling values out of a dictionary if they're there
|
||||
# and returning a default value if they're not
|
||||
def _pick(d,key,default=None):
|
||||
|
||||
# get the value out
|
||||
value = d.get(key)
|
||||
|
||||
# if it wasn't there return the default
|
||||
if value == None:
|
||||
return default
|
||||
|
||||
# if they want an int try to convert to an int
|
||||
# and return default if it fails
|
||||
if type(default) == int:
|
||||
try:
|
||||
return int(d[key])
|
||||
except:
|
||||
return default
|
||||
|
||||
# otherwise we're good to return the value
|
||||
return value
|
||||
|
8
src/calibre/utils/opensearch/url.py
Normal file
8
src/calibre/utils/opensearch/url.py
Normal file
@ -0,0 +1,8 @@
|
||||
class URL:
|
||||
"""Class for representing a URL in an opensearch v1.1 query"""
|
||||
|
||||
def __init__(self, type='', template='', method='GET'):
|
||||
self.type = type
|
||||
self.template = template
|
||||
self.method = 'GET'
|
||||
self.params = []
|
Loading…
x
Reference in New Issue
Block a user