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
a13b6065c6
@ -19,6 +19,45 @@
|
|||||||
# new recipes:
|
# new recipes:
|
||||||
# - title:
|
# - title:
|
||||||
|
|
||||||
|
- version: 0.7.56
|
||||||
|
date: 2011-04-17
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "This is primarily a bug fix release that fixes a bug in 0.7.55 that caused calibre to rescan the files on the device every time the device is connected. If you updated to 0.7.55 it is highly recommended you update to 0.7.56"
|
||||||
|
|
||||||
|
- title: "Device driver for Coby Kyros"
|
||||||
|
|
||||||
|
- title: "Remove the quick access to search options from next to the search bar, as we now have a separate search highlights toggle button"
|
||||||
|
|
||||||
|
- title: "MOBI Output: Ensure that MOBI files always have 8KB worth of null bytes at the end of record 0. This appears to be necessary for Amazon to be able to add DRM to calibre generated MOBI files sent to their publishing service."
|
||||||
|
|
||||||
|
- title: "Add a tool to inspect MOBI files. To use: calibre-debug -m file.mobi"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "Fixed regression taht caused calibre to rescan files on the device on every reconnect"
|
||||||
|
|
||||||
|
- title: "Fix donate button causing the toolbar to be too large on OS X"
|
||||||
|
|
||||||
|
- title: "MOBI Input: Fix detection of Table of Contents for MOBI files that have a page break between the location designated as the Table of Contents and the actual table of contents."
|
||||||
|
tickets: [763504]
|
||||||
|
|
||||||
|
- title: "Comic Input: Fix handling of some CBZ files that have wrongly encoded non ASCII filenames on windows."
|
||||||
|
tickets: [763280]
|
||||||
|
|
||||||
|
- title: "PML Input: Fix multi-line chapter title causing a spurious page break"
|
||||||
|
tickets: [763238]
|
||||||
|
|
||||||
|
- title: "EPUB Input: Speed up processing of files with very large manifest/spines"
|
||||||
|
|
||||||
|
- title: "Fix regression that broke cover:False searches in 0.7.55"
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Suedduetsche Zeitung
|
||||||
|
- Irish Times
|
||||||
|
- Big Oven
|
||||||
|
- NSPM
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.55
|
- version: 0.7.55
|
||||||
date: 2011-04-15
|
date: 2011-04-15
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.55'
|
__version__ = '0.7.56'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re, importlib
|
import re, importlib
|
||||||
|
@ -201,8 +201,9 @@ class ITUNES(DriverBase):
|
|||||||
# 0x1294 iPhone 3GS
|
# 0x1294 iPhone 3GS
|
||||||
# 0x1297 iPhone 4
|
# 0x1297 iPhone 4
|
||||||
# 0x129a iPad
|
# 0x129a iPad
|
||||||
|
# 0x12a2 iPad2
|
||||||
VENDOR_ID = [0x05ac]
|
VENDOR_ID = [0x05ac]
|
||||||
PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a]
|
PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a,0x12a2]
|
||||||
BCD = [0x01]
|
BCD = [0x01]
|
||||||
|
|
||||||
# Plugboard ID
|
# Plugboard ID
|
||||||
@ -421,7 +422,7 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
cached_books[this_book.path] = {
|
cached_books[this_book.path] = {
|
||||||
'title':book.name(),
|
'title':book.name(),
|
||||||
'author':[book.artist()],
|
'author':book.artist().split(' & '),
|
||||||
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
|
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
|
||||||
'dev_book':book,
|
'dev_book':book,
|
||||||
'uuid': book.composer()
|
'uuid': book.composer()
|
||||||
@ -459,7 +460,7 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
cached_books[this_book.path] = {
|
cached_books[this_book.path] = {
|
||||||
'title':book.Name,
|
'title':book.Name,
|
||||||
'author':book.Artist,
|
'author':book.artist().split(' & '),
|
||||||
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
|
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
|
||||||
'uuid': book.Composer,
|
'uuid': book.Composer,
|
||||||
'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
|
'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
|
||||||
@ -1021,7 +1022,9 @@ class ITUNES(DriverBase):
|
|||||||
if isosx:
|
if isosx:
|
||||||
for (i,file) in enumerate(files):
|
for (i,file) in enumerate(files):
|
||||||
format = file.rpartition('.')[2].lower()
|
format = file.rpartition('.')[2].lower()
|
||||||
path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
|
path = self.path_template % (metadata[i].title,
|
||||||
|
authors_to_string(metadata[i].authors),
|
||||||
|
format)
|
||||||
self._remove_existing_copy(path, metadata[i])
|
self._remove_existing_copy(path, metadata[i])
|
||||||
fpath = self._get_fpath(file, metadata[i], format, update_md=True)
|
fpath = self._get_fpath(file, metadata[i], format, update_md=True)
|
||||||
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
|
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
|
||||||
@ -1034,9 +1037,11 @@ class ITUNES(DriverBase):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES.upload_books()")
|
self.log.info("ITUNES.upload_books()")
|
||||||
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
||||||
( metadata[i].title, metadata[i].author, metadata[i].uuid))
|
(metadata[i].title,
|
||||||
|
authors_to_string(metadata[i].authors),
|
||||||
|
metadata[i].uuid))
|
||||||
self.cached_books[this_book.path] = {
|
self.cached_books[this_book.path] = {
|
||||||
'author': metadata[i].author,
|
'author': authors_to_string(metadata[i].authors),
|
||||||
'dev_book': db_added,
|
'dev_book': db_added,
|
||||||
'format': format,
|
'format': format,
|
||||||
'lib_book': lb_added,
|
'lib_book': lb_added,
|
||||||
@ -1055,7 +1060,9 @@ class ITUNES(DriverBase):
|
|||||||
|
|
||||||
for (i,file) in enumerate(files):
|
for (i,file) in enumerate(files):
|
||||||
format = file.rpartition('.')[2].lower()
|
format = file.rpartition('.')[2].lower()
|
||||||
path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
|
path = self.path_template % (metadata[i].title,
|
||||||
|
authors_to_string(metadata[i].authors),
|
||||||
|
format)
|
||||||
self._remove_existing_copy(path, metadata[i])
|
self._remove_existing_copy(path, metadata[i])
|
||||||
fpath = self._get_fpath(file, metadata[i],format, update_md=True)
|
fpath = self._get_fpath(file, metadata[i],format, update_md=True)
|
||||||
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
|
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
|
||||||
@ -1075,9 +1082,11 @@ class ITUNES(DriverBase):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info("ITUNES.upload_books()")
|
self.log.info("ITUNES.upload_books()")
|
||||||
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
|
||||||
( metadata[i].title, metadata[i].author, metadata[i].uuid))
|
(metadata[i].title,
|
||||||
|
authors_to_string(metadata[i].authors),
|
||||||
|
metadata[i].uuid))
|
||||||
self.cached_books[this_book.path] = {
|
self.cached_books[this_book.path] = {
|
||||||
'author': metadata[i].author[0],
|
'author': authors_to_string(metadata[i].authors),
|
||||||
'dev_book': db_added,
|
'dev_book': db_added,
|
||||||
'format': format,
|
'format': format,
|
||||||
'lib_book': lb_added,
|
'lib_book': lb_added,
|
||||||
@ -1190,7 +1199,7 @@ class ITUNES(DriverBase):
|
|||||||
base_fn = base_fn.rpartition('.')[0]
|
base_fn = base_fn.rpartition('.')[0]
|
||||||
db_added = self._find_device_book(
|
db_added = self._find_device_book(
|
||||||
{ 'title': base_fn if format == 'pdf' else metadata.title,
|
{ 'title': base_fn if format == 'pdf' else metadata.title,
|
||||||
'author': metadata.authors[0],
|
'author': authors_to_string(metadata.authors),
|
||||||
'uuid': metadata.uuid,
|
'uuid': metadata.uuid,
|
||||||
'format': format})
|
'format': format})
|
||||||
return db_added
|
return db_added
|
||||||
@ -1255,7 +1264,7 @@ class ITUNES(DriverBase):
|
|||||||
base_fn = base_fn.rpartition('.')[0]
|
base_fn = base_fn.rpartition('.')[0]
|
||||||
added = self._find_library_book(
|
added = self._find_library_book(
|
||||||
{ 'title': base_fn if format == 'pdf' else metadata.title,
|
{ 'title': base_fn if format == 'pdf' else metadata.title,
|
||||||
'author': metadata.author[0],
|
'author': authors_to_string(metadata.authors),
|
||||||
'uuid': metadata.uuid,
|
'uuid': metadata.uuid,
|
||||||
'format': format})
|
'format': format})
|
||||||
return added
|
return added
|
||||||
@ -1314,7 +1323,7 @@ class ITUNES(DriverBase):
|
|||||||
with open(metadata.cover,'r+b') as cd:
|
with open(metadata.cover,'r+b') as cd:
|
||||||
cover_data = cd.read()
|
cover_data = cd.read()
|
||||||
except:
|
except:
|
||||||
self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
|
self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors)))
|
||||||
self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title))
|
self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title))
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
@ -1389,7 +1398,7 @@ class ITUNES(DriverBase):
|
|||||||
thumb_path = path.rpartition('.')[0] + '.jpg'
|
thumb_path = path.rpartition('.')[0] + '.jpg'
|
||||||
zfw.writestr(thumb_path, thumb)
|
zfw.writestr(thumb_path, thumb)
|
||||||
except:
|
except:
|
||||||
self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
|
self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors)))
|
||||||
self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title))
|
self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title))
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@ -1407,7 +1416,7 @@ class ITUNES(DriverBase):
|
|||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" ITUNES._create_new_book()")
|
self.log.info(" ITUNES._create_new_book()")
|
||||||
|
|
||||||
this_book = Book(metadata.title, authors_to_string(metadata.author))
|
this_book = Book(metadata.title, authors_to_string(metadata.authors))
|
||||||
this_book.datetime = time.gmtime()
|
this_book.datetime = time.gmtime()
|
||||||
this_book.db_id = None
|
this_book.db_id = None
|
||||||
this_book.device_collections = []
|
this_book.device_collections = []
|
||||||
@ -2451,7 +2460,7 @@ class ITUNES(DriverBase):
|
|||||||
for book in self.cached_books:
|
for book in self.cached_books:
|
||||||
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
||||||
(self.cached_books[book]['title'] == metadata.title and \
|
(self.cached_books[book]['title'] == metadata.title and \
|
||||||
self.cached_books[book]['author'] == metadata.authors[0]):
|
self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
|
||||||
self.update_list.append(self.cached_books[book])
|
self.update_list.append(self.cached_books[book])
|
||||||
self._remove_from_device(self.cached_books[book])
|
self._remove_from_device(self.cached_books[book])
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -2470,7 +2479,7 @@ class ITUNES(DriverBase):
|
|||||||
for book in self.cached_books:
|
for book in self.cached_books:
|
||||||
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
if self.cached_books[book]['uuid'] == metadata.uuid or \
|
||||||
(self.cached_books[book]['title'] == metadata.title and \
|
(self.cached_books[book]['title'] == metadata.title and \
|
||||||
self.cached_books[book]['author'] == metadata.authors[0]):
|
self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
|
||||||
self.update_list.append(self.cached_books[book])
|
self.update_list.append(self.cached_books[book])
|
||||||
self._remove_from_iTunes(self.cached_books[book])
|
self._remove_from_iTunes(self.cached_books[book])
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -2939,13 +2948,13 @@ class ITUNES(DriverBase):
|
|||||||
def _xform_metadata_via_plugboard(self, book, format):
|
def _xform_metadata_via_plugboard(self, book, format):
|
||||||
''' Transform book metadata from plugboard templates '''
|
''' Transform book metadata from plugboard templates '''
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
self.log.info(" ITUNES._xform_metadata_via_plugboard()")
|
self.log.info(" ITUNES._xform_metadata_via_plugboard()")
|
||||||
|
|
||||||
if self.plugboard_func:
|
if self.plugboard_func:
|
||||||
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
|
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
|
||||||
newmi = book.deepcopy_metadata()
|
newmi = book.deepcopy_metadata()
|
||||||
newmi.template_to_attribute(book, pb)
|
newmi.template_to_attribute(book, pb)
|
||||||
if DEBUG:
|
if pb is not None and DEBUG:
|
||||||
self.log.info(" transforming %s using %s:" % (format, pb))
|
self.log.info(" transforming %s using %s:" % (format, pb))
|
||||||
self.log.info(" title: %s %s" % (book.title, ">>> %s" %
|
self.log.info(" title: %s %s" % (book.title, ">>> %s" %
|
||||||
newmi.title if book.title != newmi.title else ''))
|
newmi.title if book.title != newmi.title else ''))
|
||||||
@ -3062,7 +3071,7 @@ class ITUNES_ASYNC(ITUNES):
|
|||||||
|
|
||||||
cached_books[this_book.path] = {
|
cached_books[this_book.path] = {
|
||||||
'title':library_books[book].name(),
|
'title':library_books[book].name(),
|
||||||
'author':[library_books[book].artist()],
|
'author':library_books[book].artist().split(' & '),
|
||||||
'lib_book':library_books[book],
|
'lib_book':library_books[book],
|
||||||
'dev_book':None,
|
'dev_book':None,
|
||||||
'uuid': library_books[book].composer(),
|
'uuid': library_books[book].composer(),
|
||||||
@ -3102,7 +3111,7 @@ class ITUNES_ASYNC(ITUNES):
|
|||||||
|
|
||||||
cached_books[this_book.path] = {
|
cached_books[this_book.path] = {
|
||||||
'title':library_books[book].Name,
|
'title':library_books[book].Name,
|
||||||
'author':library_books[book].Artist,
|
'author':library_books[book].Artist.split(' & '),
|
||||||
'lib_book':library_books[book],
|
'lib_book':library_books[book],
|
||||||
'uuid': library_books[book].Composer,
|
'uuid': library_books[book].Composer,
|
||||||
'format': format
|
'format': format
|
||||||
@ -3288,7 +3297,7 @@ class Book(Metadata):
|
|||||||
See ebooks.metadata.book.base
|
See ebooks.metadata.book.base
|
||||||
'''
|
'''
|
||||||
def __init__(self,title,author):
|
def __init__(self,title,author):
|
||||||
Metadata.__init__(self, title, authors=[author])
|
Metadata.__init__(self, title, authors=author.split(' & '))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title_sorter(self):
|
def title_sorter(self):
|
||||||
|
@ -52,6 +52,9 @@ class CHMInput(InputFormatPlugin):
|
|||||||
|
|
||||||
metadata = get_metadata_from_reader(self._chm_reader)
|
metadata = get_metadata_from_reader(self._chm_reader)
|
||||||
self._chm_reader.CloseCHM()
|
self._chm_reader.CloseCHM()
|
||||||
|
#print tdir
|
||||||
|
#from calibre import ipython
|
||||||
|
#ipython()
|
||||||
|
|
||||||
odi = options.debug_pipeline
|
odi = options.debug_pipeline
|
||||||
options.debug_pipeline = None
|
options.debug_pipeline = None
|
||||||
|
@ -147,7 +147,8 @@ class CHMReader(CHMFile):
|
|||||||
if self.hhc_path == '.hhc' and self.hhc_path not in files:
|
if self.hhc_path == '.hhc' and self.hhc_path not in files:
|
||||||
from calibre import walk
|
from calibre import walk
|
||||||
for x in walk(output_dir):
|
for x in walk(output_dir):
|
||||||
if os.path.basename(x).lower() in ('index.htm', 'index.html'):
|
if os.path.basename(x).lower() in ('index.htm', 'index.html',
|
||||||
|
'contents.htm', 'contents.html'):
|
||||||
self.hhc_path = os.path.relpath(x, output_dir)
|
self.hhc_path = os.path.relpath(x, output_dir)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ from Queue import Empty
|
|||||||
|
|
||||||
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
|
||||||
from calibre import extract, CurrentDir, prints
|
from calibre import extract, CurrentDir, prints
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.utils.ipc.job import ParallelJob
|
from calibre.utils.ipc.job import ParallelJob
|
||||||
@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file):
|
|||||||
Un-archive the comic file.
|
Un-archive the comic file.
|
||||||
'''
|
'''
|
||||||
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
|
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)
|
extract(path_to_comic_file, tdir)
|
||||||
return tdir
|
return tdir
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
#define BUFFER 6000
|
#define BUFFER 6000
|
||||||
|
|
||||||
#define MIN(x, y) ( ((x) < (y)) ? (x) : (y) )
|
#define MIN(x, y) ( ((x) < (y)) ? (x) : (y) )
|
||||||
|
#define MAX(x, y) ( ((x) > (y)) ? (x) : (y) )
|
||||||
|
|
||||||
typedef unsigned short int Byte;
|
typedef unsigned short int Byte;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@ -53,7 +54,7 @@ cpalmdoc_decompress(PyObject *self, PyObject *args) {
|
|||||||
// Map chars to bytes
|
// Map chars to bytes
|
||||||
for (j = 0; j < input_len; j++)
|
for (j = 0; j < input_len; j++)
|
||||||
input[j] = (_input[j] < 0) ? _input[j]+256 : _input[j];
|
input[j] = (_input[j] < 0) ? _input[j]+256 : _input[j];
|
||||||
output = (char *)PyMem_Malloc(sizeof(char)*BUFFER);
|
output = (char *)PyMem_Malloc(sizeof(char)*(MAX(BUFFER, 5*input_len)));
|
||||||
if (output == NULL) return PyErr_NoMemory();
|
if (output == NULL) return PyErr_NoMemory();
|
||||||
|
|
||||||
while (i < input_len) {
|
while (i < input_len) {
|
||||||
|
@ -294,8 +294,24 @@ class Source(Plugin):
|
|||||||
Excludes connectives and punctuation.
|
Excludes connectives and punctuation.
|
||||||
'''
|
'''
|
||||||
if title:
|
if title:
|
||||||
pat = re.compile(r'''[-,:;+!@#$%^&*(){}.`~"'\s\[\]/]''')
|
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
|
||||||
title = pat.sub(' ', title)
|
[
|
||||||
|
# Remove things like: (2010) (Omnibus) etc.
|
||||||
|
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''),
|
||||||
|
# Remove any strings that contain the substring edition inside
|
||||||
|
# parentheses
|
||||||
|
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
|
||||||
|
# Remove commas used a separators in numbers
|
||||||
|
(r'(\d+),(\d+)', r'\1\2'),
|
||||||
|
# Remove hyphens only if they have whitespace before them
|
||||||
|
(r'(\s-)', ' '),
|
||||||
|
# Remove single quotes
|
||||||
|
(r"'", ''),
|
||||||
|
# Replace other special chars with a space
|
||||||
|
(r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ')
|
||||||
|
]]
|
||||||
|
for pat, repl in title_patterns:
|
||||||
|
title = pat.sub(repl, title)
|
||||||
tokens = title.split()
|
tokens = title.split()
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
token = token.strip()
|
token = token.strip()
|
||||||
|
@ -114,8 +114,12 @@ class ISBNMerge(object):
|
|||||||
|
|
||||||
return self.results
|
return self.results
|
||||||
|
|
||||||
def merge_metadata_results(self):
|
def merge_metadata_results(self, merge_on_identifiers=False):
|
||||||
' Merge results with identical title and authors '
|
'''
|
||||||
|
Merge results with identical title and authors or an identical
|
||||||
|
identifier
|
||||||
|
'''
|
||||||
|
# First title/author
|
||||||
groups = {}
|
groups = {}
|
||||||
for result in self.results:
|
for result in self.results:
|
||||||
title = lower(result.title if result.title else '')
|
title = lower(result.title if result.title else '')
|
||||||
@ -135,6 +139,44 @@ class ISBNMerge(object):
|
|||||||
result = rgroup[0]
|
result = rgroup[0]
|
||||||
self.results.append(result)
|
self.results.append(result)
|
||||||
|
|
||||||
|
if merge_on_identifiers:
|
||||||
|
# Now identifiers
|
||||||
|
groups, empty = {}, []
|
||||||
|
for result in self.results:
|
||||||
|
key = set()
|
||||||
|
for typ, val in result.identifiers.iteritems():
|
||||||
|
if typ and val:
|
||||||
|
key.add((typ, val))
|
||||||
|
if key:
|
||||||
|
key = frozenset(key)
|
||||||
|
match = None
|
||||||
|
for candidate in list(groups):
|
||||||
|
if candidate.intersection(key):
|
||||||
|
# We have at least one identifier in common
|
||||||
|
match = candidate.union(key)
|
||||||
|
results = groups.pop(candidate)
|
||||||
|
results.append(result)
|
||||||
|
groups[match] = results
|
||||||
|
break
|
||||||
|
if match is None:
|
||||||
|
groups[key] = [result]
|
||||||
|
else:
|
||||||
|
empty.append(result)
|
||||||
|
|
||||||
|
if len(groups) != len(self.results):
|
||||||
|
self.results = []
|
||||||
|
for rgroup in groups.itervalues():
|
||||||
|
rel = [r.average_source_relevance for r in rgroup]
|
||||||
|
if len(rgroup) > 1:
|
||||||
|
result = self.merge(rgroup, None, do_asr=False)
|
||||||
|
result.average_source_relevance = sum(rel)/len(rel)
|
||||||
|
elif rgroup:
|
||||||
|
result = rgroup[0]
|
||||||
|
self.results.append(result)
|
||||||
|
|
||||||
|
if empty:
|
||||||
|
self.results.extend(empty)
|
||||||
|
|
||||||
self.results.sort(key=attrgetter('average_source_relevance'))
|
self.results.sort(key=attrgetter('average_source_relevance'))
|
||||||
|
|
||||||
def merge_isbn_results(self):
|
def merge_isbn_results(self):
|
||||||
@ -408,7 +450,7 @@ if __name__ == '__main__': # tests {{{
|
|||||||
{'identifiers':{'isbn': '9780307459671'},
|
{'identifiers':{'isbn': '9780307459671'},
|
||||||
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
|
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
|
||||||
[title_test('The Invisible Gorilla',
|
[title_test('The Invisible Gorilla',
|
||||||
exact=True), authors_test(['Christopher F. Chabris', 'Daniel Simons'])]
|
exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])]
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -15,14 +15,17 @@ from calibre.customize.ui import metadata_plugins
|
|||||||
from calibre import prints, sanitize_file_name2
|
from calibre import prints, sanitize_file_name2
|
||||||
from calibre.ebooks.metadata import check_isbn
|
from calibre.ebooks.metadata import check_isbn
|
||||||
from calibre.ebooks.metadata.sources.base import (create_log,
|
from calibre.ebooks.metadata.sources.base import (create_log,
|
||||||
get_cached_cover_urls)
|
get_cached_cover_urls, msprefs)
|
||||||
|
|
||||||
def isbn_test(isbn):
|
def isbn_test(isbn):
|
||||||
isbn_ = check_isbn(isbn)
|
isbn_ = check_isbn(isbn)
|
||||||
|
|
||||||
def test(mi):
|
def test(mi):
|
||||||
misbn = check_isbn(mi.isbn)
|
misbn = check_isbn(mi.isbn)
|
||||||
return misbn and misbn == isbn_
|
if misbn and misbn == isbn_:
|
||||||
|
return True
|
||||||
|
prints('ISBN test failed. Expected: \'%s\' found \'%s\''%(isbn_, misbn))
|
||||||
|
return False
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
@ -32,8 +35,11 @@ def title_test(title, exact=False):
|
|||||||
|
|
||||||
def test(mi):
|
def test(mi):
|
||||||
mt = mi.title.lower()
|
mt = mi.title.lower()
|
||||||
return (exact and mt == title) or \
|
if (exact and mt == title) or \
|
||||||
(not exact and title in mt)
|
(not exact and title in mt):
|
||||||
|
return True
|
||||||
|
prints('Title test failed. Expected: \'%s\' found \'%s\''%(title, mt))
|
||||||
|
return False
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
@ -42,7 +48,22 @@ def authors_test(authors):
|
|||||||
|
|
||||||
def test(mi):
|
def test(mi):
|
||||||
au = set([x.lower() for x in mi.authors])
|
au = set([x.lower() for x in mi.authors])
|
||||||
return au == authors
|
if msprefs['swap_author_names']:
|
||||||
|
def revert_to_fn_ln(a):
|
||||||
|
if ',' not in a:
|
||||||
|
return a
|
||||||
|
parts = a.split(',', 1)
|
||||||
|
t = parts[-1]
|
||||||
|
parts = parts[:-1]
|
||||||
|
parts.insert(0, t)
|
||||||
|
return ' '.join(parts)
|
||||||
|
|
||||||
|
au = set([revert_to_fn_ln(x) for x in au])
|
||||||
|
|
||||||
|
if au == authors:
|
||||||
|
return True
|
||||||
|
prints('Author test failed. Expected: \'%s\' found \'%s\''%(authors, au))
|
||||||
|
return False
|
||||||
|
|
||||||
return test
|
return test
|
||||||
|
|
||||||
|
@ -716,6 +716,7 @@ class MobiReader(object):
|
|||||||
ent_pat = re.compile(r'&(\S+?);')
|
ent_pat = re.compile(r'&(\S+?);')
|
||||||
if elems:
|
if elems:
|
||||||
tocobj = TOC()
|
tocobj = TOC()
|
||||||
|
found = False
|
||||||
reached = False
|
reached = False
|
||||||
for x in root.iter():
|
for x in root.iter():
|
||||||
if x == elems[-1]:
|
if x == elems[-1]:
|
||||||
@ -732,7 +733,8 @@ class MobiReader(object):
|
|||||||
text = ent_pat.sub(entity_to_unicode, text)
|
text = ent_pat.sub(entity_to_unicode, text)
|
||||||
tocobj.add_item(toc.partition('#')[0], href[1:],
|
tocobj.add_item(toc.partition('#')[0], href[1:],
|
||||||
text)
|
text)
|
||||||
if reached and x.get('class', None) == 'mbp_pagebreak':
|
found = True
|
||||||
|
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
||||||
break
|
break
|
||||||
if tocobj is not None:
|
if tocobj is not None:
|
||||||
opf.set_toc(tocobj)
|
opf.set_toc(tocobj)
|
||||||
|
@ -24,7 +24,7 @@ from calibre.translations.dynamic import translate
|
|||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
|
||||||
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
|
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
|
||||||
from calibre import isbytestring
|
from calibre import isbytestring, as_unicode
|
||||||
|
|
||||||
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
|
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
|
||||||
|
|
||||||
@ -643,7 +643,7 @@ class Metadata(object):
|
|||||||
return unicode(self.value).encode('ascii', 'xmlcharrefreplace')
|
return unicode(self.value).encode('ascii', 'xmlcharrefreplace')
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return unicode(self.value)
|
return as_unicode(self.value)
|
||||||
|
|
||||||
def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}):
|
def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}):
|
||||||
attrib = {}
|
attrib = {}
|
||||||
|
@ -648,6 +648,18 @@ def open_url(qurl):
|
|||||||
if isfrozen and islinux and paths:
|
if isfrozen and islinux and paths:
|
||||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
||||||
|
|
||||||
|
def get_current_db():
|
||||||
|
'''
|
||||||
|
This method will try to return the current database in use by the user as
|
||||||
|
efficiently as possible, i.e. without constructing duplicate
|
||||||
|
LibraryDatabase objects.
|
||||||
|
'''
|
||||||
|
from calibre.gui2.ui import get_gui
|
||||||
|
gui = get_gui()
|
||||||
|
if gui is not None and gui.current_db is not None:
|
||||||
|
return gui.current_db
|
||||||
|
from calibre.library import db
|
||||||
|
return db()
|
||||||
|
|
||||||
def open_local_file(path):
|
def open_local_file(path):
|
||||||
if iswindows:
|
if iswindows:
|
||||||
|
@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction
|
|||||||
class GenerateCatalogAction(InterfaceAction):
|
class GenerateCatalogAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Generate Catalog'
|
name = 'Generate Catalog'
|
||||||
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
|
action_spec = (_('Create a catalog of the books in your calibre library'), 'catalog.png', 'Catalog builder', None)
|
||||||
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
|
||||||
|
|
||||||
def generate_catalog(self):
|
def generate_catalog(self):
|
||||||
|
@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QMenu, QToolButton, QDialog, QVBoxLayout
|
from PyQt4.Qt import QMenu
|
||||||
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
|
||||||
class StoreAction(InterfaceAction):
|
class StoreAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Store'
|
name = 'Store'
|
||||||
action_spec = (_('Store'), 'store.png', None, None)
|
action_spec = (_('Get books'), 'store.png', None, None)
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.qaction.triggered.connect(self.search)
|
self.qaction.triggered.connect(self.search)
|
||||||
self.store_menu = QMenu()
|
self.store_menu = QMenu()
|
||||||
self.load_menu()
|
self.load_menu()
|
||||||
|
|
||||||
def load_menu(self):
|
def load_menu(self):
|
||||||
self.store_menu.clear()
|
self.store_menu.clear()
|
||||||
self.store_menu.addAction(_('Search'), self.search)
|
self.store_menu.addAction(_('Search'), self.search)
|
||||||
@ -29,11 +29,11 @@ class StoreAction(InterfaceAction):
|
|||||||
for n, p in self.gui.istores.items():
|
for n, p in self.gui.istores.items():
|
||||||
self.store_menu.addAction(n, partial(self.open_store, p))
|
self.store_menu.addAction(n, partial(self.open_store, p))
|
||||||
self.qaction.setMenu(self.store_menu)
|
self.qaction.setMenu(self.store_menu)
|
||||||
|
|
||||||
def search(self):
|
def search(self):
|
||||||
from calibre.gui2.store.search import SearchDialog
|
from calibre.gui2.store.search import SearchDialog
|
||||||
sd = SearchDialog(self.gui.istores, self.gui)
|
sd = SearchDialog(self.gui.istores, self.gui)
|
||||||
sd.exec_()
|
sd.exec_()
|
||||||
|
|
||||||
def open_store(self, store_plugin):
|
def open_store(self, store_plugin):
|
||||||
store_plugin.open(self.gui)
|
store_plugin.open(self.gui)
|
||||||
|
@ -483,8 +483,15 @@ class BookDetails(QWidget): # {{{
|
|||||||
self.book_info.show_data(data)
|
self.book_info.show_data(data)
|
||||||
self.cover_view.show_data(data)
|
self.cover_view.show_data(data)
|
||||||
self._layout.do_layout(self.rect())
|
self._layout.do_layout(self.rect())
|
||||||
self.setToolTip('<p>'+_('Double-click to open Book Details window') +
|
try:
|
||||||
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
|
sz = self.cover_view.pixmap.size()
|
||||||
|
except:
|
||||||
|
sz = QSize(0, 0)
|
||||||
|
self.setToolTip(
|
||||||
|
'<p>'+_('Double-click to open Book Details window') +
|
||||||
|
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), '') +
|
||||||
|
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
|
||||||
|
)
|
||||||
|
|
||||||
def reset_info(self):
|
def reset_info(self):
|
||||||
self.show_data({})
|
self.show_data({})
|
||||||
|
@ -109,6 +109,8 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
pixmap = pixmap.scaled(new_width, new_height,
|
pixmap = pixmap.scaled(new_width, new_height,
|
||||||
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||||
self.cover.set_pixmap(pixmap)
|
self.cover.set_pixmap(pixmap)
|
||||||
|
sz = pixmap.size()
|
||||||
|
self.cover.setToolTip(_('Cover size: %dx%d')%(sz.width(), sz.height()))
|
||||||
|
|
||||||
def refresh(self, row):
|
def refresh(self, row):
|
||||||
if isinstance(row, QModelIndex):
|
if isinstance(row, QModelIndex):
|
||||||
|
@ -68,7 +68,7 @@ class DaysOfWeek(Base):
|
|||||||
def initialize(self, typ=None, val=None):
|
def initialize(self, typ=None, val=None):
|
||||||
if typ is None:
|
if typ is None:
|
||||||
typ = 'day/time'
|
typ = 'day/time'
|
||||||
val = (-1, 9, 0)
|
val = (-1, 6, 0)
|
||||||
if typ == 'day/time':
|
if typ == 'day/time':
|
||||||
val = convert_day_time_schedule(val)
|
val = convert_day_time_schedule(val)
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class DaysOfMonth(Base):
|
|||||||
|
|
||||||
def initialize(self, typ=None, val=None):
|
def initialize(self, typ=None, val=None):
|
||||||
if val is None:
|
if val is None:
|
||||||
val = ((1,), 9, 0)
|
val = ((1,), 6, 0)
|
||||||
days_of_month, hour, minute = val
|
days_of_month, hour, minute = val
|
||||||
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
self.days.setText(', '.join(map(str, map(int, days_of_month))))
|
||||||
self.time.setTime(QTime(hour, minute))
|
self.time.setTime(QTime(hour, minute))
|
||||||
@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
|
|||||||
if d < timedelta(days=366):
|
if d < timedelta(days=366):
|
||||||
ld_text = tm
|
ld_text = tm
|
||||||
else:
|
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,
|
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
|
||||||
'interval':2}[typ]
|
'interval':2}[typ]
|
||||||
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])
|
||||||
|
@ -12,6 +12,7 @@ from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
|||||||
|
|
||||||
from PyQt4.Qt import QDialog
|
from PyQt4.Qt import QDialog
|
||||||
|
|
||||||
|
from calibre.constants import isosx, iswindows
|
||||||
from calibre.gui2 import open_local_file
|
from calibre.gui2 import open_local_file
|
||||||
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
|
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
|
||||||
from calibre.libunzip import extract as zipextract
|
from calibre.libunzip import extract as zipextract
|
||||||
@ -42,11 +43,19 @@ class TweakEpub(QDialog, Ui_Dialog):
|
|||||||
self.move(parent_loc.x(),parent_loc.y())
|
self.move(parent_loc.x(),parent_loc.y())
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
|
if isosx:
|
||||||
|
try:
|
||||||
|
import appscript
|
||||||
|
self.finder = appscript.app('Finder')
|
||||||
|
self.finder.Finder_windows[os.path.basename(self._exploded)].close()
|
||||||
|
except:
|
||||||
|
# appscript fails to load on 10.4
|
||||||
|
pass
|
||||||
|
|
||||||
# Delete directory containing exploded ePub
|
# Delete directory containing exploded ePub
|
||||||
if self._exploded is not None:
|
if self._exploded is not None:
|
||||||
shutil.rmtree(self._exploded, ignore_errors=True)
|
shutil.rmtree(self._exploded, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def display_exploded(self):
|
def display_exploded(self):
|
||||||
'''
|
'''
|
||||||
Generic subprocess launch of native file browser
|
Generic subprocess launch of native file browser
|
||||||
|
@ -317,6 +317,8 @@ class BaseToolBar(QToolBar): # {{{
|
|||||||
QToolBar.resizeEvent(self, ev)
|
QToolBar.resizeEvent(self, ev)
|
||||||
style = self.get_text_style()
|
style = self.get_text_style()
|
||||||
self.setToolButtonStyle(style)
|
self.setToolButtonStyle(style)
|
||||||
|
if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'):
|
||||||
|
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
|
||||||
|
|
||||||
def get_text_style(self):
|
def get_text_style(self):
|
||||||
style = Qt.ToolButtonTextUnderIcon
|
style = Qt.ToolButtonTextUnderIcon
|
||||||
@ -399,7 +401,10 @@ class ToolBar(BaseToolBar): # {{{
|
|||||||
self.d_widget.layout().addWidget(self.donate_button)
|
self.d_widget.layout().addWidget(self.donate_button)
|
||||||
if isosx:
|
if isosx:
|
||||||
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
|
||||||
self.d_widget.layout().addWidget(QLabel(u'\u00a0'))
|
self.d_widget.layout().setContentsMargins(0,0,0,0)
|
||||||
|
self.d_widget.setContentsMargins(0,0,0,0)
|
||||||
|
self.d_widget.filler = QLabel(u'\u00a0')
|
||||||
|
self.d_widget.layout().addWidget(self.d_widget.filler)
|
||||||
bar.addWidget(self.d_widget)
|
bar.addWidget(self.d_widget)
|
||||||
self.showing_donate = True
|
self.showing_donate = True
|
||||||
elif what in self.gui.iactions:
|
elif what in self.gui.iactions:
|
||||||
|
@ -223,7 +223,7 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
LABEL = _('Author s&ort:')
|
LABEL = _('Author s&ort:')
|
||||||
|
|
||||||
def __init__(self, parent, authors_edit, autogen_button, db,
|
def __init__(self, parent, authors_edit, autogen_button, db,
|
||||||
copy_as_to_a_action):
|
copy_a_to_as_action, copy_as_to_a_action):
|
||||||
EnLineEdit.__init__(self, parent)
|
EnLineEdit.__init__(self, parent)
|
||||||
self.authors_edit = authors_edit
|
self.authors_edit = authors_edit
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -242,6 +242,7 @@ class AuthorSortEdit(EnLineEdit):
|
|||||||
self.textChanged.connect(self.update_state)
|
self.textChanged.connect(self.update_state)
|
||||||
|
|
||||||
autogen_button.clicked.connect(self.auto_generate)
|
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)
|
copy_as_to_a_action.triggered.connect(self.copy_to_authors)
|
||||||
self.update_state()
|
self.update_state()
|
||||||
|
|
||||||
|
@ -109,10 +109,12 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
'Using this button to create author sort will change author sort from'
|
'Using this button to create author sort will change author sort from'
|
||||||
' red to green.'))
|
' red to green.'))
|
||||||
b.m = m = QMenu()
|
b.m = m = QMenu()
|
||||||
ac = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
|
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)
|
b.setMenu(m)
|
||||||
self.authors = AuthorsEdit(self)
|
self.authors = AuthorsEdit(self)
|
||||||
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac)
|
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
|
||||||
|
ac2)
|
||||||
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
|
||||||
|
|
||||||
self.swap_title_author_button = QToolButton(self)
|
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
|
:return: True iff a restart is required for the changes made by the user to
|
||||||
take effect
|
take effect
|
||||||
'''
|
'''
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
pl = get_plugin(category, name)
|
pl = get_plugin(category, name)
|
||||||
d = ConfigDialog(parent)
|
d = ConfigDialog(parent)
|
||||||
d.resize(750, 550)
|
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.setWindowTitle(_('Configure ') + name)
|
||||||
d.setWindowIcon(QIcon(I('config.png')))
|
d.setWindowIcon(QIcon(I('config.png')))
|
||||||
bb = QDialogButtonBox(d)
|
bb = QDialogButtonBox(d)
|
||||||
@ -345,7 +348,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
|
|||||||
mygui = True
|
mygui = True
|
||||||
w.genesis(gui)
|
w.genesis(gui)
|
||||||
w.initialize()
|
w.initialize()
|
||||||
|
if geom is not None:
|
||||||
|
d.restoreGeometry(geom)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
geom = bytearray(d.saveGeometry())
|
||||||
|
gprefs[conf_name] = geom
|
||||||
rr = getattr(d, 'restart_required', False)
|
rr = getattr(d, 'restart_required', False)
|
||||||
if show_restart_msg and rr:
|
if show_restart_msg and rr:
|
||||||
from calibre.gui2 import warning_dialog
|
from calibre.gui2 import warning_dialog
|
||||||
|
@ -73,13 +73,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
choices=sorted(list(choices), key=sort_key))
|
choices=sorted(list(choices), key=sort_key))
|
||||||
|
|
||||||
|
|
||||||
self.current_font = None
|
self.current_font = self.initial_font = None
|
||||||
self.change_font_button.clicked.connect(self.change_font)
|
self.change_font_button.clicked.connect(self.change_font)
|
||||||
|
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
self.current_font = gprefs['font']
|
self.current_font = self.initial_font = gprefs['font']
|
||||||
self.update_font_display()
|
self.update_font_display()
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
@ -119,7 +119,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
def commit(self, *args):
|
def commit(self, *args):
|
||||||
rr = ConfigWidgetBase.commit(self, *args)
|
rr = ConfigWidgetBase.commit(self, *args)
|
||||||
if self.current_font != gprefs['font']:
|
if self.current_font != self.initial_font:
|
||||||
gprefs['font'] = self.current_font
|
gprefs['font'] = self.current_font
|
||||||
QApplication.setFont(self.font_display.font())
|
QApplication.setFont(self.font_display.font())
|
||||||
rr = True
|
rr = True
|
||||||
|
@ -19,27 +19,27 @@ class StorePlugin(object): # {{{
|
|||||||
|
|
||||||
If two :class:`StorePlugin` objects have the same name, the one with higher
|
If two :class:`StorePlugin` objects have the same name, the one with higher
|
||||||
priority takes precedence.
|
priority takes precedence.
|
||||||
|
|
||||||
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
Sub-classes must implement :meth:`open`, and :meth:`search`.
|
||||||
|
|
||||||
Regarding :meth:`open`. Most stores only make themselves available
|
Regarding :meth:`open`. Most stores only make themselves available
|
||||||
though a web site thus most store plugins will open using
|
though a web site thus most store plugins will open using
|
||||||
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
|
||||||
open a modal window and display the store website in a QWebView.
|
open a modal window and display the store website in a QWebView.
|
||||||
|
|
||||||
Sub-classes should implement and use the :meth:`genesis` if they require
|
Sub-classes should implement and use the :meth:`genesis` if they require
|
||||||
plugin specific initialization. They should not override or otherwise
|
plugin specific initialization. They should not override or otherwise
|
||||||
reimplement :meth:`__init__`.
|
reimplement :meth:`__init__`.
|
||||||
|
|
||||||
Once initialized, this plugin has access to the main calibre GUI via the
|
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::
|
:attr:`gui` member. You can access other plugins by name, for example::
|
||||||
|
|
||||||
self.gui.istores['Amazon Kindle']
|
self.gui.istores['Amazon Kindle']
|
||||||
|
|
||||||
Plugin authors can use affiliate programs within their plugin. The
|
Plugin authors can use affiliate programs within their plugin. The
|
||||||
distribution of money earned from a store plugin is 70/30. 70% going
|
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.
|
to the pluin author / maintainer and 30% going to the calibre project.
|
||||||
|
|
||||||
The easiest way to handle affiliate money payouts is to randomly select
|
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
|
between the author's affiliate id and calibre's affiliate id so that
|
||||||
70% of the time the author's id is used.
|
70% of the time the author's id is used.
|
||||||
@ -49,61 +49,61 @@ class StorePlugin(object): # {{{
|
|||||||
self.gui = gui
|
self.gui = gui
|
||||||
self.name = name
|
self.name = name
|
||||||
self.base_plugin = None
|
self.base_plugin = None
|
||||||
|
|
||||||
def open(self, gui, parent=None, detail_item=None, external=False):
|
def open(self, gui, parent=None, detail_item=None, external=False):
|
||||||
'''
|
'''
|
||||||
Open the store.
|
Open the store.
|
||||||
|
|
||||||
:param gui: The main GUI. This will be used to have the job
|
:param gui: The main GUI. This will be used to have the job
|
||||||
system start downloading an item from the store.
|
system start downloading an item from the store.
|
||||||
|
|
||||||
:param parent: The parent of the store dialog. This is used
|
:param parent: The parent of the store dialog. This is used
|
||||||
to create modal dialogs.
|
to create modal dialogs.
|
||||||
|
|
||||||
:param detail_item: A plugin specific reference to an item
|
:param detail_item: A plugin specific reference to an item
|
||||||
in the store that the user should be shown.
|
in the store that the user should be shown.
|
||||||
|
|
||||||
:param external: When False open an internal dialog with the
|
:param external: When False open an internal dialog with the
|
||||||
store. When True open the users default browser to the store's
|
store. When True open the users default browser to the store's
|
||||||
web site. :param:`detail_item` should still be respected when external
|
web site. :param:`detail_item` should still be respected when external
|
||||||
is True.
|
is True.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
'''
|
'''
|
||||||
Searches the store for items matching query. This should
|
Searches the store for items matching query. This should
|
||||||
return items as a generator.
|
return items as a generator.
|
||||||
|
|
||||||
Don't be lazy with the search! Load as much data as possible in the
|
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
|
: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)
|
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
|
isn't available because the store does not display cover images then it's okay to
|
||||||
ignore it.
|
ignore it.
|
||||||
|
|
||||||
Also, by default search results can only include ebooks. A plugin can offer users
|
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
|
an option to include physical books in the search results but this must be
|
||||||
disabled by default.
|
disabled by default.
|
||||||
|
|
||||||
If a store doesn't provide search on it's own use something like a site specific
|
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.
|
google search to get search results for this funtion.
|
||||||
|
|
||||||
:param query: The string query search with.
|
:param query: The string query search with.
|
||||||
:param max_results: The maximum number of results to return.
|
: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.
|
:param timeout: The maximum amount of time in seconds to spend download the search results.
|
||||||
|
|
||||||
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
: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.
|
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def get_settings(self):
|
def get_settings(self):
|
||||||
'''
|
'''
|
||||||
This is only useful for plugins that implement
|
This is only useful for plugins that implement
|
||||||
:attr:`config_widget` that is the only way to save
|
:attr:`config_widget` that is the only way to save
|
||||||
settings. This is used by plugins to get the saved
|
settings. This is used by plugins to get the saved
|
||||||
settings and apply when necessary.
|
settings and apply when necessary.
|
||||||
|
|
||||||
:return: A dictionary filled with the settings used
|
:return: A dictionary filled with the settings used
|
||||||
by this plugin.
|
by this plugin.
|
||||||
'''
|
'''
|
||||||
@ -117,23 +117,23 @@ class StorePlugin(object): # {{{
|
|||||||
Plugin specific initialization.
|
Plugin specific initialization.
|
||||||
'''
|
'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def config_widget(self):
|
def config_widget(self):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def save_settings(self, config_widget):
|
def save_settings(self, config_widget):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
def customization_help(self, gui=False):
|
||||||
'''
|
'''
|
||||||
See :class:`calibre.customize.Plugin` for details.
|
See :class:`calibre.customize.Plugin` for details.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -21,14 +21,14 @@ from calibre.gui2.store import StorePlugin
|
|||||||
from calibre.gui2.store.search_result import SearchResult
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
|
||||||
class AmazonKindleStore(StorePlugin):
|
class AmazonKindleStore(StorePlugin):
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
'''
|
'''
|
||||||
Amazon comes with a number of difficulties.
|
Amazon comes with a number of difficulties.
|
||||||
|
|
||||||
QWebView has major issues with Amazon.com. The largest of
|
QWebView has major issues with Amazon.com. The largest of
|
||||||
issues is it simply doesn't work on a number of pages.
|
issues is it simply doesn't work on a number of pages.
|
||||||
|
|
||||||
When connecting to a number parts of Amazon.com (Kindle library
|
When connecting to a number parts of Amazon.com (Kindle library
|
||||||
for instance) QNetworkAccessManager fails to connect with a
|
for instance) QNetworkAccessManager fails to connect with a
|
||||||
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
NetworkError of 399 - ProtocolFailure. The strange thing is,
|
||||||
@ -37,19 +37,19 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
the QNetworkAccessManager decides there was a NetworkError it
|
the QNetworkAccessManager decides there was a NetworkError it
|
||||||
does not download the page from Amazon. So I can't even set the
|
does not download the page from Amazon. So I can't even set the
|
||||||
HTML in the QWebView myself.
|
HTML in the QWebView myself.
|
||||||
|
|
||||||
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
|
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
|
open bug about the issue but it is not correct. We can set the
|
||||||
useragent (Arora does) to something else and the above issue
|
useragent (Arora does) to something else and the above issue
|
||||||
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
|
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)
|
gives a bit more information about the issue but as of now (27/Feb/2011)
|
||||||
there is no solution or work around.
|
there is no solution or work around.
|
||||||
|
|
||||||
We cannot change the The linkDelegationPolicy to allow us to avoid
|
We cannot change the The linkDelegationPolicy to allow us to avoid
|
||||||
QNetworkAccessManager because it only works links. Forms aren't
|
QNetworkAccessManager because it only works links. Forms aren't
|
||||||
included so the same issue persists on any part of the site (login)
|
included so the same issue persists on any part of the site (login)
|
||||||
that use a form to load a new page.
|
that use a form to load a new page.
|
||||||
|
|
||||||
Using an aStore was evaluated but I've decided against using it.
|
Using an aStore was evaluated but I've decided against using it.
|
||||||
There are three major issues with an aStore. Because checkout is
|
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.
|
handled by sending the user to Amazon we can't put it in a QWebView.
|
||||||
@ -57,7 +57,7 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
nicer. Also, we cannot put the aStore in a QWebView and let it open the
|
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
|
redirection the users default browser because the cookies with the
|
||||||
shopping cart won't transfer.
|
shopping cart won't transfer.
|
||||||
|
|
||||||
Another issue with the aStore is how it handles the referral. It only
|
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
|
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
|
that directed the user to Amazon. Kindle books do not use the shopping
|
||||||
@ -65,44 +65,44 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
instance we would only get referral credit for the one book that the
|
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
|
aStore directs to Amazon that the user buys. Any other purchases we
|
||||||
won't get credit for.
|
won't get credit for.
|
||||||
|
|
||||||
The last issue with the aStore is performance. Even though it's an
|
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
|
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
|
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
|
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
|
with calibre. This can cause some support issues because we can't
|
||||||
do much for issues with Amazon.com purchase hiccups.
|
do much for issues with Amazon.com purchase hiccups.
|
||||||
|
|
||||||
Another option that was evaluated was the Product Advertising API.
|
Another option that was evaluated was the Product Advertising API.
|
||||||
The reasons against this are complexity. It would take a lot of work
|
The reasons against this are complexity. It would take a lot of work
|
||||||
to basically re-create Amazon.com within calibre. The Product
|
to basically re-create Amazon.com within calibre. The Product
|
||||||
Advertising API is also designed with being run on a server not
|
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
|
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.
|
calibre user which means bad things could be done with our account.
|
||||||
|
|
||||||
The Product Advertising API also assumes the same browser for easy
|
The Product Advertising API also assumes the same browser for easy
|
||||||
shopping cart transfer to Amazon. With QWebView not working and there
|
shopping cart transfer to Amazon. With QWebView not working and there
|
||||||
not being an easy way to transfer cookies between a QWebView and the
|
not being an easy way to transfer cookies between a QWebView and the
|
||||||
users default browser this won't work well.
|
users default browser this won't work well.
|
||||||
|
|
||||||
We could create our own website on the calibre server and create an
|
We could create our own website on the calibre server and create an
|
||||||
Amazon Product Advertising API store. However, this goes back to the
|
Amazon Product Advertising API store. However, this goes back to the
|
||||||
complexity argument. Why spend the time recreating Amazon.com
|
complexity argument. Why spend the time recreating Amazon.com
|
||||||
|
|
||||||
The final and largest issue against using the Product Advertising API
|
The final and largest issue against using the Product Advertising API
|
||||||
is the Efficiency Guidelines:
|
is the Efficiency Guidelines:
|
||||||
|
|
||||||
"Each account used to access the Product Advertising API will be allowed
|
"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
|
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
|
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
|
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
|
in a trailing 30-day period. Usage thresholds are recalculated daily based
|
||||||
on revenue performance."
|
on revenue performance."
|
||||||
|
|
||||||
With over two million users a limit of 2,000 request per hour could
|
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
|
render our store unusable for no other reason than Amazon rate
|
||||||
limiting our traffic.
|
limiting our traffic.
|
||||||
|
|
||||||
The best (I use the term lightly here) solution is to open Amazon.com
|
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.
|
in the users default browser and set the affiliate id as part of the url.
|
||||||
'''
|
'''
|
||||||
@ -119,14 +119,14 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
def search(self, query, max_results=10, timeout=60):
|
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)
|
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query)
|
||||||
br = browser()
|
br = browser()
|
||||||
|
|
||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
for data in doc.xpath('//div[@class="productData"]'):
|
for data in doc.xpath('//div[@class="productData"]'):
|
||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Even though we are searching digital-text only Amazon will still
|
# Even though we are searching digital-text only Amazon will still
|
||||||
# put in results for non Kindle books (author pages). Se we need
|
# 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
|
# to explicitly check if the item is a Kindle book and ignore it
|
||||||
@ -134,7 +134,7 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
type = ''.join(data.xpath('//span[@class="format"]/text()'))
|
||||||
if 'kindle' not in type.lower():
|
if 'kindle' not in type.lower():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We must have an asin otherwise we can't easily reference the
|
# We must have an asin otherwise we can't easily reference the
|
||||||
# book later.
|
# book later.
|
||||||
asin_href = None
|
asin_href = None
|
||||||
@ -148,25 +148,25 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cover_url = ''
|
cover_url = ''
|
||||||
if asin_href:
|
if asin_href:
|
||||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||||
if cover_img:
|
if cover_img:
|
||||||
cover_url = cover_img[0]
|
cover_url = cover_img[0]
|
||||||
|
|
||||||
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||||
author = author.split('by')[-1]
|
author = author.split('by')[-1]
|
||||||
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
s.cover_url = cover_url
|
s.cover_url = cover_url
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = asin.strip()
|
s.detail_item = asin.strip()
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -6,7 +6,6 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
|
||||||
import urllib2
|
import urllib2
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
@ -22,7 +21,7 @@ from calibre.gui2.store.search_result import SearchResult
|
|||||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
|
||||||
class BeWriteStore(BasicStoreConfig, StorePlugin):
|
class BeWriteStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
settings = self.get_settings()
|
settings = self.get_settings()
|
||||||
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
|
||||||
@ -42,9 +41,9 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
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)
|
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query)
|
||||||
|
|
||||||
br = browser()
|
br = browser()
|
||||||
|
|
||||||
counter = max_results
|
counter = max_results
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
with closing(br.open(url, timeout=timeout)) as f:
|
||||||
doc = html.fromstring(f.read())
|
doc = html.fromstring(f.read())
|
||||||
@ -55,12 +54,12 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
id = ''.join(data.xpath('.//a/@href'))
|
id = ''.join(data.xpath('.//a/@href'))
|
||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
heading = ''.join(data.xpath('./td[2]//text()'))
|
heading = ''.join(data.xpath('./td[2]//text()'))
|
||||||
title, q, author = heading.partition('by ')
|
title, q, author = heading.partition('by ')
|
||||||
cover_url = ''
|
cover_url = ''
|
||||||
price = ''
|
price = ''
|
||||||
|
|
||||||
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
|
||||||
idata = html.fromstring(nf.read())
|
idata = html.fromstring(nf.read())
|
||||||
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
|
||||||
@ -68,14 +67,14 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
|
||||||
if cover_img:
|
if cover_img:
|
||||||
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
s.cover_url = cover_url.strip()
|
s.cover_url = cover_url.strip()
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -13,9 +13,8 @@ from random import shuffle
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QAbstractItemModel, QDialog, QTimer, QVariant, \
|
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
|
||||||
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout, QHBoxLayout, \
|
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
|
||||||
QPushButton, QString, QByteArray
|
|
||||||
|
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.gui2 import NONE
|
from calibre.gui2 import NONE
|
||||||
@ -35,7 +34,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
def __init__(self, istores, *args):
|
def __init__(self, istores, *args):
|
||||||
QDialog.__init__(self, *args)
|
QDialog.__init__(self, *args)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
|
|
||||||
self.config = DynamicConfig('store_search')
|
self.config = DynamicConfig('store_search')
|
||||||
|
|
||||||
# We keep a cache of store plugins and reference them by name.
|
# We keep a cache of store plugins and reference them by name.
|
||||||
@ -44,7 +43,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
# Check for results and hung threads.
|
# Check for results and hung threads.
|
||||||
self.checker = QTimer()
|
self.checker = QTimer()
|
||||||
self.hang_check = 0
|
self.hang_check = 0
|
||||||
|
|
||||||
self.model = Matches()
|
self.model = Matches()
|
||||||
self.results_view.setModel(self.model)
|
self.results_view.setModel(self.model)
|
||||||
|
|
||||||
@ -59,7 +58,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
stores_group_layout.addWidget(cbox)
|
stores_group_layout.addWidget(cbox)
|
||||||
setattr(self, 'store_check_' + x, cbox)
|
setattr(self, 'store_check_' + x, cbox)
|
||||||
stores_group_layout.addStretch()
|
stores_group_layout.addStretch()
|
||||||
|
|
||||||
# Create and add the progress indicator
|
# Create and add the progress indicator
|
||||||
self.pi = ProgressIndicator(self, 24)
|
self.pi = ProgressIndicator(self, 24)
|
||||||
self.bottom_layout.insertWidget(0, self.pi)
|
self.bottom_layout.insertWidget(0, self.pi)
|
||||||
@ -71,9 +70,9 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
||||||
self.select_none_stores.clicked.connect(self.stores_select_none)
|
self.select_none_stores.clicked.connect(self.stores_select_none)
|
||||||
self.finished.connect(self.dialog_closed)
|
self.finished.connect(self.dialog_closed)
|
||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
def resize_columns(self):
|
def resize_columns(self):
|
||||||
total = 600
|
total = 600
|
||||||
# Cover
|
# Cover
|
||||||
@ -87,19 +86,19 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.results_view.setColumnWidth(3, int(total*.10))
|
self.results_view.setColumnWidth(3, int(total*.10))
|
||||||
# Store
|
# Store
|
||||||
self.results_view.setColumnWidth(4, int(total*.20))
|
self.results_view.setColumnWidth(4, int(total*.20))
|
||||||
|
|
||||||
def do_search(self, checked=False):
|
def do_search(self, checked=False):
|
||||||
# Stop all running threads.
|
# Stop all running threads.
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
# Clear the visible results.
|
# Clear the visible results.
|
||||||
self.results_view.model().clear_results()
|
self.results_view.model().clear_results()
|
||||||
|
|
||||||
# Don't start a search if there is nothing to search for.
|
# Don't start a search if there is nothing to search for.
|
||||||
query = unicode(self.search_edit.text())
|
query = unicode(self.search_edit.text())
|
||||||
if not query.strip():
|
if not query.strip():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Plugins are in alphebetic order. Randomize the
|
# Plugins are in alphebetic order. Randomize the
|
||||||
# order of plugin names. This way plugins closer
|
# order of plugin names. This way plugins closer
|
||||||
# to a don't have an unfair advantage over
|
# to a don't have an unfair advantage over
|
||||||
@ -117,12 +116,12 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.checker.start(100)
|
self.checker.start(100)
|
||||||
self.search_pool.start_threads()
|
self.search_pool.start_threads()
|
||||||
self.pi.startAnimation()
|
self.pi.startAnimation()
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
self.config['store_search_geometry'] = self.saveGeometry()
|
self.config['store_search_geometry'] = self.saveGeometry()
|
||||||
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
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())]
|
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||||
|
|
||||||
store_check = {}
|
store_check = {}
|
||||||
for n in self.store_plugins:
|
for n in self.store_plugins:
|
||||||
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
|
||||||
@ -132,11 +131,11 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
geometry = self.config['store_search_geometry']
|
geometry = self.config['store_search_geometry']
|
||||||
if geometry:
|
if geometry:
|
||||||
self.restoreGeometry(geometry)
|
self.restoreGeometry(geometry)
|
||||||
|
|
||||||
splitter_state = self.config['store_search_store_splitter_state']
|
splitter_state = self.config['store_search_store_splitter_state']
|
||||||
if splitter_state:
|
if splitter_state:
|
||||||
self.store_splitter.restoreState(splitter_state)
|
self.store_splitter.restoreState(splitter_state)
|
||||||
|
|
||||||
results_cwidth = self.config['store_search_results_view_column_width']
|
results_cwidth = self.config['store_search_results_view_column_width']
|
||||||
if results_cwidth:
|
if results_cwidth:
|
||||||
for i, x in enumerate(results_cwidth):
|
for i, x in enumerate(results_cwidth):
|
||||||
@ -145,7 +144,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.results_view.setColumnWidth(i, x)
|
self.results_view.setColumnWidth(i, x)
|
||||||
else:
|
else:
|
||||||
self.resize_columns()
|
self.resize_columns()
|
||||||
|
|
||||||
store_check = self.config['store_search_store_checked']
|
store_check = self.config['store_search_store_checked']
|
||||||
if store_check:
|
if store_check:
|
||||||
for n in store_check:
|
for n in store_check:
|
||||||
@ -165,7 +164,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.pi.stopAnimation()
|
self.pi.stopAnimation()
|
||||||
|
|
||||||
while self.search_pool.has_results():
|
while self.search_pool.has_results():
|
||||||
res = self.search_pool.get_result()
|
res = self.search_pool.get_result()
|
||||||
if res:
|
if res:
|
||||||
@ -189,15 +188,15 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
def stores_select_all(self):
|
def stores_select_all(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(True)
|
check.setChecked(True)
|
||||||
|
|
||||||
def stores_select_invert(self):
|
def stores_select_invert(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(not check.isChecked())
|
check.setChecked(not check.isChecked())
|
||||||
|
|
||||||
def stores_select_none(self):
|
def stores_select_none(self):
|
||||||
for check in self.get_store_checks():
|
for check in self.get_store_checks():
|
||||||
check.setChecked(False)
|
check.setChecked(False)
|
||||||
|
|
||||||
def dialog_closed(self, result):
|
def dialog_closed(self, result):
|
||||||
self.model.closing()
|
self.model.closing()
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
@ -208,46 +207,46 @@ class GenericDownloadThreadPool(object):
|
|||||||
'''
|
'''
|
||||||
add_task must be implemented in a subclass.
|
add_task must be implemented in a subclass.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, thread_type, thread_count):
|
def __init__(self, thread_type, thread_count):
|
||||||
self.thread_type = thread_type
|
self.thread_type = thread_type
|
||||||
self.thread_count = thread_count
|
self.thread_count = thread_count
|
||||||
|
|
||||||
self.tasks = Queue()
|
self.tasks = Queue()
|
||||||
self.results = Queue()
|
self.results = Queue()
|
||||||
self.threads = []
|
self.threads = []
|
||||||
|
|
||||||
def add_task(self):
|
def add_task(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def start_threads(self):
|
def start_threads(self):
|
||||||
for i in range(self.thread_count):
|
for i in range(self.thread_count):
|
||||||
t = self.thread_type(self.tasks, self.results)
|
t = self.thread_type(self.tasks, self.results)
|
||||||
self.threads.append(t)
|
self.threads.append(t)
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self.tasks = Queue()
|
self.tasks = Queue()
|
||||||
self.results = Queue()
|
self.results = Queue()
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
t.abort()
|
t.abort()
|
||||||
self.threads = []
|
self.threads = []
|
||||||
|
|
||||||
def has_tasks(self):
|
def has_tasks(self):
|
||||||
return not self.tasks.empty()
|
return not self.tasks.empty()
|
||||||
|
|
||||||
def get_result(self):
|
def get_result(self):
|
||||||
return self.results.get()
|
return self.results.get()
|
||||||
|
|
||||||
def get_result_no_wait(self):
|
def get_result_no_wait(self):
|
||||||
return self.results.get_nowait()
|
return self.results.get_nowait()
|
||||||
|
|
||||||
def result_count(self):
|
def result_count(self):
|
||||||
return len(self.results)
|
return len(self.results)
|
||||||
|
|
||||||
def has_results(self):
|
def has_results(self):
|
||||||
return not self.results.empty()
|
return not self.results.empty()
|
||||||
|
|
||||||
def threads_running(self):
|
def threads_running(self):
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
if t.is_alive():
|
if t.is_alive():
|
||||||
@ -260,7 +259,7 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
|||||||
Threads will run until there is no work or
|
Threads will run until there is no work or
|
||||||
abort is called. Create and start new threads
|
abort is called. Create and start new threads
|
||||||
using start_threads(). Reset by calling abort().
|
using start_threads(). Reset by calling abort().
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
sp = SearchThreadPool(SearchThread, 3)
|
sp = SearchThreadPool(SearchThread, 3)
|
||||||
add tasks using add_task(...)
|
add tasks using add_task(...)
|
||||||
@ -270,13 +269,13 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
|||||||
add tasks using add_task(...)
|
add tasks using add_task(...)
|
||||||
sp.start_threads()
|
sp.start_threads()
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def add_task(self, query, store_name, store_plugin, timeout):
|
def add_task(self, query, store_name, store_plugin, timeout):
|
||||||
self.tasks.put((query, store_name, store_plugin, timeout))
|
self.tasks.put((query, store_name, store_plugin, timeout))
|
||||||
|
|
||||||
|
|
||||||
class SearchThread(Thread):
|
class SearchThread(Thread):
|
||||||
|
|
||||||
def __init__(self, tasks, results):
|
def __init__(self, tasks, results):
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.daemon = True
|
self.daemon = True
|
||||||
@ -286,7 +285,7 @@ class SearchThread(Thread):
|
|||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run and not self.tasks.empty():
|
while self._run and not self.tasks.empty():
|
||||||
try:
|
try:
|
||||||
@ -305,7 +304,7 @@ class CoverThreadPool(GenericDownloadThreadPool):
|
|||||||
'''
|
'''
|
||||||
Once started all threads run until abort is called.
|
Once started all threads run until abort is called.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def add_task(self, search_result, update_callback, timeout=5):
|
def add_task(self, search_result, update_callback, timeout=5):
|
||||||
self.tasks.put((search_result, update_callback, timeout))
|
self.tasks.put((search_result, update_callback, timeout))
|
||||||
|
|
||||||
@ -318,12 +317,12 @@ class CoverThread(Thread):
|
|||||||
self.tasks = tasks
|
self.tasks = tasks
|
||||||
self.results = results
|
self.results = results
|
||||||
self._run = True
|
self._run = True
|
||||||
|
|
||||||
self.br = browser()
|
self.br = browser()
|
||||||
|
|
||||||
def abort(self):
|
def abort(self):
|
||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run:
|
while self._run:
|
||||||
try:
|
try:
|
||||||
@ -354,13 +353,13 @@ class Matches(QAbstractItemModel):
|
|||||||
|
|
||||||
def closing(self):
|
def closing(self):
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
|
|
||||||
def clear_results(self):
|
def clear_results(self):
|
||||||
self.matches = []
|
self.matches = []
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
self.cover_pool.start_threads()
|
self.cover_pool.start_threads()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def add_result(self, result):
|
def add_result(self, result):
|
||||||
self.layoutAboutToBeChanged.emit()
|
self.layoutAboutToBeChanged.emit()
|
||||||
self.matches.append(result)
|
self.matches.append(result)
|
||||||
@ -391,7 +390,7 @@ class Matches(QAbstractItemModel):
|
|||||||
|
|
||||||
def columnCount(self, *args):
|
def columnCount(self, *args):
|
||||||
return len(self.HEADERS)
|
return len(self.HEADERS)
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role != Qt.DisplayRole:
|
if role != Qt.DisplayRole:
|
||||||
return NONE
|
return NONE
|
||||||
@ -434,7 +433,7 @@ class Matches(QAbstractItemModel):
|
|||||||
elif col == 3:
|
elif col == 3:
|
||||||
text = result.price
|
text = result.price
|
||||||
if len(text) < 3 or text[-3] not in ('.', ','):
|
if len(text) < 3 or text[-3] not in ('.', ','):
|
||||||
text += '00'
|
text += '00'
|
||||||
text = re.sub(r'\D', '', text)
|
text = re.sub(r'\D', '', text)
|
||||||
text = text.rjust(6, '0')
|
text = text.rjust(6, '0')
|
||||||
elif col == 4:
|
elif col == 4:
|
||||||
@ -444,7 +443,7 @@ class Matches(QAbstractItemModel):
|
|||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
if not self.matches:
|
if not self.matches:
|
||||||
return
|
return
|
||||||
descending = order == Qt.DescendingOrder
|
descending = order == Qt.DescendingOrder
|
||||||
self.matches.sort(None,
|
self.matches.sort(None,
|
||||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||||
descending)
|
descending)
|
||||||
|
@ -9,8 +9,8 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os
|
import os
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from PyQt4.Qt import QWebView, QWebPage, QNetworkCookieJar, QNetworkRequest, QString, \
|
from PyQt4.Qt import QNetworkCookieJar, QFileDialog, QNetworkProxy
|
||||||
QFileDialog, QNetworkProxy
|
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||||
|
|
||||||
from calibre import USER_AGENT, get_proxies, get_download_filename
|
from calibre import USER_AGENT, get_proxies, get_download_filename
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
@ -35,13 +35,13 @@ class NPWebView(QWebView):
|
|||||||
proxy.setPassword(proxy_parts.password)
|
proxy.setPassword(proxy_parts.password)
|
||||||
proxy.setHostName(proxy_parts.hostname)
|
proxy.setHostName(proxy_parts.hostname)
|
||||||
proxy.setPort(proxy_parts.port)
|
proxy.setPort(proxy_parts.port)
|
||||||
self.page().networkAccessManager().setProxy(proxy)
|
self.page().networkAccessManager().setProxy(proxy)
|
||||||
|
|
||||||
self.page().setForwardUnsupportedContent(True)
|
self.page().setForwardUnsupportedContent(True)
|
||||||
self.page().unsupportedContent.connect(self.start_download)
|
self.page().unsupportedContent.connect(self.start_download)
|
||||||
self.page().downloadRequested.connect(self.start_download)
|
self.page().downloadRequested.connect(self.start_download)
|
||||||
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
|
||||||
|
|
||||||
def createWindow(self, type):
|
def createWindow(self, type):
|
||||||
if type == QWebPage.WebBrowserWindow:
|
if type == QWebPage.WebBrowserWindow:
|
||||||
return self
|
return self
|
||||||
@ -50,17 +50,17 @@ class NPWebView(QWebView):
|
|||||||
|
|
||||||
def set_gui(self, gui):
|
def set_gui(self, gui):
|
||||||
self.gui = gui
|
self.gui = gui
|
||||||
|
|
||||||
def set_tags(self, tags):
|
def set_tags(self, tags):
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
|
|
||||||
def start_download(self, request):
|
def start_download(self, request):
|
||||||
if not self.gui:
|
if not self.gui:
|
||||||
return
|
return
|
||||||
|
|
||||||
url = unicode(request.url().toString())
|
url = unicode(request.url().toString())
|
||||||
cf = self.get_cookies()
|
cf = self.get_cookies()
|
||||||
|
|
||||||
filename = get_download_filename(url, cf)
|
filename = get_download_filename(url, cf)
|
||||||
ext = os.path.splitext(filename)[1][1:].lower()
|
ext = os.path.splitext(filename)[1][1:].lower()
|
||||||
if ext not in BOOK_EXTENSIONS:
|
if ext not in BOOK_EXTENSIONS:
|
||||||
@ -76,21 +76,21 @@ class NPWebView(QWebView):
|
|||||||
|
|
||||||
def ignore_ssl_errors(self, reply, errors):
|
def ignore_ssl_errors(self, reply, errors):
|
||||||
reply.ignoreSslErrors(errors)
|
reply.ignoreSslErrors(errors)
|
||||||
|
|
||||||
def get_cookies(self):
|
def get_cookies(self):
|
||||||
'''
|
'''
|
||||||
Writes QNetworkCookies to Mozilla cookie .txt file.
|
Writes QNetworkCookies to Mozilla cookie .txt file.
|
||||||
|
|
||||||
:return: The file path to the cookie file.
|
:return: The file path to the cookie file.
|
||||||
'''
|
'''
|
||||||
cf = PersistentTemporaryFile(suffix='.txt')
|
cf = PersistentTemporaryFile(suffix='.txt')
|
||||||
|
|
||||||
cf.write('# Netscape HTTP Cookie File\n\n')
|
cf.write('# Netscape HTTP Cookie File\n\n')
|
||||||
|
|
||||||
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
for c in self.page().networkAccessManager().cookieJar().allCookies():
|
||||||
cookie = []
|
cookie = []
|
||||||
domain = unicode(c.domain())
|
domain = unicode(c.domain())
|
||||||
|
|
||||||
cookie.append(domain)
|
cookie.append(domain)
|
||||||
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
|
||||||
cookie.append(unicode(c.path()))
|
cookie.append(unicode(c.path()))
|
||||||
@ -98,15 +98,15 @@ class NPWebView(QWebView):
|
|||||||
cookie.append(unicode(c.expirationDate().toTime_t()))
|
cookie.append(unicode(c.expirationDate().toTime_t()))
|
||||||
cookie.append(unicode(c.name()))
|
cookie.append(unicode(c.name()))
|
||||||
cookie.append(unicode(c.value()))
|
cookie.append(unicode(c.value()))
|
||||||
|
|
||||||
cf.write('\t'.join(cookie))
|
cf.write('\t'.join(cookie))
|
||||||
cf.write('\n')
|
cf.write('\n')
|
||||||
|
|
||||||
cf.close()
|
cf.close()
|
||||||
return cf.name
|
return cf.name
|
||||||
|
|
||||||
|
|
||||||
class NPWebPage(QWebPage):
|
class NPWebPage(QWebPage):
|
||||||
|
|
||||||
def userAgentForUrl(self, url):
|
def userAgentForUrl(self, url):
|
||||||
return USER_AGENT
|
return USER_AGENT
|
||||||
|
@ -88,6 +88,11 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
_gui = None
|
||||||
|
|
||||||
|
def get_gui():
|
||||||
|
return _gui
|
||||||
|
|
||||||
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
||||||
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
|
||||||
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
|
||||||
@ -97,7 +102,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
|
|
||||||
|
|
||||||
def __init__(self, opts, parent=None, gui_debug=None):
|
def __init__(self, opts, parent=None, gui_debug=None):
|
||||||
|
global _gui
|
||||||
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
|
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
|
||||||
|
_gui = self
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.device_connected = None
|
self.device_connected = None
|
||||||
self.gui_debug = gui_debug
|
self.gui_debug = gui_debug
|
||||||
|
@ -426,7 +426,7 @@ def do_show_metadata(db, id, as_opf):
|
|||||||
mi = OPFCreator(os.getcwd(), mi)
|
mi = OPFCreator(os.getcwd(), mi)
|
||||||
mi.render(sys.stdout)
|
mi.render(sys.stdout)
|
||||||
else:
|
else:
|
||||||
print unicode(mi).encode(preferred_encoding)
|
prints(unicode(mi))
|
||||||
|
|
||||||
def show_metadata_option_parser():
|
def show_metadata_option_parser():
|
||||||
parser = get_parser(_(
|
parser = get_parser(_(
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user