mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
11af1ca7a8
@ -1,4 +1,3 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
@ -19,11 +18,11 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
encoding = 'cp1252'
|
||||
needs_subscription = True
|
||||
remove_empty_feeds = True
|
||||
delay = 2
|
||||
delay = 1
|
||||
PREFIX = 'http://www.sueddeutsche.de'
|
||||
INDEX = PREFIX + '/app/epaper/textversion/'
|
||||
use_embedded_content = False
|
||||
masthead_url = 'http://pix.sueddeutsche.de/img/layout/header/logo.gif'
|
||||
masthead_url = 'http://pix.sueddeutsche.de/img/layout/header/SZ_solo288x31.gif'
|
||||
language = 'de'
|
||||
publication_type = 'newspaper'
|
||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} '
|
||||
@ -36,7 +35,7 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
, 'linearize_tables' : True
|
||||
}
|
||||
|
||||
remove_attributes = ['height','width']
|
||||
remove_attributes = ['height','width','style']
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
@ -50,7 +49,7 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
|
||||
remove_tags =[
|
||||
dict(attrs={'class':'hidePrint'})
|
||||
,dict(name=['link','object','embed','base','iframe'])
|
||||
,dict(name=['link','object','embed','base','iframe','br'])
|
||||
]
|
||||
keep_only_tags = [dict(attrs={'class':'artikelBox'})]
|
||||
remove_tags_before = dict(attrs={'class':'artikelTitel'})
|
||||
@ -68,6 +67,19 @@ class SueddeutcheZeitung(BasicNewsRecipe):
|
||||
,(u'Sport' , INDEX + 'Sport/' )
|
||||
,(u'Bayern' , INDEX + 'Bayern/' )
|
||||
,(u'Muenchen' , INDEX + 'M%FCnchen/' )
|
||||
,(u'Muenchen City' , INDEX + 'M%FCnchen+City/' )
|
||||
,(u'Jetzt.de' , INDEX + 'Jetzt.de/' )
|
||||
,(u'Reise' , INDEX + 'Reise/' )
|
||||
,(u'SZ Extra' , INDEX + 'SZ+Extra/' )
|
||||
,(u'Wochenende' , INDEX + 'SZ+am+Wochenende/' )
|
||||
,(u'Stellen-Markt' , INDEX + 'Stellen-Markt/')
|
||||
,(u'Motormarkt' , INDEX + 'Motormarkt/')
|
||||
,(u'Immobilien-Markt', INDEX + 'Immobilien-Markt/')
|
||||
,(u'Thema' , INDEX + 'Thema/' )
|
||||
,(u'Forum' , INDEX + 'Forum/' )
|
||||
,(u'Leute' , INDEX + 'Leute/' )
|
||||
,(u'Jugend' , INDEX + 'Jugend/' )
|
||||
,(u'Beilage' , INDEX + 'Beilage/' )
|
||||
]
|
||||
|
||||
def parse_index(self):
|
||||
|
BIN
resources/images/store.png
Normal file
BIN
resources/images/store.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -5,7 +5,9 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import uuid, sys, os, re, logging, time, random, \
|
||||
__builtin__, warnings, multiprocessing
|
||||
from contextlib import closing
|
||||
from urllib import getproxies
|
||||
from urllib2 import unquote as urllib2_unquote
|
||||
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
|
||||
from htmlentitydefs import name2codepoint
|
||||
from math import floor
|
||||
@ -290,6 +292,9 @@ def get_parsed_proxy(typ='http', debug=True):
|
||||
prints('Using http proxy', str(ans))
|
||||
return ans
|
||||
|
||||
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
USER_AGENT_MOBILE = 'Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016'
|
||||
|
||||
def random_user_agent():
|
||||
choices = [
|
||||
'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
|
||||
@ -305,7 +310,6 @@ def random_user_agent():
|
||||
#return choices[-1]
|
||||
return choices[random.randint(0, len(choices)-1)]
|
||||
|
||||
|
||||
def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
'''
|
||||
Create a mechanize browser for web scraping. The browser handles cookies,
|
||||
@ -319,8 +323,7 @@ def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
|
||||
opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time)
|
||||
opener.set_handle_robots(False)
|
||||
if user_agent is None:
|
||||
user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \
|
||||
'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13'
|
||||
user_agent = USER_AGENT_MOBILE if mobile_browser else USER_AGENT
|
||||
opener.addheaders = [('User-agent', user_agent)]
|
||||
http_proxy = get_proxies().get('http', None)
|
||||
if http_proxy:
|
||||
@ -537,7 +540,49 @@ def as_unicode(obj, enc=preferred_encoding):
|
||||
obj = repr(obj)
|
||||
return force_unicode(obj, enc=enc)
|
||||
|
||||
def url_slash_cleaner(url):
|
||||
'''
|
||||
Removes redundant /'s from url's.
|
||||
'''
|
||||
return re.sub(r'(?<!:)/{2,}', '/', url)
|
||||
|
||||
def get_download_filename(url, cookie_file=None):
|
||||
'''
|
||||
Get a local filename for a URL using the content disposition header
|
||||
'''
|
||||
filename = ''
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
from mechanize import MozillaCookieJar
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
|
||||
try:
|
||||
with closing(br.open(url)) as r:
|
||||
disposition = r.info().get('Content-disposition', '')
|
||||
for p in disposition.split(';'):
|
||||
if 'filename' in p:
|
||||
if '*=' in disposition:
|
||||
parts = disposition.split('*=')[-1]
|
||||
filename = parts.split('\'')[-1]
|
||||
else:
|
||||
filename = disposition.split('=')[-1]
|
||||
if filename[0] in ('\'', '"'):
|
||||
filename = filename[1:]
|
||||
if filename[-1] in ('\'', '"'):
|
||||
filename = filename[:-1]
|
||||
filename = urllib2_unquote(filename)
|
||||
break
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if not filename:
|
||||
filename = r.geturl().split('/')[-1]
|
||||
|
||||
return filename
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
|
@ -602,3 +602,35 @@ class PreferencesPlugin(Plugin): # {{{
|
||||
|
||||
# }}}
|
||||
|
||||
class StoreBase(Plugin): # {{{
|
||||
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
author = 'John Schember'
|
||||
type = _('Store')
|
||||
|
||||
actual_plugin = None
|
||||
|
||||
def load_actual_plugin(self, gui):
|
||||
'''
|
||||
This method must return the actual interface action plugin object.
|
||||
'''
|
||||
mod, cls = self.actual_plugin.split(':')
|
||||
self.actual_plugin_object = getattr(importlib.import_module(mod), cls)(gui, self.name)
|
||||
return self.actual_plugin_object
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.customization_help(gui)
|
||||
raise NotImplementedError()
|
||||
|
||||
def config_widget(self):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.config_widget()
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
if getattr(self, 'actual_plugin_object', None) is not None:
|
||||
return self.actual_plugin_object.save_settings(config_widget)
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
||||
|
@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import textwrap, os, glob, functools, re
|
||||
from calibre import guess_type
|
||||
from calibre.customize import FileTypePlugin, MetadataReaderPlugin, \
|
||||
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase
|
||||
MetadataWriterPlugin, PreferencesPlugin, InterfaceActionBase, StoreBase
|
||||
from calibre.constants import numeric_version
|
||||
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
|
||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||
@ -855,6 +855,11 @@ class ActionNextMatch(InterfaceActionBase):
|
||||
name = 'Next Match'
|
||||
actual_plugin = 'calibre.gui2.actions.next_match:NextMatchAction'
|
||||
|
||||
class ActionStore(InterfaceActionBase):
|
||||
name = 'Store'
|
||||
author = 'John Schember'
|
||||
actual_plugin = 'calibre.gui2.actions.store:StoreAction'
|
||||
|
||||
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
|
||||
ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails,
|
||||
@ -863,6 +868,9 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
|
||||
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,
|
||||
ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch]
|
||||
|
||||
if test_eight_code:
|
||||
plugins += [ActionStore]
|
||||
|
||||
# }}}
|
||||
|
||||
# Preferences Plugins {{{
|
||||
@ -1094,12 +1102,81 @@ if test_eight_code:
|
||||
|
||||
#}}}
|
||||
|
||||
# New metadata download plugins {{{
|
||||
from calibre.ebooks.metadata.sources.google import GoogleBooks
|
||||
from calibre.ebooks.metadata.sources.amazon import Amazon
|
||||
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
|
||||
from calibre.ebooks.metadata.sources.overdrive import OverDrive
|
||||
# Store plugins {{{
|
||||
class StoreAmazonKindleStore(StoreBase):
|
||||
name = 'Amazon Kindle'
|
||||
description = _('Kindle books from Amazon')
|
||||
actual_plugin = 'calibre.gui2.store.amazon_plugin:AmazonKindleStore'
|
||||
|
||||
plugins += [GoogleBooks, Amazon, OpenLibrary]
|
||||
class StoreBaenWebScriptionStore(StoreBase):
|
||||
name = 'Baen WebScription'
|
||||
description = _('Ebooks for readers.')
|
||||
actual_plugin = 'calibre.gui2.store.baen_webscription_plugin:BaenWebScriptionStore'
|
||||
|
||||
class StoreBNStore(StoreBase):
|
||||
name = 'Barnes and Noble'
|
||||
description = _('Books, Textbooks, eBooks, Toys, Games and More.')
|
||||
actual_plugin = 'calibre.gui2.store.bn_plugin:BNStore'
|
||||
|
||||
class StoreBeWriteStore(StoreBase):
|
||||
name = 'BeWrite Books'
|
||||
description = _('Publishers of fine books.')
|
||||
actual_plugin = 'calibre.gui2.store.bewrite_plugin:BeWriteStore'
|
||||
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = _('World Famous eBook Store.')
|
||||
actual_plugin = 'calibre.gui2.store.diesel_ebooks_plugin:DieselEbooksStore'
|
||||
|
||||
class StoreEbookscomStore(StoreBase):
|
||||
name = 'eBooks.com'
|
||||
description = _('The digital bookstore.')
|
||||
actual_plugin = 'calibre.gui2.store.ebooks_com_plugin:EbookscomStore'
|
||||
|
||||
class StoreEHarlequinStoretore(StoreBase):
|
||||
name = 'eHarlequin'
|
||||
description = _('entertain, enrich, inspire.')
|
||||
actual_plugin = 'calibre.gui2.store.eharlequin_plugin:EHarlequinStore'
|
||||
|
||||
class StoreFeedbooksStore(StoreBase):
|
||||
name = 'Feedbooks'
|
||||
description = _('Read anywhere.')
|
||||
actual_plugin = 'calibre.gui2.store.feedbooks_plugin:FeedbooksStore'
|
||||
|
||||
class StoreGutenbergStore(StoreBase):
|
||||
name = 'Project Gutenberg'
|
||||
description = _('The first producer of free ebooks.')
|
||||
actual_plugin = 'calibre.gui2.store.gutenberg_plugin:GutenbergStore'
|
||||
|
||||
class StoreKoboStore(StoreBase):
|
||||
name = 'Kobo'
|
||||
description = _('eReading: anytime. anyplace.')
|
||||
actual_plugin = 'calibre.gui2.store.kobo_plugin:KoboStore'
|
||||
|
||||
class StoreManyBooksStore(StoreBase):
|
||||
name = 'ManyBooks'
|
||||
description = _('The best ebooks at the best price: free!')
|
||||
actual_plugin = 'calibre.gui2.store.manybooks_plugin:ManyBooksStore'
|
||||
|
||||
class StoreMobileReadStore(StoreBase):
|
||||
name = 'MobileRead'
|
||||
description = _('Ebooks handcrafted with the utmost care')
|
||||
actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore'
|
||||
|
||||
class StoreOpenLibraryStore(StoreBase):
|
||||
name = 'Open Library'
|
||||
description = _('One web page for every book.')
|
||||
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
|
||||
|
||||
class StoreSmashwordsStore(StoreBase):
|
||||
name = 'Smashwords'
|
||||
description = _('Your ebook. Your way.')
|
||||
actual_plugin = 'calibre.gui2.store.smashwords_plugin:SmashwordsStore'
|
||||
|
||||
plugins += [StoreAmazonKindleStore, StoreBaenWebScriptionStore, StoreBNStore,
|
||||
StoreBeWriteStore, StoreDieselEbooksStore, StoreEbookscomStore,
|
||||
StoreEHarlequinStoretore,
|
||||
StoreFeedbooksStore, StoreGutenbergStore, StoreKoboStore, StoreManyBooksStore,
|
||||
StoreMobileReadStore, StoreOpenLibraryStore, StoreSmashwordsStore]
|
||||
|
||||
# }}}
|
||||
|
@ -7,7 +7,8 @@ import os, shutil, traceback, functools, sys
|
||||
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
|
||||
MetadataReaderPlugin, MetadataWriterPlugin,
|
||||
InterfaceActionBase as InterfaceAction,
|
||||
PreferencesPlugin, platform, InvalidPlugin)
|
||||
PreferencesPlugin, platform, InvalidPlugin,
|
||||
StoreBase as Store)
|
||||
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
|
||||
from calibre.customize.zipplugin import loader
|
||||
from calibre.customize.profiles import InputProfile, OutputProfile
|
||||
@ -244,6 +245,17 @@ def preferences_plugins():
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Store Plugins # {{{
|
||||
|
||||
def store_plugins():
|
||||
customization = config['plugin_customization']
|
||||
for plugin in _initialized_plugins:
|
||||
if isinstance(plugin, Store):
|
||||
if not is_disabled(plugin):
|
||||
plugin.site_customization = customization.get(plugin.name, '')
|
||||
yield plugin
|
||||
# }}}
|
||||
|
||||
# Metadata read/write {{{
|
||||
_metadata_readers = {}
|
||||
_metadata_writers = {}
|
||||
|
@ -55,7 +55,7 @@ class ANDROID(USBMS):
|
||||
},
|
||||
|
||||
# Viewsonic
|
||||
0x0489 : { 0xc001 : [0x0226] },
|
||||
0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], },
|
||||
|
||||
# Acer
|
||||
0x502 : { 0x3203 : [0x0100]},
|
||||
@ -108,7 +108,7 @@ class ANDROID(USBMS):
|
||||
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
|
||||
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
|
||||
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
|
||||
'MB860', 'MULTI-CARD']
|
||||
'MB860', 'MULTI-CARD', 'MID7015A']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7']
|
||||
|
@ -12,6 +12,7 @@ from Queue import Empty
|
||||
|
||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||
from calibre import extract, CurrentDir, prints
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file):
|
||||
Un-archive the comic file.
|
||||
'''
|
||||
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
|
||||
if not isinstance(tdir, unicode):
|
||||
# Needed in case the zip file has wrongly encoded unicode file/dir
|
||||
# names
|
||||
tdir = tdir.decode(filesystem_encoding)
|
||||
extract(path_to_comic_file, tdir)
|
||||
return tdir
|
||||
|
||||
|
@ -716,6 +716,7 @@ class MobiReader(object):
|
||||
ent_pat = re.compile(r'&(\S+?);')
|
||||
if elems:
|
||||
tocobj = TOC()
|
||||
found = False
|
||||
reached = False
|
||||
for x in root.iter():
|
||||
if x == elems[-1]:
|
||||
@ -732,7 +733,8 @@ class MobiReader(object):
|
||||
text = ent_pat.sub(entity_to_unicode, text)
|
||||
tocobj.add_item(toc.partition('#')[0], href[1:],
|
||||
text)
|
||||
if reached and x.get('class', None) == 'mbp_pagebreak':
|
||||
found = True
|
||||
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
||||
break
|
||||
if tocobj is not None:
|
||||
opf.set_toc(tocobj)
|
||||
|
@ -11,6 +11,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import os
|
||||
import re
|
||||
import StringIO
|
||||
from copy import deepcopy
|
||||
|
||||
from calibre import my_unichr, prepare_string_for_xml
|
||||
from calibre.ebooks.metadata.toc import TOC
|
||||
@ -25,6 +26,7 @@ class PML_HTMLizer(object):
|
||||
'sp',
|
||||
'sb',
|
||||
'h1',
|
||||
'h1c',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
@ -58,6 +60,7 @@ class PML_HTMLizer(object):
|
||||
|
||||
STATES_TAGS = {
|
||||
'h1': ('<h1 style="page-break-before: always;">', '</h1>'),
|
||||
'h1c': ('<h1>', '</h1>'),
|
||||
'h2': ('<h2>', '</h2>'),
|
||||
'h3': ('<h3>', '</h3>'),
|
||||
'h4': ('<h4>', '</h4>'),
|
||||
@ -141,6 +144,10 @@ class PML_HTMLizer(object):
|
||||
'b',
|
||||
]
|
||||
|
||||
NEW_LINE_EXCHANGE_STATES = {
|
||||
'h1': 'h1c',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.state = {}
|
||||
# toc consists of a tuple
|
||||
@ -219,11 +226,17 @@ class PML_HTMLizer(object):
|
||||
def start_line(self):
|
||||
start = u''
|
||||
|
||||
state = deepcopy(self.state)
|
||||
div = []
|
||||
span = []
|
||||
other = []
|
||||
|
||||
for key, val in self.state.items():
|
||||
for key, val in state.items():
|
||||
if key in self.NEW_LINE_EXCHANGE_STATES and val[0]:
|
||||
state[self.NEW_LINE_EXCHANGE_STATES[key]] = val
|
||||
state[key] = [False, '']
|
||||
|
||||
for key, val in state.items():
|
||||
if val[0]:
|
||||
if key in self.DIV_STATES:
|
||||
div.append((key, val[1]))
|
||||
|
39
src/calibre/gui2/actions/store.py
Normal file
39
src/calibre/gui2/actions/store.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- 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 functools import partial
|
||||
|
||||
from PyQt4.Qt import QMenu
|
||||
|
||||
from calibre.gui2.actions import InterfaceAction
|
||||
|
||||
class StoreAction(InterfaceAction):
|
||||
|
||||
name = 'Store'
|
||||
action_spec = (_('Get books'), 'store.png', None, None)
|
||||
|
||||
def genesis(self):
|
||||
self.qaction.triggered.connect(self.search)
|
||||
self.store_menu = QMenu()
|
||||
self.load_menu()
|
||||
|
||||
def load_menu(self):
|
||||
self.store_menu.clear()
|
||||
self.store_menu.addAction(_('Search'), self.search)
|
||||
self.store_menu.addSeparator()
|
||||
for n, p in self.gui.istores.items():
|
||||
self.store_menu.addAction(n, partial(self.open_store, p))
|
||||
self.qaction.setMenu(self.store_menu)
|
||||
|
||||
def search(self):
|
||||
from calibre.gui2.store.search import SearchDialog
|
||||
sd = SearchDialog(self.gui.istores, self.gui)
|
||||
sd.exec_()
|
||||
|
||||
def open_store(self, store_plugin):
|
||||
store_plugin.open(self.gui)
|
@ -90,7 +90,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -68,7 +68,7 @@ class DaysOfWeek(Base):
|
||||
def initialize(self, typ=None, val=None):
|
||||
if typ is None:
|
||||
typ = 'day/time'
|
||||
val = (-1, 9, 0)
|
||||
val = (-1, 6, 0)
|
||||
if typ == 'day/time':
|
||||
val = convert_day_time_schedule(val)
|
||||
|
||||
@ -118,7 +118,7 @@ class DaysOfMonth(Base):
|
||||
|
||||
def initialize(self, typ=None, val=None):
|
||||
if val is None:
|
||||
val = ((1,), 9, 0)
|
||||
val = ((1,), 6, 0)
|
||||
days_of_month, hour, minute = val
|
||||
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
||||
self.time.setTime(QTime(hour, minute))
|
||||
@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
||||
if d < timedelta(days=366):
|
||||
ld_text = tm
|
||||
else:
|
||||
typ, sch = 'day/time', (-1, 9, 0)
|
||||
typ, sch = 'day/time', (-1, 6, 0)
|
||||
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
|
||||
'interval':2}[typ]
|
||||
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
||||
|
@ -79,7 +79,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset>
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
106
src/calibre/gui2/ebook_download.py
Normal file
106
src/calibre/gui2/ebook_download.py
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- 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 os
|
||||
import shutil
|
||||
from contextlib import closing
|
||||
from mechanize import MozillaCookieJar
|
||||
|
||||
from calibre import browser, get_download_filename
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.gui2 import Dispatcher
|
||||
from calibre.gui2.threaded_jobs import ThreadedJob
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class EbookDownload(object):
|
||||
|
||||
def __call__(self, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[], log=None, abort=None, notifications=None):
|
||||
dfilename = ''
|
||||
try:
|
||||
dfilename = self._download(cookie_file, url, filename, save_loc, add_to_lib)
|
||||
self._add(dfilename, gui, add_to_lib, tags)
|
||||
self._save_as(dfilename, save_loc)
|
||||
except Exception as e:
|
||||
raise e
|
||||
finally:
|
||||
try:
|
||||
if dfilename:
|
||||
os.remove(dfilename)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _download(self, cookie_file, url, filename, save_loc, add_to_lib):
|
||||
dfilename = ''
|
||||
|
||||
if not url:
|
||||
raise Exception(_('No file specified to download.'))
|
||||
if not save_loc and not add_to_lib:
|
||||
# Nothing to do.
|
||||
return dfilename
|
||||
|
||||
if not filename:
|
||||
filename = get_download_filename(url, cookie_file)
|
||||
|
||||
br = browser()
|
||||
if cookie_file:
|
||||
cj = MozillaCookieJar()
|
||||
cj.load(cookie_file)
|
||||
br.set_cookiejar(cj)
|
||||
with closing(br.open(url)) as r:
|
||||
tf = PersistentTemporaryFile(suffix=filename)
|
||||
tf.write(r.read())
|
||||
dfilename = tf.name
|
||||
|
||||
return dfilename
|
||||
|
||||
def _add(self, filename, gui, add_to_lib, tags):
|
||||
if not add_to_lib or not filename:
|
||||
return
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in BOOK_EXTENSIONS:
|
||||
raise Exception(_('Not a support ebook format.'))
|
||||
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
with open(filename) as f:
|
||||
mi = get_metadata(f, ext)
|
||||
mi.tags.extend(tags)
|
||||
|
||||
id = gui.library_view.model().db.create_book_entry(mi)
|
||||
gui.library_view.model().db.add_format_with_hooks(id, ext.upper(), filename, index_is_id=True)
|
||||
gui.library_view.model().books_added(1)
|
||||
gui.library_view.model().count_changed()
|
||||
|
||||
def _save_as(self, dfilename, save_loc):
|
||||
if not save_loc or not dfilename:
|
||||
return
|
||||
shutil.copy(dfilename, save_loc)
|
||||
|
||||
|
||||
gui_ebook_download = EbookDownload()
|
||||
|
||||
def start_ebook_download(callback, job_manager, gui, cookie_file=None, url='', filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
description = _('Downloading %s') % filename if filename else url
|
||||
job = ThreadedJob('ebook_download', description, gui_ebook_download, (gui, cookie_file, url, filename, save_loc, add_to_lib, tags), {}, callback, max_concurrent_count=2, killable=False)
|
||||
job_manager.run_threaded_job(job)
|
||||
|
||||
|
||||
class EbookDownloadMixin(object):
|
||||
|
||||
def download_ebook(self, url='', cookie_file=None, filename='', save_loc='', add_to_lib=True, tags=[]):
|
||||
if tags:
|
||||
if isinstance(tags, basestring):
|
||||
tags = tags.split(',')
|
||||
start_ebook_download(Dispatcher(self.downloaded_ebook), self.job_manager, self, cookie_file, url, filename, save_loc, add_to_lib, tags)
|
||||
self.status_bar.show_message(_('Downloading') + ' ' + filename if filename else url, 3000)
|
||||
|
||||
def downloaded_ebook(self, job):
|
||||
if job.failed:
|
||||
self.job_exception(job, dialog_title=_('Failed to download ebook'))
|
||||
return
|
||||
|
||||
self.status_bar.show_message(job.description + ' ' + _('finished'), 5000)
|
@ -200,12 +200,6 @@ class SearchBar(QWidget): # {{{
|
||||
x.setIcon(QIcon(I('arrow-down.png')))
|
||||
l.addWidget(x)
|
||||
|
||||
x = parent.search_options_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I('config.png')))
|
||||
x.setObjectName("search_option_button")
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Change the way searching for books works"))
|
||||
|
||||
x = parent.saved_search = SavedSearchBox(self)
|
||||
x.setMaximumSize(QSize(150, 16777215))
|
||||
x.setMinimumContentsLength(15)
|
||||
@ -224,13 +218,6 @@ class SearchBar(QWidget): # {{{
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Save current search under the name shown in the box"))
|
||||
|
||||
x = parent.delete_search_button = QToolButton(self)
|
||||
x.setIcon(QIcon(I("search_delete_saved.png")))
|
||||
x.setObjectName("delete_search_button")
|
||||
l.addWidget(x)
|
||||
x.setToolTip(_("Delete current saved search"))
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class Spacer(QWidget): # {{{
|
||||
|
@ -743,6 +743,8 @@ class BooksView(QTableView): # {{{
|
||||
id_to_select = self._model.get_current_highlighted_id()
|
||||
if id_to_select is not None:
|
||||
self.select_rows([id_to_select], using_ids=True)
|
||||
elif self._model.highlight_only:
|
||||
self.clearSelection()
|
||||
self.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def connect_to_search_box(self, sb, search_done):
|
||||
|
@ -222,7 +222,8 @@ class AuthorSortEdit(EnLineEdit):
|
||||
'red, then the authors and this text do not match.')
|
||||
LABEL = _('Author s&ort:')
|
||||
|
||||
def __init__(self, parent, authors_edit, autogen_button, db):
|
||||
def __init__(self, parent, authors_edit, autogen_button, db,
|
||||
copy_a_to_as_action, copy_as_to_a_action):
|
||||
EnLineEdit.__init__(self, parent)
|
||||
self.authors_edit = authors_edit
|
||||
self.db = db
|
||||
@ -241,6 +242,8 @@ class AuthorSortEdit(EnLineEdit):
|
||||
self.textChanged.connect(self.update_state)
|
||||
|
||||
autogen_button.clicked.connect(self.auto_generate)
|
||||
copy_a_to_as_action.triggered.connect(self.auto_generate)
|
||||
copy_as_to_a_action.triggered.connect(self.copy_to_authors)
|
||||
self.update_state()
|
||||
|
||||
@dynamic_property
|
||||
@ -273,6 +276,14 @@ class AuthorSortEdit(EnLineEdit):
|
||||
self.setToolTip(tt)
|
||||
self.setWhatsThis(tt)
|
||||
|
||||
def copy_to_authors(self):
|
||||
aus = self.current_val
|
||||
if aus:
|
||||
ln, _, rest = aus.partition(',')
|
||||
if rest:
|
||||
au = rest.strip() + ' ' + ln.strip()
|
||||
self.authors_edit.current_val = [au]
|
||||
|
||||
def auto_generate(self, *args):
|
||||
au = unicode(self.authors_edit.text())
|
||||
au = re.sub(r'\s+et al\.$', '', au)
|
||||
|
@ -13,7 +13,7 @@ from functools import partial
|
||||
from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
|
||||
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
|
||||
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
|
||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
|
||||
QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu)
|
||||
|
||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors
|
||||
from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
|
||||
@ -102,15 +102,19 @@ class MetadataSingleDialogBase(ResizableDialog):
|
||||
self.deduce_title_sort_button)
|
||||
self.basic_metadata_widgets.extend([self.title, self.title_sort])
|
||||
|
||||
self.authors = AuthorsEdit(self)
|
||||
self.deduce_author_sort_button = QToolButton(self)
|
||||
self.deduce_author_sort_button.setToolTip(_(
|
||||
self.deduce_author_sort_button = b = QToolButton(self)
|
||||
b.setToolTip(_(
|
||||
'Automatically create the author sort entry based on the current'
|
||||
' author entry.\n'
|
||||
'Using this button to create author sort will change author sort from'
|
||||
' red to green.'))
|
||||
self.author_sort = AuthorSortEdit(self, self.authors,
|
||||
self.deduce_author_sort_button, self.db)
|
||||
b.m = m = QMenu()
|
||||
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
|
||||
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
||||
b.setMenu(m)
|
||||
self.authors = AuthorsEdit(self)
|
||||
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||
ac2)
|
||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||
|
||||
self.swap_title_author_button = QToolButton(self)
|
||||
|
@ -319,9 +319,12 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
||||
:return: True iff a restart is required for the changes made by the user to
|
||||
take effect
|
||||
'''
|
||||
from calibre.gui2 import gprefs
|
||||
pl = get_plugin(category, name)
|
||||
d = ConfigDialog(parent)
|
||||
d.resize(750, 550)
|
||||
conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name)
|
||||
geom = gprefs.get(conf_name, None)
|
||||
d.setWindowTitle(_('Configure ') + name)
|
||||
d.setWindowIcon(QIcon(I('config.png')))
|
||||
bb = QDialogButtonBox(d)
|
||||
@ -345,7 +348,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
||||
mygui = True
|
||||
w.genesis(gui)
|
||||
w.initialize()
|
||||
if geom is not None:
|
||||
d.restoreGeometry(geom)
|
||||
d.exec_()
|
||||
geom = bytearray(d.saveGeometry())
|
||||
gprefs[conf_name] = geom
|
||||
rr = getattr(d, 'restart_required', False)
|
||||
if show_restart_msg and rr:
|
||||
from calibre.gui2 import warning_dialog
|
||||
|
@ -79,7 +79,7 @@
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../../../resources/images.qrc">
|
||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
|
@ -218,6 +218,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
self.search.search.connect(self.find)
|
||||
self.next_button.clicked.connect(self.find_next)
|
||||
self.previous_button.clicked.connect(self.find_previous)
|
||||
self.changed_signal.connect(self.reload_store_plugins)
|
||||
|
||||
def find(self, query):
|
||||
idx = self._plugin_model.find(query)
|
||||
@ -344,6 +345,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
plugin.name + _(' cannot be removed. It is a '
|
||||
'builtin plugin. Try disabling it instead.')).exec_()
|
||||
|
||||
def reload_store_plugins(self):
|
||||
self.gui.load_store_plugins()
|
||||
if self.gui.iactions.has_key('Store'):
|
||||
self.gui.iactions['Store'].load_menu()
|
||||
|
||||
def check_for_add_to_toolbars(self, plugin):
|
||||
from calibre.gui2.preferences.toolbar import ConfigWidget
|
||||
from calibre.customize import InterfaceActionBase
|
||||
@ -376,6 +382,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
installed_actions.append(plugin_action.name)
|
||||
gprefs['action-layout-'+key] = tuple(installed_actions)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from PyQt4.Qt import QApplication
|
||||
app = QApplication([])
|
||||
|
@ -13,7 +13,6 @@ from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \
|
||||
QString, QIcon
|
||||
|
||||
from calibre.gui2 import config
|
||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||
from calibre.gui2.dialogs.search import SearchDialog
|
||||
from calibre.utils.search_query_parser import saved_searches
|
||||
@ -316,23 +315,6 @@ class SavedSearchBox(QComboBox): # {{{
|
||||
self.addItems(qnames)
|
||||
self.setCurrentIndex(-1)
|
||||
|
||||
# SIGNALed from the main UI
|
||||
def delete_search_button_clicked(self):
|
||||
if not confirm('<p>'+_('The selected search will be '
|
||||
'<b>permanently deleted</b>. Are you sure?')
|
||||
+'</p>', 'saved_search_delete', self):
|
||||
return
|
||||
idx = self.currentIndex
|
||||
if idx < 0:
|
||||
return
|
||||
ss = saved_searches().lookup(unicode(self.currentText()))
|
||||
if ss is None:
|
||||
return
|
||||
saved_searches().delete(unicode(self.currentText()))
|
||||
self.clear()
|
||||
self.search_box.clear()
|
||||
self.changed.emit()
|
||||
|
||||
# SIGNALed from the main UI
|
||||
def save_search_button_clicked(self):
|
||||
name = unicode(self.currentText())
|
||||
@ -382,7 +364,6 @@ class SearchBoxMixin(object): # {{{
|
||||
unicode(self.search.toolTip())))
|
||||
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
||||
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
||||
self.search_options_button.clicked.connect(self.search_options_button_clicked)
|
||||
self.set_highlight_only_button_icon()
|
||||
self.highlight_only_button.clicked.connect(self.highlight_only_clicked)
|
||||
tt = _('Enable or disable search highlighting.') + '<br><br>'
|
||||
@ -392,6 +373,8 @@ class SearchBoxMixin(object): # {{{
|
||||
def highlight_only_clicked(self, state):
|
||||
config['highlight_search_matches'] = not config['highlight_search_matches']
|
||||
self.set_highlight_only_button_icon()
|
||||
self.search.do_search()
|
||||
self.focus_to_library()
|
||||
|
||||
def set_highlight_only_button_icon(self):
|
||||
if config['highlight_search_matches']:
|
||||
@ -422,10 +405,6 @@ class SearchBoxMixin(object): # {{{
|
||||
self.search.do_search()
|
||||
self.focus_to_library()
|
||||
|
||||
def search_options_button_clicked(self):
|
||||
self.iactions['Preferences'].do_config(initial_plugin=('Interface',
|
||||
'Search'), close_after_initial=True)
|
||||
|
||||
def focus_to_library(self):
|
||||
self.current_view().setFocus(Qt.OtherFocusReason)
|
||||
|
||||
@ -438,8 +417,6 @@ class SavedSearchBoxMixin(object): # {{{
|
||||
self.clear_button.clicked.connect(self.saved_search.clear)
|
||||
self.save_search_button.clicked.connect(
|
||||
self.saved_search.save_search_button_clicked)
|
||||
self.delete_search_button.clicked.connect(
|
||||
self.saved_search.delete_search_button_clicked)
|
||||
self.copy_search_button.clicked.connect(
|
||||
self.saved_search.copy_search_button_clicked)
|
||||
self.saved_searches_changed()
|
||||
@ -448,7 +425,7 @@ class SavedSearchBoxMixin(object): # {{{
|
||||
self.saved_search.setToolTip(
|
||||
_('Choose saved search or enter name for new saved search'))
|
||||
self.saved_search.setStatusTip(self.saved_search.toolTip())
|
||||
for x in ('copy', 'save', 'delete'):
|
||||
for x in ('copy', 'save'):
|
||||
b = getattr(self, x+'_search_button')
|
||||
b.setStatusTip(b.toolTip())
|
||||
|
||||
|
139
src/calibre/gui2/store/__init__.py
Normal file
139
src/calibre/gui2/store/__init__.py
Normal file
@ -0,0 +1,139 @@
|
||||
# -*- 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'
|
||||
|
||||
class StorePlugin(object): # {{{
|
||||
'''
|
||||
A plugin representing an online ebook repository (store). The store can
|
||||
be a comercial store that sells ebooks or a source of free downloadable
|
||||
ebooks.
|
||||
|
||||
Note that this class is the base class for these plugins, however, to
|
||||
integrate the plugin with calibre's plugin system, you have to make a
|
||||
wrapper class that references the actual plugin. See the
|
||||
:mod:`calibre.customize.builtins` module for examples.
|
||||
|
||||
If two :class:`StorePlugin` objects have the same name, the one with higher
|
||||
priority takes precedence.
|
||||
|
||||
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
||||
|
||||
Regarding :meth:`open`. Most stores only make themselves available
|
||||
though a web site thus most store plugins will open using
|
||||
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
||||
open a modal window and display the store website in a QWebView.
|
||||
|
||||
Sub-classes should implement and use the :meth:`genesis` if they require
|
||||
plugin specific initialization. They should not override or otherwise
|
||||
reimplement :meth:`__init__`.
|
||||
|
||||
Once initialized, this plugin has access to the main calibre GUI via the
|
||||
:attr:`gui` member. You can access other plugins by name, for example::
|
||||
|
||||
self.gui.istores['Amazon Kindle']
|
||||
|
||||
Plugin authors can use affiliate programs within their plugin. The
|
||||
distribution of money earned from a store plugin is 70/30. 70% going
|
||||
to the pluin author / maintainer and 30% going to the calibre project.
|
||||
|
||||
The easiest way to handle affiliate money payouts is to randomly select
|
||||
between the author's affiliate id and calibre's affiliate id so that
|
||||
70% of the time the author's id is used.
|
||||
'''
|
||||
|
||||
def __init__(self, gui, name):
|
||||
self.gui = gui
|
||||
self.name = name
|
||||
self.base_plugin = None
|
||||
|
||||
def open(self, gui, parent=None, detail_item=None, external=False):
|
||||
'''
|
||||
Open the store.
|
||||
|
||||
:param gui: The main GUI. This will be used to have the job
|
||||
system start downloading an item from the store.
|
||||
|
||||
:param parent: The parent of the store dialog. This is used
|
||||
to create modal dialogs.
|
||||
|
||||
:param detail_item: A plugin specific reference to an item
|
||||
in the store that the user should be shown.
|
||||
|
||||
:param external: When False open an internal dialog with the
|
||||
store. When True open the users default browser to the store's
|
||||
web site. :param:`detail_item` should still be respected when external
|
||||
is True.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
'''
|
||||
Searches the store for items matching query. This should
|
||||
return items as a generator.
|
||||
|
||||
Don't be lazy with the search! Load as much data as possible in the
|
||||
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
|
||||
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
||||
isn't available because the store does not display cover images then it's okay to
|
||||
ignore it.
|
||||
|
||||
Also, by default search results can only include ebooks. A plugin can offer users
|
||||
an option to include physical books in the search results but this must be
|
||||
disabled by default.
|
||||
|
||||
If a store doesn't provide search on it's own use something like a site specific
|
||||
google search to get search results for this funtion.
|
||||
|
||||
:param query: The string query search with.
|
||||
:param max_results: The maximum number of results to return.
|
||||
:param timeout: The maximum amount of time in seconds to spend download the search results.
|
||||
|
||||
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
||||
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_settings(self):
|
||||
'''
|
||||
This is only useful for plugins that implement
|
||||
:attr:`config_widget` that is the only way to save
|
||||
settings. This is used by plugins to get the saved
|
||||
settings and apply when necessary.
|
||||
|
||||
:return: A dictionary filled with the settings used
|
||||
by this plugin.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def do_genesis(self):
|
||||
self.genesis()
|
||||
|
||||
def genesis(self):
|
||||
'''
|
||||
Plugin specific initialization.
|
||||
'''
|
||||
pass
|
||||
|
||||
def config_widget(self):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
'''
|
||||
See :class:`calibre.customize.Plugin` for details.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
# }}}
|
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
172
src/calibre/gui2/store/amazon_plugin.py
Normal file
@ -0,0 +1,172 @@
|
||||
# -*- 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 random
|
||||
import re
|
||||
import urllib2
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
|
||||
class AmazonKindleStore(StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
'''
|
||||
Amazon comes with a number of difficulties.
|
||||
|
||||
QWebView has major issues with Amazon.com. The largest of
|
||||
issues is it simply doesn't work on a number of pages.
|
||||
|
||||
When connecting to a number parts of Amazon.com (Kindle library
|
||||
for instance) QNetworkAccessManager fails to connect with a
|
||||
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
||||
when I check QNetworkRequest.HttpStatusCodeAttribute when the
|
||||
399 error is returned the status code is 200 (Ok). However, once
|
||||
the QNetworkAccessManager decides there was a NetworkError it
|
||||
does not download the page from Amazon. So I can't even set the
|
||||
HTML in the QWebView myself.
|
||||
|
||||
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
||||
open bug about the issue but it is not correct. We can set the
|
||||
useragent (Arora does) to something else and the above issue
|
||||
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
||||
gives a bit more information about the issue but as of now (27/Feb/2011)
|
||||
there is no solution or work around.
|
||||
|
||||
We cannot change the The linkDelegationPolicy to allow us to avoid
|
||||
QNetworkAccessManager because it only works links. Forms aren't
|
||||
included so the same issue persists on any part of the site (login)
|
||||
that use a form to load a new page.
|
||||
|
||||
Using an aStore was evaluated but I've decided against using it.
|
||||
There are three major issues with an aStore. Because checkout is
|
||||
handled by sending the user to Amazon we can't put it in a QWebView.
|
||||
If we're sending the user to Amazon sending them there directly is
|
||||
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
||||
redirection the users default browser because the cookies with the
|
||||
shopping cart won't transfer.
|
||||
|
||||
Another issue with the aStore is how it handles the referral. It only
|
||||
counts the referral for the items in the shopping card / the item
|
||||
that directed the user to Amazon. Kindle books do not use the shopping
|
||||
cart and send the user directly to Amazon for the purchase. In this
|
||||
instance we would only get referral credit for the one book that the
|
||||
aStore directs to Amazon that the user buys. Any other purchases we
|
||||
won't get credit for.
|
||||
|
||||
The last issue with the aStore is performance. Even though it's an
|
||||
Amazon site it's alow. So much slower than Amazon.com that it makes
|
||||
me not want to browse books using it. The look and feel are lesser
|
||||
issues. So is the fact that it almost seems like the purchase is
|
||||
with calibre. This can cause some support issues because we can't
|
||||
do much for issues with Amazon.com purchase hiccups.
|
||||
|
||||
Another option that was evaluated was the Product Advertising API.
|
||||
The reasons against this are complexity. It would take a lot of work
|
||||
to basically re-create Amazon.com within calibre. The Product
|
||||
Advertising API is also designed with being run on a server not
|
||||
in an app. The signing keys would have to be made avaliable to ever
|
||||
calibre user which means bad things could be done with our account.
|
||||
|
||||
The Product Advertising API also assumes the same browser for easy
|
||||
shopping cart transfer to Amazon. With QWebView not working and there
|
||||
not being an easy way to transfer cookies between a QWebView and the
|
||||
users default browser this won't work well.
|
||||
|
||||
We could create our own website on the calibre server and create an
|
||||
Amazon Product Advertising API store. However, this goes back to the
|
||||
complexity argument. Why spend the time recreating Amazon.com
|
||||
|
||||
The final and largest issue against using the Product Advertising API
|
||||
is the Efficiency Guidelines:
|
||||
|
||||
"Each account used to access the Product Advertising API will be allowed
|
||||
an initial usage limit of 2,000 requests per hour. Each account will
|
||||
receive an additional 500 requests per hour (up to a maximum of 25,000
|
||||
requests per hour) for every $1 of shipped item revenue driven per hour
|
||||
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
||||
on revenue performance."
|
||||
|
||||
With over two million users a limit of 2,000 request per hour could
|
||||
render our store unusable for no other reason than Amazon rate
|
||||
limiting our traffic.
|
||||
|
||||
The best (I use the term lightly here) solution is to open Amazon.com
|
||||
in the users default browser and set the affiliate id as part of the url.
|
||||
'''
|
||||
aff_id = {'tag': 'josbl0e-cpb-20'}
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id['tag'] = 'calibrebs-20'
|
||||
store_link = 'http://www.amazon.com/Kindle-eBooks/b/?ie=UTF&node=1286228011&ref_=%(tag)s&ref=%(tag)s&tag=%(tag)s&linkCode=ur2&camp=1789&creative=390957' % aff_id
|
||||
if detail_item:
|
||||
aff_id['asin'] = detail_item
|
||||
store_link = 'http://www.amazon.com/dp/%(asin)s/?tag=%(tag)s' % aff_id
|
||||
open_url(QUrl(store_link))
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + 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('//div[@class="productData"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
# Even though we are searching digital-text only Amazon will still
|
||||
# put in results for non Kindle books (author pages). Se we need
|
||||
# to explicitly check if the item is a Kindle book and ignore it
|
||||
# if it isn't.
|
||||
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
||||
if 'kindle' not in type.lower():
|
||||
continue
|
||||
|
||||
# We must have an asin otherwise we can't easily reference the
|
||||
# book later.
|
||||
asin_href = None
|
||||
asin_a = data.xpath('div[@class="productTitle"]/a[1]')
|
||||
if asin_a:
|
||||
asin_href = asin_a[0].get('href', '')
|
||||
m = re.search(r'/dp/(?P<asin>.+?)(/|$)', asin_href)
|
||||
if m:
|
||||
asin = m.group('asin')
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
cover_url = ''
|
||||
if asin_href:
|
||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||
if cover_img:
|
||||
cover_url = cover_img[0]
|
||||
|
||||
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||
author = author.split('by')[-1]
|
||||
price = ''.join(data.xpath('div[@class="newPrice"]/span/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 = asin.strip()
|
||||
|
||||
yield s
|
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
89
src/calibre/gui2/store/baen_webscription_plugin.py
Normal file
@ -0,0 +1,89 @@
|
||||
# -*- 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 re
|
||||
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 BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.webscription.net/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.webscription.net/searchadv.aspx?IsSubmit=true&SearchTerm=' + 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('//table/tr/td/img[@src="skins/Skin_1/images/matchingproducts.gif"]/..//tr'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('./td[1]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('./td[1]/a/text()'))
|
||||
|
||||
author = ''
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open('http://www.webscription.net/' + id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
author = ''.join(idata.xpath('//span[@class="ProductNameText"]/../b/text()'))
|
||||
author = author.split('by ')[-1]
|
||||
price = ''.join(idata.xpath('//span[@class="variantprice"]/text()'))
|
||||
a, b, price = price.partition('$')
|
||||
price = b + price
|
||||
|
||||
pnum = ''
|
||||
mo = re.search(r'p-(?P<num>\d+)-', id.strip())
|
||||
if mo:
|
||||
pnum = mo.group('num')
|
||||
if pnum:
|
||||
cover_url = 'http://www.webscription.net/' + ''.join(idata.xpath('//img[@id="ProductPic%s"]/@src' % pnum))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
52
src/calibre/gui2/store/basic_config.py
Normal file
52
src/calibre/gui2/store/basic_config.py
Normal file
@ -0,0 +1,52 @@
|
||||
# -*- 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 PyQt4.Qt import QWidget
|
||||
|
||||
from calibre.gui2 import gprefs
|
||||
from calibre.gui2.store.basic_config_widget_ui import Ui_Form
|
||||
|
||||
def save_settings(config_widget):
|
||||
gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked()
|
||||
tags = unicode(config_widget.tags.text())
|
||||
gprefs[config_widget.store.name + '_tags'] = tags
|
||||
|
||||
class BasicStoreConfigWidget(QWidget, Ui_Form):
|
||||
|
||||
def __init__(self, store):
|
||||
QWidget.__init__(self)
|
||||
self.setupUi(self)
|
||||
|
||||
self.store = store
|
||||
|
||||
self.load_setings()
|
||||
|
||||
def load_setings(self):
|
||||
settings = self.store.get_settings()
|
||||
|
||||
self.open_external.setChecked(settings.get(self.store.name + '_open_external'))
|
||||
self.tags.setText(settings.get(self.store.name + '_tags', ''))
|
||||
|
||||
class BasicStoreConfig(object):
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Customize the behavior of this store.'
|
||||
|
||||
def config_widget(self):
|
||||
return BasicStoreConfigWidget(self)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
save_settings(config_widget)
|
||||
|
||||
def get_settings(self):
|
||||
settings = {}
|
||||
|
||||
settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False)
|
||||
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
|
||||
|
||||
return settings
|
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
38
src/calibre/gui2/store/basic_config_widget.ui
Normal file
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>460</width>
|
||||
<height>69</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Added Tags:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="tags"/>
|
||||
</item>
|
||||
<item row="0" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="open_external">
|
||||
<property name="text">
|
||||
<string>Open store in external web browswer</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
80
src/calibre/gui2/store/bewrite_plugin.py
Normal file
80
src/calibre/gui2/store/bewrite_plugin.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- 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 BeWriteStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + 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('//div[@id="content"]//table/tr[position() > 1]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
heading = ''.join(data.xpath('./td[2]//text()'))
|
||||
title, q, author = heading.partition('by ')
|
||||
cover_url = ''
|
||||
price = ''
|
||||
|
||||
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
||||
idata = html.fromstring(nf.read())
|
||||
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
||||
price = '$' + price.split('$')[-1]
|
||||
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
||||
if cover_img:
|
||||
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url.strip()
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
82
src/calibre/gui2/store/bn_plugin.py
Normal file
82
src/calibre/gui2/store/bn_plugin.py
Normal file
@ -0,0 +1,82 @@
|
||||
# -*- 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 random
|
||||
import re
|
||||
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 BNStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
pub_id = '21000000000352219'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
pub_id = '21000000000352583'
|
||||
|
||||
url = 'http://gan.doubleclick.net/gan_click?lid=41000000028437369&pubid=' + pub_id
|
||||
|
||||
if detail_item:
|
||||
mo = re.search(r'(?<=/)(?P<isbn>\d+)(?=/|$)', detail_item)
|
||||
if mo:
|
||||
isbn = mo.group('isbn')
|
||||
detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://productsearch.barnesandnoble.com/search/results.aspx?STORE=EBOOK&SZE=%s&WRD=' % max_results
|
||||
url += 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[contains(@class, "wgt-search-results-display")]/li[contains(@class, "search-result-item") and contains(@class, "nook-result-item")]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
cover_url = ''.join(data.xpath('.//div[contains(@class, "wgt-product-image-module")]/a/img/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//span[@class="product-title"]/a/text()'))
|
||||
author = ', '.join(data.xpath('.//span[@class="contributers-line"]/a/text()'))
|
||||
price = ''.join(data.xpath('.//span[contains(@class, "onlinePriceValue2")]/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
87
src/calibre/gui2/store/diesel_ebooks_plugin.py
Normal file
87
src/calibre/gui2/store/diesel_ebooks_plugin.py
Normal file
@ -0,0 +1,87 @@
|
||||
# -*- 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 random
|
||||
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 DieselEbooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.diesel-ebooks.com/'
|
||||
|
||||
aff_id = '?aid=2049'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id = '?aid=2053'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item + aff_id
|
||||
url = url + aff_id
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.diesel-ebooks.com/index.php?page=seek&id[m]=&id[c]=scope%253Dinventory&id[q]=' + 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('//div[@class="item clearfix"]'):
|
||||
data = html.fromstring(html.tostring(data))
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('div[@class="cover"]/a/@href'))
|
||||
if not id or '/item/' not in id:
|
||||
continue
|
||||
a, b, id = id.partition('/item/')
|
||||
|
||||
cover_url = ''.join(data.xpath('div[@class="cover"]//img/@src'))
|
||||
if cover_url.startswith('/'):
|
||||
cover_url = cover_url[1:]
|
||||
cover_url = 'http://www.diesel-ebooks.com/' + cover_url
|
||||
|
||||
title = ''.join(data.xpath('.//div[@class="content"]//h2/text()'))
|
||||
author = ''.join(data.xpath('//div[@class="content"]//div[@class="author"]/a/text()'))
|
||||
price = ''
|
||||
price_elem = data.xpath('//td[@class="price"]/text()')
|
||||
if price_elem:
|
||||
price = price_elem[0]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/item/' + id.strip()
|
||||
|
||||
yield s
|
95
src/calibre/gui2/store/ebooks_com_plugin.py
Normal file
95
src/calibre/gui2/store/ebooks_com_plugin.py
Normal file
@ -0,0 +1,95 @@
|
||||
# -*- 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 random
|
||||
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 EbookscomStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-10364500'
|
||||
d_click = 'click-4879827-10281551'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-10364500'
|
||||
d_click = 'click-4913808-10281551'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.ebooks.com/SearchApp/SearchResults.net?term=' + 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('//div[@class="book_a" or @class="book_b"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[1]/@href'))
|
||||
id = id.split('=')[-1]
|
||||
if not id:
|
||||
continue
|
||||
|
||||
price = ''
|
||||
with closing(br.open('http://www.ebooks.com/ebooks/book_display.asp?IID=' + id.strip(), timeout=timeout)) as fp:
|
||||
pdoc = html.fromstring(fp.read())
|
||||
pdata = pdoc.xpath('//table[@class="price"]/tr/td/text()')
|
||||
if len(pdata) >= 2:
|
||||
price = pdata[1]
|
||||
if not price:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//img[1]/@src'))
|
||||
|
||||
title = ''
|
||||
author = ''
|
||||
heading_a = data.xpath('.//a[1]/text()')
|
||||
if heading_a:
|
||||
title = heading_a[0]
|
||||
if len(heading_a) >= 2:
|
||||
author = heading_a[1]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '?url=http://www.ebooks.com/cj.asp?IID=' + id.strip() + '&cjsku=' + id.strip()
|
||||
|
||||
yield s
|
80
src/calibre/gui2/store/eharlequin_plugin.py
Normal file
80
src/calibre/gui2/store/eharlequin_plugin.py
Normal file
@ -0,0 +1,80 @@
|
||||
# -*- 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 random
|
||||
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 EHarlequinStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-534091'
|
||||
d_click = 'click-4879827-10375439'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-534091'
|
||||
d_click = 'click-4913808-10375439'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://ebooks.eharlequin.com/BANGSearch.dll?Type=FullText&FullTextField=All&FullTextCriteria=' + 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('//table[not(.//@class="sidelink")]/tr[.//ul[@id="details"]]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
title = ''.join(data.xpath('.//ul[@id="details"]/li[@id="title-results"]/a/text()'))
|
||||
author = ''.join(data.xpath('.//ul[@id="details"]/li[@id="author"][1]//a/text()'))
|
||||
price = ''.join(data.xpath('.//div[@class="ourprice"]/font/text()'))
|
||||
cover_url = ''.join(data.xpath('.//a[@href="%s"]/img/@src' % id))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip()
|
||||
|
||||
yield s
|
92
src/calibre/gui2/store/feedbooks_plugin.py
Normal file
92
src/calibre/gui2/store/feedbooks_plugin.py
Normal file
@ -0,0 +1,92 @@
|
||||
# -*- 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):
|
||||
settings = self.get_settings()
|
||||
url = 'http://m.feedbooks.com/'
|
||||
ext_url = 'http://feedbooks.com/'
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_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()'))
|
||||
if not price:
|
||||
price = '$0.00'
|
||||
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()
|
||||
|
||||
yield s
|
83
src/calibre/gui2/store/gutenberg_plugin.py
Normal file
83
src/calibre/gui2/store/gutenberg_plugin.py
Normal file
@ -0,0 +1,83 @@
|
||||
# -*- 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 GutenbergStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://m.gutenberg.org/'
|
||||
ext_url = 'http://gutenberg.org/'
|
||||
|
||||
if external or settings.get(self.name + '_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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
# Gutenberg's website does not allow searching both author and title.
|
||||
# Using a google search so we can search on both fields at once.
|
||||
url = 'http://www.google.com/xhtml?q=site:gutenberg.org+' + 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('//div[@class="edewpi"]//div[@class="r ld"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
url = ''
|
||||
url_a = data.xpath('div[@class="jd"]/a')
|
||||
if url_a:
|
||||
url_a = url_a[0]
|
||||
url = url_a.get('href', None)
|
||||
if url:
|
||||
url = url.split('u=')[-1].split('&')[0]
|
||||
if '/ebooks/' not in url:
|
||||
continue
|
||||
id = url.split('/')[-1]
|
||||
|
||||
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'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = ''
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/ebooks/' + id.strip()
|
||||
|
||||
yield s
|
84
src/calibre/gui2/store/kobo_plugin.py
Normal file
84
src/calibre/gui2/store/kobo_plugin.py
Normal file
@ -0,0 +1,84 @@
|
||||
# -*- 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 random
|
||||
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 KoboStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
|
||||
m_url = 'http://www.dpbolvw.net/'
|
||||
h_click = 'click-4879827-10762497'
|
||||
d_click = 'click-4879827-10772898'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
h_click = 'click-4913808-10762497'
|
||||
d_click = 'click-4913808-10772898'
|
||||
|
||||
url = m_url + h_click
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = m_url + d_click + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.kobobooks.com/search/search.html?q=' + 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="SCShortCoverList"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//div[@class="SearchImageContainer"]/a[1]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
price = ''.join(data.xpath('.//span[@class="SCOurPrice"]/strong/text()'))
|
||||
if not price:
|
||||
price = '$0.00'
|
||||
|
||||
cover_url = ''.join(data.xpath('.//div[@class="SearchImageContainer"]//img[1]/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()'))
|
||||
author = ''.join(data.xpath('.//div[@class="SCItemSummary"]/span/a[1]/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 = '?url=http://www.kobobooks.com/' + id.strip()
|
||||
|
||||
yield s
|
93
src/calibre/gui2/store/manybooks_plugin.py
Normal file
93
src/calibre/gui2/store/manybooks_plugin.py
Normal file
@ -0,0 +1,93 @@
|
||||
# -*- 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 re
|
||||
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 ManyBooksStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://manybooks.net/'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
# ManyBooks website separates results for title and author.
|
||||
# It also doesn't do a clear job of references authors and
|
||||
# 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+' + 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('//div[@class="edewpi"]//div[@class="r ld"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
url = ''
|
||||
url_a = data.xpath('div[@class="jd"]/a')
|
||||
if url_a:
|
||||
url_a = url_a[0]
|
||||
url = url_a.get('href', None)
|
||||
if url:
|
||||
url = url.split('u=')[-1][:-2]
|
||||
if '/titles/' not in url:
|
||||
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:
|
||||
cover_name = mo.group()
|
||||
cover_name = cover_name.replace('etext', '')
|
||||
cover_id = id.split('.')[0]
|
||||
cover_url = 'http://manybooks_images.s3.amazonaws.com/original_covers/' + id[0] + '/' + cover_name + '/' + cover_id + '-thumb.jpg'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/titles/' + id
|
||||
|
||||
yield s
|
304
src/calibre/gui2/store/mobileread_plugin.py
Normal file
304
src/calibre/gui2/store/mobileread_plugin.py
Normal file
@ -0,0 +1,304 @@
|
||||
# -*- 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 difflib
|
||||
import heapq
|
||||
import time
|
||||
from contextlib import closing
|
||||
from threading import RLock
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \
|
||||
pyqtSignal
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import open_url, NONE
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
from calibre.utils.config import DynamicConfig
|
||||
from calibre.utils.icu import sort_key
|
||||
|
||||
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def genesis(self):
|
||||
self.config = DynamicConfig('store_' + self.name)
|
||||
self.rlock = RLock()
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.mobileread.com/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(detail_item if detail_item else url))
|
||||
else:
|
||||
if detail_item:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
else:
|
||||
d = MobeReadStoreDialog(self, parent)
|
||||
d.setWindowTitle(self.name)
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
books = self.get_book_list(timeout=timeout)
|
||||
|
||||
query = query.lower()
|
||||
query_parts = query.split(' ')
|
||||
matches = []
|
||||
s = difflib.SequenceMatcher()
|
||||
for x in books:
|
||||
ratio = 0
|
||||
t_string = '%s %s' % (x.author.lower(), x.title.lower())
|
||||
query_matches = []
|
||||
for q in query_parts:
|
||||
if q in t_string:
|
||||
query_matches.append(q)
|
||||
for q in query_matches:
|
||||
s.set_seq2(q)
|
||||
for p in t_string.split(' '):
|
||||
s.set_seq1(p)
|
||||
ratio += s.ratio()
|
||||
if ratio > 0:
|
||||
matches.append((ratio, x))
|
||||
|
||||
# Move the best scorers to head of list.
|
||||
matches = heapq.nlargest(max_results, matches)
|
||||
for score, book in matches:
|
||||
book.price = '$0.00'
|
||||
yield book
|
||||
|
||||
def update_book_list(self, timeout=10):
|
||||
with self.rlock:
|
||||
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
|
||||
|
||||
last_download = self.config.get(self.name + '_last_download', None)
|
||||
# Don't update the book list if our cache is less than one week old.
|
||||
if last_download and (time.time() - last_download) < 604800:
|
||||
return
|
||||
|
||||
# Download the book list HTML file from MobileRead.
|
||||
br = browser()
|
||||
raw_data = None
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
raw_data = f.read()
|
||||
|
||||
if not raw_data:
|
||||
return
|
||||
|
||||
# Turn books listed in the HTML file into BookRef's.
|
||||
books = []
|
||||
try:
|
||||
data = html.fromstring(raw_data)
|
||||
for book_data in data.xpath('//ul/li'):
|
||||
book = BookRef()
|
||||
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
||||
book.format = ''.join(book_data.xpath('.//i/text()'))
|
||||
book.format = book.format.strip()
|
||||
|
||||
text = ''.join(book_data.xpath('.//a/text()'))
|
||||
if ':' in text:
|
||||
book.author, q, text = text.partition(':')
|
||||
book.author = book.author.strip()
|
||||
book.title = text.strip()
|
||||
books.append(book)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Save the book list and it's create time.
|
||||
if books:
|
||||
self.config[self.name + '_last_download'] = time.time()
|
||||
self.config[self.name + '_book_list'] = books
|
||||
|
||||
def get_book_list(self, timeout=10):
|
||||
self.update_book_list(timeout=timeout)
|
||||
return self.config.get(self.name + '_book_list', [])
|
||||
|
||||
|
||||
class BookRef(SearchResult):
|
||||
|
||||
def __init__(self):
|
||||
SearchResult.__init__(self)
|
||||
|
||||
self.format = ''
|
||||
|
||||
|
||||
class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, plugin, *args):
|
||||
QDialog.__init__(self, *args)
|
||||
self.setupUi(self)
|
||||
|
||||
self.plugin = plugin
|
||||
|
||||
self.model = BooksModel()
|
||||
self.results_view.setModel(self.model)
|
||||
self.results_view.model().set_books(self.plugin.get_book_list())
|
||||
self.total.setText('%s' % self.model.rowCount())
|
||||
|
||||
self.results_view.activated.connect(self.open_store)
|
||||
self.search_query.textChanged.connect(self.model.set_filter)
|
||||
self.results_view.model().total_changed.connect(self.total.setText)
|
||||
self.finished.connect(self.dialog_closed)
|
||||
|
||||
self.restore_state()
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_book(index)
|
||||
if result:
|
||||
self.plugin.open(self, result.detail_item)
|
||||
|
||||
def restore_state(self):
|
||||
geometry = self.plugin.config['store_mobileread_dialog_geometry']
|
||||
if geometry:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
results_cwidth = self.plugin.config['store_mobileread_dialog_results_view_column_width']
|
||||
if results_cwidth:
|
||||
for i, x in enumerate(results_cwidth):
|
||||
if i >= self.results_view.model().columnCount():
|
||||
break
|
||||
self.results_view.setColumnWidth(i, x)
|
||||
else:
|
||||
for i in xrange(self.results_view.model().columnCount()):
|
||||
self.results_view.resizeColumnToContents(i)
|
||||
|
||||
self.results_view.model().sort_col = self.plugin.config.get('store_mobileread_dialog_sort_col', 0)
|
||||
self.results_view.model().sort_order = self.plugin.config.get('store_mobileread_dialog_sort_order', Qt.AscendingOrder)
|
||||
self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
||||
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
||||
|
||||
def save_state(self):
|
||||
self.plugin.config['store_mobileread_dialog_geometry'] = self.saveGeometry()
|
||||
self.plugin.config['store_mobileread_dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||
self.plugin.config['store_mobileread_dialog_sort_col'] = self.results_view.model().sort_col
|
||||
self.plugin.config['store_mobileread_dialog_sort_order'] = self.results_view.model().sort_order
|
||||
|
||||
def dialog_closed(self, result):
|
||||
self.save_state()
|
||||
|
||||
|
||||
class BooksModel(QAbstractItemModel):
|
||||
|
||||
total_changed = pyqtSignal(unicode)
|
||||
|
||||
HEADERS = [_('Title'), _('Author(s)'), _('Format')]
|
||||
|
||||
def __init__(self):
|
||||
QAbstractItemModel.__init__(self)
|
||||
self.books = []
|
||||
self.all_books = []
|
||||
self.filter = ''
|
||||
self.sort_col = 0
|
||||
self.sort_order = Qt.AscendingOrder
|
||||
|
||||
def set_books(self, books):
|
||||
self.books = books
|
||||
self.all_books = books
|
||||
|
||||
self.sort(self.sort_col, self.sort_order)
|
||||
|
||||
def get_book(self, index):
|
||||
row = index.row()
|
||||
if row < len(self.books):
|
||||
return self.books[row]
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_filter(self, filter):
|
||||
#self.layoutAboutToBeChanged.emit()
|
||||
self.beginResetModel()
|
||||
|
||||
self.filter = unicode(filter)
|
||||
self.books = []
|
||||
if self.filter:
|
||||
for b in self.all_books:
|
||||
test = '%s %s %s' % (b.title, b.author, b.format)
|
||||
test = test.lower()
|
||||
include = True
|
||||
for item in self.filter.split(' '):
|
||||
item = item.lower()
|
||||
if item not in test:
|
||||
include = False
|
||||
break
|
||||
if include:
|
||||
self.books.append(b)
|
||||
else:
|
||||
self.books = self.all_books
|
||||
|
||||
self.sort(self.sort_col, self.sort_order, reset=False)
|
||||
self.total_changed.emit('%s' % self.rowCount())
|
||||
|
||||
self.endResetModel()
|
||||
#self.layoutChanged.emit()
|
||||
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid() or index.internalId() == 0:
|
||||
return QModelIndex()
|
||||
return self.createIndex(0, 0)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.books)
|
||||
|
||||
def columnCount(self, *args):
|
||||
return len(self.HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ''
|
||||
if orientation == Qt.Horizontal:
|
||||
if section < len(self.HEADERS):
|
||||
text = self.HEADERS[section]
|
||||
return QVariant(text)
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
result = self.books[row]
|
||||
if role == Qt.DisplayRole:
|
||||
if col == 0:
|
||||
return QVariant(result.title)
|
||||
elif col == 1:
|
||||
return QVariant(result.author)
|
||||
elif col == 2:
|
||||
return QVariant(result.format)
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, result, col):
|
||||
text = ''
|
||||
if col == 0:
|
||||
text = result.title
|
||||
elif col == 1:
|
||||
text = result.author
|
||||
elif col == 2:
|
||||
text = result.format
|
||||
return text
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
self.sort_col = col
|
||||
self.sort_order = order
|
||||
|
||||
if not self.books:
|
||||
return
|
||||
descending = order == Qt.DescendingOrder
|
||||
self.books.sort(None,
|
||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||
descending)
|
||||
if reset:
|
||||
self.reset()
|
||||
|
112
src/calibre/gui2/store/mobileread_store_dialog.ui
Normal file
112
src/calibre/gui2/store/mobileread_store_dialog.ui
Normal file
@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>691</width>
|
||||
<height>614</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Search:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_query"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTreeView" name="results_view">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerCascadingSectionResizes">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Books:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="total">
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>308</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="close_button">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close_button</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>440</x>
|
||||
<y>432</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>245</x>
|
||||
<y>230</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
72
src/calibre/gui2/store/open_library_plugin.py
Normal file
72
src/calibre/gui2/store/open_library_plugin.py
Normal file
@ -0,0 +1,72 @@
|
||||
# -*- 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 OpenLibraryStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://openlibrary.org/'
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(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(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://openlibrary.org/search?q=' + urllib2.quote(query) + '&has_fulltext=true'
|
||||
|
||||
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[@id="searchResults"]/ul[@id="siteSearch"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href'))
|
||||
if not id:
|
||||
continue
|
||||
cover_url = ''.join(data.xpath('./span[@class="bookcover"]/a/img/@src'))
|
||||
|
||||
title = ''.join(data.xpath('.//h3[@class="booktitle"]/a[@class="results"]/text()'))
|
||||
author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()'))
|
||||
price = '$0.00'
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = id.strip()
|
||||
|
||||
yield s
|
452
src/calibre/gui2/store/search.py
Normal file
452
src/calibre/gui2/store/search.py
Normal file
@ -0,0 +1,452 @@
|
||||
# -*- 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 re
|
||||
import time
|
||||
from contextlib import closing
|
||||
from random import shuffle
|
||||
from threading import Thread
|
||||
from Queue import Queue
|
||||
|
||||
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
|
||||
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
|
||||
|
||||
from calibre import browser
|
||||
from calibre.gui2 import NONE
|
||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||
from calibre.gui2.store.search_ui import Ui_Dialog
|
||||
from calibre.utils.config import DynamicConfig
|
||||
from calibre.utils.icu import sort_key
|
||||
from calibre.utils.magick.draw import thumbnail
|
||||
|
||||
HANG_TIME = 75000 # milliseconds seconds
|
||||
TIMEOUT = 75 # seconds
|
||||
SEARCH_THREAD_TOTAL = 4
|
||||
COVER_DOWNLOAD_THREAD_TOTAL = 2
|
||||
|
||||
class SearchDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, istores, *args):
|
||||
QDialog.__init__(self, *args)
|
||||
self.setupUi(self)
|
||||
|
||||
self.config = DynamicConfig('store_search')
|
||||
|
||||
# We keep a cache of store plugins and reference them by name.
|
||||
self.store_plugins = istores
|
||||
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
|
||||
# Check for results and hung threads.
|
||||
self.checker = QTimer()
|
||||
self.hang_check = 0
|
||||
|
||||
self.model = Matches()
|
||||
self.results_view.setModel(self.model)
|
||||
|
||||
# Add check boxes for each store so the user
|
||||
# can disable searching specific stores on a
|
||||
# per search basis.
|
||||
stores_group_layout = QVBoxLayout()
|
||||
self.stores_group.setLayout(stores_group_layout)
|
||||
for x in self.store_plugins:
|
||||
cbox = QCheckBox(x)
|
||||
cbox.setChecked(True)
|
||||
stores_group_layout.addWidget(cbox)
|
||||
setattr(self, 'store_check_' + x, cbox)
|
||||
stores_group_layout.addStretch()
|
||||
|
||||
# Create and add the progress indicator
|
||||
self.pi = ProgressIndicator(self, 24)
|
||||
self.bottom_layout.insertWidget(0, self.pi)
|
||||
|
||||
self.search.clicked.connect(self.do_search)
|
||||
self.checker.timeout.connect(self.get_results)
|
||||
self.results_view.activated.connect(self.open_store)
|
||||
self.select_all_stores.clicked.connect(self.stores_select_all)
|
||||
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
||||
self.select_none_stores.clicked.connect(self.stores_select_none)
|
||||
self.finished.connect(self.dialog_closed)
|
||||
|
||||
self.restore_state()
|
||||
|
||||
def resize_columns(self):
|
||||
total = 600
|
||||
# Cover
|
||||
self.results_view.setColumnWidth(0, 85)
|
||||
total = total - 85
|
||||
# Title
|
||||
self.results_view.setColumnWidth(1,int(total*.35))
|
||||
# Author
|
||||
self.results_view.setColumnWidth(2,int(total*.35))
|
||||
# Price
|
||||
self.results_view.setColumnWidth(3, int(total*.10))
|
||||
# Store
|
||||
self.results_view.setColumnWidth(4, int(total*.20))
|
||||
|
||||
def do_search(self, checked=False):
|
||||
# Stop all running threads.
|
||||
self.checker.stop()
|
||||
self.search_pool.abort()
|
||||
# Clear the visible results.
|
||||
self.results_view.model().clear_results()
|
||||
|
||||
# Don't start a search if there is nothing to search for.
|
||||
query = unicode(self.search_edit.text())
|
||||
if not query.strip():
|
||||
return
|
||||
|
||||
# Plugins are in alphebetic order. Randomize the
|
||||
# order of plugin names. This way plugins closer
|
||||
# to a don't have an unfair advantage over
|
||||
# plugins further from a.
|
||||
store_names = self.store_plugins.keys()
|
||||
if not store_names:
|
||||
return
|
||||
shuffle(store_names)
|
||||
# Add plugins that the user has checked to the search pool's work queue.
|
||||
for n in store_names:
|
||||
if getattr(self, 'store_check_' + n).isChecked():
|
||||
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT)
|
||||
if self.search_pool.has_tasks():
|
||||
self.hang_check = 0
|
||||
self.checker.start(100)
|
||||
self.search_pool.start_threads()
|
||||
self.pi.startAnimation()
|
||||
|
||||
def save_state(self):
|
||||
self.config['store_search_geometry'] = self.saveGeometry()
|
||||
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
||||
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||
|
||||
store_check = {}
|
||||
for n in self.store_plugins:
|
||||
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
||||
self.config['store_search_store_checked'] = store_check
|
||||
|
||||
def restore_state(self):
|
||||
geometry = self.config['store_search_geometry']
|
||||
if geometry:
|
||||
self.restoreGeometry(geometry)
|
||||
|
||||
splitter_state = self.config['store_search_store_splitter_state']
|
||||
if splitter_state:
|
||||
self.store_splitter.restoreState(splitter_state)
|
||||
|
||||
results_cwidth = self.config['store_search_results_view_column_width']
|
||||
if results_cwidth:
|
||||
for i, x in enumerate(results_cwidth):
|
||||
if i >= self.model.columnCount():
|
||||
break
|
||||
self.results_view.setColumnWidth(i, x)
|
||||
else:
|
||||
self.resize_columns()
|
||||
|
||||
store_check = self.config['store_search_store_checked']
|
||||
if store_check:
|
||||
for n in store_check:
|
||||
if hasattr(self, 'store_check_' + n):
|
||||
getattr(self, 'store_check_' + n).setChecked(store_check[n])
|
||||
|
||||
def get_results(self):
|
||||
# We only want the search plugins to run
|
||||
# a maximum set amount of time before giving up.
|
||||
self.hang_check += 1
|
||||
if self.hang_check >= HANG_TIME:
|
||||
self.search_pool.abort()
|
||||
self.checker.stop()
|
||||
self.pi.stopAnimation()
|
||||
else:
|
||||
# Stop the checker if not threads are running.
|
||||
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
||||
self.checker.stop()
|
||||
self.pi.stopAnimation()
|
||||
|
||||
while self.search_pool.has_results():
|
||||
res = self.search_pool.get_result()
|
||||
if res:
|
||||
self.results_view.model().add_result(res)
|
||||
|
||||
def open_store(self, index):
|
||||
result = self.results_view.model().get_result(index)
|
||||
self.store_plugins[result.store_name].open(self, result.detail_item)
|
||||
|
||||
def get_store_checks(self):
|
||||
'''
|
||||
Returns a list of QCheckBox's for each store.
|
||||
'''
|
||||
checks = []
|
||||
for x in self.store_plugins:
|
||||
check = getattr(self, 'store_check_' + x, None)
|
||||
if check:
|
||||
checks.append(check)
|
||||
return checks
|
||||
|
||||
def stores_select_all(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(True)
|
||||
|
||||
def stores_select_invert(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(not check.isChecked())
|
||||
|
||||
def stores_select_none(self):
|
||||
for check in self.get_store_checks():
|
||||
check.setChecked(False)
|
||||
|
||||
def dialog_closed(self, result):
|
||||
self.model.closing()
|
||||
self.search_pool.abort()
|
||||
self.save_state()
|
||||
|
||||
|
||||
class GenericDownloadThreadPool(object):
|
||||
'''
|
||||
add_task must be implemented in a subclass.
|
||||
'''
|
||||
|
||||
def __init__(self, thread_type, thread_count):
|
||||
self.thread_type = thread_type
|
||||
self.thread_count = thread_count
|
||||
|
||||
self.tasks = Queue()
|
||||
self.results = Queue()
|
||||
self.threads = []
|
||||
|
||||
def add_task(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def start_threads(self):
|
||||
for i in range(self.thread_count):
|
||||
t = self.thread_type(self.tasks, self.results)
|
||||
self.threads.append(t)
|
||||
t.start()
|
||||
|
||||
def abort(self):
|
||||
self.tasks = Queue()
|
||||
self.results = Queue()
|
||||
for t in self.threads:
|
||||
t.abort()
|
||||
self.threads = []
|
||||
|
||||
def has_tasks(self):
|
||||
return not self.tasks.empty()
|
||||
|
||||
def get_result(self):
|
||||
return self.results.get()
|
||||
|
||||
def get_result_no_wait(self):
|
||||
return self.results.get_nowait()
|
||||
|
||||
def result_count(self):
|
||||
return len(self.results)
|
||||
|
||||
def has_results(self):
|
||||
return not self.results.empty()
|
||||
|
||||
def threads_running(self):
|
||||
for t in self.threads:
|
||||
if t.is_alive():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class SearchThreadPool(GenericDownloadThreadPool):
|
||||
'''
|
||||
Threads will run until there is no work or
|
||||
abort is called. Create and start new threads
|
||||
using start_threads(). Reset by calling abort().
|
||||
|
||||
Example:
|
||||
sp = SearchThreadPool(SearchThread, 3)
|
||||
add tasks using add_task(...)
|
||||
sp.start_threads()
|
||||
all threads have finished.
|
||||
sp.abort()
|
||||
add tasks using add_task(...)
|
||||
sp.start_threads()
|
||||
'''
|
||||
|
||||
def add_task(self, query, store_name, store_plugin, timeout):
|
||||
self.tasks.put((query, store_name, store_plugin, timeout))
|
||||
|
||||
|
||||
class SearchThread(Thread):
|
||||
|
||||
def __init__(self, tasks, results):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.tasks = tasks
|
||||
self.results = results
|
||||
self._run = True
|
||||
|
||||
def abort(self):
|
||||
self._run = False
|
||||
|
||||
def run(self):
|
||||
while self._run and not self.tasks.empty():
|
||||
try:
|
||||
query, store_name, store_plugin, timeout = self.tasks.get()
|
||||
for res in store_plugin.search(query, timeout=timeout):
|
||||
if not self._run:
|
||||
return
|
||||
res.store_name = store_name
|
||||
self.results.put(res)
|
||||
self.tasks.task_done()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class CoverThreadPool(GenericDownloadThreadPool):
|
||||
'''
|
||||
Once started all threads run until abort is called.
|
||||
'''
|
||||
|
||||
def add_task(self, search_result, update_callback, timeout=5):
|
||||
self.tasks.put((search_result, update_callback, timeout))
|
||||
|
||||
|
||||
class CoverThread(Thread):
|
||||
|
||||
def __init__(self, tasks, results):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.tasks = tasks
|
||||
self.results = results
|
||||
self._run = True
|
||||
|
||||
self.br = browser()
|
||||
|
||||
def abort(self):
|
||||
self._run = False
|
||||
|
||||
def run(self):
|
||||
while self._run:
|
||||
try:
|
||||
time.sleep(.1)
|
||||
while not self.tasks.empty():
|
||||
if not self._run:
|
||||
break
|
||||
result, callback, timeout = self.tasks.get()
|
||||
if result and result.cover_url:
|
||||
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
|
||||
result.cover_data = f.read()
|
||||
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
|
||||
callback()
|
||||
self.tasks.task_done()
|
||||
except:
|
||||
continue
|
||||
|
||||
|
||||
class Matches(QAbstractItemModel):
|
||||
|
||||
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('Store')]
|
||||
|
||||
def __init__(self):
|
||||
QAbstractItemModel.__init__(self)
|
||||
self.matches = []
|
||||
self.cover_pool = CoverThreadPool(CoverThread, 2)
|
||||
self.cover_pool.start_threads()
|
||||
|
||||
def closing(self):
|
||||
self.cover_pool.abort()
|
||||
|
||||
def clear_results(self):
|
||||
self.matches = []
|
||||
self.cover_pool.abort()
|
||||
self.cover_pool.start_threads()
|
||||
self.reset()
|
||||
|
||||
def add_result(self, result):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.matches.append(result)
|
||||
self.cover_pool.add_task(result, self.update_result)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def get_result(self, index):
|
||||
row = index.row()
|
||||
if row < len(self.matches):
|
||||
return self.matches[row]
|
||||
else:
|
||||
return None
|
||||
|
||||
def update_result(self):
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid() or index.internalId() == 0:
|
||||
return QModelIndex()
|
||||
return self.createIndex(0, 0)
|
||||
|
||||
def rowCount(self, *args):
|
||||
return len(self.matches)
|
||||
|
||||
def columnCount(self, *args):
|
||||
return len(self.HEADERS)
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if role != Qt.DisplayRole:
|
||||
return NONE
|
||||
text = ''
|
||||
if orientation == Qt.Horizontal:
|
||||
if section < len(self.HEADERS):
|
||||
text = self.HEADERS[section]
|
||||
return QVariant(text)
|
||||
else:
|
||||
return QVariant(section+1)
|
||||
|
||||
def data(self, index, role):
|
||||
row, col = index.row(), index.column()
|
||||
result = self.matches[row]
|
||||
if role == Qt.DisplayRole:
|
||||
if col == 1:
|
||||
return QVariant(result.title)
|
||||
elif col == 2:
|
||||
return QVariant(result.author)
|
||||
elif col == 3:
|
||||
return QVariant(result.price)
|
||||
elif col == 4:
|
||||
return QVariant(result.store_name)
|
||||
return NONE
|
||||
elif role == Qt.DecorationRole:
|
||||
if col == 0 and result.cover_data:
|
||||
p = QPixmap()
|
||||
p.loadFromData(result.cover_data)
|
||||
return QVariant(p)
|
||||
elif role == Qt.SizeHintRole:
|
||||
return QSize(64, 64)
|
||||
return NONE
|
||||
|
||||
def data_as_text(self, result, col):
|
||||
text = ''
|
||||
if col == 1:
|
||||
text = result.title
|
||||
elif col == 2:
|
||||
text = result.author
|
||||
elif col == 3:
|
||||
text = result.price
|
||||
if len(text) < 3 or text[-3] not in ('.', ','):
|
||||
text += '00'
|
||||
text = re.sub(r'\D', '', text)
|
||||
text = text.rjust(6, '0')
|
||||
elif col == 4:
|
||||
text = result.store_name
|
||||
return text
|
||||
|
||||
def sort(self, col, order, reset=True):
|
||||
if not self.matches:
|
||||
return
|
||||
descending = order == Qt.DescendingOrder
|
||||
self.matches.sort(None,
|
||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||
descending)
|
||||
if reset:
|
||||
self.reset()
|
||||
|
196
src/calibre/gui2/store/search.ui
Normal file
196
src/calibre/gui2/store/search.ui
Normal file
@ -0,0 +1,196 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>937</width>
|
||||
<height>669</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>calibre Store Search</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Query:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="search_edit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="search">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSplitter" name="store_splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Stores</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QScrollArea" name="stores_group">
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>215</width>
|
||||
<height>116</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_all_stores">
|
||||
<property name="text">
|
||||
<string>All</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_invert_stores">
|
||||
<property name="text">
|
||||
<string>Invert</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="select_none_stores">
|
||||
<property name="text">
|
||||
<string>None</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QSplitter" name="splitter_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>2</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<widget class="QTreeView" name="results_view">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>32</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="bottom_layout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="close">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>526</x>
|
||||
<y>525</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>307</x>
|
||||
<y>272</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
18
src/calibre/gui2/store/search_result.py
Normal file
18
src/calibre/gui2/store/search_result.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- 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'
|
||||
|
||||
class SearchResult(object):
|
||||
|
||||
def __init__(self):
|
||||
self.store_name = ''
|
||||
self.cover_url = ''
|
||||
self.cover_data = None
|
||||
self.title = ''
|
||||
self.author = ''
|
||||
self.price = ''
|
||||
self.detail_item = ''
|
94
src/calibre/gui2/store/smashwords_plugin.py
Normal file
94
src/calibre/gui2/store/smashwords_plugin.py
Normal file
@ -0,0 +1,94 @@
|
||||
# -*- 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 random
|
||||
import re
|
||||
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 SmashwordsStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
settings = self.get_settings()
|
||||
url = 'http://www.smashwords.com/'
|
||||
|
||||
aff_id = '?ref=usernone'
|
||||
# Use Kovid's affiliate id 30% of the time.
|
||||
if random.randint(1, 10) in (1, 2, 3):
|
||||
aff_id = '?ref=kovidgoyal'
|
||||
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item + aff_id
|
||||
url = url + aff_id
|
||||
|
||||
if external or settings.get(self.name + '_open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://www.smashwords.com/books/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('//div[@id="pageCenterContent2"]//div[@class="bookCoverImg"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
data = html.fromstring(html.tostring(data))
|
||||
|
||||
id = None
|
||||
id_a = data.xpath('//a[@class="bookTitle"]')
|
||||
if id_a:
|
||||
id = id_a[0].get('href', None)
|
||||
if id:
|
||||
id = id.split('/')[-1]
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''
|
||||
c_url = data.get('style', None)
|
||||
if c_url:
|
||||
mo = re.search(r'http://[^\'"]+', c_url)
|
||||
if mo:
|
||||
cover_url = mo.group()
|
||||
|
||||
title = ''.join(data.xpath('//a[@class="bookTitle"]/text()'))
|
||||
subnote = ''.join(data.xpath('//span[@class="subnote"]/text()'))
|
||||
author = ''.join(data.xpath('//span[@class="subnote"]/a/text()'))
|
||||
price = subnote.partition('$')[2]
|
||||
price = price.split(u'\xa0')[0]
|
||||
price = '$' + price
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price.strip()
|
||||
s.detail_item = '/books/view/' + id.strip()
|
||||
|
||||
yield s
|
112
src/calibre/gui2/store/web_control.py
Normal file
112
src/calibre/gui2/store/web_control.py
Normal file
@ -0,0 +1,112 @@
|
||||
# -*- 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 os
|
||||
from urlparse import urlparse
|
||||
|
||||
from PyQt4.Qt import (QWebView, QWebPage, QNetworkCookieJar,
|
||||
QFileDialog, QNetworkProxy)
|
||||
|
||||
from calibre import USER_AGENT, get_proxies, get_download_filename
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
|
||||
class NPWebView(QWebView):
|
||||
|
||||
def __init__(self, *args):
|
||||
QWebView.__init__(self, *args)
|
||||
self.gui = None
|
||||
self.tags = ''
|
||||
|
||||
self.setPage(NPWebPage())
|
||||
self.page().networkAccessManager().setCookieJar(QNetworkCookieJar())
|
||||
|
||||
http_proxy = get_proxies().get('http', None)
|
||||
if http_proxy:
|
||||
proxy_parts = urlparse(http_proxy)
|
||||
proxy = QNetworkProxy()
|
||||
proxy.setType(QNetworkProxy.HttpProxy)
|
||||
proxy.setUser(proxy_parts.username)
|
||||
proxy.setPassword(proxy_parts.password)
|
||||
proxy.setHostName(proxy_parts.hostname)
|
||||
proxy.setPort(proxy_parts.port)
|
||||
self.page().networkAccessManager().setProxy(proxy)
|
||||
|
||||
self.page().setForwardUnsupportedContent(True)
|
||||
self.page().unsupportedContent.connect(self.start_download)
|
||||
self.page().downloadRequested.connect(self.start_download)
|
||||
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
||||
|
||||
def createWindow(self, type):
|
||||
if type == QWebPage.WebBrowserWindow:
|
||||
return self
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_gui(self, gui):
|
||||
self.gui = gui
|
||||
|
||||
def set_tags(self, tags):
|
||||
self.tags = tags
|
||||
|
||||
def start_download(self, request):
|
||||
if not self.gui:
|
||||
return
|
||||
|
||||
url = unicode(request.url().toString())
|
||||
cf = self.get_cookies()
|
||||
|
||||
filename = get_download_filename(url, cf)
|
||||
ext = os.path.splitext(filename)[1][1:].lower()
|
||||
if ext not in BOOK_EXTENSIONS:
|
||||
home = os.path.expanduser('~')
|
||||
name = QFileDialog.getSaveFileName(self,
|
||||
_('File is not a supported ebook type. Save to disk?'),
|
||||
os.path.join(home, filename),
|
||||
'*.*')
|
||||
if name:
|
||||
self.gui.download_ebook(url, cf, name, name, False)
|
||||
else:
|
||||
self.gui.download_ebook(url, cf, filename, tags=self.tags)
|
||||
|
||||
def ignore_ssl_errors(self, reply, errors):
|
||||
reply.ignoreSslErrors(errors)
|
||||
|
||||
def get_cookies(self):
|
||||
'''
|
||||
Writes QNetworkCookies to Mozilla cookie .txt file.
|
||||
|
||||
:return: The file path to the cookie file.
|
||||
'''
|
||||
cf = PersistentTemporaryFile(suffix='.txt')
|
||||
|
||||
cf.write('# Netscape HTTP Cookie File\n\n')
|
||||
|
||||
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
||||
cookie = []
|
||||
domain = unicode(c.domain())
|
||||
|
||||
cookie.append(domain)
|
||||
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
||||
cookie.append(unicode(c.path()))
|
||||
cookie.append('TRUE' if c.isSecure() else 'FALSE')
|
||||
cookie.append(unicode(c.expirationDate().toTime_t()))
|
||||
cookie.append(unicode(c.name()))
|
||||
cookie.append(unicode(c.value()))
|
||||
|
||||
cf.write('\t'.join(cookie))
|
||||
cf.write('\n')
|
||||
|
||||
cf.close()
|
||||
return cf.name
|
||||
|
||||
|
||||
class NPWebPage(QWebPage):
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
return USER_AGENT
|
55
src/calibre/gui2/store/web_store_dialog.py
Normal file
55
src/calibre/gui2/store/web_store_dialog.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- 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 PyQt4.Qt import QDialog, QUrl
|
||||
|
||||
from calibre import url_slash_cleaner
|
||||
from calibre.gui2.store.web_store_dialog_ui import Ui_Dialog
|
||||
|
||||
class WebStoreDialog(QDialog, Ui_Dialog):
|
||||
|
||||
def __init__(self, gui, base_url, parent=None, detail_url=None):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
self.setupUi(self)
|
||||
|
||||
self.gui = gui
|
||||
self.base_url = base_url
|
||||
|
||||
self.view.set_gui(self.gui)
|
||||
self.view.loadStarted.connect(self.load_started)
|
||||
self.view.loadProgress.connect(self.load_progress)
|
||||
self.view.loadFinished.connect(self.load_finished)
|
||||
self.home.clicked.connect(self.go_home)
|
||||
self.reload.clicked.connect(self.view.reload)
|
||||
self.back.clicked.connect(self.view.back)
|
||||
|
||||
self.go_home(detail_url=detail_url)
|
||||
|
||||
def set_tags(self, tags):
|
||||
self.view.set_tags(tags)
|
||||
|
||||
def load_started(self):
|
||||
self.progress.setValue(0)
|
||||
|
||||
def load_progress(self, val):
|
||||
self.progress.setValue(val)
|
||||
|
||||
def load_finished(self, ok=True):
|
||||
self.progress.setValue(100)
|
||||
|
||||
def go_home(self, checked=False, detail_url=None):
|
||||
if detail_url:
|
||||
url = detail_url
|
||||
else:
|
||||
url = self.base_url
|
||||
|
||||
# Reduce redundant /'s because some stores
|
||||
# (Feedbooks) and server frameworks (cherrypy)
|
||||
# choke on them.
|
||||
url = url_slash_cleaner(url)
|
||||
self.view.load(QUrl(url))
|
115
src/calibre/gui2/store/web_store_dialog.ui
Normal file
115
src/calibre/gui2/store/web_store_dialog.ui
Normal file
@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>962</width>
|
||||
<height>656</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0" colspan="5">
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="NPWebView" name="view">
|
||||
<property name="url">
|
||||
<url>
|
||||
<string>about:blank</string>
|
||||
</url>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="home">
|
||||
<property name="text">
|
||||
<string>Home</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="reload">
|
||||
<property name="text">
|
||||
<string>Reload</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QProgressBar" name="progress">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="format">
|
||||
<string>%p%</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="back">
|
||||
<property name="text">
|
||||
<string>Back</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="4">
|
||||
<widget class="QPushButton" name="close">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QWebView</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>QtWebKit/QWebView</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>NPWebView</class>
|
||||
<extends>QWebView</extends>
|
||||
<header>web_control.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>close</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>917</x>
|
||||
<y>635</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>480</x>
|
||||
<y>327</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -12,11 +12,11 @@ import traceback, copy, cPickle
|
||||
from itertools import izip, repeat
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \
|
||||
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\
|
||||
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\
|
||||
QPushButton, QWidget, QItemDelegate, QString, QLabel, \
|
||||
QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton
|
||||
from PyQt4.Qt import (Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize,
|
||||
QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,
|
||||
QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,
|
||||
QWidget, QItemDelegate, QString, QLabel, QPushButton,
|
||||
QShortcut, QKeySequence, SIGNAL, QMimeData, QToolButton)
|
||||
|
||||
from calibre.ebooks.metadata import title_sort
|
||||
from calibre.gui2 import config, NONE, gprefs
|
||||
@ -1829,8 +1829,24 @@ class TagBrowserMixin(object): # {{{
|
||||
self.tags_view.drag_drop_finished.connect(self.drag_drop_finished)
|
||||
self.tags_view.restriction_error.connect(self.do_restriction_error,
|
||||
type=Qt.QueuedConnection)
|
||||
self.edit_categories.clicked.connect(lambda x:
|
||||
self.do_edit_user_categories())
|
||||
|
||||
for text, func, args, cat_name in (
|
||||
(_('Manage Authors'),
|
||||
self.do_author_sort_edit, (self, None), 'authors'),
|
||||
(_('Manage Series'),
|
||||
self.do_tags_list_edit, (None, 'series'), 'series'),
|
||||
(_('Manage Publishers'),
|
||||
self.do_tags_list_edit, (None, 'publisher'), 'publisher'),
|
||||
(_('Manage Tags'),
|
||||
self.do_tags_list_edit, (None, 'tags'), 'tags'),
|
||||
(_('Manage User Categories'),
|
||||
self.do_edit_user_categories, (None,), 'user:'),
|
||||
(_('Manage Saved Searches'),
|
||||
self.do_saved_search_edit, (None,), 'search')
|
||||
):
|
||||
self.manage_items_button.menu().addAction(
|
||||
QIcon(I(category_icon_map[cat_name])),
|
||||
text, partial(func, *args))
|
||||
|
||||
def do_restriction_error(self):
|
||||
error_dialog(self.tags_view, _('Invalid search restriction'),
|
||||
@ -2149,11 +2165,15 @@ class TagBrowserWidget(QWidget): # {{{
|
||||
'match any or all of them'))
|
||||
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
|
||||
|
||||
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
|
||||
self._layout.addWidget(parent.edit_categories)
|
||||
parent.edit_categories.setToolTip(
|
||||
_('Add your own categories to the Tag Browser'))
|
||||
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())
|
||||
|
||||
l = parent.manage_items_button = QPushButton(self)
|
||||
l.setStyleSheet('QPushButton {text-align: left; }')
|
||||
l.setText(_('Manage authors, tags, etc'))
|
||||
l.setToolTip(_('All of these category_managers are available by right-clicking '
|
||||
'on items in the tag browser above'))
|
||||
l.m = QMenu()
|
||||
l.setMenu(l.m)
|
||||
self._layout.addWidget(l)
|
||||
|
||||
# self.leak_test_timer = QTimer(self)
|
||||
# self.leak_test_timer.timeout.connect(self.test_for_leak)
|
||||
|
@ -38,7 +38,7 @@ class ThreadedJob(BaseJob):
|
||||
|
||||
:func: The function that actually does the work. This function *must*
|
||||
accept at least three keyword arguments: abort, log and notifications. abort is
|
||||
An Event object. func should periodically check abort.is_set(0 and if
|
||||
An Event object. func should periodically check abort.is_set() and if
|
||||
it is True, it should stop processing as soon as possible. notifications
|
||||
is a Queue. func should put progress notifications into it in the form
|
||||
of a tuple (frac, msg). frac is a number between 0 and 1 indicating
|
||||
|
@ -23,7 +23,7 @@ from calibre.constants import __appname__, isosx
|
||||
from calibre.utils.config import prefs, dynamic
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.customize.ui import interface_actions
|
||||
from calibre.customize.ui import interface_actions, store_plugins
|
||||
from calibre.gui2 import error_dialog, GetMetadata, open_url, \
|
||||
gprefs, max_available_height, config, info_dialog, Dispatcher, \
|
||||
question_dialog
|
||||
@ -34,6 +34,7 @@ from calibre.gui2.main_window import MainWindow
|
||||
from calibre.gui2.layout import MainWindowMixin
|
||||
from calibre.gui2.device import DeviceMixin
|
||||
from calibre.gui2.email import EmailMixin
|
||||
from calibre.gui2.ebook_download import EbookDownloadMixin
|
||||
from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton
|
||||
from calibre.gui2.init import LibraryViewMixin, LayoutMixin
|
||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||
@ -89,7 +90,8 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
||||
|
||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin
|
||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
||||
EbookDownloadMixin
|
||||
):
|
||||
'The main GUI'
|
||||
|
||||
@ -100,6 +102,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
self.device_connected = None
|
||||
self.gui_debug = gui_debug
|
||||
self.iactions = OrderedDict()
|
||||
# Actions
|
||||
for action in interface_actions():
|
||||
if opts.ignore_plugins and action.plugin_path is not None:
|
||||
continue
|
||||
@ -112,11 +115,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
if action.plugin_path is None:
|
||||
raise
|
||||
continue
|
||||
|
||||
ac.plugin_path = action.plugin_path
|
||||
ac.interface_action_base_plugin = action
|
||||
|
||||
self.add_iaction(ac)
|
||||
self.load_store_plugins()
|
||||
|
||||
def init_iaction(self, action):
|
||||
ac = action.load_actual_plugin(self)
|
||||
@ -133,6 +135,37 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
else:
|
||||
acmap[ac.name] = ac
|
||||
|
||||
def load_store_plugins(self):
|
||||
self.istores = OrderedDict()
|
||||
for store in store_plugins():
|
||||
if self.opts.ignore_plugins and store.plugin_path is not None:
|
||||
continue
|
||||
try:
|
||||
st = self.init_istore(store)
|
||||
self.add_istore(st)
|
||||
except:
|
||||
# Ignore errors in loading user supplied plugins
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if store.plugin_path is None:
|
||||
raise
|
||||
continue
|
||||
|
||||
def init_istore(self, store):
|
||||
st = store.load_actual_plugin(self)
|
||||
st.plugin_path = store.plugin_path
|
||||
st.base_plugin = store
|
||||
store.actual_istore_plugin_loaded = True
|
||||
return st
|
||||
|
||||
def add_istore(self, st):
|
||||
stmap = self.istores
|
||||
if st.name in stmap:
|
||||
if st.priority >= stmap[st.name].priority:
|
||||
stmap[st.name] = st
|
||||
else:
|
||||
stmap[st.name] = st
|
||||
|
||||
|
||||
def initialize(self, library_path, db, listener, actions, show_gui=True):
|
||||
opts = self.opts
|
||||
@ -154,6 +187,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
for ac in self.iactions.values():
|
||||
ac.do_genesis()
|
||||
self.donate_action = QAction(QIcon(I('donate.png')), _('&Donate to support calibre'), self)
|
||||
for st in self.istores.values():
|
||||
st.do_genesis()
|
||||
MainWindowMixin.__init__(self, db)
|
||||
|
||||
# Jobs Button {{{
|
||||
@ -165,6 +200,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
|
||||
LayoutMixin.__init__(self)
|
||||
EmailMixin.__init__(self)
|
||||
EbookDownloadMixin.__init__(self)
|
||||
DeviceMixin.__init__(self)
|
||||
|
||||
self.progress_indicator = ProgressIndicator(self)
|
||||
@ -493,10 +529,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||
action.location_selected(location)
|
||||
if location == 'library':
|
||||
self.search_restriction.setEnabled(True)
|
||||
self.search_options_button.setEnabled(True)
|
||||
self.highlight_only_button.setEnabled(True)
|
||||
else:
|
||||
self.search_restriction.setEnabled(False)
|
||||
self.search_options_button.setEnabled(False)
|
||||
self.highlight_only_button.setEnabled(False)
|
||||
# Reset the view in case something changed while it was invisible
|
||||
self.current_view().reset()
|
||||
self.set_number_of_books_shown()
|
||||
|
@ -426,7 +426,7 @@ def do_show_metadata(db, id, as_opf):
|
||||
mi = OPFCreator(os.getcwd(), mi)
|
||||
mi.render(sys.stdout)
|
||||
else:
|
||||
print unicode(mi).encode(preferred_encoding)
|
||||
prints(unicode(mi))
|
||||
|
||||
def show_metadata_option_parser():
|
||||
parser = get_parser(_(
|
||||
|
@ -854,7 +854,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
mi.uuid = row[fm['uuid']]
|
||||
mi.title_sort = row[fm['sort']]
|
||||
mi.last_modified = row[fm['last_modified']]
|
||||
mi.size = row[fm['size']]
|
||||
formats = row[fm['formats']]
|
||||
if not formats:
|
||||
formats = None
|
||||
|
@ -487,7 +487,13 @@ menu, choose "Validate fonts".
|
||||
I downloaded the installer, but it is not working?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location <http://sourceforge.net/projects/calibre/files/>`_. If the installer still doesn't work, then something on your computer is preventing it from running. Try rebooting your computer and running a registry cleaner like `Wise registry cleaner <http://www.wisecleaner.com>`_. Best place to ask for more help is in the `forums <http://www.mobileread.com/forums/usercp.php>`_.
|
||||
Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location <http://sourceforge.net/projects/calibre/files/>`_. If the installer still doesn't work, then something on your computer is preventing it from running.
|
||||
|
||||
* Try temporarily disabling your antivirus program (Microsoft Security Essentials, or Kaspersky or Norton or McAfee or whatever). This is most likely the culprit if the upgrade process is hanging in the middle.
|
||||
* Try rebooting your computer and running a registry cleaner like `Wise registry cleaner <http://www.wisecleaner.com>`_.
|
||||
* Try downloading the installer with an alternate browser. For example if you are using Internet Explorer, try using Firefox or Chrome instead.
|
||||
|
||||
Best place to ask for more help is in the `forums <http://www.mobileread.com/forums/forumdisplay.php?f=166>`_.
|
||||
|
||||
My antivirus program claims |app| is a virus/trojan?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -130,7 +130,14 @@ def utcnow():
|
||||
return datetime.utcnow().replace(tzinfo=_utc_tz)
|
||||
|
||||
def utcfromtimestamp(stamp):
|
||||
try:
|
||||
return datetime.utcfromtimestamp(stamp).replace(tzinfo=_utc_tz)
|
||||
except ValueError:
|
||||
# Raised if stamp if out of range for the platforms gmtime function
|
||||
# We print the error for debugging, but otherwise ignore it
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return utcnow()
|
||||
|
||||
def format_date(dt, format, assume_utc=False, as_utc=False):
|
||||
''' Return a date formatted as a string using a subset of Qt's formatting codes '''
|
||||
|
Loading…
x
Reference in New Issue
Block a user