Merge from trunk

This commit is contained in:
Charles Haley 2013-04-24 14:34:41 +02:00
commit 05c3fe3775
46 changed files with 635 additions and 286 deletions

View File

@ -79,13 +79,6 @@ License: GPL2+
The full text of the GPL is distributed as in
/usr/share/common-licenses/GPL-2 on Debian systems.
Files: src/pyPdf/*
Copyright: Copyright (c) 2006, Mathieu Fenniak
Copyright: Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>
License: BSD
The full text of the BSD license is distributed as in
/usr/share/common-licenses/BSD on Debian systems.
Files: src/calibre/utils/lzx/*
Copyright: Copyright (C) 2002, Matthew T. Russotto
Copyright: Copyright (C) 2008, Marshall T. Vandegrift <llasram@gmail.com>
@ -100,49 +93,6 @@ License: BSD
The full text of the BSD license is distributed as in
/usr/share/common-licenses/BSD on Debian systems.
Files: src/calibre/utils/pyparsing.py
Copyright: Copyright (c) 2003-2008, Paul T. McGuire
License: MIT
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Files: src/calibre/utils/PythonMagickWand.py
Copyright: (c) 2007 - Achim Domma - domma@procoders.net
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Files: src/calibre/utils/msdes/d3des.h:
Files: src/calibre/utils/msdes/des.c:
Copyright: Copyright (C) 1988,1989,1990,1991,1992, Richard Outerbridge

View File

@ -426,6 +426,8 @@ Identifiers (e.g., isbn, doi, lccn etc) also use an extended syntax. First, note
:guilabel:`Advanced Search Dialog`
.. _saved_searches:
Saving searches
-----------------
@ -435,6 +437,15 @@ Now you can access your saved search in the Tag Browser under "Searches". A sing
.. _config_filename_metadata:
Virtual Libraries
-------------------
A :guilabel:`Virtual Library` is a way to pretend that your |app| library has
only a few books instead of its full collection. This is an excellent way to
partition your large collection of books into smaller, manageable chunks. To
learn how to create and use virtual libraries, see the tutorial:
:ref:`virtual_libraries`.
Guessing metadata from file names
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the :guilabel:`Add/Save` section of the configuration dialog, you can specify a regular expression that |app| will use to try and guess metadata from the names of ebook files

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -20,4 +20,5 @@ Here you will find tutorials to get you started using |app|'s more advanced feat
creating_plugins
typesetting_math
catalogs
virtual_libraries

View File

@ -0,0 +1,88 @@
.. include:: global.rst
.. _virtual_libraries:
Virtual Libraries
============================
In |app|, a virtual library is a way to tell |app| to open only a subset of a
normal library. For example, if you want to only work with books by a certain
author, or books having only a certain tag. Using virtual libraries is the
preferred way of partitioning your large book collection into smaller sub
collections. It is superior to splitting up your library into multiple smaller
libraries as, when you want to search through your entire collection, you can
simply go back to the full library. There is no way to search through multiple
separate libraries simultaneously in |app|.
A virtual library is different to a simple search. A search will only restrict
the list of books shown in the book list. A virtual library does that an in
addition, it also restricts the entries shown in the :guilabel:`Tag Browser` to
the left. The Tag Browser will only show tags, authors, series, publishers, etc.
that come from the books in the virtual library. A virtual library thus behaves
as though the actual library contains only the restricted set of books.
Creating Virtual Libraries
----------------------------
.. |vlb| image:: images/virtual_library_button.png
:class: float-left-img
|vlb| To use a virtual library click the :guilabel:`Virtual Library` button located
to the left of the search bar and select the :guilabel:`Create Virtual Library`
option. As a first example, let's create a virtual library that shows us only
the books by a particular author. Click the :guilabel:`Authors` link as shown
in the image below and choose the author you want to use and click OK.
.. image:: images/vl_by_author.png
:align: center
The Create Virtual Library dialog has been filled in for you. Click OK and you
will see that a new Virtual Library has been created, and automatically
switched to, that displays only the books by the selected author. As far as
|app| is concerned, it is as if your library contains only the books by the
selected author.
You can switch back to the full library at any time by once again clicking the
:guilabel:`Virtual Library` and selecting the entry named :guilabel:`<None>`.
Virtual Libraries are based on *searches*. You can use any search as the basis
of a virtual library. The virtual library will contain only the books matched
by that search. First, type in the search you want to use in the search bar,
when you are happy with the returned results, click the Virtual Library button,
choose Create Library and enter a name for the new virtual library. The virtual
library will then be created based on the search you just typed in. Searches
are very powerful, for examples of the kinds of things you can do with them,
see :ref:`search_interface`.
Working with Virtual Libraries
-------------------------------------
You can edit a previously created virtual library or remove it, by clicking the
:guilabel:`Virtual Library` and choosing the appropriate action.
You can tell |app| that you always want to apply a particular virtual library
when the current library is opened, by going to
:guilabel:`Preferences->Behavior`.
If you use the |app| Content Server, you can have it share a virtual library
instead of the full library by going to :guilabel:`Preferences->Sharing over the net`.
You can quickly use the current search as a temporary virtual library by
clicking the :guilabel:`Virtual Library` button and choosing the
:guilabel:`*current search` entry.
Using additional restrictions
-------------------------------
You can further restrict the books shown in a Virtual Library by using
:guilabel:`Additional restrictions`. An additional restriction is saved search
you previously created that can be applied to the current Virtual Library to
further restrict the books shown in a virtual library. For example, say you
have a Virtual Library for books tagged as :guilabel:`Historical Fiction` and a
saved search that shows you unread books, you can click the :guilabel:`Virtual
Library` button and choose the :guilabel:`Additional restriction` option to
show only unread Historical Fiction books. To learn about saved searches, see
:ref:`saved_searches`.

View File

@ -1,7 +1,7 @@
__license__ = 'GPL v3'
__copyright__ = '2010-2012, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2010-2013, Darko Miletic <darko.miletic at gmail.com>'
'''
www.ft.com/uk-edition
www.ft.com/intl/uk-edition
'''
import datetime
@ -29,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe):
masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg'
LOGIN = 'https://registration.ft.com/registration/barrier/login'
LOGIN2 = 'http://media.ft.com/h/subs3.html'
INDEX = 'http://www.ft.com/uk-edition'
INDEX = 'http://www.ft.com/intl/uk-edition'
PREFIX = 'http://www.ft.com'
conversion_options = {

View File

@ -1,20 +1,21 @@
__license__ = 'GPL v3'
__copyright__ = '2013, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2010-2013, Darko Miletic <darko.miletic at gmail.com>'
'''
http://www.ft.com/intl/us-edition
www.ft.com/intl/international-edition
'''
import datetime
from calibre.ptempfile import PersistentTemporaryFile
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
from collections import OrderedDict
class FinancialTimes(BasicNewsRecipe):
title = 'Financial Times (US) printed edition'
title = 'Financial Times (International) printed edition'
__author__ = 'Darko Miletic'
description = "The Financial Times (FT) is one of the world's leading business news and information organisations, recognised internationally for its authority, integrity and accuracy."
publisher = 'The Financial Times Ltd.'
category = 'news, finances, politics, UK, World'
category = 'news, finances, politics, World'
oldest_article = 2
language = 'en'
max_articles_per_feed = 250
@ -28,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe):
masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg'
LOGIN = 'https://registration.ft.com/registration/barrier/login'
LOGIN2 = 'http://media.ft.com/h/subs3.html'
INDEX = 'http://www.ft.com/intl/us-edition'
INDEX = 'http://www.ft.com/intl/international-edition'
PREFIX = 'http://www.ft.com'
conversion_options = {
@ -93,7 +94,7 @@ class FinancialTimes(BasicNewsRecipe):
try:
urlverified = self.browser.open_novisit(url).geturl() # resolve redirect.
except:
continue
continue
title = self.tag_to_string(item)
date = strftime(self.timefmt)
articles.append({
@ -105,29 +106,30 @@ class FinancialTimes(BasicNewsRecipe):
return articles
def parse_index(self):
feeds = []
feeds = OrderedDict()
soup = self.index_to_soup(self.INDEX)
dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div'))
self.timefmt = ' [%s]'%dates
wide = soup.find('div',attrs={'class':'wide'})
if not wide:
return feeds
allsections = wide.findAll(attrs={'class':lambda x: x and 'footwell' in x.split()})
if not allsections:
return feeds
count = 0
for item in allsections:
count = count + 1
if self.test and count > 2:
return feeds
fitem = item.h3
if not fitem:
fitem = item.h4
ftitle = self.tag_to_string(fitem)
self.report_progress(0, _('Fetching feed')+' %s...'%(ftitle))
feedarts = self.get_artlinks(item.ul)
feeds.append((ftitle,feedarts))
return feeds
#dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div'))
#self.timefmt = ' [%s]'%dates
section_title = 'Untitled'
for column in soup.findAll('div', attrs = {'class':'feedBoxes clearfix'}):
for section in column. findAll('div', attrs = {'class':'feedBox'}):
sectiontitle=self.tag_to_string(section.find('h4'))
if '...' not in sectiontitle: section_title=sectiontitle
for article in section.ul.findAll('li'):
articles = []
title=self.tag_to_string(article.a)
url=article.a['href']
articles.append({'title':title, 'url':url, 'description':'', 'date':''})
if articles:
if section_title not in feeds:
feeds[section_title] = []
feeds[section_title] += articles
ans = [(key, val) for key, val in feeds.iteritems()]
return ans
def preprocess_html(self, soup):
items = ['promo-box','promo-title',
@ -174,9 +176,6 @@ class FinancialTimes(BasicNewsRecipe):
count += 1
tfile = PersistentTemporaryFile('_fa.html')
tfile.write(html)
tfile.close()
tfile.close()
self.temp_files.append(tfile)
return tfile.name
def cleanup(self):
self.browser.open('https://registration.ft.com/registration/login/logout?location=')

View File

@ -4,7 +4,7 @@ class AdvancedUserRecipe1366025923(BasicNewsRecipe):
title = u'Lightspeed Magazine'
language = 'en'
__author__ = 'Jose Pinto'
oldest_article = 7
oldest_article = 31
max_articles_per_feed = 100
auto_cleanup = True
use_embedded_content = False

View File

@ -36,6 +36,9 @@ from BeautifulSoup import BeautifulSoup
Changed order of regex to speedup proces
Version 1.9.3 23-05-2012
Updated Cover image
Version 1.9.4 19-04-2013
Added regex filter for mailto
Updated for new layout of metro-site
'''
class AdvancedUserRecipe1306097511(BasicNewsRecipe):
@ -43,7 +46,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
oldest_article = 1.2
max_articles_per_feed = 25
__author__ = u'DrMerry'
description = u'Metro Nederland'
description = u'Metro Nederland v1.9.4 2013-04-19'
language = u'nl'
simultaneous_downloads = 5
masthead_url = 'http://blog.metronieuws.nl/wp-content/themes/metro/images/header.gif'
@ -68,13 +71,17 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
#(re.compile('(</?)h2', re.DOTALL|re.IGNORECASE),lambda match:'\1em')
]
remove_tags_before= dict(id='date')
remove_tags_after = [dict(name='div', attrs={'class':['column-1-3','gallery-text']})]#id='share-and-byline')]
remove_tags_before= dict(id='subwrapper')
remove_tags_after = dict(name='div', attrs={'class':['body-area','article-main-area']})
#name='div', attrs={'class':['subwrapper']})]
#'column-1-3','gallery-text']})]#id='share-and-byline')]
filter_regexps = [r'mailto:.*']
remove_tags = [
dict(name=['iframe','script','noscript','style']),
dict(name='div', attrs={'class':['column-4-5','column-1-5','ad-msg','col-179 ','col-373 ','clear','ad','navigation',re.compile('share-tools(-top)?'),'tools','metroCommentFormWrap','article-tools-below-title','related-links','padding-top-15',re.compile('^promo.*?$'),'teaser-component',re.compile('fb(-comments|_iframe_widget)'),'promos','header-links','promo-2']}),
dict(id=['column-1-5-bottom','column-4-5',re.compile('^ad(\d+|adcomp.*?)?$'),'adadcomp-4','margin-5','sidebar',re.compile('^article-\d'),'comments','gallery-1']),
dict(name='div', attrs={'class':['aside clearfix','aside clearfix middle-col-line','comments','share-tools','article-right-column','column-4-5','column-1-5','ad-msg','col-179 ','col-373 ','clear','ad','navigation',re.compile('share-tools(-top)?'),'tools','metroCommentFormWrap','article-tools-below-title','related-links','padding-top-15',re.compile('^promo.*?$'),'teaser-component',re.compile('fb(-comments|_iframe_widget)'),'promos','header-links','promo-2']}),
dict(id=['article-2','googleads','column-1-5-bottom','column-4-5',re.compile('^ad(\d+|adcomp.*?)?$'),'adadcomp-4','margin-5','sidebar',re.compile('^article-\d'),'comments','gallery-1','sharez_container','ts-container','topshares','ts-title']),
dict(name='a', attrs={'name':'comments'}),
#dict(name='div', attrs={'data-href'}),
dict(name='img', attrs={'class':'top-line','title':'volledig scherm'}),

View File

@ -0,0 +1,27 @@
from calibre.web.feeds.news import BasicNewsRecipe
class HindustanTimes(BasicNewsRecipe):
title = u'Voice of America'
language = 'en'
__author__ = 'Krittika Goyal'
oldest_article = 15 #days
max_articles_per_feed = 25
#encoding = 'cp1252'
use_embedded_content = False
no_stylesheets = True
auto_cleanup = True
feeds = [
('All Zones',
'http://learningenglish.voanews.com/rss/?count=20'),
('World',
'http://learningenglish.voanews.com/rss/?count=20&zoneid=957'),
('USA',
'http://learningenglish.voanews.com/rss/?count=20&zoneid=958'),
('Health',
'http://learningenglish.voanews.com/rss/?count=20&zoneid=955'),
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 397 KiB

View File

@ -38,7 +38,7 @@ class Check(Command):
if cache.get(y, 0) == mtime:
continue
if (f.endswith('.py') and f not in (
'feedparser.py', 'pyparsing.py', 'markdown.py') and
'feedparser.py', 'markdown.py') and
'prs500/driver.py' not in y):
yield y, mtime
if f.endswith('.coffee'):

View File

@ -48,7 +48,7 @@ binary_includes = [
'/usr/lib/libpng14.so.14',
'/usr/lib/libexslt.so.0',
# Ensure that libimobiledevice is compiled against openssl, not gnutls
'/usr/lib/libimobiledevice.so.3',
'/usr/lib/libimobiledevice.so.4',
'/usr/lib/libusbmuxd.so.2',
'/usr/lib/libplist.so.1',
MAGICK_PREFIX+'/lib/libMagickWand.so.5',
@ -112,7 +112,6 @@ class LinuxFreeze(Command):
else:
ffi = glob.glob('/usr/lib/libffi.so.?')[-1]
for x in binary_includes + [stdcpp, ffi]:
dest = self.bin_dir if '/bin/' in x else self.lib_dir
shutil.copy2(x, dest)
@ -226,7 +225,6 @@ class LinuxFreeze(Command):
except:
self.warn('Failed to byte-compile', y)
def run_builder(self, cmd, verbose=True):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
@ -256,7 +254,6 @@ class LinuxFreeze(Command):
self.info('Archive %s created: %.2f MB'%(dist,
os.stat(dist).st_size/(1024.**2)))
def build_launchers(self):
self.obj_dir = self.j(self.src_root, 'build', 'launcher')
if not os.path.exists(self.obj_dir):
@ -268,7 +265,8 @@ class LinuxFreeze(Command):
cflags = '-fno-strict-aliasing -W -Wall -c -O2 -pipe -DPYTHON_VER="python%s"'%self.py_ver
cflags = cflags.split() + ['-I/usr/include/python'+self.py_ver]
for src, obj in zip(sources, objects):
if not self.newer(obj, headers+[src, __file__]): continue
if not self.newer(obj, headers+[src, __file__]):
continue
cmd = ['gcc'] + cflags + ['-fPIC', '-o', obj, src]
self.run_builder(cmd)
@ -330,8 +328,7 @@ class LinuxFreeze(Command):
self.run_builder(cmd, verbose=False)
def create_site_py(self): # {{{
def create_site_py(self): # {{{
with open(self.j(self.py_dir, 'site.py'), 'wb') as f:
f.write(textwrap.dedent('''\
import sys

View File

@ -37,7 +37,6 @@ class OSX32_Freeze(Command):
action='store_true',
help='Only build launchers')
def run(self, opts):
global info, warn
info, warn = self.info, self.warn
@ -332,7 +331,7 @@ class Py2App(object):
def create_plist(self):
from calibre.ebooks import BOOK_EXTENSIONS
env = dict(**ENV)
env['CALIBRE_LAUNCHED_FROM_BUNDLE']='1';
env['CALIBRE_LAUNCHED_FROM_BUNDLE']='1'
docs = [{'CFBundleTypeName':'E-book',
'CFBundleTypeExtensions':list(BOOK_EXTENSIONS),
'CFBundleTypeRole':'Viewer',
@ -395,12 +394,11 @@ class Py2App(object):
self.install_dylib(os.path.join(SW, 'lib', 'libpng12.0.dylib'))
self.install_dylib(os.path.join(SW, 'lib', 'libpng.3.dylib'))
@flush
def add_fontconfig(self):
info('\nAdding fontconfig')
for x in ('fontconfig.1', 'freetype.6', 'expat.1',
'plist.1', 'usbmuxd.2', 'imobiledevice.3'):
'plist.1', 'usbmuxd.2', 'imobiledevice.4'):
src = os.path.join(SW, 'lib', 'lib'+x+'.dylib')
self.install_dylib(src)
dst = os.path.join(self.resources_dir, 'fonts')
@ -568,7 +566,7 @@ class Py2App(object):
@flush
def compile_py_modules(self):
info( '\nCompiling Python modules')
info('\nCompiling Python modules')
base = join(self.resources_dir, 'Python')
for x in os.walk(base):
root = x[0]
@ -584,7 +582,7 @@ class Py2App(object):
@flush
def create_console_app(self):
info( '\nCreating console.app')
info('\nCreating console.app')
cc_dir = os.path.join(self.contents_dir, 'console.app', 'Contents')
os.makedirs(cc_dir)
for x in os.listdir(self.contents_dir):
@ -607,7 +605,6 @@ class Py2App(object):
shutil.copy2(join(base, 'site.py'), join(self.resources_dir, 'Python',
'lib', 'python'+self.version_info))
@flush
def makedmg(self, d, volname,
destdir='dist',
@ -630,7 +627,7 @@ class Py2App(object):
'-volname', volname, '-format', format, dmg])
shutil.rmtree(tdir)
if internet_enable:
subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
size = os.stat(dmg).st_size/(1024*1024.)
info('\nInstaller size: %.2fMB\n'%size)
return dmg

View File

@ -10253,7 +10253,7 @@ msgstr ""
#. name for inh
msgid "Ingush"
msgstr "Engelsk"
msgstr "Ingush"
#. name for inj
msgid "Inga; Jungle"

View File

@ -1448,7 +1448,6 @@ class StoreGoogleBooksStore(StoreBase):
headquarters = 'US'
formats = ['EPUB', 'PDF', 'TXT']
affiliate = True
class StoreGutenbergStore(StoreBase):
name = 'Project Gutenberg'

View File

@ -114,6 +114,19 @@ class Cache(object):
if self.dirtied_cache:
self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1
@write_api
def initialize_template_cache(self):
self.formatter_template_cache = {}
@write_api
def refresh(self):
self._initialize_template_cache()
for field in self.fields.itervalues():
if hasattr(field, 'clear_cache'):
field.clear_cache() # Clear the composite cache
if hasattr(field, 'table'):
field.table.read(self.backend) # Reread data from metadata.db
@property
def field_metadata(self):
return self.backend.field_metadata

View File

@ -6,12 +6,13 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
import os, traceback
from functools import partial
from calibre.db.backend import DB
from calibre.db.cache import Cache
from calibre.db.view import View
from calibre.utils.date import utcnow
class LibraryDatabase(object):
@ -29,6 +30,7 @@ class LibraryDatabase(object):
progress_callback=lambda x, y:True, restore_all_prefs=False):
self.is_second_db = is_second_db # TODO: Use is_second_db
self.listeners = set([])
backend = self.backend = DB(library_path, default_prefs=default_prefs,
read_only=read_only, restore_all_prefs=restore_all_prefs,
@ -50,6 +52,8 @@ class LibraryDatabase(object):
setattr(self, prop, partial(self.get_property,
loc=self.FIELD_MAP[fm]))
self.last_update_check = self.last_modified()
def close(self):
self.backend.close()
@ -71,9 +75,22 @@ class LibraryDatabase(object):
def library_id(self):
return self.backend.library_id
@property
def library_path(self):
return self.backend.library_path
@property
def dbpath(self):
return self.backend.dbpath
def last_modified(self):
return self.backend.last_modified()
def check_if_modified(self):
if self.last_modified() > self.last_update_check:
self.refresh()
self.last_update_check = utcnow()
@property
def custom_column_num_map(self):
return self.backend.custom_column_num_map
@ -86,9 +103,48 @@ class LibraryDatabase(object):
def FIELD_MAP(self):
return self.backend.FIELD_MAP
@property
def formatter_template_cache(self):
return self.data.cache.formatter_template_cache
def initialize_template_cache(self):
self.data.cache.initialize_template_cache()
def all_ids(self):
for book_id in self.data.cache.all_book_ids():
yield book_id
def refresh(self, field=None, ascending=True):
self.data.cache.refresh()
self.data.refresh(field=field, ascending=ascending)
def add_listener(self, listener):
'''
Add a listener. Will be called on change events with two arguments.
Event name and list of affected ids.
'''
self.listeners.add(listener)
def notify(self, event, ids=[]):
'Notify all listeners'
for listener in self.listeners:
try:
listener(event, ids)
except:
traceback.print_exc()
continue
# }}}
def path(self, index, index_is_id=False):
'Return the relative path to the directory containing this books files as a unicode string.'
book_id = index if index_is_id else self.data.index_to_id(index)
return self.data.cache.field_for('path', book_id).replace('/', os.sep)
def abspath(self, index, index_is_id=False, create_dirs=True):
'Return the absolute path to the directory containing this books files as a unicode string.'
path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
if create_dirs and not os.path.exists(path):
os.makedirs(path)
return path

View File

@ -16,7 +16,7 @@ class LegacyTest(BaseTest):
'Test library wide properties'
def get_props(db):
props = ('user_version', 'is_second_db', 'library_id', 'field_metadata',
'custom_column_label_map', 'custom_column_num_map')
'custom_column_label_map', 'custom_column_num_map', 'library_path', 'dbpath')
fprops = ('last_modified', )
ans = {x:getattr(db, x) for x in props}
ans.update({x:getattr(db, x)() for x in fprops})
@ -51,6 +51,11 @@ class LegacyTest(BaseTest):
if label in {'tags', 'formats'}:
# Order is random in the old db for these
ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label])
if label == 'series_sort':
# The old db code did not take book language into account
# when generating series_sort values (the first book has
# lang=deu)
ans[label] = ans[label][1:]
return ans
old = self.init_old()
@ -64,3 +69,31 @@ class LegacyTest(BaseTest):
# }}}
def test_refresh(self): # {{{
' Test refreshing the view after a change to metadata.db '
db = self.init_legacy()
db2 = self.init_legacy()
self.assertEqual(db2.data.cache.set_field('title', {1:'xxx'}), set([1]))
db2.close()
del db2
self.assertNotEqual(db.title(1, index_is_id=True), 'xxx')
db.check_if_modified()
self.assertEqual(db.title(1, index_is_id=True), 'xxx')
# }}}
def test_legacy_getters(self): # {{{
old = self.init_old()
getters = ('path', 'abspath', 'title', 'authors', 'series',
'publisher', 'author_sort', 'authors', 'comments',
'comment', 'publisher', 'rating', 'series_index', 'tags',
'timestamp', 'uuid', 'pubdate', 'ondevice',
'metadata_last_modified', 'languages')
oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) + tuple(getattr(old, g)(x, True) for x in (1,2,3)) for g in getters}
old.close()
db = self.init_legacy()
newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) + tuple(getattr(db, g)(x, True) for x in (1,2,3)) for g in getters}
for x in (oldvals, newvals):
x['tags'] = tuple(set(y.split(',')) if y else y for y in x['tags'])
self.assertEqual(oldvals, newvals)
# }}}

View File

@ -294,3 +294,11 @@ class View(object):
self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode,
id_dict.itervalues())))
def refresh(self, field=None, ascending=True):
self._map = tuple(self.cache.all_book_ids())
self._map_filtered = tuple(self._map)
if field is not None:
self.sort(field, ascending)
if self.search_restriction or self.base_restriction:
self.search('', return_matches=False)

View File

@ -71,6 +71,7 @@ class ANDROID(USBMS):
0x42f7 : [0x216],
0x4365 : [0x216],
0x4366 : [0x216],
0x4371 : [0x216],
},
# Freescale
0x15a2 : {
@ -239,7 +240,7 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD']
'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD', 'XT894']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -250,7 +251,7 @@ class ANDROID(USBMS):
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1']
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1', 'XT894']
OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -35,11 +35,11 @@ class KOBO(USBMS):
gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader')
author = 'Timothy Legge and David Forrester'
version = (2, 0, 7)
version = (2, 0, 8)
dbversion = 0
fwversion = 0
supported_dbversion = 75
supported_dbversion = 80
has_kepubs = False
supported_platforms = ['windows', 'osx', 'linux']
@ -419,7 +419,7 @@ class KOBO(USBMS):
# If all this succeeds we need to delete the images files via the ImageID
return ImageID
def delete_images(self, ImageID):
def delete_images(self, ImageID, book_path):
if ImageID != None:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID
@ -449,7 +449,7 @@ class KOBO(USBMS):
ImageID = self.delete_via_sql(ContentID, ContentType)
#print " We would now delete the Images for" + ImageID
self.delete_images(ImageID)
self.delete_images(ImageID, path)
if os.path.exists(path):
# Delete the ebook
@ -1199,15 +1199,21 @@ class KOBO(USBMS):
class KOBOTOUCH(KOBO):
name = 'KoboTouch'
gui_name = 'Kobo Touch'
gui_name = 'Kobo Touch/Glo/Mini/Aura HD'
author = 'David Forrester'
description = 'Communicate with the Kobo Touch, Glo and Mini firmware. Based on the existing Kobo driver by %s.' % (KOBO.author)
description = 'Communicate with the Kobo Touch, Glo, Mini and Aura HD ereaders. Based on the existing Kobo driver by %s.' % (KOBO.author)
# icon = I('devices/kobotouch.jpg')
supported_dbversion = 75
min_supported_dbversion = 53
min_dbversion_series = 65
min_dbversion_archive = 71
supported_dbversion = 80
min_supported_dbversion = 53
min_dbversion_series = 65
min_dbversion_archive = 71
min_dbversion_images_on_sdcard = 77
max_supported_fwversion = (2,5,1)
min_fwversion_images_on_sdcard = (2,4,1)
has_kepubs = True
booklist_class = KTCollectionsBookList
book_class = Book
@ -1291,12 +1297,13 @@ class KOBOTOUCH(KOBO):
TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ"
GLO_PRODUCT_ID = [0x4173]
MINI_PRODUCT_ID = [0x4183]
TOUCH_PRODUCT_ID = [0x4163]
PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID
AURA_HD_PRODUCT_ID = [0x4193]
GLO_PRODUCT_ID = [0x4173]
MINI_PRODUCT_ID = [0x4183]
TOUCH_PRODUCT_ID = [0x4163]
PRODUCT_ID = AURA_HD_PRODUCT_ID + GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID
BCD = [0x0110, 0x0326]
BCD = [0x0110, 0x0326]
# Image file name endings. Made up of: image size, min_dbversion, max_dbversion,
COVER_FILE_ENDINGS = {
@ -1313,6 +1320,11 @@ class KOBOTOUCH(KOBO):
# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,],
# ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,],
}
AURA_HD_COVER_FILE_ENDINGS = {
' - N3_FULL.parsed': [(1080,1440), 0, 99,True,], # Used for screensaver, home screen
' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 99,False,], # Used for Details screen
' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 99,False,], # Used for library lists
}
#Following are the sizes used with pre2.1.4 firmware
# COVER_FILE_ENDINGS = {
# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen
@ -1328,6 +1340,10 @@ class KOBOTOUCH(KOBO):
super(KOBOTOUCH, self).initialize()
self.bookshelvelist = []
def get_device_information(self, end_session=True):
self.set_device_name()
return super(KOBOTOUCH, self).get_device_information(end_session)
def books(self, oncard=None, end_session=True):
debug_print("KoboTouch:books - oncard='%s'"%oncard)
from calibre.ebooks.metadata.meta import path_to_ext
@ -1354,14 +1370,13 @@ class KOBOTOUCH(KOBO):
# Determine the firmware version
try:
with open(self.normalize_path(self._main_prefix + '.kobo/version'),
'rb') as f:
with open(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f:
self.fwversion = f.readline().split(',')[2]
self.fwversion = tuple((int(x) for x in self.fwversion.split('.')))
except:
self.fwversion = 'unknown'
self.fwversion = (0,0,0)
if self.fwversion != '1.0' and self.fwversion != '1.4':
self.has_kepubs = True
debug_print('Kobo device: %s' % self.gui_name)
debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs)
debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs)
@ -1374,7 +1389,7 @@ class KOBOTOUCH(KOBO):
debug_print(opts.extra_customization)
if opts.extra_customization:
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:books - set_debugging_title to", debugging_title )
debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title )
bl.set_debugging_title(debugging_title)
debug_print("KoboTouch:books - length bl=%d"%len(bl))
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
@ -1466,6 +1481,7 @@ class KOBOTOUCH(KOBO):
if show_debug:
self.debug_index = idx
debug_print("KoboTouch:update_booklist - idx=%d"%idx)
debug_print("KoboTouch:update_booklist - lpath=%s"%lpath)
debug_print('KoboTouch:update_booklist - bl[idx].device_collections=', bl[idx].device_collections)
debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map)
debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves)
@ -1477,7 +1493,7 @@ class KOBOTOUCH(KOBO):
bl_cache[lpath] = None
if ImageID is not None:
imagename = self.imagefilename_from_imageID(ImageID)
imagename = self.imagefilename_from_imageID(prefix, ImageID)
if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType == '6' and MimeType != 'application/x-kobo-epub+zip'):
@ -1717,12 +1733,14 @@ class KOBOTOUCH(KOBO):
debug_print("KoboTouch:books - end - oncard='%s'"%oncard)
return bl
def imagefilename_from_imageID(self, ImageID):
def imagefilename_from_imageID(self, prefix, ImageID):
show_debug = self.is_debugging_title(ImageID)
path = self.images_path(prefix)
path = self.normalize_path(path.replace('/', os.sep))
for ending, cover_options in self.cover_file_endings().items():
fpath = self._main_prefix + '.kobo/images/' + ImageID + ending
fpath = self.normalize_path(fpath.replace('/', os.sep))
fpath = path + ImageID + ending
if os.path.exists(fpath):
if show_debug:
debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath))
@ -1764,7 +1782,7 @@ class KOBOTOUCH(KOBO):
if not self.copying_covers():
imageID = self.imageid_from_contentid(contentID)
self.delete_images(imageID)
self.delete_images(imageID, fname)
connection.commit()
cursor.close()
@ -1821,11 +1839,11 @@ class KOBOTOUCH(KOBO):
return imageId
def delete_images(self, ImageID):
def delete_images(self, ImageID, book_path):
debug_print("KoboTouch:delete_images - ImageID=", ImageID)
if ImageID != None:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID
path = self.images_path(book_path)
path = path + ImageID
for ending in self.cover_file_endings().keys():
fpath = path + ending
@ -1872,12 +1890,14 @@ class KOBOTOUCH(KOBO):
def get_content_type_from_extension(self, extension):
debug_print("KoboTouch:get_content_type_from_extension - start")
# With new firmware, ContentType appears to be 6 for all types of sideloaded books.
if self.fwversion.startswith('2.'):
if self.fwversion >= (1,9,17) or extension == '.kobo' or extension == '.mobi':
debug_print("KoboTouch:get_content_type_from_extension - V2 firmware")
ContentType = 6
# For older firmware, it depends on the type of file.
elif extension == '.kobo' or extension == '.mobi':
ContentType = 6
else:
debug_print("KoboTouch:get_content_type_from_extension - calling super")
ContentType = super(KOBOTOUCH, self).get_content_type_from_extension(extension)
ContentType = 901
return ContentType
def update_device_database_collections(self, booklists, collections_attributes, oncard):
@ -1920,7 +1940,7 @@ class KOBOTOUCH(KOBO):
delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves()
update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series()
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE]
debug_print("KoboTouch:update_device_database_collections - set_debugging_title to", debugging_title )
debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title )
booklists.set_debugging_title(debugging_title)
else:
delete_empty_shelves = False
@ -2088,8 +2108,8 @@ class KOBOTOUCH(KOBO):
# debug_print('KoboTouch: not uploading cover')
return
# Don't upload covers if book is on the SD card
if self._card_a_prefix and path.startswith(self._card_a_prefix):
# Only upload covers to SD card if that is supported
if self._card_a_prefix and path.startswith(self._card_a_prefix) and not self.supports_covers_on_sdcard():
return
if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]:
@ -2111,6 +2131,16 @@ class KOBOTOUCH(KOBO):
ImageID = ImageID.replace('.', '_')
return ImageID
def images_path(self, path):
if self._card_a_prefix and path.startswith(self._card_a_prefix) and self.supports_covers_on_sdcard():
path_prefix = 'koboExtStorage/images/'
path = self._card_a_prefix + path_prefix
else:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix
return path
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, keep_cover_aspect=False):
from calibre.utils.magick.draw import save_cover_data_to, identify_data
debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale))
@ -2151,8 +2181,8 @@ class KOBOTOUCH(KOBO):
cursor.close()
if ImageID != None:
path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID
path = self.images_path(path) + ImageID
if show_debug:
debug_print("KoboTouch:_upload_cover - About to loop over cover endings")
@ -2496,6 +2526,8 @@ class KOBOTOUCH(KOBO):
return opts
def isAuraHD(self):
return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID
def isGlo(self):
return self.detected_device.idProduct in self.GLO_PRODUCT_ID
def isMini(self):
@ -2504,7 +2536,21 @@ class KOBOTOUCH(KOBO):
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
def cover_file_endings(self):
return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.COVER_FILE_ENDINGS
return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() else self.COVER_FILE_ENDINGS
def set_device_name(self):
device_name = self.gui_name
if self.isAuraHD():
device_name = 'Kobo Aura HD'
elif self.isGlo():
device_name = 'Kobo Glo'
elif self.isMini():
device_name = 'Kobo Mini'
elif self.isTouch():
device_name = 'Kobo Touch'
self.__class__.gui_name = device_name
return device_name
def copying_covers(self):
opts = self.settings()
@ -2524,6 +2570,44 @@ class KOBOTOUCH(KOBO):
def supports_kobo_archive(self):
return self.dbversion >= self.min_dbversion_archive
def supports_covers_on_sdcard(self):
return self.dbversion >= 77 and self.fwversion >= self.min_fwversion_images_on_sdcard
def modify_database_check(self, function):
# Checks to see whether the database version is supported
# and whether the user has chosen to support the firmware version
# debug_print("KoboTouch:modify_database_check - self.fwversion <= self.max_supported_fwversion=", self.fwversion > self.max_supported_fwversion)
if self.dbversion > self.supported_dbversion or self.fwversion > self.max_supported_fwversion:
# Unsupported database
opts = self.settings()
if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]:
debug_print('The database has been upgraded past supported version')
self.report_progress(1.0, _('Removing books from device...'))
from calibre.devices.errors import UserFeedback
raise UserFeedback(_("Kobo database version unsupported - See details"),
_('Your Kobo is running an updated firmware/database version.'
' As calibre does not know about this updated firmware,'
' database editing is disabled, to prevent corruption.'
' You can still send books to your Kobo with calibre, '
' but deleting books and managing collections is disabled.'
' If you are willing to experiment and know how to reset'
' your Kobo to Factory defaults, you can override this'
' check by right clicking the device icon in calibre and'
' selecting "Configure this device" and then the '
' "Attempt to support newer firmware" option.'
' Doing so may require you to perform a factory reset of'
' your Kobo.'
),
UserFeedback.WARN)
return False
else:
# The user chose to edit the database anyway
return True
else:
# Supported database version
return True
@classmethod
def is_debugging_title(cls, title):

View File

@ -95,7 +95,6 @@ class PDNOVEL(USBMS):
SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata, filepath):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
@ -226,9 +225,9 @@ class TREKSTOR(USBMS):
VENDOR_ID = [0x1e68]
PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056,
0x0067, # This is for the Pyrus Mini
0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091
0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318
0x0067, # This is for the Pyrus Mini
0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091
0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318
]
BCD = [0x0002, 0x100]
@ -427,8 +426,8 @@ class WAYTEQ(USBMS):
EBOOK_DIR_MAIN = 'Documents'
SCAN_FROM_ROOT = True
VENDOR_NAME = 'ROCKCHIP'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'RK28_SDK_DEMO'
VENDOR_NAME = ['ROCKCHIP', 'CBR']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['RK28_SDK_DEMO', 'EINK_EBOOK_READE']
SUPPORTS_SUB_DIRS = True
def get_gui_name(self):
@ -445,7 +444,8 @@ class WAYTEQ(USBMS):
return self.EBOOK_DIR_CARD_A
def windows_sort_drives(self, drives):
if len(drives) < 2: return drives
if len(drives) < 2:
return drives
main = drives.get('main', None)
carda = drives.get('carda', None)
if main and carda:
@ -455,7 +455,8 @@ class WAYTEQ(USBMS):
def linux_swap_drives(self, drives):
# See https://bugs.launchpad.net/bugs/1151901
if len(drives) < 2 or not drives[1] or not drives[2]: return drives
if len(drives) < 2 or not drives[1] or not drives[2]:
return drives
drives = list(drives)
t = drives[0]
drives[0] = drives[1]
@ -463,7 +464,8 @@ class WAYTEQ(USBMS):
return tuple(drives)
def osx_sort_names(self, names):
if len(names) < 2: return names
if len(names) < 2:
return names
main = names.get('main', None)
card = names.get('carda', None)

View File

@ -58,8 +58,8 @@ class PICO(NEWSMY):
gui_name = 'Pico'
description = _('Communicate with the Pico reader.')
VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', '']
WINDOWS_MAIN_MEM = ['USBDISK__USER', 'EB720']
VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', 'LASER', '']
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['USBDISK__USER', 'EB720', 'EBOOK-EB720']
EBOOK_DIR_MAIN = 'Books'
FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT']
SCAN_FROM_ROOT = True

View File

@ -188,7 +188,6 @@ class EPUBInput(InputFormatPlugin):
raise DRMError(os.path.basename(path))
self.encrypted_fonts = self._encrypted_font_uris
if len(parts) > 1 and parts[0]:
delta = '/'.join(parts[:-1])+'/'
for elem in opf.itermanifest():

View File

@ -4,12 +4,15 @@ __copyright__ = '2010, Fabian Grassl <fg@jusmeum.de>'
__docformat__ = 'restructuredtext en'
import os, re, shutil
from os.path import dirname, abspath, relpath, exists, basename
from os.path import dirname, abspath, relpath as _relpath, exists, basename
from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation
from calibre import CurrentDir
from calibre.ptempfile import PersistentTemporaryDirectory
def relpath(*args):
return _relpath(*args).replace(os.sep, '/')
class HTMLOutput(OutputFormatPlugin):
name = 'HTML Output'

View File

@ -1,7 +1,6 @@
'''
Basic support for manipulating OEB 1.x/2.0 content and metadata.
'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
@ -11,7 +10,7 @@ import os, re, uuid, logging
from collections import defaultdict
from itertools import count
from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote
from urllib import unquote
from lxml import etree, html
from calibre.constants import filesystem_encoding, __version__
@ -40,11 +39,11 @@ CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata'
RE_NS = 'http://exslt.org/regular-expressions'
MBP_NS = 'http://www.mobipocket.com'
XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS }
XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
@ -142,7 +141,6 @@ def iterlinks(root, find_links_in_css=True):
if attr in link_attrs:
yield (el, attr, attribs[attr], 0)
if not find_links_in_css:
continue
if tag == XHTML('style') and el.text:
@ -363,7 +361,9 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE]
def urlquote(href):
"""Quote URL-unsafe characters, allowing IRI-safe characters."""
""" Quote URL-unsafe characters, allowing IRI-safe characters.
That is, this function returns valid IRIs not valid URIs. In particular,
IRIs can contain non-ascii characters. """
result = []
unsafe = 0 if isinstance(href, unicode) else 1
unsafe = URL_UNSAFE[unsafe]
@ -373,6 +373,19 @@ def urlquote(href):
result.append(char)
return ''.join(result)
def urlunquote(href):
# unquote must run on a bytestring and will return a bytestring
# If it runs on a unicode object, it returns a double encoded unicode
# string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct
want_unicode = isinstance(href, unicode)
if want_unicode:
href = href.encode('utf-8')
href = unquote(href)
if want_unicode:
href = href.decode('utf-8')
return href
def urlnormalize(href):
"""Convert a URL into normalized form, with all and only URL-unsafe
characters URL quoted.
@ -469,7 +482,7 @@ class DirContainer(object):
return
def _unquote(self, path):
# urlunquote must run on a bytestring and will return a bytestring
# unquote must run on a bytestring and will return a bytestring
# If it runs on a unicode object, it returns a double encoded unicode
# string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct
@ -497,7 +510,7 @@ class DirContainer(object):
return False
try:
path = os.path.join(self.rootdir, self._unquote(path))
except ValueError: #Happens if path contains quoted special chars
except ValueError: # Happens if path contains quoted special chars
return False
try:
return os.path.isfile(path)
@ -577,12 +590,13 @@ class Metadata(object):
allowed = self.allowed
if allowed is not None and term not in allowed:
raise AttributeError(
'attribute %r not valid for metadata term %r' \
'attribute %r not valid for metadata term %r'
% (self.attr(term), barename(obj.term)))
return self.attr(term)
def __get__(self, obj, cls):
if obj is None: return None
if obj is None:
return None
return obj.attrib.get(self.term_attr(obj), '')
def __set__(self, obj, value):
@ -628,8 +642,8 @@ class Metadata(object):
self.value = value
return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \
term == OPF('meta') else OPF('scheme'),
scheme = Attribute(lambda term: 'scheme' if
term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')])
file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'),
DC('title')])
@ -882,7 +896,6 @@ class Manifest(object):
return self._parse_xhtml(convert_markdown(data, title=title))
def _parse_css(self, data):
from cssutils import CSSParser, log, resolveImports
log.setLevel(logging.WARN)
@ -935,7 +948,7 @@ class Manifest(object):
data = self._loader(getattr(self, 'html_input_href',
self.href))
if not isinstance(data, basestring):
pass # already parsed
pass # already parsed
elif self.media_type.lower() in OEB_DOCS:
data = self._parse_xhtml(data)
elif self.media_type.lower()[-4:] in ('+xml', '/xml'):
@ -1022,7 +1035,8 @@ class Manifest(object):
target, frag = urldefrag(href)
target = target.split('/')
for index in xrange(min(len(base), len(target))):
if base[index] != target[index]: break
if base[index] != target[index]:
break
else:
index += 1
relhref = (['..'] * (len(base) - index)) + target[index:]

View File

@ -46,10 +46,11 @@ def is_raster_image(media_type):
return media_type and media_type.lower() in {
'image/png', 'image/jpeg', 'image/jpg', 'image/gif'}
COVER_TYPES = { 'coverimagestandard', 'other.ms-coverimage-standard',
'other.ms-titleimage-standard', 'other.ms-titleimage',
'other.ms-coverimage', 'other.ms-thumbimage-standard',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'}
COVER_TYPES = {
'coverimagestandard', 'other.ms-coverimage-standard',
'other.ms-titleimage-standard', 'other.ms-titleimage',
'other.ms-coverimage', 'other.ms-thumbimage-standard',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'}
def find_cover_image(container):
'Find a raster image marked as a cover in the OPF'
@ -92,7 +93,8 @@ def find_cover_page(container):
def find_cover_image_in_page(container, cover_page):
root = container.parsed(cover_page)
body = XPath('//h:body')(root)
if len(body) != 1: return
if len(body) != 1:
return
body = body[0]
images = []
for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body):
@ -152,7 +154,7 @@ def create_epub_cover(container, cover_path):
ar = 'xMidYMid meet' if keep_aspect else 'none'
templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar)
templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height))
templ = templ.replace('__width__', str(width))
templ = templ.replace('__width__', str(width))
templ = templ.replace('__height__', str(height))
titlepage_item = container.generate_item('titlepage.xhtml',
id_prefix='titlepage')
@ -179,7 +181,7 @@ def create_epub_cover(container, cover_path):
guide = container.opf_get_or_create('guide')
container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', title=_('Cover'),
href=container.name_to_href(titlepage)))
href=container.name_to_href(titlepage, base=container.opf_name)))
metadata = container.opf_get_or_create('metadata')
meta = metadata.makeelement(OPF('meta'), name='cover')
meta.set('content', raster_cover_item.get('id'))

View File

@ -148,7 +148,6 @@ class OEBReader(object):
if not has_aut:
m.add('creator', self.oeb.translate(__('Unknown')), role='aut')
def _manifest_prune_invalid(self):
'''
Remove items from manifest that contain invalid data. This prevents
@ -197,6 +196,8 @@ class OEBReader(object):
item.media_type[-4:] in ('/xml', '+xml')):
hrefs = [r[2] for r in iterlinks(data)]
for href in hrefs:
if isinstance(href, bytes):
href = href.decode('utf-8')
href, _ = urldefrag(href)
if not href:
continue
@ -293,7 +294,7 @@ class OEBReader(object):
continue
try:
href = item.abshref(urlnormalize(href))
except ValueError: # Malformed URL
except ValueError: # Malformed URL
continue
if href not in manifest.hrefs:
continue
@ -394,9 +395,9 @@ class OEBReader(object):
authorElement = xpath(child,
'descendant::calibre:meta[@name = "author"]')
if authorElement :
if authorElement:
author = authorElement[0].text
else :
else:
author = None
descriptionElement = xpath(child,
@ -406,7 +407,7 @@ class OEBReader(object):
method='text', encoding=unicode).strip()
if not description:
description = None
else :
else:
description = None
index_image = xpath(child,
@ -497,7 +498,8 @@ class OEBReader(object):
titles = []
headers = []
for item in self.oeb.spine:
if not item.linear: continue
if not item.linear:
continue
html = item.data
title = ''.join(xpath(html, '/h:html/h:head/h:title/text()'))
title = COLLAPSE_RE.sub(' ', title.strip())
@ -515,17 +517,21 @@ class OEBReader(object):
if len(titles) > len(set(titles)):
use = headers
for title, item in izip(use, self.oeb.spine):
if not item.linear: continue
if not item.linear:
continue
toc.add(title, item.href)
return True
def _toc_from_opf(self, opf, item):
self.oeb.auto_generated_toc = False
if self._toc_from_ncx(item): return
if self._toc_from_ncx(item):
return
# Prefer HTML to tour based TOC, since several LIT files
# have good HTML TOCs but bad tour based TOCs
if self._toc_from_html(opf): return
if self._toc_from_tour(opf): return
if self._toc_from_html(opf):
return
if self._toc_from_tour(opf):
return
self._toc_from_spine(opf)
self.oeb.auto_generated_toc = True
@ -589,8 +595,10 @@ class OEBReader(object):
return True
def _pages_from_opf(self, opf, item):
if self._pages_from_ncx(opf, item): return
if self._pages_from_page_map(opf): return
if self._pages_from_ncx(opf, item):
return
if self._pages_from_page_map(opf):
return
return
def _cover_from_html(self, hcover):

View File

@ -47,6 +47,8 @@ class ManifestTrimmer(object):
item.data is not None:
hrefs = [r[2] for r in iterlinks(item.data)]
for href in hrefs:
if isinstance(href, bytes):
href = href.decode('utf-8')
try:
href = item.abshref(urlnormalize(href))
except:

View File

@ -51,7 +51,7 @@ class Links(object):
for link in self.links:
path, href, frag = link[0]
page, rect = link[1:]
combined_path = os.path.abspath(os.path.join(os.path.dirname(path), *href.split('/')))
combined_path = os.path.abspath(os.path.join(os.path.dirname(path), *unquote(href).split('/')))
is_local = not href or combined_path in self.anchors
annot = Dictionary({
'Type':Name('Annot'), 'Subtype':Name('Link'),

View File

@ -406,6 +406,7 @@ class BookInfo(QWebView):
remove_format = pyqtSignal(int, object)
save_format = pyqtSignal(int, object)
restore_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object)
def __init__(self, vertical, parent=None):
QWebView.__init__(self, parent)
@ -419,26 +420,33 @@ class BookInfo(QWebView):
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
self.css = P('templates/book_details.css', data=True).decode('utf-8')
for x, icon in [('remove', 'trash.png'), ('save', 'save.png'), ('restore', 'edit-undo.png')]:
for x, icon in [('remove_format', 'trash.png'), ('save_format', 'save.png'), ('restore_format', 'edit-undo.png'), ('copy_link','edit-copy.png')]:
ac = QAction(QIcon(I(icon)), '', self)
ac.current_fmt = None
ac.triggered.connect(getattr(self, '%s_format_triggerred'%x))
setattr(self, '%s_format_action'%x, ac)
ac.current_url = None
ac.triggered.connect(getattr(self, '%s_triggerred'%x))
setattr(self, '%s_action'%x, ac)
def context_action_triggered(self, which):
f = getattr(self, '%s_format_action'%which).current_fmt
if f:
f = getattr(self, '%s_action'%which).current_fmt
url = getattr(self, '%s_action'%which).current_url
if f and 'format' in which:
book_id, fmt = f
getattr(self, '%s_format'%which).emit(book_id, fmt)
getattr(self, which).emit(book_id, fmt)
if url and 'link' in which:
getattr(self, which).emit(url)
def remove_format_triggerred(self):
self.context_action_triggered('remove')
self.context_action_triggered('remove_format')
def save_format_triggerred(self):
self.context_action_triggered('save')
self.context_action_triggered('save_format')
def restore_format_triggerred(self):
self.context_action_triggered('restore')
self.context_action_triggered('restore_format')
def copy_link_triggerred(self):
self.context_action_triggered('copy_link')
def link_activated(self, link):
self._link_clicked = True
@ -474,24 +482,33 @@ class BookInfo(QWebView):
for action in list(menu.actions()):
if action is not ca:
menu.removeAction(action)
if not r.isNull() and url.startswith('format:'):
parts = url.split(':')
try:
book_id, fmt = int(parts[1]), parts[2]
except:
import traceback
traceback.print_exc()
else:
for a, t in [('remove', _('Delete the %s format')),
('save', _('Save the %s format to disk')),
('restore', _('Restore the %s format')),
if not r.isNull():
if url.startswith('http'):
for a, t in [('copy', _('&Copy Link')),
]:
if a == 'restore' and not fmt.upper().startswith('ORIGINAL_'):
continue
ac = getattr(self, '%s_format_action'%a)
ac.current_fmt = (book_id, fmt)
ac.setText(t%parts[2])
ac = getattr(self, '%s_link_action'%a)
ac.current_url = url
ac.setText(t)
menu.addAction(ac)
if url.startswith('format:'):
parts = url.split(':')
try:
book_id, fmt = int(parts[1]), parts[2]
except:
import traceback
traceback.print_exc()
else:
for a, t in [('remove', _('Delete the %s format')),
('save', _('Save the %s format to disk')),
('restore', _('Restore the %s format')),
]:
if a == 'restore' and not fmt.upper().startswith('ORIGINAL_'):
continue
ac = getattr(self, '%s_format_action'%a)
ac.current_fmt = (book_id, fmt)
ac.setText(t%parts[2])
menu.addAction(ac)
if len(menu.actions()) > 0:
menu.exec_(ev.globalPos())
@ -594,6 +611,7 @@ class BookDetails(QWidget): # {{{
remove_specific_format = pyqtSignal(int, object)
save_specific_format = pyqtSignal(int, object)
restore_specific_format = pyqtSignal(int, object)
copy_link = pyqtSignal(object)
remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object)
@ -664,6 +682,7 @@ class BookDetails(QWidget): # {{{
self.book_info.remove_format.connect(self.remove_specific_format)
self.book_info.save_format.connect(self.save_specific_format)
self.book_info.restore_format.connect(self.restore_specific_format)
self.book_info.copy_link.connect(self.copy_link)
self.setCursor(Qt.PointingHandCursor)
def handle_click(self, link):

View File

@ -75,7 +75,7 @@ class GroupModel(QAbstractListModel):
def get_preferred_input_format_for_book(db, book_id):
recs = load_specifics(db, book_id)
if recs:
return recs.get('gui_preferred_input_format', None)
return recs.get('gui_preferred_input_format', None)
def get_available_formats_for_book(db, book_id):
available_formats = db.formats(book_id, index_is_id=True)
@ -147,6 +147,7 @@ class Config(ResizableDialog, Ui_Dialog):
self.connect(self.groups, SIGNAL('entered(QModelIndex)'),
self.show_group_help)
rb = self.buttonBox.button(self.buttonBox.RestoreDefaults)
rb.setText(_('Restore &Defaults'))
self.connect(rb, SIGNAL('clicked()'), self.restore_defaults)
self.groups.setMouseTracking(True)
geom = gprefs.get('convert_single_dialog_geom', None)
@ -188,7 +189,6 @@ class Config(ResizableDialog, Ui_Dialog):
return cls(self.stack, self.plumber.get_option_by_name,
self.plumber.get_option_help, self.db, self.book_id)
self.mw = widget_factory(MetadataWidget)
self.setWindowTitle(_('Convert')+ ' ' + unicode(self.mw.title.text()))
lf = widget_factory(LookAndFeelWidget)
@ -209,7 +209,8 @@ class Config(ResizableDialog, Ui_Dialog):
self.plumber.get_option_help, self.db, self.book_id)
while True:
c = self.stack.currentWidget()
if not c: break
if not c:
break
self.stack.removeWidget(c)
widgets = [self.mw, lf, hw, ps, sd, toc, sr]
@ -234,7 +235,6 @@ class Config(ResizableDialog, Ui_Dialog):
except:
pass
def setup_input_output_formats(self, db, book_id, preferred_input_format,
preferred_output_format):
if preferred_output_format:

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
import functools
from PyQt4.Qt import (Qt, QStackedWidget, QMenu, QTimer,
from PyQt4.Qt import (Qt, QApplication, QStackedWidget, QMenu, QTimer,
QSize, QSizePolicy, QStatusBar, QLabel, QFont)
from calibre.utils.config import prefs
@ -274,6 +274,8 @@ class LayoutMixin(object): # {{{
self.iactions['Save To Disk'].save_library_format_by_ids)
self.book_details.restore_specific_format.connect(
self.iactions['Remove Books'].restore_format)
self.book_details.copy_link.connect(self.bd_copy_link,
type=Qt.QueuedConnection)
self.book_details.view_device_book.connect(
self.iactions['View'].view_device_book)
@ -295,6 +297,10 @@ class LayoutMixin(object): # {{{
if self.cover_flow:
self.cover_flow.dataChanged()
def bd_copy_link(self, url):
if url:
QApplication.clipboard().setText(url)
def save_layout_state(self):
for x in ('library', 'memory', 'card_a', 'card_b'):
getattr(self, x+'_view').save_state()

View File

@ -21,7 +21,7 @@ from PyQt4.Qt import (
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel)
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
@ -40,7 +40,7 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError
from calibre.ptempfile import TemporaryDirectory
# }}}
class RichTextDelegate(QStyledItemDelegate): # {{{
class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent)
@ -77,7 +77,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore()
# }}}
class CoverDelegate(QStyledItemDelegate): # {{{
class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal()
@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{
# }}}
class ResultsModel(QAbstractTableModel): # {{{
class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{
p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role):
row, col = index.row(), index.column()
try:
@ -233,7 +232,7 @@ class ResultsModel(QAbstractTableModel): # {{{
# }}}
class ResultsView(QTableView): # {{{
class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object)
@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{
# }}}
class Comments(QWebView): # {{{
class Comments(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
@ -384,7 +383,7 @@ class Comments(QWebView): # {{{
return QSize(800, 300)
# }}}
class IdentifyWorker(Thread): # {{{
class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self)
@ -441,7 +440,7 @@ class IdentifyWorker(Thread): # {{{
# }}}
class IdentifyWidget(QWidget): # {{{
class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal()
results_found = pyqtSignal()
@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{
self.results_view.show_results(self.worker.results)
self.results_found.emit()
def cancel(self):
self.abort.set()
# }}}
class CoverWorker(Thread): # {{{
class CoverWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self)
@ -609,7 +607,8 @@ class CoverWorker(Thread): # {{{
def scan_once(self, tdir, seen):
for x in list(os.listdir(tdir)):
if x in seen: continue
if x in seen:
continue
if x.endswith('.cover') and os.path.exists(os.path.join(tdir,
x+'.done')):
name = x.rpartition('.')[0]
@ -635,7 +634,7 @@ class CoverWorker(Thread): # {{{
# }}}
class CoversModel(QAbstractListModel): # {{{
class CoversModel(QAbstractListModel): # {{{
def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent)
@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{
# }}}
class CoversView(QListView): # {{{
class CoversView(QListView): # {{{
chosen = pyqtSignal()
@ -793,6 +792,8 @@ class CoversView(QListView): # {{{
type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
def select(self, num):
current = self.model().index(num)
@ -814,9 +815,24 @@ class CoversView(QListView): # {{{
else:
self.select(self.m.index_from_pointer(pointer).row())
def show_context_menu(self, point):
idx = self.currentIndex()
if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject():
m = QMenu()
m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover)
m.exec_(QCursor.pos())
def show_cover(self):
idx = self.currentIndex()
pmap = self.model().cover_pixmap(idx)
if pmap is not None:
from calibre.gui2.viewer.image_popup import ImageView
d = ImageView(self, pmap, unicode(idx.data(Qt.DisplayRole).toString()), geom_name='metadata_download_cover_popup_geom')
d(use_exec=True)
# }}}
class CoversWidget(QWidget): # {{{
class CoversWidget(QWidget): # {{{
chosen = pyqtSignal()
finished = pyqtSignal()
@ -922,7 +938,7 @@ class CoversWidget(QWidget): # {{{
# }}}
class LogViewer(QDialog): # {{{
class LogViewer(QDialog): # {{{
def __init__(self, log, parent=None):
QDialog.__init__(self, parent)
@ -970,7 +986,7 @@ class LogViewer(QDialog): # {{{
# }}}
class FullFetch(QDialog): # {{{
class FullFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)
@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{
return self.exec_()
# }}}
class CoverFetch(QDialog): # {{{
class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent)

View File

@ -164,7 +164,7 @@ Author matching is exact.</string>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Ignore files with the following extensions when automatically adding </string>
<string>&lt;b&gt;Ignore&lt;/b&gt; files with the following extensions when automatically adding </string>
</property>
<property name="wordWrap">
<bool>true</bool>

View File

@ -129,7 +129,7 @@
<item row="6" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Max. OPDS &amp;ungrouped items:</string>
<string>Max. &amp;ungrouped items:</string>
</property>
<property name="buddy">
<cstring>opt_max_opds_ungrouped_items</cstring>

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
store_version = 1 # Needed for dynamic plugin loading
store_version = 2 # Needed for dynamic plugin loading
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
@ -25,25 +25,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class GoogleBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
aff_id = {
'lid': '41000000033185143',
'pubid': '21000000000352219',
'ganpub': 'k352219',
'ganclk': 'GOOG_1335334761',
}
# Use Kovid's affiliate id 30% of the time.
if random.randint(1, 10) in (1, 2, 3):
aff_id = {
'lid': '41000000031855266',
'pubid': '21000000000352583',
'ganpub': 'k352583',
'ganclk': 'GOOG_1335335464',
}
url = 'http://gan.doubleclick.net/gan_click?lid=%(lid)s&pubid=%(pubid)s' % aff_id
if detail_item:
detail_item += '&ganpub=%(ganpub)s&ganclk=%(ganclk)s' % aff_id
url = 'http://books.google.com/books'
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
else:

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
store_version = 1 # Needed for dynamic plugin loading
store_version = 2 # Needed for dynamic plugin loading
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
@ -31,10 +31,10 @@ class KoboStore(BasicStoreConfig, StorePlugin):
if random.randint(1, 10) in (1, 2, 3):
pub_id = '0dsO3kDu/AU'
murl = 'http://click.linksynergy.com/fs-bin/click?id=%s&offerid=268429.4&type=3&subid=0' % pub_id
murl = 'http://click.linksynergy.com/fs-bin/click?id=%s&subid=&offerid=280046.1&type=10&tmpid=9310&RD_PARM1=http%%3A%%2F%%2Fkobo.com' % pub_id
if detail_item:
purl = 'http://click.linksynergy.com/link?id=%s&offerid=268429&type=2&murl=%s' % (pub_id, urllib.quote_plus(detail_item))
purl = 'http://click.linksynergy.com/link?id=%s&offerid=280046&type=2&murl=%s' % (pub_id, urllib.quote_plus(detail_item))
url = purl
else:
purl = None

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
store_version = 1 # Needed for dynamic plugin loading
store_version = 2 # Needed for dynamic plugin loading
__license__ = 'GPL 3'
__copyright__ = '2012, John Schember <john@nachtimwald.com>'
@ -25,11 +25,19 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class NookUKStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url = "http://uk.nook.com"
url = 'http://www.awin1.com/awclick.php?mid=5266&id=120917'
detail_url = 'http://www.awin1.com/cread.php?awinmid=5266&awinaffid=120917&clickref=&p='
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
if detail_item:
url = detail_url + detail_item
open_url(QUrl(url_slash_cleaner(url)))
else:
if detail_item:
detail_url = detail_url + detail_item
else:
detail_url = None
d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', ''))

View File

@ -15,16 +15,17 @@ from calibre.gui2 import choose_save_file, gprefs
class ImageView(QDialog):
def __init__(self, parent, current_img, current_url):
def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'):
QDialog.__init__(self)
dw = QApplication.instance().desktop()
self.avail_geom = dw.availableGeometry(parent)
self.current_img = current_img
self.current_url = current_url
self.factor = 1.0
self.geom_name = geom_name
self.label = l = QLabel()
l.setBackgroundRole(QPalette.Base);
l.setBackgroundRole(QPalette.Base)
l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
l.setScaledContents(True)
@ -88,21 +89,27 @@ class ImageView(QDialog):
self.label.setPixmap(pm)
self.label.adjustSize()
def __call__(self):
def __call__(self, use_exec=False):
geom = self.avail_geom
self.label.setPixmap(self.current_img)
self.label.adjustSize()
self.resize(QSize(int(geom.width()/2.5), geom.height()-50))
geom = gprefs.get('viewer_image_popup_geometry', None)
geom = gprefs.get(self.geom_name, None)
if geom is not None:
self.restoreGeometry(geom)
self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1]
try:
self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1]
except AttributeError:
self.current_image_name = self.current_url
title = _('View Image: %s')%self.current_image_name
self.setWindowTitle(title)
self.show()
if use_exec:
self.exec_()
else:
self.show()
def done(self, e):
gprefs['viewer_image_popup_geometry'] = bytearray(self.saveGeometry())
gprefs[self.geom_name] = bytearray(self.saveGeometry())
return QDialog.done(self, e)
def wheelEvent(self, event):

View File

@ -493,7 +493,6 @@ class ResultCache(SearchQueryParser): # {{{
return matches
def get_keypair_matches(self, location, query, candidates):
print query
matches = set([])
if query.find(':') >= 0:
q = [q.strip() for q in query.split(':')]

View File

@ -0,0 +1,11 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
# Dummy file for backwards compatibility with older plugins
from calibre.utils.search_query_parser import ParseException # noqa