diff --git a/COPYRIGHT b/COPYRIGHT index 85d70a8aa8..5644a52f69 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -79,13 +79,6 @@ License: GPL2+ The full text of the GPL is distributed as in /usr/share/common-licenses/GPL-2 on Debian systems. -Files: src/pyPdf/* -Copyright: Copyright (c) 2006, Mathieu Fenniak -Copyright: Copyright (c) 2007, Ashish Kulkarni -License: BSD - The full text of the BSD license is distributed as in - /usr/share/common-licenses/BSD on Debian systems. - Files: src/calibre/utils/lzx/* Copyright: Copyright (C) 2002, Matthew T. Russotto Copyright: Copyright (C) 2008, Marshall T. Vandegrift @@ -100,49 +93,6 @@ License: BSD The full text of the BSD license is distributed as in /usr/share/common-licenses/BSD on Debian systems. -Files: src/calibre/utils/pyparsing.py -Copyright: Copyright (c) 2003-2008, Paul T. McGuire -License: MIT - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Files: src/calibre/utils/PythonMagickWand.py -Copyright: (c) 2007 - Achim Domma - domma@procoders.net -License: MIT - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - Files: src/calibre/utils/msdes/d3des.h: Files: src/calibre/utils/msdes/des.c: Copyright: Copyright (C) 1988,1989,1990,1991,1992, Richard Outerbridge diff --git a/manual/gui.rst b/manual/gui.rst index e7d8a4f616..1bcf53747c 100755 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -426,6 +426,8 @@ Identifiers (e.g., isbn, doi, lccn etc) also use an extended syntax. First, note :guilabel:`Advanced Search Dialog` +.. _saved_searches: + Saving searches ----------------- @@ -435,6 +437,15 @@ Now you can access your saved search in the Tag Browser under "Searches". A sing .. _config_filename_metadata: +Virtual Libraries +------------------- + +A :guilabel:`Virtual Library` is a way to pretend that your |app| library has +only a few books instead of its full collection. This is an excellent way to +partition your large collection of books into smaller, manageable chunks. To +learn how to create and use virtual libraries, see the tutorial: +:ref:`virtual_libraries`. + Guessing metadata from file names ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In the :guilabel:`Add/Save` section of the configuration dialog, you can specify a regular expression that |app| will use to try and guess metadata from the names of ebook files diff --git a/manual/images/virtual_library_button.png b/manual/images/virtual_library_button.png new file mode 100644 index 0000000000..f78f381a33 Binary files /dev/null and b/manual/images/virtual_library_button.png differ diff --git a/manual/images/vl_by_author.png b/manual/images/vl_by_author.png new file mode 100644 index 0000000000..bf4db61d3c Binary files /dev/null and b/manual/images/vl_by_author.png differ diff --git a/manual/tutorials.rst b/manual/tutorials.rst index ed4287abe8..8668d3b594 100755 --- a/manual/tutorials.rst +++ b/manual/tutorials.rst @@ -20,4 +20,5 @@ Here you will find tutorials to get you started using |app|'s more advanced feat creating_plugins typesetting_math catalogs + virtual_libraries diff --git a/manual/virtual_libraries.rst b/manual/virtual_libraries.rst new file mode 100644 index 0000000000..66448ac680 --- /dev/null +++ b/manual/virtual_libraries.rst @@ -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:``. + +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`. + diff --git a/recipes/financial_times_uk.recipe b/recipes/financial_times_uk.recipe index 8105a9777f..6aa926a076 100644 --- a/recipes/financial_times_uk.recipe +++ b/recipes/financial_times_uk.recipe @@ -1,7 +1,7 @@ __license__ = 'GPL v3' -__copyright__ = '2010-2012, Darko Miletic ' +__copyright__ = '2010-2013, Darko Miletic ' ''' -www.ft.com/uk-edition +www.ft.com/intl/uk-edition ''' import datetime @@ -29,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe): masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg' LOGIN = 'https://registration.ft.com/registration/barrier/login' LOGIN2 = 'http://media.ft.com/h/subs3.html' - INDEX = 'http://www.ft.com/uk-edition' + INDEX = 'http://www.ft.com/intl/uk-edition' PREFIX = 'http://www.ft.com' conversion_options = { diff --git a/recipes/financial_times_us.recipe b/recipes/financial_times_us.recipe index 3821e5ea0e..7d8eed92f9 100644 --- a/recipes/financial_times_us.recipe +++ b/recipes/financial_times_us.recipe @@ -1,20 +1,21 @@ __license__ = 'GPL v3' -__copyright__ = '2013, Darko Miletic ' +__copyright__ = '2010-2013, Darko Miletic ' ''' -http://www.ft.com/intl/us-edition +www.ft.com/intl/international-edition ''' import datetime from calibre.ptempfile import PersistentTemporaryFile from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe +from collections import OrderedDict class FinancialTimes(BasicNewsRecipe): - title = 'Financial Times (US) printed edition' + title = 'Financial Times (International) printed edition' __author__ = 'Darko Miletic' description = "The Financial Times (FT) is one of the world's leading business news and information organisations, recognised internationally for its authority, integrity and accuracy." publisher = 'The Financial Times Ltd.' - category = 'news, finances, politics, UK, World' + category = 'news, finances, politics, World' oldest_article = 2 language = 'en' max_articles_per_feed = 250 @@ -28,7 +29,7 @@ class FinancialTimes(BasicNewsRecipe): masthead_url = 'http://im.media.ft.com/m/img/masthead_main.jpg' LOGIN = 'https://registration.ft.com/registration/barrier/login' LOGIN2 = 'http://media.ft.com/h/subs3.html' - INDEX = 'http://www.ft.com/intl/us-edition' + INDEX = 'http://www.ft.com/intl/international-edition' PREFIX = 'http://www.ft.com' conversion_options = { @@ -93,7 +94,7 @@ class FinancialTimes(BasicNewsRecipe): try: urlverified = self.browser.open_novisit(url).geturl() # resolve redirect. except: - continue + continue title = self.tag_to_string(item) date = strftime(self.timefmt) articles.append({ @@ -105,29 +106,30 @@ class FinancialTimes(BasicNewsRecipe): return articles def parse_index(self): - feeds = [] + feeds = OrderedDict() soup = self.index_to_soup(self.INDEX) - dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div')) - self.timefmt = ' [%s]'%dates - wide = soup.find('div',attrs={'class':'wide'}) - if not wide: - return feeds - allsections = wide.findAll(attrs={'class':lambda x: x and 'footwell' in x.split()}) - if not allsections: - return feeds - count = 0 - for item in allsections: - count = count + 1 - if self.test and count > 2: - return feeds - fitem = item.h3 - if not fitem: - fitem = item.h4 - ftitle = self.tag_to_string(fitem) - self.report_progress(0, _('Fetching feed')+' %s...'%(ftitle)) - feedarts = self.get_artlinks(item.ul) - feeds.append((ftitle,feedarts)) - return feeds + #dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div')) + #self.timefmt = ' [%s]'%dates + section_title = 'Untitled' + + for column in soup.findAll('div', attrs = {'class':'feedBoxes clearfix'}): + for section in column. findAll('div', attrs = {'class':'feedBox'}): + sectiontitle=self.tag_to_string(section.find('h4')) + if '...' not in sectiontitle: section_title=sectiontitle + for article in section.ul.findAll('li'): + articles = [] + title=self.tag_to_string(article.a) + url=article.a['href'] + articles.append({'title':title, 'url':url, 'description':'', 'date':''}) + + if articles: + if section_title not in feeds: + feeds[section_title] = [] + feeds[section_title] += articles + + + ans = [(key, val) for key, val in feeds.iteritems()] + return ans def preprocess_html(self, soup): items = ['promo-box','promo-title', @@ -174,9 +176,6 @@ class FinancialTimes(BasicNewsRecipe): count += 1 tfile = PersistentTemporaryFile('_fa.html') tfile.write(html) - tfile.close() + tfile.close() self.temp_files.append(tfile) return tfile.name - - def cleanup(self): - self.browser.open('https://registration.ft.com/registration/login/logout?location=') \ No newline at end of file diff --git a/recipes/lightspeed_magazine.recipe b/recipes/lightspeed_magazine.recipe index 9bb5ec913f..ab20f81387 100644 --- a/recipes/lightspeed_magazine.recipe +++ b/recipes/lightspeed_magazine.recipe @@ -4,7 +4,7 @@ class AdvancedUserRecipe1366025923(BasicNewsRecipe): title = u'Lightspeed Magazine' language = 'en' __author__ = 'Jose Pinto' - oldest_article = 7 + oldest_article = 31 max_articles_per_feed = 100 auto_cleanup = True use_embedded_content = False diff --git a/recipes/metro_news_nl.recipe b/recipes/metro_news_nl.recipe index 81a184b7dc..0995719939 100644 --- a/recipes/metro_news_nl.recipe +++ b/recipes/metro_news_nl.recipe @@ -36,6 +36,9 @@ from BeautifulSoup import BeautifulSoup Changed order of regex to speedup proces Version 1.9.3 23-05-2012 Updated Cover image + Version 1.9.4 19-04-2013 + Added regex filter for mailto + Updated for new layout of metro-site ''' class AdvancedUserRecipe1306097511(BasicNewsRecipe): @@ -43,7 +46,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): oldest_article = 1.2 max_articles_per_feed = 25 __author__ = u'DrMerry' - description = u'Metro Nederland' + description = u'Metro Nederland v1.9.4 2013-04-19' language = u'nl' simultaneous_downloads = 5 masthead_url = 'http://blog.metronieuws.nl/wp-content/themes/metro/images/header.gif' @@ -68,13 +71,17 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): #(re.compile('(' -import os +import os, traceback from functools import partial from calibre.db.backend import DB from calibre.db.cache import Cache from calibre.db.view import View +from calibre.utils.date import utcnow class LibraryDatabase(object): @@ -29,6 +30,7 @@ class LibraryDatabase(object): progress_callback=lambda x, y:True, restore_all_prefs=False): self.is_second_db = is_second_db # TODO: Use is_second_db + self.listeners = set([]) backend = self.backend = DB(library_path, default_prefs=default_prefs, read_only=read_only, restore_all_prefs=restore_all_prefs, @@ -50,6 +52,8 @@ class LibraryDatabase(object): setattr(self, prop, partial(self.get_property, loc=self.FIELD_MAP[fm])) + self.last_update_check = self.last_modified() + def close(self): self.backend.close() @@ -71,9 +75,22 @@ class LibraryDatabase(object): def library_id(self): return self.backend.library_id + @property + def library_path(self): + return self.backend.library_path + + @property + def dbpath(self): + return self.backend.dbpath + def last_modified(self): return self.backend.last_modified() + def check_if_modified(self): + if self.last_modified() > self.last_update_check: + self.refresh() + self.last_update_check = utcnow() + @property def custom_column_num_map(self): return self.backend.custom_column_num_map @@ -86,9 +103,48 @@ class LibraryDatabase(object): def FIELD_MAP(self): return self.backend.FIELD_MAP + @property + def formatter_template_cache(self): + return self.data.cache.formatter_template_cache + + def initialize_template_cache(self): + self.data.cache.initialize_template_cache() + def all_ids(self): for book_id in self.data.cache.all_book_ids(): yield book_id + + def refresh(self, field=None, ascending=True): + self.data.cache.refresh() + self.data.refresh(field=field, ascending=ascending) + + def add_listener(self, listener): + ''' + Add a listener. Will be called on change events with two arguments. + Event name and list of affected ids. + ''' + self.listeners.add(listener) + + def notify(self, event, ids=[]): + 'Notify all listeners' + for listener in self.listeners: + try: + listener(event, ids) + except: + traceback.print_exc() + continue + # }}} + def path(self, index, index_is_id=False): + 'Return the relative path to the directory containing this books files as a unicode string.' + book_id = index if index_is_id else self.data.index_to_id(index) + return self.data.cache.field_for('path', book_id).replace('/', os.sep) + + def abspath(self, index, index_is_id=False, create_dirs=True): + 'Return the absolute path to the directory containing this books files as a unicode string.' + path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id)) + if create_dirs and not os.path.exists(path): + os.makedirs(path) + return path diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 6d5734d6b5..353e1bc4b5 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -16,7 +16,7 @@ class LegacyTest(BaseTest): 'Test library wide properties' def get_props(db): props = ('user_version', 'is_second_db', 'library_id', 'field_metadata', - 'custom_column_label_map', 'custom_column_num_map') + 'custom_column_label_map', 'custom_column_num_map', 'library_path', 'dbpath') fprops = ('last_modified', ) ans = {x:getattr(db, x) for x in props} ans.update({x:getattr(db, x)() for x in fprops}) @@ -51,6 +51,11 @@ class LegacyTest(BaseTest): if label in {'tags', 'formats'}: # Order is random in the old db for these ans[label] = tuple(set(x.split(',')) if x else x for x in ans[label]) + if label == 'series_sort': + # The old db code did not take book language into account + # when generating series_sort values (the first book has + # lang=deu) + ans[label] = ans[label][1:] return ans old = self.init_old() @@ -64,3 +69,31 @@ class LegacyTest(BaseTest): # }}} + def test_refresh(self): # {{{ + ' Test refreshing the view after a change to metadata.db ' + db = self.init_legacy() + db2 = self.init_legacy() + self.assertEqual(db2.data.cache.set_field('title', {1:'xxx'}), set([1])) + db2.close() + del db2 + self.assertNotEqual(db.title(1, index_is_id=True), 'xxx') + db.check_if_modified() + self.assertEqual(db.title(1, index_is_id=True), 'xxx') + # }}} + + def test_legacy_getters(self): # {{{ + old = self.init_old() + getters = ('path', 'abspath', 'title', 'authors', 'series', + 'publisher', 'author_sort', 'authors', 'comments', + 'comment', 'publisher', 'rating', 'series_index', 'tags', + 'timestamp', 'uuid', 'pubdate', 'ondevice', + 'metadata_last_modified', 'languages') + oldvals = {g:tuple(getattr(old, g)(x) for x in xrange(3)) + tuple(getattr(old, g)(x, True) for x in (1,2,3)) for g in getters} + old.close() + db = self.init_legacy() + newvals = {g:tuple(getattr(db, g)(x) for x in xrange(3)) + tuple(getattr(db, g)(x, True) for x in (1,2,3)) for g in getters} + for x in (oldvals, newvals): + x['tags'] = tuple(set(y.split(',')) if y else y for y in x['tags']) + self.assertEqual(oldvals, newvals) + # }}} + diff --git a/src/calibre/db/view.py b/src/calibre/db/view.py index 4ffa1dd074..ff41f20614 100644 --- a/src/calibre/db/view.py +++ b/src/calibre/db/view.py @@ -294,3 +294,11 @@ class View(object): self.marked_ids = dict(izip(id_dict.iterkeys(), imap(unicode, id_dict.itervalues()))) + def refresh(self, field=None, ascending=True): + self._map = tuple(self.cache.all_book_ids()) + self._map_filtered = tuple(self._map) + if field is not None: + self.sort(field, ascending) + if self.search_restriction or self.base_restriction: + self.search('', return_matches=False) + diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 36ab076417..9d5ce152d3 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -71,6 +71,7 @@ class ANDROID(USBMS): 0x42f7 : [0x216], 0x4365 : [0x216], 0x4366 : [0x216], + 0x4371 : [0x216], }, # Freescale 0x15a2 : { @@ -239,7 +240,7 @@ class ANDROID(USBMS): 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E', 'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS', - 'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD'] + 'ICS', 'E400', '__FILE-STOR_GADG', 'ST80208-1', 'GT-S5660M_CARD', 'XT894'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', @@ -250,7 +251,7 @@ class ANDROID(USBMS): 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875', 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E', - 'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1'] + 'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1', 'XT894'] OSX_MAIN_MEM = 'Android Device Main Memory' diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index dc2ff0e400..800dfd9d88 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -35,11 +35,11 @@ class KOBO(USBMS): gui_name = 'Kobo Reader' description = _('Communicate with the Kobo Reader') author = 'Timothy Legge and David Forrester' - version = (2, 0, 7) + version = (2, 0, 8) dbversion = 0 fwversion = 0 - supported_dbversion = 75 + supported_dbversion = 80 has_kepubs = False supported_platforms = ['windows', 'osx', 'linux'] @@ -419,7 +419,7 @@ class KOBO(USBMS): # If all this succeeds we need to delete the images files via the ImageID return ImageID - def delete_images(self, ImageID): + def delete_images(self, ImageID, book_path): if ImageID != None: path_prefix = '.kobo/images/' path = self._main_prefix + path_prefix + ImageID @@ -449,7 +449,7 @@ class KOBO(USBMS): ImageID = self.delete_via_sql(ContentID, ContentType) #print " We would now delete the Images for" + ImageID - self.delete_images(ImageID) + self.delete_images(ImageID, path) if os.path.exists(path): # Delete the ebook @@ -1199,15 +1199,21 @@ class KOBO(USBMS): class KOBOTOUCH(KOBO): name = 'KoboTouch' - gui_name = 'Kobo Touch' + gui_name = 'Kobo Touch/Glo/Mini/Aura HD' author = 'David Forrester' - description = 'Communicate with the Kobo Touch, Glo and Mini firmware. Based on the existing Kobo driver by %s.' % (KOBO.author) + description = 'Communicate with the Kobo Touch, Glo, Mini and Aura HD ereaders. Based on the existing Kobo driver by %s.' % (KOBO.author) # icon = I('devices/kobotouch.jpg') - supported_dbversion = 75 - min_supported_dbversion = 53 - min_dbversion_series = 65 - min_dbversion_archive = 71 + supported_dbversion = 80 + min_supported_dbversion = 53 + min_dbversion_series = 65 + min_dbversion_archive = 71 + min_dbversion_images_on_sdcard = 77 + + max_supported_fwversion = (2,5,1) + min_fwversion_images_on_sdcard = (2,4,1) + + has_kepubs = True booklist_class = KTCollectionsBookList book_class = Book @@ -1291,12 +1297,13 @@ class KOBOTOUCH(KOBO): TIMESTAMP_STRING = "%Y-%m-%dT%H:%M:%SZ" - GLO_PRODUCT_ID = [0x4173] - MINI_PRODUCT_ID = [0x4183] - TOUCH_PRODUCT_ID = [0x4163] - PRODUCT_ID = GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID + AURA_HD_PRODUCT_ID = [0x4193] + GLO_PRODUCT_ID = [0x4173] + MINI_PRODUCT_ID = [0x4183] + TOUCH_PRODUCT_ID = [0x4163] + PRODUCT_ID = AURA_HD_PRODUCT_ID + GLO_PRODUCT_ID + MINI_PRODUCT_ID + TOUCH_PRODUCT_ID - BCD = [0x0110, 0x0326] + BCD = [0x0110, 0x0326] # Image file name endings. Made up of: image size, min_dbversion, max_dbversion, COVER_FILE_ENDINGS = { @@ -1313,6 +1320,11 @@ class KOBOTOUCH(KOBO): # ' - N3_LIBRARY_LIST.parsed':[(60,90),0, 53,], # ' - N3_LIBRARY_SHELF.parsed': [(40,60),0, 52,], } + AURA_HD_COVER_FILE_ENDINGS = { + ' - N3_FULL.parsed': [(1080,1440), 0, 99,True,], # Used for screensaver, home screen + ' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 99,False,], # Used for Details screen + ' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 99,False,], # Used for library lists + } #Following are the sizes used with pre2.1.4 firmware # COVER_FILE_ENDINGS = { # ' - N3_LIBRARY_FULL.parsed':[(355,530),0, 99,], # Used for Details screen @@ -1328,6 +1340,10 @@ class KOBOTOUCH(KOBO): super(KOBOTOUCH, self).initialize() self.bookshelvelist = [] + def get_device_information(self, end_session=True): + self.set_device_name() + return super(KOBOTOUCH, self).get_device_information(end_session) + def books(self, oncard=None, end_session=True): debug_print("KoboTouch:books - oncard='%s'"%oncard) from calibre.ebooks.metadata.meta import path_to_ext @@ -1354,14 +1370,13 @@ class KOBOTOUCH(KOBO): # Determine the firmware version try: - with open(self.normalize_path(self._main_prefix + '.kobo/version'), - 'rb') as f: + with open(self.normalize_path(self._main_prefix + '.kobo/version'), 'rb') as f: self.fwversion = f.readline().split(',')[2] + self.fwversion = tuple((int(x) for x in self.fwversion.split('.'))) except: - self.fwversion = 'unknown' + self.fwversion = (0,0,0) - if self.fwversion != '1.0' and self.fwversion != '1.4': - self.has_kepubs = True + debug_print('Kobo device: %s' % self.gui_name) debug_print('Version of driver:', self.version, 'Has kepubs:', self.has_kepubs) debug_print('Version of firmware:', self.fwversion, 'Has kepubs:', self.has_kepubs) @@ -1374,7 +1389,7 @@ class KOBOTOUCH(KOBO): debug_print(opts.extra_customization) if opts.extra_customization: debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:books - set_debugging_title to", debugging_title ) + debug_print("KoboTouch:books - set_debugging_title to '%s'" % debugging_title ) bl.set_debugging_title(debugging_title) debug_print("KoboTouch:books - length bl=%d"%len(bl)) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) @@ -1466,6 +1481,7 @@ class KOBOTOUCH(KOBO): if show_debug: self.debug_index = idx debug_print("KoboTouch:update_booklist - idx=%d"%idx) + debug_print("KoboTouch:update_booklist - lpath=%s"%lpath) debug_print('KoboTouch:update_booklist - bl[idx].device_collections=', bl[idx].device_collections) debug_print('KoboTouch:update_booklist - playlist_map=', playlist_map) debug_print('KoboTouch:update_booklist - bookshelves=', bookshelves) @@ -1477,7 +1493,7 @@ class KOBOTOUCH(KOBO): bl_cache[lpath] = None if ImageID is not None: - imagename = self.imagefilename_from_imageID(ImageID) + imagename = self.imagefilename_from_imageID(prefix, ImageID) if imagename is not None: bl[idx].thumbnail = ImageWrapper(imagename) if (ContentType == '6' and MimeType != 'application/x-kobo-epub+zip'): @@ -1717,12 +1733,14 @@ class KOBOTOUCH(KOBO): debug_print("KoboTouch:books - end - oncard='%s'"%oncard) return bl - def imagefilename_from_imageID(self, ImageID): + def imagefilename_from_imageID(self, prefix, ImageID): show_debug = self.is_debugging_title(ImageID) + path = self.images_path(prefix) + path = self.normalize_path(path.replace('/', os.sep)) + for ending, cover_options in self.cover_file_endings().items(): - fpath = self._main_prefix + '.kobo/images/' + ImageID + ending - fpath = self.normalize_path(fpath.replace('/', os.sep)) + fpath = path + ImageID + ending if os.path.exists(fpath): if show_debug: debug_print("KoboTouch:imagefilename_from_imageID - have cover image fpath=%s" % (fpath)) @@ -1764,7 +1782,7 @@ class KOBOTOUCH(KOBO): if not self.copying_covers(): imageID = self.imageid_from_contentid(contentID) - self.delete_images(imageID) + self.delete_images(imageID, fname) connection.commit() cursor.close() @@ -1821,11 +1839,11 @@ class KOBOTOUCH(KOBO): return imageId - def delete_images(self, ImageID): + def delete_images(self, ImageID, book_path): debug_print("KoboTouch:delete_images - ImageID=", ImageID) if ImageID != None: - path_prefix = '.kobo/images/' - path = self._main_prefix + path_prefix + ImageID + path = self.images_path(book_path) + path = path + ImageID for ending in self.cover_file_endings().keys(): fpath = path + ending @@ -1872,12 +1890,14 @@ class KOBOTOUCH(KOBO): def get_content_type_from_extension(self, extension): debug_print("KoboTouch:get_content_type_from_extension - start") # With new firmware, ContentType appears to be 6 for all types of sideloaded books. - if self.fwversion.startswith('2.'): + if self.fwversion >= (1,9,17) or extension == '.kobo' or extension == '.mobi': debug_print("KoboTouch:get_content_type_from_extension - V2 firmware") ContentType = 6 + # For older firmware, it depends on the type of file. + elif extension == '.kobo' or extension == '.mobi': + ContentType = 6 else: - debug_print("KoboTouch:get_content_type_from_extension - calling super") - ContentType = super(KOBOTOUCH, self).get_content_type_from_extension(extension) + ContentType = 901 return ContentType def update_device_database_collections(self, booklists, collections_attributes, oncard): @@ -1920,7 +1940,7 @@ class KOBOTOUCH(KOBO): delete_empty_shelves = opts.extra_customization[self.OPT_DELETE_BOOKSHELVES] and self.supports_bookshelves() update_series_details = opts.extra_customization[self.OPT_UPDATE_SERIES_DETAILS] and self.supports_series() debugging_title = opts.extra_customization[self.OPT_DEBUGGING_TITLE] - debug_print("KoboTouch:update_device_database_collections - set_debugging_title to", debugging_title ) + debug_print("KoboTouch:update_device_database_collections - set_debugging_title to '%s'" % debugging_title ) booklists.set_debugging_title(debugging_title) else: delete_empty_shelves = False @@ -2088,8 +2108,8 @@ class KOBOTOUCH(KOBO): # debug_print('KoboTouch: not uploading cover') return - # Don't upload covers if book is on the SD card - if self._card_a_prefix and path.startswith(self._card_a_prefix): + # Only upload covers to SD card if that is supported + if self._card_a_prefix and path.startswith(self._card_a_prefix) and not self.supports_covers_on_sdcard(): return if not opts.extra_customization[self.OPT_UPLOAD_GRAYSCALE_COVERS]: @@ -2111,6 +2131,16 @@ class KOBOTOUCH(KOBO): ImageID = ImageID.replace('.', '_') return ImageID + + def images_path(self, path): + if self._card_a_prefix and path.startswith(self._card_a_prefix) and self.supports_covers_on_sdcard(): + path_prefix = 'koboExtStorage/images/' + path = self._card_a_prefix + path_prefix + else: + path_prefix = '.kobo/images/' + path = self._main_prefix + path_prefix + return path + def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, keep_cover_aspect=False): from calibre.utils.magick.draw import save_cover_data_to, identify_data debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale)) @@ -2151,8 +2181,8 @@ class KOBOTOUCH(KOBO): cursor.close() if ImageID != None: - path_prefix = '.kobo/images/' - path = self._main_prefix + path_prefix + ImageID + path = self.images_path(path) + ImageID + if show_debug: debug_print("KoboTouch:_upload_cover - About to loop over cover endings") @@ -2496,6 +2526,8 @@ class KOBOTOUCH(KOBO): return opts + def isAuraHD(self): + return self.detected_device.idProduct in self.AURA_HD_PRODUCT_ID def isGlo(self): return self.detected_device.idProduct in self.GLO_PRODUCT_ID def isMini(self): @@ -2504,7 +2536,21 @@ class KOBOTOUCH(KOBO): return self.detected_device.idProduct in self.TOUCH_PRODUCT_ID def cover_file_endings(self): - return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.COVER_FILE_ENDINGS + return self.GLO_COVER_FILE_ENDINGS if self.isGlo() else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() else self.COVER_FILE_ENDINGS + + def set_device_name(self): + device_name = self.gui_name + if self.isAuraHD(): + device_name = 'Kobo Aura HD' + elif self.isGlo(): + device_name = 'Kobo Glo' + elif self.isMini(): + device_name = 'Kobo Mini' + elif self.isTouch(): + device_name = 'Kobo Touch' + self.__class__.gui_name = device_name + return device_name + def copying_covers(self): opts = self.settings() @@ -2524,6 +2570,44 @@ class KOBOTOUCH(KOBO): def supports_kobo_archive(self): return self.dbversion >= self.min_dbversion_archive + def supports_covers_on_sdcard(self): + return self.dbversion >= 77 and self.fwversion >= self.min_fwversion_images_on_sdcard + + def modify_database_check(self, function): + # Checks to see whether the database version is supported + # and whether the user has chosen to support the firmware version +# debug_print("KoboTouch:modify_database_check - self.fwversion <= self.max_supported_fwversion=", self.fwversion > self.max_supported_fwversion) + if self.dbversion > self.supported_dbversion or self.fwversion > self.max_supported_fwversion: + # Unsupported database + opts = self.settings() + if not opts.extra_customization[self.OPT_SUPPORT_NEWER_FIRMWARE]: + debug_print('The database has been upgraded past supported version') + self.report_progress(1.0, _('Removing books from device...')) + from calibre.devices.errors import UserFeedback + raise UserFeedback(_("Kobo database version unsupported - See details"), + _('Your Kobo is running an updated firmware/database version.' + ' As calibre does not know about this updated firmware,' + ' database editing is disabled, to prevent corruption.' + ' You can still send books to your Kobo with calibre, ' + ' but deleting books and managing collections is disabled.' + ' If you are willing to experiment and know how to reset' + ' your Kobo to Factory defaults, you can override this' + ' check by right clicking the device icon in calibre and' + ' selecting "Configure this device" and then the ' + ' "Attempt to support newer firmware" option.' + ' Doing so may require you to perform a factory reset of' + ' your Kobo.' + ), + UserFeedback.WARN) + + return False + else: + # The user chose to edit the database anyway + return True + else: + # Supported database version + return True + @classmethod def is_debugging_title(cls, title): diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index f033fb9a2f..4a2e6aa864 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -95,7 +95,6 @@ class PDNOVEL(USBMS): SUPPORTS_SUB_DIRS = False DELETE_EXTS = ['.jpg', '.jpeg', '.png'] - def upload_cover(self, path, filename, metadata, filepath): coverdata = getattr(metadata, 'thumbnail', None) if coverdata and coverdata[2]: @@ -226,9 +225,9 @@ class TREKSTOR(USBMS): VENDOR_ID = [0x1e68] PRODUCT_ID = [0x0041, 0x0042, 0x0052, 0x004e, 0x0056, - 0x0067, # This is for the Pyrus Mini - 0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091 - 0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318 + 0x0067, # This is for the Pyrus Mini + 0x003e, # This is for the EBOOK_PLAYER_5M https://bugs.launchpad.net/bugs/792091 + 0x5cL, # This is for the 4ink http://www.mobileread.com/forums/showthread.php?t=191318 ] BCD = [0x0002, 0x100] @@ -427,8 +426,8 @@ class WAYTEQ(USBMS): EBOOK_DIR_MAIN = 'Documents' SCAN_FROM_ROOT = True - VENDOR_NAME = 'ROCKCHIP' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'RK28_SDK_DEMO' + VENDOR_NAME = ['ROCKCHIP', 'CBR'] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['RK28_SDK_DEMO', 'EINK_EBOOK_READE'] SUPPORTS_SUB_DIRS = True def get_gui_name(self): @@ -445,7 +444,8 @@ class WAYTEQ(USBMS): return self.EBOOK_DIR_CARD_A def windows_sort_drives(self, drives): - if len(drives) < 2: return drives + if len(drives) < 2: + return drives main = drives.get('main', None) carda = drives.get('carda', None) if main and carda: @@ -455,7 +455,8 @@ class WAYTEQ(USBMS): def linux_swap_drives(self, drives): # See https://bugs.launchpad.net/bugs/1151901 - if len(drives) < 2 or not drives[1] or not drives[2]: return drives + if len(drives) < 2 or not drives[1] or not drives[2]: + return drives drives = list(drives) t = drives[0] drives[0] = drives[1] @@ -463,7 +464,8 @@ class WAYTEQ(USBMS): return tuple(drives) def osx_sort_names(self, names): - if len(names) < 2: return names + if len(names) < 2: + return names main = names.get('main', None) card = names.get('carda', None) diff --git a/src/calibre/devices/teclast/driver.py b/src/calibre/devices/teclast/driver.py index acd20308ad..95d8c3cf3f 100644 --- a/src/calibre/devices/teclast/driver.py +++ b/src/calibre/devices/teclast/driver.py @@ -58,8 +58,8 @@ class PICO(NEWSMY): gui_name = 'Pico' description = _('Communicate with the Pico reader.') - VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', ''] - WINDOWS_MAIN_MEM = ['USBDISK__USER', 'EB720'] + VENDOR_NAME = ['TECLAST', 'IMAGIN', 'LASER-', 'LASER', ''] + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['USBDISK__USER', 'EB720', 'EBOOK-EB720'] EBOOK_DIR_MAIN = 'Books' FORMATS = ['EPUB', 'FB2', 'TXT', 'LRC', 'PDB', 'PDF', 'HTML', 'WTXT'] SCAN_FROM_ROOT = True diff --git a/src/calibre/ebooks/conversion/plugins/epub_input.py b/src/calibre/ebooks/conversion/plugins/epub_input.py index b602ac9cd0..b6024c9b84 100644 --- a/src/calibre/ebooks/conversion/plugins/epub_input.py +++ b/src/calibre/ebooks/conversion/plugins/epub_input.py @@ -188,7 +188,6 @@ class EPUBInput(InputFormatPlugin): raise DRMError(os.path.basename(path)) self.encrypted_fonts = self._encrypted_font_uris - if len(parts) > 1 and parts[0]: delta = '/'.join(parts[:-1])+'/' for elem in opf.itermanifest(): diff --git a/src/calibre/ebooks/conversion/plugins/html_output.py b/src/calibre/ebooks/conversion/plugins/html_output.py index 3821ba41a4..68d32d1aec 100644 --- a/src/calibre/ebooks/conversion/plugins/html_output.py +++ b/src/calibre/ebooks/conversion/plugins/html_output.py @@ -4,12 +4,15 @@ __copyright__ = '2010, Fabian Grassl ' __docformat__ = 'restructuredtext en' import os, re, shutil -from os.path import dirname, abspath, relpath, exists, basename +from os.path import dirname, abspath, relpath as _relpath, exists, basename from calibre.customize.conversion import OutputFormatPlugin, OptionRecommendation from calibre import CurrentDir from calibre.ptempfile import PersistentTemporaryDirectory +def relpath(*args): + return _relpath(*args).replace(os.sep, '/') + class HTMLOutput(OutputFormatPlugin): name = 'HTML Output' diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 2a2d89b894..eb5b0042e7 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -1,7 +1,6 @@ ''' Basic support for manipulating OEB 1.x/2.0 content and metadata. ''' -from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Marshall T. Vandegrift ' @@ -11,7 +10,7 @@ import os, re, uuid, logging from collections import defaultdict from itertools import count from urlparse import urldefrag, urlparse, urlunparse, urljoin -from urllib import unquote as urlunquote +from urllib import unquote from lxml import etree, html from calibre.constants import filesystem_encoding, __version__ @@ -40,11 +39,11 @@ CALIBRE_NS = 'http://calibre.kovidgoyal.net/2009/metadata' RE_NS = 'http://exslt.org/regular-expressions' MBP_NS = 'http://www.mobipocket.com' -XPNSMAP = {'h' : XHTML_NS, 'o1' : OPF1_NS, 'o2' : OPF2_NS, - 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, - 'xsi': XSI_NS, 'dt' : DCTERMS_NS, 'ncx': NCX_NS, - 'svg': SVG_NS, 'xl' : XLINK_NS, 're': RE_NS, - 'mbp': MBP_NS, 'calibre': CALIBRE_NS } +XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS, + 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, + 'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS, + 'svg': SVG_NS, 'xl': XLINK_NS, 're': RE_NS, + 'mbp': MBP_NS, 'calibre': CALIBRE_NS} OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS} OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS, @@ -142,7 +141,6 @@ def iterlinks(root, find_links_in_css=True): if attr in link_attrs: yield (el, attr, attribs[attr], 0) - if not find_links_in_css: continue if tag == XHTML('style') and el.text: @@ -363,7 +361,9 @@ URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ' URL_UNSAFE = [ASCII_CHARS - URL_SAFE, UNIBYTE_CHARS - URL_SAFE] def urlquote(href): - """Quote URL-unsafe characters, allowing IRI-safe characters.""" + """ Quote URL-unsafe characters, allowing IRI-safe characters. + That is, this function returns valid IRIs not valid URIs. In particular, + IRIs can contain non-ascii characters. """ result = [] unsafe = 0 if isinstance(href, unicode) else 1 unsafe = URL_UNSAFE[unsafe] @@ -373,6 +373,19 @@ def urlquote(href): result.append(char) return ''.join(result) +def urlunquote(href): + # unquote must run on a bytestring and will return a bytestring + # If it runs on a unicode object, it returns a double encoded unicode + # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8') + # and the latter is correct + want_unicode = isinstance(href, unicode) + if want_unicode: + href = href.encode('utf-8') + href = unquote(href) + if want_unicode: + href = href.decode('utf-8') + return href + def urlnormalize(href): """Convert a URL into normalized form, with all and only URL-unsafe characters URL quoted. @@ -469,7 +482,7 @@ class DirContainer(object): return def _unquote(self, path): - # urlunquote must run on a bytestring and will return a bytestring + # unquote must run on a bytestring and will return a bytestring # If it runs on a unicode object, it returns a double encoded unicode # string: unquote(u'%C3%A4') != unquote(b'%C3%A4').decode('utf-8') # and the latter is correct @@ -497,7 +510,7 @@ class DirContainer(object): return False try: path = os.path.join(self.rootdir, self._unquote(path)) - except ValueError: #Happens if path contains quoted special chars + except ValueError: # Happens if path contains quoted special chars return False try: return os.path.isfile(path) @@ -577,12 +590,13 @@ class Metadata(object): allowed = self.allowed if allowed is not None and term not in allowed: raise AttributeError( - 'attribute %r not valid for metadata term %r' \ + 'attribute %r not valid for metadata term %r' % (self.attr(term), barename(obj.term))) return self.attr(term) def __get__(self, obj, cls): - if obj is None: return None + if obj is None: + return None return obj.attrib.get(self.term_attr(obj), '') def __set__(self, obj, value): @@ -628,8 +642,8 @@ class Metadata(object): self.value = value return property(fget=fget, fset=fset) - scheme = Attribute(lambda term: 'scheme' if \ - term == OPF('meta') else OPF('scheme'), + scheme = Attribute(lambda term: 'scheme' if + term == OPF('meta') else OPF('scheme'), [DC('identifier'), OPF('meta')]) file_as = Attribute(OPF('file-as'), [DC('creator'), DC('contributor'), DC('title')]) @@ -882,7 +896,6 @@ class Manifest(object): return self._parse_xhtml(convert_markdown(data, title=title)) - def _parse_css(self, data): from cssutils import CSSParser, log, resolveImports log.setLevel(logging.WARN) @@ -935,7 +948,7 @@ class Manifest(object): data = self._loader(getattr(self, 'html_input_href', self.href)) if not isinstance(data, basestring): - pass # already parsed + pass # already parsed elif self.media_type.lower() in OEB_DOCS: data = self._parse_xhtml(data) elif self.media_type.lower()[-4:] in ('+xml', '/xml'): @@ -1022,7 +1035,8 @@ class Manifest(object): target, frag = urldefrag(href) target = target.split('/') for index in xrange(min(len(base), len(target))): - if base[index] != target[index]: break + if base[index] != target[index]: + break else: index += 1 relhref = (['..'] * (len(base) - index)) + target[index:] diff --git a/src/calibre/ebooks/oeb/polish/cover.py b/src/calibre/ebooks/oeb/polish/cover.py index 01b9e25e59..5cee827d72 100644 --- a/src/calibre/ebooks/oeb/polish/cover.py +++ b/src/calibre/ebooks/oeb/polish/cover.py @@ -46,10 +46,11 @@ def is_raster_image(media_type): return media_type and media_type.lower() in { 'image/png', 'image/jpeg', 'image/jpg', 'image/gif'} -COVER_TYPES = { 'coverimagestandard', 'other.ms-coverimage-standard', - 'other.ms-titleimage-standard', 'other.ms-titleimage', - 'other.ms-coverimage', 'other.ms-thumbimage-standard', - 'other.ms-thumbimage', 'thumbimagestandard', 'cover'} +COVER_TYPES = { + 'coverimagestandard', 'other.ms-coverimage-standard', + 'other.ms-titleimage-standard', 'other.ms-titleimage', + 'other.ms-coverimage', 'other.ms-thumbimage-standard', + 'other.ms-thumbimage', 'thumbimagestandard', 'cover'} def find_cover_image(container): 'Find a raster image marked as a cover in the OPF' @@ -92,7 +93,8 @@ def find_cover_page(container): def find_cover_image_in_page(container, cover_page): root = container.parsed(cover_page) body = XPath('//h:body')(root) - if len(body) != 1: return + if len(body) != 1: + return body = body[0] images = [] for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body): @@ -152,7 +154,7 @@ def create_epub_cover(container, cover_path): ar = 'xMidYMid meet' if keep_aspect else 'none' templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar) templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height)) - templ = templ.replace('__width__', str(width)) + templ = templ.replace('__width__', str(width)) templ = templ.replace('__height__', str(height)) titlepage_item = container.generate_item('titlepage.xhtml', id_prefix='titlepage') @@ -179,7 +181,7 @@ def create_epub_cover(container, cover_path): guide = container.opf_get_or_create('guide') container.insert_into_xml(guide, guide.makeelement( OPF('reference'), type='cover', title=_('Cover'), - href=container.name_to_href(titlepage))) + href=container.name_to_href(titlepage, base=container.opf_name))) metadata = container.opf_get_or_create('metadata') meta = metadata.makeelement(OPF('meta'), name='cover') meta.set('content', raster_cover_item.get('id')) diff --git a/src/calibre/ebooks/oeb/reader.py b/src/calibre/ebooks/oeb/reader.py index d0474fa7e8..6a3747d2d3 100644 --- a/src/calibre/ebooks/oeb/reader.py +++ b/src/calibre/ebooks/oeb/reader.py @@ -148,7 +148,6 @@ class OEBReader(object): if not has_aut: m.add('creator', self.oeb.translate(__('Unknown')), role='aut') - def _manifest_prune_invalid(self): ''' Remove items from manifest that contain invalid data. This prevents @@ -197,6 +196,8 @@ class OEBReader(object): item.media_type[-4:] in ('/xml', '+xml')): hrefs = [r[2] for r in iterlinks(data)] for href in hrefs: + if isinstance(href, bytes): + href = href.decode('utf-8') href, _ = urldefrag(href) if not href: continue @@ -293,7 +294,7 @@ class OEBReader(object): continue try: href = item.abshref(urlnormalize(href)) - except ValueError: # Malformed URL + except ValueError: # Malformed URL continue if href not in manifest.hrefs: continue @@ -394,9 +395,9 @@ class OEBReader(object): authorElement = xpath(child, 'descendant::calibre:meta[@name = "author"]') - if authorElement : + if authorElement: author = authorElement[0].text - else : + else: author = None descriptionElement = xpath(child, @@ -406,7 +407,7 @@ class OEBReader(object): method='text', encoding=unicode).strip() if not description: description = None - else : + else: description = None index_image = xpath(child, @@ -497,7 +498,8 @@ class OEBReader(object): titles = [] headers = [] for item in self.oeb.spine: - if not item.linear: continue + if not item.linear: + continue html = item.data title = ''.join(xpath(html, '/h:html/h:head/h:title/text()')) title = COLLAPSE_RE.sub(' ', title.strip()) @@ -515,17 +517,21 @@ class OEBReader(object): if len(titles) > len(set(titles)): use = headers for title, item in izip(use, self.oeb.spine): - if not item.linear: continue + if not item.linear: + continue toc.add(title, item.href) return True def _toc_from_opf(self, opf, item): self.oeb.auto_generated_toc = False - if self._toc_from_ncx(item): return + if self._toc_from_ncx(item): + return # Prefer HTML to tour based TOC, since several LIT files # have good HTML TOCs but bad tour based TOCs - if self._toc_from_html(opf): return - if self._toc_from_tour(opf): return + if self._toc_from_html(opf): + return + if self._toc_from_tour(opf): + return self._toc_from_spine(opf) self.oeb.auto_generated_toc = True @@ -589,8 +595,10 @@ class OEBReader(object): return True def _pages_from_opf(self, opf, item): - if self._pages_from_ncx(opf, item): return - if self._pages_from_page_map(opf): return + if self._pages_from_ncx(opf, item): + return + if self._pages_from_page_map(opf): + return return def _cover_from_html(self, hcover): diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index 3d56f0ef3d..67d55a581e 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -47,6 +47,8 @@ class ManifestTrimmer(object): item.data is not None: hrefs = [r[2] for r in iterlinks(item.data)] for href in hrefs: + if isinstance(href, bytes): + href = href.decode('utf-8') try: href = item.abshref(urlnormalize(href)) except: diff --git a/src/calibre/ebooks/pdf/render/links.py b/src/calibre/ebooks/pdf/render/links.py index 97a6551dbd..c9d4d8d43c 100644 --- a/src/calibre/ebooks/pdf/render/links.py +++ b/src/calibre/ebooks/pdf/render/links.py @@ -51,7 +51,7 @@ class Links(object): for link in self.links: path, href, frag = link[0] page, rect = link[1:] - combined_path = os.path.abspath(os.path.join(os.path.dirname(path), *href.split('/'))) + combined_path = os.path.abspath(os.path.join(os.path.dirname(path), *unquote(href).split('/'))) is_local = not href or combined_path in self.anchors annot = Dictionary({ 'Type':Name('Annot'), 'Subtype':Name('Link'), diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 45430da6f4..44c324fa43 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -406,6 +406,7 @@ class BookInfo(QWebView): remove_format = pyqtSignal(int, object) save_format = pyqtSignal(int, object) restore_format = pyqtSignal(int, object) + copy_link = pyqtSignal(object) def __init__(self, vertical, parent=None): QWebView.__init__(self, parent) @@ -419,26 +420,33 @@ class BookInfo(QWebView): palette.setBrush(QPalette.Base, Qt.transparent) self.page().setPalette(palette) self.css = P('templates/book_details.css', data=True).decode('utf-8') - for x, icon in [('remove', 'trash.png'), ('save', 'save.png'), ('restore', 'edit-undo.png')]: + for x, icon in [('remove_format', 'trash.png'), ('save_format', 'save.png'), ('restore_format', 'edit-undo.png'), ('copy_link','edit-copy.png')]: ac = QAction(QIcon(I(icon)), '', self) ac.current_fmt = None - ac.triggered.connect(getattr(self, '%s_format_triggerred'%x)) - setattr(self, '%s_format_action'%x, ac) + ac.current_url = None + ac.triggered.connect(getattr(self, '%s_triggerred'%x)) + setattr(self, '%s_action'%x, ac) def context_action_triggered(self, which): - f = getattr(self, '%s_format_action'%which).current_fmt - if f: + f = getattr(self, '%s_action'%which).current_fmt + url = getattr(self, '%s_action'%which).current_url + if f and 'format' in which: book_id, fmt = f - getattr(self, '%s_format'%which).emit(book_id, fmt) + getattr(self, which).emit(book_id, fmt) + if url and 'link' in which: + getattr(self, which).emit(url) def remove_format_triggerred(self): - self.context_action_triggered('remove') + self.context_action_triggered('remove_format') def save_format_triggerred(self): - self.context_action_triggered('save') + self.context_action_triggered('save_format') def restore_format_triggerred(self): - self.context_action_triggered('restore') + self.context_action_triggered('restore_format') + + def copy_link_triggerred(self): + self.context_action_triggered('copy_link') def link_activated(self, link): self._link_clicked = True @@ -474,24 +482,33 @@ class BookInfo(QWebView): for action in list(menu.actions()): if action is not ca: menu.removeAction(action) - if not r.isNull() and url.startswith('format:'): - parts = url.split(':') - try: - book_id, fmt = int(parts[1]), parts[2] - except: - import traceback - traceback.print_exc() - else: - for a, t in [('remove', _('Delete the %s format')), - ('save', _('Save the %s format to disk')), - ('restore', _('Restore the %s format')), + if not r.isNull(): + if url.startswith('http'): + for a, t in [('copy', _('&Copy Link')), ]: - if a == 'restore' and not fmt.upper().startswith('ORIGINAL_'): - continue - ac = getattr(self, '%s_format_action'%a) - ac.current_fmt = (book_id, fmt) - ac.setText(t%parts[2]) + ac = getattr(self, '%s_link_action'%a) + ac.current_url = url + ac.setText(t) menu.addAction(ac) + + if url.startswith('format:'): + parts = url.split(':') + try: + book_id, fmt = int(parts[1]), parts[2] + except: + import traceback + traceback.print_exc() + else: + for a, t in [('remove', _('Delete the %s format')), + ('save', _('Save the %s format to disk')), + ('restore', _('Restore the %s format')), + ]: + if a == 'restore' and not fmt.upper().startswith('ORIGINAL_'): + continue + ac = getattr(self, '%s_format_action'%a) + ac.current_fmt = (book_id, fmt) + ac.setText(t%parts[2]) + menu.addAction(ac) if len(menu.actions()) > 0: menu.exec_(ev.globalPos()) @@ -594,6 +611,7 @@ class BookDetails(QWidget): # {{{ remove_specific_format = pyqtSignal(int, object) save_specific_format = pyqtSignal(int, object) restore_specific_format = pyqtSignal(int, object) + copy_link = pyqtSignal(object) remote_file_dropped = pyqtSignal(object, object) files_dropped = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object) @@ -664,6 +682,7 @@ class BookDetails(QWidget): # {{{ self.book_info.remove_format.connect(self.remove_specific_format) self.book_info.save_format.connect(self.save_specific_format) self.book_info.restore_format.connect(self.restore_specific_format) + self.book_info.copy_link.connect(self.copy_link) self.setCursor(Qt.PointingHandCursor) def handle_click(self, link): diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index 469091162b..1a915288a8 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -75,7 +75,7 @@ class GroupModel(QAbstractListModel): def get_preferred_input_format_for_book(db, book_id): recs = load_specifics(db, book_id) if recs: - return recs.get('gui_preferred_input_format', None) + return recs.get('gui_preferred_input_format', None) def get_available_formats_for_book(db, book_id): available_formats = db.formats(book_id, index_is_id=True) @@ -147,6 +147,7 @@ class Config(ResizableDialog, Ui_Dialog): self.connect(self.groups, SIGNAL('entered(QModelIndex)'), self.show_group_help) rb = self.buttonBox.button(self.buttonBox.RestoreDefaults) + rb.setText(_('Restore &Defaults')) self.connect(rb, SIGNAL('clicked()'), self.restore_defaults) self.groups.setMouseTracking(True) geom = gprefs.get('convert_single_dialog_geom', None) @@ -188,7 +189,6 @@ class Config(ResizableDialog, Ui_Dialog): return cls(self.stack, self.plumber.get_option_by_name, self.plumber.get_option_help, self.db, self.book_id) - self.mw = widget_factory(MetadataWidget) self.setWindowTitle(_('Convert')+ ' ' + unicode(self.mw.title.text())) lf = widget_factory(LookAndFeelWidget) @@ -209,7 +209,8 @@ class Config(ResizableDialog, Ui_Dialog): self.plumber.get_option_help, self.db, self.book_id) while True: c = self.stack.currentWidget() - if not c: break + if not c: + break self.stack.removeWidget(c) widgets = [self.mw, lf, hw, ps, sd, toc, sr] @@ -234,7 +235,6 @@ class Config(ResizableDialog, Ui_Dialog): except: pass - def setup_input_output_formats(self, db, book_id, preferred_input_format, preferred_output_format): if preferred_output_format: diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 2a5b061819..131adc3216 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' import functools -from PyQt4.Qt import (Qt, QStackedWidget, QMenu, QTimer, +from PyQt4.Qt import (Qt, QApplication, QStackedWidget, QMenu, QTimer, QSize, QSizePolicy, QStatusBar, QLabel, QFont) from calibre.utils.config import prefs @@ -274,6 +274,8 @@ class LayoutMixin(object): # {{{ self.iactions['Save To Disk'].save_library_format_by_ids) self.book_details.restore_specific_format.connect( self.iactions['Remove Books'].restore_format) + self.book_details.copy_link.connect(self.bd_copy_link, + type=Qt.QueuedConnection) self.book_details.view_device_book.connect( self.iactions['View'].view_device_book) @@ -295,6 +297,10 @@ class LayoutMixin(object): # {{{ if self.cover_flow: self.cover_flow.dataChanged() + def bd_copy_link(self, url): + if url: + QApplication.clipboard().setText(url) + def save_layout_state(self): for x in ('library', 'memory', 'card_a', 'card_b'): getattr(self, x+'_view').save_state() diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index ffa83b6ea8..3e9bb87687 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -21,7 +21,7 @@ from PyQt4.Qt import ( QDialog, QVBoxLayout, QLabel, QDialogButtonBox, QStyle, QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette, QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView, QPixmap, QModelIndex, - QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel) + QAbstractListModel, QColor, QRect, QTextBrowser, QStringListModel, QMenu, QCursor) from PyQt4.QtWebKit import QWebView from calibre.customize.ui import metadata_plugins @@ -40,7 +40,7 @@ from calibre.utils.ipc.simple_worker import fork_job, WorkerError from calibre.ptempfile import TemporaryDirectory # }}} -class RichTextDelegate(QStyledItemDelegate): # {{{ +class RichTextDelegate(QStyledItemDelegate): # {{{ def __init__(self, parent=None, max_width=160): QStyledItemDelegate.__init__(self, parent) @@ -77,7 +77,7 @@ class RichTextDelegate(QStyledItemDelegate): # {{{ painter.restore() # }}} -class CoverDelegate(QStyledItemDelegate): # {{{ +class CoverDelegate(QStyledItemDelegate): # {{{ needs_redraw = pyqtSignal() @@ -143,7 +143,7 @@ class CoverDelegate(QStyledItemDelegate): # {{{ # }}} -class ResultsModel(QAbstractTableModel): # {{{ +class ResultsModel(QAbstractTableModel): # {{{ COLUMNS = ( '#', _('Title'), _('Published'), _('Has cover'), _('Has summary') @@ -182,7 +182,6 @@ class ResultsModel(QAbstractTableModel): # {{{ p = book.publisher if book.publisher else '' return '%s
%s' % (d, p) - def data(self, index, role): row, col = index.row(), index.column() try: @@ -233,7 +232,7 @@ class ResultsModel(QAbstractTableModel): # {{{ # }}} -class ResultsView(QTableView): # {{{ +class ResultsView(QTableView): # {{{ show_details_signal = pyqtSignal(object) book_selected = pyqtSignal(object) @@ -316,7 +315,7 @@ class ResultsView(QTableView): # {{{ # }}} -class Comments(QWebView): # {{{ +class Comments(QWebView): # {{{ def __init__(self, parent=None): QWebView.__init__(self, parent) @@ -384,7 +383,7 @@ class Comments(QWebView): # {{{ return QSize(800, 300) # }}} -class IdentifyWorker(Thread): # {{{ +class IdentifyWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): Thread.__init__(self) @@ -441,7 +440,7 @@ class IdentifyWorker(Thread): # {{{ # }}} -class IdentifyWidget(QWidget): # {{{ +class IdentifyWidget(QWidget): # {{{ rejected = pyqtSignal() results_found = pyqtSignal() @@ -552,12 +551,11 @@ class IdentifyWidget(QWidget): # {{{ self.results_view.show_results(self.worker.results) self.results_found.emit() - def cancel(self): self.abort.set() # }}} -class CoverWorker(Thread): # {{{ +class CoverWorker(Thread): # {{{ def __init__(self, log, abort, title, authors, identifiers, caches): Thread.__init__(self) @@ -609,7 +607,8 @@ class CoverWorker(Thread): # {{{ def scan_once(self, tdir, seen): for x in list(os.listdir(tdir)): - if x in seen: continue + if x in seen: + continue if x.endswith('.cover') and os.path.exists(os.path.join(tdir, x+'.done')): name = x.rpartition('.')[0] @@ -635,7 +634,7 @@ class CoverWorker(Thread): # {{{ # }}} -class CoversModel(QAbstractListModel): # {{{ +class CoversModel(QAbstractListModel): # {{{ def __init__(self, current_cover, parent=None): QAbstractListModel.__init__(self, parent) @@ -770,7 +769,7 @@ class CoversModel(QAbstractListModel): # {{{ # }}} -class CoversView(QListView): # {{{ +class CoversView(QListView): # {{{ chosen = pyqtSignal() @@ -793,6 +792,8 @@ class CoversView(QListView): # {{{ type=Qt.QueuedConnection) self.doubleClicked.connect(self.chosen, type=Qt.QueuedConnection) + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) def select(self, num): current = self.model().index(num) @@ -814,9 +815,24 @@ class CoversView(QListView): # {{{ else: self.select(self.m.index_from_pointer(pointer).row()) + def show_context_menu(self, point): + idx = self.currentIndex() + if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject(): + m = QMenu() + m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover) + m.exec_(QCursor.pos()) + + def show_cover(self): + idx = self.currentIndex() + pmap = self.model().cover_pixmap(idx) + if pmap is not None: + from calibre.gui2.viewer.image_popup import ImageView + d = ImageView(self, pmap, unicode(idx.data(Qt.DisplayRole).toString()), geom_name='metadata_download_cover_popup_geom') + d(use_exec=True) + # }}} -class CoversWidget(QWidget): # {{{ +class CoversWidget(QWidget): # {{{ chosen = pyqtSignal() finished = pyqtSignal() @@ -922,7 +938,7 @@ class CoversWidget(QWidget): # {{{ # }}} -class LogViewer(QDialog): # {{{ +class LogViewer(QDialog): # {{{ def __init__(self, log, parent=None): QDialog.__init__(self, parent) @@ -970,7 +986,7 @@ class LogViewer(QDialog): # {{{ # }}} -class FullFetch(QDialog): # {{{ +class FullFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): QDialog.__init__(self, parent) @@ -1085,7 +1101,7 @@ class FullFetch(QDialog): # {{{ return self.exec_() # }}} -class CoverFetch(QDialog): # {{{ +class CoverFetch(QDialog): # {{{ def __init__(self, current_cover=None, parent=None): QDialog.__init__(self, parent) diff --git a/src/calibre/gui2/preferences/adding.ui b/src/calibre/gui2/preferences/adding.ui index b98a476864..8b9b9c0cd1 100644 --- a/src/calibre/gui2/preferences/adding.ui +++ b/src/calibre/gui2/preferences/adding.ui @@ -164,7 +164,7 @@ Author matching is exact. - Ignore files with the following extensions when automatically adding + <b>Ignore</b> files with the following extensions when automatically adding true diff --git a/src/calibre/gui2/preferences/server.ui b/src/calibre/gui2/preferences/server.ui index 674e4bdbc2..85d27eab57 100644 --- a/src/calibre/gui2/preferences/server.ui +++ b/src/calibre/gui2/preferences/server.ui @@ -129,7 +129,7 @@ - Max. OPDS &ungrouped items: + Max. &ungrouped items: opt_max_opds_ungrouped_items diff --git a/src/calibre/gui2/store/stores/google_books_plugin.py b/src/calibre/gui2/store/stores/google_books_plugin.py index f6f91fd81d..65a7ccdfb4 100644 --- a/src/calibre/gui2/store/stores/google_books_plugin.py +++ b/src/calibre/gui2/store/stores/google_books_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (unicode_literals, division, absolute_import, print_function) -store_version = 1 # Needed for dynamic plugin loading +store_version = 2 # Needed for dynamic plugin loading __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' @@ -25,25 +25,7 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog class GoogleBooksStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): - aff_id = { - 'lid': '41000000033185143', - 'pubid': '21000000000352219', - 'ganpub': 'k352219', - 'ganclk': 'GOOG_1335334761', - } - # Use Kovid's affiliate id 30% of the time. - if random.randint(1, 10) in (1, 2, 3): - aff_id = { - 'lid': '41000000031855266', - 'pubid': '21000000000352583', - 'ganpub': 'k352583', - 'ganclk': 'GOOG_1335335464', - } - - url = 'http://gan.doubleclick.net/gan_click?lid=%(lid)s&pubid=%(pubid)s' % aff_id - if detail_item: - detail_item += '&ganpub=%(ganpub)s&ganclk=%(ganclk)s' % aff_id - + url = 'http://books.google.com/books' if external or self.config.get('open_external', False): open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) else: diff --git a/src/calibre/gui2/store/stores/kobo_plugin.py b/src/calibre/gui2/store/stores/kobo_plugin.py index 44f4f4001c..62652ca855 100644 --- a/src/calibre/gui2/store/stores/kobo_plugin.py +++ b/src/calibre/gui2/store/stores/kobo_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (unicode_literals, division, absolute_import, print_function) -store_version = 1 # Needed for dynamic plugin loading +store_version = 2 # Needed for dynamic plugin loading __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' @@ -31,10 +31,10 @@ class KoboStore(BasicStoreConfig, StorePlugin): if random.randint(1, 10) in (1, 2, 3): pub_id = '0dsO3kDu/AU' - murl = 'http://click.linksynergy.com/fs-bin/click?id=%s&offerid=268429.4&type=3&subid=0' % pub_id + murl = 'http://click.linksynergy.com/fs-bin/click?id=%s&subid=&offerid=280046.1&type=10&tmpid=9310&RD_PARM1=http%%3A%%2F%%2Fkobo.com' % pub_id if detail_item: - purl = 'http://click.linksynergy.com/link?id=%s&offerid=268429&type=2&murl=%s' % (pub_id, urllib.quote_plus(detail_item)) + purl = 'http://click.linksynergy.com/link?id=%s&offerid=280046&type=2&murl=%s' % (pub_id, urllib.quote_plus(detail_item)) url = purl else: purl = None diff --git a/src/calibre/gui2/store/stores/nook_uk_plugin.py b/src/calibre/gui2/store/stores/nook_uk_plugin.py index cc97d5cf93..84d6c214f2 100644 --- a/src/calibre/gui2/store/stores/nook_uk_plugin.py +++ b/src/calibre/gui2/store/stores/nook_uk_plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import (unicode_literals, division, absolute_import, print_function) -store_version = 1 # Needed for dynamic plugin loading +store_version = 2 # Needed for dynamic plugin loading __license__ = 'GPL 3' __copyright__ = '2012, John Schember ' @@ -25,11 +25,19 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog class NookUKStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): - url = "http://uk.nook.com" + url = 'http://www.awin1.com/awclick.php?mid=5266&id=120917' + detail_url = 'http://www.awin1.com/cread.php?awinmid=5266&awinaffid=120917&clickref=&p=' if external or self.config.get('open_external', False): - open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + if detail_item: + url = detail_url + detail_item + + open_url(QUrl(url_slash_cleaner(url))) else: + if detail_item: + detail_url = detail_url + detail_item + else: + detail_url = None d = WebStoreDialog(self.gui, url, parent, detail_item) d.setWindowTitle(self.name) d.set_tags(self.config.get('tags', '')) diff --git a/src/calibre/gui2/viewer/image_popup.py b/src/calibre/gui2/viewer/image_popup.py index 075143f3c3..1b616a12b3 100644 --- a/src/calibre/gui2/viewer/image_popup.py +++ b/src/calibre/gui2/viewer/image_popup.py @@ -15,16 +15,17 @@ from calibre.gui2 import choose_save_file, gprefs class ImageView(QDialog): - def __init__(self, parent, current_img, current_url): + def __init__(self, parent, current_img, current_url, geom_name='viewer_image_popup_geometry'): QDialog.__init__(self) dw = QApplication.instance().desktop() self.avail_geom = dw.availableGeometry(parent) self.current_img = current_img self.current_url = current_url self.factor = 1.0 + self.geom_name = geom_name self.label = l = QLabel() - l.setBackgroundRole(QPalette.Base); + l.setBackgroundRole(QPalette.Base) l.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) l.setScaledContents(True) @@ -88,21 +89,27 @@ class ImageView(QDialog): self.label.setPixmap(pm) self.label.adjustSize() - def __call__(self): + def __call__(self, use_exec=False): geom = self.avail_geom self.label.setPixmap(self.current_img) self.label.adjustSize() self.resize(QSize(int(geom.width()/2.5), geom.height()-50)) - geom = gprefs.get('viewer_image_popup_geometry', None) + geom = gprefs.get(self.geom_name, None) if geom is not None: self.restoreGeometry(geom) - self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1] + try: + self.current_image_name = unicode(self.current_url.toString()).rpartition('/')[-1] + except AttributeError: + self.current_image_name = self.current_url title = _('View Image: %s')%self.current_image_name self.setWindowTitle(title) - self.show() + if use_exec: + self.exec_() + else: + self.show() def done(self, e): - gprefs['viewer_image_popup_geometry'] = bytearray(self.saveGeometry()) + gprefs[self.geom_name] = bytearray(self.saveGeometry()) return QDialog.done(self, e) def wheelEvent(self, event): diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 2b46ab922b..b1344167f2 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -493,7 +493,6 @@ class ResultCache(SearchQueryParser): # {{{ return matches def get_keypair_matches(self, location, query, candidates): - print query matches = set([]) if query.find(':') >= 0: q = [q.strip() for q in query.split(':')] diff --git a/src/calibre/utils/pyparsing.py b/src/calibre/utils/pyparsing.py new file mode 100644 index 0000000000..8c3521f69f --- /dev/null +++ b/src/calibre/utils/pyparsing.py @@ -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 ' + +# Dummy file for backwards compatibility with older plugins +from calibre.utils.search_query_parser import ParseException # noqa +