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 The full text of the GPL is distributed as in
/usr/share/common-licenses/GPL-2 on Debian systems. /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/* Files: src/calibre/utils/lzx/*
Copyright: Copyright (C) 2002, Matthew T. Russotto Copyright: Copyright (C) 2002, Matthew T. Russotto
Copyright: Copyright (C) 2008, Marshall T. Vandegrift <llasram@gmail.com> 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 The full text of the BSD license is distributed as in
/usr/share/common-licenses/BSD on Debian systems. /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/d3des.h:
Files: src/calibre/utils/msdes/des.c: Files: src/calibre/utils/msdes/des.c:
Copyright: Copyright (C) 1988,1989,1990,1991,1992, Richard Outerbridge 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` :guilabel:`Advanced Search Dialog`
.. _saved_searches:
Saving 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: .. _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 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 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 creating_plugins
typesetting_math typesetting_math
catalogs 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' __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 import datetime
@ -29,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe):
masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg' masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg'
LOGIN = 'https://registration.ft.com/registration/barrier/login' LOGIN = 'https://registration.ft.com/registration/barrier/login'
LOGIN2 = 'http://media.ft.com/h/subs3.html' 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' PREFIX = 'http://www.ft.com'
conversion_options = { conversion_options = {

View File

@ -1,20 +1,21 @@
__license__ = 'GPL v3' __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 import datetime
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import strftime from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from collections import OrderedDict
class FinancialTimes(BasicNewsRecipe): class FinancialTimes(BasicNewsRecipe):
title = 'Financial Times (US) printed edition' title = 'Financial Times (International) printed edition'
__author__ = 'Darko Miletic' __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." 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.' publisher = 'The Financial Times Ltd.'
category = 'news, finances, politics, UK, World' category = 'news, finances, politics, World'
oldest_article = 2 oldest_article = 2
language = 'en' language = 'en'
max_articles_per_feed = 250 max_articles_per_feed = 250
@ -28,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe):
masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg' masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg'
LOGIN = 'https://registration.ft.com/registration/barrier/login' LOGIN = 'https://registration.ft.com/registration/barrier/login'
LOGIN2 = 'http://media.ft.com/h/subs3.html' 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' PREFIX = 'http://www.ft.com'
conversion_options = { conversion_options = {
@ -105,29 +106,30 @@ class FinancialTimes(BasicNewsRecipe):
return articles return articles
def parse_index(self): def parse_index(self):
feeds = [] feeds = OrderedDict()
soup = self.index_to_soup(self.INDEX) soup = self.index_to_soup(self.INDEX)
dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div')) #dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div'))
self.timefmt = ' [%s]'%dates #self.timefmt = ' [%s]'%dates
wide = soup.find('div',attrs={'class':'wide'}) section_title = 'Untitled'
if not wide:
return feeds for column in soup.findAll('div', attrs = {'class':'feedBoxes clearfix'}):
allsections = wide.findAll(attrs={'class':lambda x: x and 'footwell' in x.split()}) for section in column. findAll('div', attrs = {'class':'feedBox'}):
if not allsections: sectiontitle=self.tag_to_string(section.find('h4'))
return feeds if '...' not in sectiontitle: section_title=sectiontitle
count = 0 for article in section.ul.findAll('li'):
for item in allsections: articles = []
count = count + 1 title=self.tag_to_string(article.a)
if self.test and count > 2: url=article.a['href']
return feeds articles.append({'title':title, 'url':url, 'description':'', 'date':''})
fitem = item.h3
if not fitem: if articles:
fitem = item.h4 if section_title not in feeds:
ftitle = self.tag_to_string(fitem) feeds[section_title] = []
self.report_progress(0, _('Fetching feed')+' %s...'%(ftitle)) feeds[section_title] += articles
feedarts = self.get_artlinks(item.ul)
feeds.append((ftitle,feedarts))
return feeds ans = [(key, val) for key, val in feeds.iteritems()]
return ans
def preprocess_html(self, soup): def preprocess_html(self, soup):
items = ['promo-box','promo-title', items = ['promo-box','promo-title',
@ -177,6 +179,3 @@ class FinancialTimes(BasicNewsRecipe):
tfile.close() tfile.close()
self.temp_files.append(tfile) self.temp_files.append(tfile)
return tfile.name 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' title = u'Lightspeed Magazine'
language = 'en' language = 'en'
__author__ = 'Jose Pinto' __author__ = 'Jose Pinto'
oldest_article = 7 oldest_article = 31
max_articles_per_feed = 100 max_articles_per_feed = 100
auto_cleanup = True auto_cleanup = True
use_embedded_content = False use_embedded_content = False

View File

@ -36,6 +36,9 @@ from BeautifulSoup import BeautifulSoup
Changed order of regex to speedup proces Changed order of regex to speedup proces
Version 1.9.3 23-05-2012 Version 1.9.3 23-05-2012
Updated Cover image 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): class AdvancedUserRecipe1306097511(BasicNewsRecipe):
@ -43,7 +46,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
oldest_article = 1.2 oldest_article = 1.2
max_articles_per_feed = 25 max_articles_per_feed = 25
__author__ = u'DrMerry' __author__ = u'DrMerry'
description = u'Metro Nederland' description = u'Metro Nederland v1.9.4 2013-04-19'
language = u'nl' language = u'nl'
simultaneous_downloads = 5 simultaneous_downloads = 5
masthead_url = 'http://blog.metronieuws.nl/wp-content/themes/metro/images/header.gif' 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') #(re.compile('(</?)h2', re.DOTALL|re.IGNORECASE),lambda match:'\1em')
] ]
remove_tags_before= dict(id='date') remove_tags_before= dict(id='subwrapper')
remove_tags_after = [dict(name='div', attrs={'class':['column-1-3','gallery-text']})]#id='share-and-byline')] 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 = [ remove_tags = [
dict(name=['iframe','script','noscript','style']), 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(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=['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(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='a', attrs={'name':'comments'}),
#dict(name='div', attrs={'data-href'}), #dict(name='div', attrs={'data-href'}),
dict(name='img', attrs={'class':'top-line','title':'volledig scherm'}), 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: if cache.get(y, 0) == mtime:
continue continue
if (f.endswith('.py') and f not in ( 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): 'prs500/driver.py' not in y):
yield y, mtime yield y, mtime
if f.endswith('.coffee'): if f.endswith('.coffee'):

View File

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

View File

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

View File

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

View File

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

View File

@ -114,6 +114,19 @@ class Cache(object):
if self.dirtied_cache: if self.dirtied_cache:
self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1 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 @property
def field_metadata(self): def field_metadata(self):
return self.backend.field_metadata return self.backend.field_metadata

View File

@ -6,12 +6,13 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os import os, traceback
from functools import partial from functools import partial
from calibre.db.backend import DB from calibre.db.backend import DB
from calibre.db.cache import Cache from calibre.db.cache import Cache
from calibre.db.view import View from calibre.db.view import View
from calibre.utils.date import utcnow
class LibraryDatabase(object): class LibraryDatabase(object):
@ -29,6 +30,7 @@ class LibraryDatabase(object):
progress_callback=lambda x, y:True, restore_all_prefs=False): progress_callback=lambda x, y:True, restore_all_prefs=False):
self.is_second_db = is_second_db # TODO: Use is_second_db 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, backend = self.backend = DB(library_path, default_prefs=default_prefs,
read_only=read_only, restore_all_prefs=restore_all_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, setattr(self, prop, partial(self.get_property,
loc=self.FIELD_MAP[fm])) loc=self.FIELD_MAP[fm]))
self.last_update_check = self.last_modified()
def close(self): def close(self):
self.backend.close() self.backend.close()
@ -71,9 +75,22 @@ class LibraryDatabase(object):
def library_id(self): def library_id(self):
return self.backend.library_id 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): def last_modified(self):
return self.backend.last_modified() 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 @property
def custom_column_num_map(self): def custom_column_num_map(self):
return self.backend.custom_column_num_map return self.backend.custom_column_num_map
@ -86,9 +103,48 @@ class LibraryDatabase(object):
def FIELD_MAP(self): def FIELD_MAP(self):
return self.backend.FIELD_MAP 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): def all_ids(self):
for book_id in self.data.cache.all_book_ids(): for book_id in self.data.cache.all_book_ids():
yield book_id 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' 'Test library wide properties'
def get_props(db): def get_props(db):
props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', 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', ) fprops = ('last_modified', )
ans = {x:getattr(db, x) for x in props} ans = {x:getattr(db, x) for x in props}
ans.update({x:getattr(db, x)() for x in fprops}) ans.update({x:getattr(db, x)() for x in fprops})
@ -51,6 +51,11 @@ class LegacyTest(BaseTest):
if label in {'tags', 'formats'}: if label in {'tags', 'formats'}:
# Order is random in the old db for these # Order is random in the old db for these
ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label]) 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 return ans
old = self.init_old() 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, self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode,
id_dict.itervalues()))) 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], 0x42f7 : [0x216],
0x4365 : [0x216], 0x4365 : [0x216],
0x4366 : [0x216], 0x4366 : [0x216],
0x4371 : [0x216],
}, },
# Freescale # Freescale
0x15a2 : { 0x15a2 : {
@ -239,7 +240,7 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E', 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS', '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', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_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', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E', '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' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -35,11 +35,11 @@ class KOBO(USBMS):
gui_name = 'Kobo Reader' gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader') description = _('Communicate with the Kobo Reader')
author = 'Timothy Legge and David Forrester' author = 'Timothy Legge and David Forrester'
version = (2, 0, 7) version = (2, 0, 8)
dbversion = 0 dbversion = 0
fwversion = 0 fwversion = 0
supported_dbversion = 75 supported_dbversion = 80
has_kepubs = False has_kepubs = False
supported_platforms = ['windows', 'osx', 'linux'] 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 # If all this succeeds we need to delete the images files via the ImageID
return ImageID return ImageID
def delete_images(self, ImageID): def delete_images(self, ImageID, book_path):
if ImageID != None: if ImageID != None:
path_prefix = '.kobo/images/' path_prefix = '.kobo/images/'
path = self._main_prefix + path_prefix + ImageID path = self._main_prefix + path_prefix + ImageID
@ -449,7 +449,7 @@ class KOBO(USBMS):
ImageID = self.delete_via_sql(ContentID, ContentType) ImageID = self.delete_via_sql(ContentID, ContentType)
#print " We would now delete the Images for" + ImageID #print " We would now delete the Images for" + ImageID
self.delete_images(ImageID) self.delete_images(ImageID, path)
if os.path.exists(path): if os.path.exists(path):
# Delete the ebook # Delete the ebook
@ -1199,15 +1199,21 @@ class KOBO(USBMS):
class KOBOTOUCH(KOBO): class KOBOTOUCH(KOBO):
name = 'KoboTouch' name = 'KoboTouch'
gui_name = 'Kobo Touch' gui_name = 'Kobo Touch/Glo/Mini/Aura HD'
author = 'David Forrester' 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') # icon = I('devices/kobotouch.jpg')
supported_dbversion = 75 supported_dbversion = 80
min_supported_dbversion = 53 min_supported_dbversion = 53
min_dbversion_series = 65 min_dbversion_series = 65
min_dbversion_archive = 71 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 booklist_class = KTCollectionsBookList
book_class = Book book_class = Book
@ -1291,12 +1297,13 @@ class KOBOTOUCH(KOBO):
TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ"
GLO_PRODUCT_ID = [0x4173] AURA_HD_PRODUCT_ID = [0x4193]
MINI_PRODUCT_ID = [0x4183] GLO_PRODUCT_ID = [0x4173]
TOUCH_PRODUCT_ID = [0x4163] MINI_PRODUCT_ID = [0x4183]
PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID 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, # Image file name endings. Made up of: image size, min_dbversion, max_dbversion,
COVER_FILE_ENDINGS = { COVER_FILE_ENDINGS = {
@ -1313,6 +1320,11 @@ class KOBOTOUCH(KOBO):
# ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,], # ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,],
# ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,], # ' - 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 #Following are the sizes used with pre2.1.4 firmware
# COVER_FILE_ENDINGS = { # COVER_FILE_ENDINGS = {
# ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen # ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen
@ -1328,6 +1340,10 @@ class KOBOTOUCH(KOBO):
super(KOBOTOUCH, self).initialize() super(KOBOTOUCH, self).initialize()
self.bookshelvelist = [] 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): def books(self, oncard=None, end_session=True):
debug_print("KoboTouch:books - oncard='%s'"%oncard) debug_print("KoboTouch:books - oncard='%s'"%oncard)
from calibre.ebooks.metadata.meta import path_to_ext from calibre.ebooks.metadata.meta import path_to_ext
@ -1354,14 +1370,13 @@ class KOBOTOUCH(KOBO):
# Determine the firmware version # Determine the firmware version
try: try:
with open(self.normalize_path(self._main_prefix + '.kobo/version'), with open(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f:
'rb') as f:
self.fwversion = f.readline().split(',')[2] self.fwversion = f.readline().split(',')[2]
self.fwversion = tuple((int(x) for x in self.fwversion.split('.')))
except: except:
self.fwversion = 'unknown' self.fwversion = (0,0,0)
if self.fwversion != '1.0' and self.fwversion != '1.4': debug_print('Kobo device: %s' % self.gui_name)
self.has_kepubs = True
debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs) debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs)
debug_print('Version of firmware:', self.fwversion, '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) debug_print(opts.extra_customization)
if opts.extra_customization: if opts.extra_customization:
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] 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) bl.set_debugging_title(debugging_title)
debug_print("KoboTouch:books - length bl=%d"%len(bl)) debug_print("KoboTouch:books - length bl=%d"%len(bl))
need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE)
@ -1466,6 +1481,7 @@ class KOBOTOUCH(KOBO):
if show_debug: if show_debug:
self.debug_index = idx self.debug_index = idx
debug_print("KoboTouch:update_booklist - idx=%d"%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 - bl[idx].device_collections=', bl[idx].device_collections)
debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map) debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map)
debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves) debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves)
@ -1477,7 +1493,7 @@ class KOBOTOUCH(KOBO):
bl_cache[lpath] = None bl_cache[lpath] = None
if ImageID is not None: if ImageID is not None:
imagename = self.imagefilename_from_imageID(ImageID) imagename = self.imagefilename_from_imageID(prefix, ImageID)
if imagename is not None: if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename) bl[idx].thumbnail = ImageWrapper(imagename)
if (ContentType == '6' and MimeType != 'application/x-kobo-epub+zip'): 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) debug_print("KoboTouch:books - end - oncard='%s'"%oncard)
return bl return bl
def imagefilename_from_imageID(self, ImageID): def imagefilename_from_imageID(self, prefix, ImageID):
show_debug = self.is_debugging_title(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(): for ending, cover_options in self.cover_file_endings().items():
fpath = self._main_prefix + '.kobo/images/' + ImageID + ending fpath = path + ImageID + ending
fpath = self.normalize_path(fpath.replace('/', os.sep))
if os.path.exists(fpath): if os.path.exists(fpath):
if show_debug: if show_debug:
debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath)) debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath))
@ -1764,7 +1782,7 @@ class KOBOTOUCH(KOBO):
if not self.copying_covers(): if not self.copying_covers():
imageID = self.imageid_from_contentid(contentID) imageID = self.imageid_from_contentid(contentID)
self.delete_images(imageID) self.delete_images(imageID, fname)
connection.commit() connection.commit()
cursor.close() cursor.close()
@ -1821,11 +1839,11 @@ class KOBOTOUCH(KOBO):
return imageId return imageId
def delete_images(self, ImageID): def delete_images(self, ImageID, book_path):
debug_print("KoboTouch:delete_images - ImageID=", ImageID) debug_print("KoboTouch:delete_images - ImageID=", ImageID)
if ImageID != None: if ImageID != None:
path_prefix = '.kobo/images/' path = self.images_path(book_path)
path = self._main_prefix + path_prefix + ImageID path = path + ImageID
for ending in self.cover_file_endings().keys(): for ending in self.cover_file_endings().keys():
fpath = path + ending fpath = path + ending
@ -1872,12 +1890,14 @@ class KOBOTOUCH(KOBO):
def get_content_type_from_extension(self, extension): def get_content_type_from_extension(self, extension):
debug_print("KoboTouch:get_content_type_from_extension - start") debug_print("KoboTouch:get_content_type_from_extension - start")
# With new firmware, ContentType appears to be 6 for all types of sideloaded books. # 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") debug_print("KoboTouch:get_content_type_from_extension - V2 firmware")
ContentType = 6 ContentType = 6
# For older firmware, it depends on the type of file.
elif extension == '.kobo' or extension == '.mobi':
ContentType = 6
else: else:
debug_print("KoboTouch:get_content_type_from_extension - calling super") ContentType = 901
ContentType = super(KOBOTOUCH, self).get_content_type_from_extension(extension)
return ContentType return ContentType
def update_device_database_collections(self, booklists, collections_attributes, oncard): 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() 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() update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series()
debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] 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) booklists.set_debugging_title(debugging_title)
else: else:
delete_empty_shelves = False delete_empty_shelves = False
@ -2088,8 +2108,8 @@ class KOBOTOUCH(KOBO):
# debug_print('KoboTouch: not uploading cover') # debug_print('KoboTouch: not uploading cover')
return return
# Don't upload covers if book is on the SD card # Only upload covers to SD card if that is supported
if self._card_a_prefix and path.startswith(self._card_a_prefix): if self._card_a_prefix and path.startswith(self._card_a_prefix) and not self.supports_covers_on_sdcard():
return return
if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]: if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]:
@ -2111,6 +2131,16 @@ class KOBOTOUCH(KOBO):
ImageID = ImageID.replace('.', '_') ImageID = ImageID.replace('.', '_')
return ImageID 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): 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 from calibre.utils.magick.draw import save_cover_data_to, identify_data
debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale)) debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale))
@ -2151,8 +2181,8 @@ class KOBOTOUCH(KOBO):
cursor.close() cursor.close()
if ImageID != None: if ImageID != None:
path_prefix = '.kobo/images/' path = self.images_path(path) + ImageID
path = self._main_prefix + path_prefix + ImageID
if show_debug: if show_debug:
debug_print("KoboTouch:_upload_cover - About to loop over cover endings") debug_print("KoboTouch:_upload_cover - About to loop over cover endings")
@ -2496,6 +2526,8 @@ class KOBOTOUCH(KOBO):
return opts return opts
def isAuraHD(self):
return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID
def isGlo(self): def isGlo(self):
return self.detected_device.idProduct in self.GLO_PRODUCT_ID return self.detected_device.idProduct in self.GLO_PRODUCT_ID
def isMini(self): def isMini(self):
@ -2504,7 +2536,21 @@ class KOBOTOUCH(KOBO):
return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID
def cover_file_endings(self): 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): def copying_covers(self):
opts = self.settings() opts = self.settings()
@ -2524,6 +2570,44 @@ class KOBOTOUCH(KOBO):
def supports_kobo_archive(self): def supports_kobo_archive(self):
return self.dbversion >= self.min_dbversion_archive 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 @classmethod
def is_debugging_title(cls, title): def is_debugging_title(cls, title):

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
''' '''
Basic support for manipulating OEB 1.x/2.0 content and metadata. Basic support for manipulating OEB 1.x/2.0 content and metadata.
''' '''
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>' __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
@ -11,7 +10,7 @@ import os, re, uuid, logging
from collections import defaultdict from collections import defaultdict
from itertools import count from itertools import count
from urlparse import urldefrag, urlparse, urlunparse, urljoin from urlparse import urldefrag, urlparse, urlunparse, urljoin
from urllib import unquote as urlunquote from urllib import unquote
from lxml import etree, html from lxml import etree, html
from calibre.constants import filesystem_encoding, __version__ 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' RE_NS = 'http://exslt.org/regular-expressions'
MBP_NS = 'http://www.mobipocket.com' MBP_NS = 'http://www.mobipocket.com'
XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS, XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS, 'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS,
'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS, 'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS,
'mbp': MBP_NS, 'calibre': CALIBRE_NS } 'mbp': MBP_NS, 'calibre': CALIBRE_NS}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_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: if attr in link_attrs:
yield (el, attr, attribs[attr], 0) yield (el, attr, attribs[attr], 0)
if not find_links_in_css: if not find_links_in_css:
continue continue
if tag == XHTML('style') and el.text: 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] URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE]
def urlquote(href): 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 = [] result = []
unsafe = 0 if isinstance(href, unicode) else 1 unsafe = 0 if isinstance(href, unicode) else 1
unsafe = URL_UNSAFE[unsafe] unsafe = URL_UNSAFE[unsafe]
@ -373,6 +373,19 @@ def urlquote(href):
result.append(char) result.append(char)
return ''.join(result) 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): def urlnormalize(href):
"""Convert a URL into normalized form, with all and only URL-unsafe """Convert a URL into normalized form, with all and only URL-unsafe
characters URL quoted. characters URL quoted.
@ -469,7 +482,7 @@ class DirContainer(object):
return return
def _unquote(self, path): 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 # 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') # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8')
# and the latter is correct # and the latter is correct
@ -497,7 +510,7 @@ class DirContainer(object):
return False return False
try: try:
path = os.path.join(self.rootdir, self._unquote(path)) 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 return False
try: try:
return os.path.isfile(path) return os.path.isfile(path)
@ -577,12 +590,13 @@ class Metadata(object):
allowed = self.allowed allowed = self.allowed
if allowed is not None and term not in allowed: if allowed is not None and term not in allowed:
raise AttributeError( raise AttributeError(
'attribute %r not valid for metadata term %r' \ 'attribute %r not valid for metadata term %r'
% (self.attr(term), barename(obj.term))) % (self.attr(term), barename(obj.term)))
return self.attr(term) return self.attr(term)
def __get__(self, obj, cls): 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), '') return obj.attrib.get(self.term_attr(obj), '')
def __set__(self, obj, value): def __set__(self, obj, value):
@ -628,8 +642,8 @@ class Metadata(object):
self.value = value self.value = value
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
scheme = Attribute(lambda term: 'scheme' if \ scheme = Attribute(lambda term: 'scheme' if
term == OPF('meta') else OPF('scheme'), term == OPF('meta') else OPF('scheme'),
[DC('identifier'), OPF('meta')]) [DC('identifier'), OPF('meta')])
file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'), file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'),
DC('title')]) DC('title')])
@ -882,7 +896,6 @@ class Manifest(object):
return self._parse_xhtml(convert_markdown(data, title=title)) return self._parse_xhtml(convert_markdown(data, title=title))
def _parse_css(self, data): def _parse_css(self, data):
from cssutils import CSSParser, log, resolveImports from cssutils import CSSParser, log, resolveImports
log.setLevel(logging.WARN) log.setLevel(logging.WARN)
@ -935,7 +948,7 @@ class Manifest(object):
data = self._loader(getattr(self, 'html_input_href', data = self._loader(getattr(self, 'html_input_href',
self.href)) self.href))
if not isinstance(data, basestring): if not isinstance(data, basestring):
pass # already parsed pass # already parsed
elif self.media_type.lower() in OEB_DOCS: elif self.media_type.lower() in OEB_DOCS:
data = self._parse_xhtml(data) data = self._parse_xhtml(data)
elif self.media_type.lower()[-4:] in ('+xml', '/xml'): elif self.media_type.lower()[-4:] in ('+xml', '/xml'):
@ -1022,7 +1035,8 @@ class Manifest(object):
target, frag = urldefrag(href) target, frag = urldefrag(href)
target = target.split('/') target = target.split('/')
for index in xrange(min(len(base), len(target))): for index in xrange(min(len(base), len(target))):
if base[index] != target[index]: break if base[index] != target[index]:
break
else: else:
index += 1 index += 1
relhref = (['..'] * (len(base) - index)) + target[index:] 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 { return media_type and media_type.lower() in {
'image/png', 'image/jpeg', 'image/jpg', 'image/gif'} 'image/png', 'image/jpeg', 'image/jpg', 'image/gif'}
COVER_TYPES = { 'coverimagestandard', 'other.ms-coverimage-standard', COVER_TYPES = {
'other.ms-titleimage-standard', 'other.ms-titleimage', 'coverimagestandard', 'other.ms-coverimage-standard',
'other.ms-coverimage', 'other.ms-thumbimage-standard', 'other.ms-titleimage-standard', 'other.ms-titleimage',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'} 'other.ms-coverimage', 'other.ms-thumbimage-standard',
'other.ms-thumbimage', 'thumbimagestandard', 'cover'}
def find_cover_image(container): def find_cover_image(container):
'Find a raster image marked as a cover in the OPF' '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): def find_cover_image_in_page(container, cover_page):
root = container.parsed(cover_page) root = container.parsed(cover_page)
body = XPath('//h:body')(root) body = XPath('//h:body')(root)
if len(body) != 1: return if len(body) != 1:
return
body = body[0] body = body[0]
images = [] images = []
for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body): 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' ar = 'xMidYMid meet' if keep_aspect else 'none'
templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar) templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar)
templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height)) 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)) templ = templ.replace('__height__', str(height))
titlepage_item = container.generate_item('titlepage.xhtml', titlepage_item = container.generate_item('titlepage.xhtml',
id_prefix='titlepage') id_prefix='titlepage')
@ -179,7 +181,7 @@ def create_epub_cover(container, cover_path):
guide = container.opf_get_or_create('guide') guide = container.opf_get_or_create('guide')
container.insert_into_xml(guide, guide.makeelement( container.insert_into_xml(guide, guide.makeelement(
OPF('reference'), type='cover', title=_('Cover'), 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') metadata = container.opf_get_or_create('metadata')
meta = metadata.makeelement(OPF('meta'), name='cover') meta = metadata.makeelement(OPF('meta'), name='cover')
meta.set('content', raster_cover_item.get('id')) meta.set('content', raster_cover_item.get('id'))

View File

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

View File

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

View File

@ -51,7 +51,7 @@ class Links(object):
for link in self.links: for link in self.links:
path, href, frag = link[0] path, href, frag = link[0]
page, rect = link[1:] 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 is_local = not href or combined_path in self.anchors
annot = Dictionary({ annot = Dictionary({
'Type':Name('Annot'), 'Subtype':Name('Link'), 'Type':Name('Annot'), 'Subtype':Name('Link'),

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ from PyQt4.Qt import (
QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget, QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget,
QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal,
QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex, QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex,
QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel) QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins 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 from calibre.ptempfile import TemporaryDirectory
# }}} # }}}
class RichTextDelegate(QStyledItemDelegate): # {{{ class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None, max_width=160): def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
@ -77,7 +77,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
painter.restore() painter.restore()
# }}} # }}}
class CoverDelegate(QStyledItemDelegate): # {{{ class CoverDelegate(QStyledItemDelegate): # {{{
needs_redraw = pyqtSignal() needs_redraw = pyqtSignal()
@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{
# }}} # }}}
class ResultsModel(QAbstractTableModel): # {{{ class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = ( COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary') '#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{
p = book.publisher if book.publisher else '' p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p) return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role): def data(self, index, role):
row, col = index.row(), index.column() row, col = index.row(), index.column()
try: try:
@ -233,7 +232,7 @@ class ResultsModel(QAbstractTableModel): # {{{
# }}} # }}}
class ResultsView(QTableView): # {{{ class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object) show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object) book_selected = pyqtSignal(object)
@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{
# }}} # }}}
class Comments(QWebView): # {{{ class Comments(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWebView.__init__(self, parent) QWebView.__init__(self, parent)
@ -384,7 +383,7 @@ class Comments(QWebView): # {{{
return QSize(800, 300) return QSize(800, 300)
# }}} # }}}
class IdentifyWorker(Thread): # {{{ class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches): def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self) Thread.__init__(self)
@ -441,7 +440,7 @@ class IdentifyWorker(Thread): # {{{
# }}} # }}}
class IdentifyWidget(QWidget): # {{{ class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal() rejected = pyqtSignal()
results_found = pyqtSignal() results_found = pyqtSignal()
@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{
self.results_view.show_results(self.worker.results) self.results_view.show_results(self.worker.results)
self.results_found.emit() self.results_found.emit()
def cancel(self): def cancel(self):
self.abort.set() self.abort.set()
# }}} # }}}
class CoverWorker(Thread): # {{{ class CoverWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers, caches): def __init__(self, log, abort, title, authors, identifiers, caches):
Thread.__init__(self) Thread.__init__(self)
@ -609,7 +607,8 @@ class CoverWorker(Thread): # {{{
def scan_once(self, tdir, seen): def scan_once(self, tdir, seen):
for x in list(os.listdir(tdir)): 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, if x.endswith('.cover') and os.path.exists(os.path.join(tdir,
x+'.done')): x+'.done')):
name = x.rpartition('.')[0] name = x.rpartition('.')[0]
@ -635,7 +634,7 @@ class CoverWorker(Thread): # {{{
# }}} # }}}
class CoversModel(QAbstractListModel): # {{{ class CoversModel(QAbstractListModel): # {{{
def __init__(self, current_cover, parent=None): def __init__(self, current_cover, parent=None):
QAbstractListModel.__init__(self, parent) QAbstractListModel.__init__(self, parent)
@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{
# }}} # }}}
class CoversView(QListView): # {{{ class CoversView(QListView): # {{{
chosen = pyqtSignal() chosen = pyqtSignal()
@ -793,6 +792,8 @@ class CoversView(QListView): # {{{
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.doubleClicked.connect(self.chosen, 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): def select(self, num):
current = self.model().index(num) current = self.model().index(num)
@ -814,9 +815,24 @@ class CoversView(QListView): # {{{
else: else:
self.select(self.m.index_from_pointer(pointer).row()) 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() chosen = pyqtSignal()
finished = pyqtSignal() finished = pyqtSignal()
@ -922,7 +938,7 @@ class CoversWidget(QWidget): # {{{
# }}} # }}}
class LogViewer(QDialog): # {{{ class LogViewer(QDialog): # {{{
def __init__(self, log, parent=None): def __init__(self, log, parent=None):
QDialog.__init__(self, parent) 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): def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{
return self.exec_() return self.exec_()
# }}} # }}}
class CoverFetch(QDialog): # {{{ class CoverFetch(QDialog): # {{{
def __init__(self, current_cover=None, parent=None): def __init__(self, current_cover=None, parent=None):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)

View File

@ -164,7 +164,7 @@ Author matching is exact.</string>
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <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>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>

View File

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

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function) 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' __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>' __copyright__ = '2011, John Schember <john@nachtimwald.com>'
@ -25,25 +25,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class GoogleBooksStore(BasicStoreConfig, StorePlugin): class GoogleBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): def open(self, parent=None, detail_item=None, external=False):
aff_id = { url = 'http://books.google.com/books'
'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
if external or self.config.get('open_external', False): if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
else: else:

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function) 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' __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>' __copyright__ = '2011, John Schember <john@nachtimwald.com>'
@ -31,10 +31,10 @@ class KoboStore(BasicStoreConfig, StorePlugin):
if random.randint(1, 10) in (1, 2, 3): if random.randint(1, 10) in (1, 2, 3):
pub_id = '0dsO3kDu/AU' 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: 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 url = purl
else: else:
purl = None purl = None

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function) 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' __license__ = 'GPL 3'
__copyright__ = '2012, John Schember <john@nachtimwald.com>' __copyright__ = '2012, John Schember <john@nachtimwald.com>'
@ -25,11 +25,19 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class NookUKStore(BasicStoreConfig, StorePlugin): class NookUKStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False): 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): 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: else:
if detail_item:
detail_url = detail_url + detail_item
else:
detail_url = None
d = WebStoreDialog(self.gui, url, parent, detail_item) d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name) d.setWindowTitle(self.name)
d.set_tags(self.config.get('tags', '')) d.set_tags(self.config.get('tags', ''))

View File

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

View File

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