From dc2fc23d3c6430c801cd140dfdebb64e0d75a8ff Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 09:46:02 +0530 Subject: [PATCH 01/38] Update O Globo --- recipes/bbc_brasil.recipe | 2 +- recipes/o_globo.recipe | 52 +++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/recipes/bbc_brasil.recipe b/recipes/bbc_brasil.recipe index 4a0fc03d96..947f51082f 100644 --- a/recipes/bbc_brasil.recipe +++ b/recipes/bbc_brasil.recipe @@ -147,7 +147,7 @@ class BBCBrasilRecipe(BasicNewsRecipe): # Author of this recipe. - __author__ = 'claviola' + __author__ = 'Carlos Laviola' # Specify English as the language of the RSS feeds (ISO-639 code). language = 'en_GB' diff --git a/recipes/o_globo.recipe b/recipes/o_globo.recipe index 0cf00d874c..2fa0043c0e 100644 --- a/recipes/o_globo.recipe +++ b/recipes/o_globo.recipe @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python __license__ = 'GPL v3' __copyright__ = '2009, Darko Miletic ' @@ -10,11 +10,11 @@ from calibre.web.feeds.news import BasicNewsRecipe class OGlobo(BasicNewsRecipe): title = 'O Globo' - __author__ = 'Darko Miletic and Sujata Raman' + __author__ = 'Darko Miletic and Carlos Laviola' description = 'News from Brasil' publisher = 'O Globo' category = 'news, politics, Brasil' - oldest_article = 2 + oldest_article = 7 max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False @@ -35,47 +35,39 @@ class OGlobo(BasicNewsRecipe): body{font-family:Arial,Helvetica,sans-serif;font-size:x-small;} h3{font-size:large; color:#082963; font-weight:bold;} #ident{color:#0179B4; font-size:xx-small;} - p{color:#000000;font-weight:normal;} + p{color:#000000;font-weight:normal;} .commentario p{color:#007BB5; font-style:italic;} ''' - - keep_only_tags = [dict(name='div', attrs={'id':'ltintb'}), - dict(name='a', attrs={'class':['img imgLoader','img ftr imgLoader']}),] - remove_tags = [ dict(name='script') - ,dict(name='object') ,dict(name='form') - ,dict(name='div', attrs={'id':['linksPatGoogle','rdpm','cor','com','env','rcm_st','coment',]}) - ,dict(name='div', attrs={'class':'box-zap-anu2'}) - ,dict(name='a', attrs={'class':'assine'}) - ,dict(name='link') + ,dict(name='div', attrs={'id':'header'}) + ,dict(name='p', attrs={'id':'info-date-press'}) ] feeds = [ - (u'Todos os canais', u'http://oglobo.globo.com/rss/plantao.xml') - ,(u'Ciencia', u'http://oglobo.globo.com/rss/plantaociencia.xml') - ,(u'Educacao', u'http://oglobo.globo.com/rss/plantaoeducacao.xml') - ,(u'Opiniao', u'http://oglobo.globo.com/rss/plantaoopiniao.xml') - ,(u'Sao Paulo', u'http://oglobo.globo.com/rss/plantaosaopaulo.xml') - ,(u'Viagem', u'http://oglobo.globo.com/rss/plantaoviagem.xml') - ,(u'Cultura', u'http://oglobo.globo.com/rss/plantaocultura.xml') - ,(u'Esportes', u'http://oglobo.globo.com/rss/plantaoesportes.xml') - ,(u'Mundo', u'http://oglobo.globo.com/rss/plantaomundo.xml') - ,(u'Pais', u'http://oglobo.globo.com/rss/plantaopais.xml') - ,(u'Rio', u'http://oglobo.globo.com/rss/plantaorio.xml') - ,(u'Saude', u'http://oglobo.globo.com/rss/plantaosaude.xml') - ,(u'Viver Melhor', u'http://oglobo.globo.com/rss/plantaovivermelhor.xml') - ,(u'Economia', u'http://oglobo.globo.com/rss/plantaoeconomia.xml') - ,(u'Tecnologia', u'http://oglobo.globo.com/rss/plantaotecnologia.xml') + (u'Todos os canais', u'http://oglobo.globo.com/rss.xml?completo=true') + ,(u'Ciencia', u'http://oglobo.globo.com/rss.xml?secao=ciencia&completo=true') + ,(u'Educacao', u'http://oglobo.globo.com/rss.xml?secao=educacao&completo=true') + ,(u'Opiniao', u'http://oglobo.globo.com/rss.xml?secao=opiniao&completo=true') + ,(u'Cultura', u'http://oglobo.globo.com/rss.xml?secao=cultura&completo=true') + ,(u'Esportes', u'http://oglobo.globo.com/rss.xml?secao=esportes&completo=true') + ,(u'Mundo', u'http://oglobo.globo.com/rss.xml?secao=mundo&completo=true') + ,(u'Pais', u'http://oglobo.globo.com/rss.xml?secao=pais&completo=true') + ,(u'Rio', u'http://oglobo.globo.com/rss.xml?secao=rio&completo=true') + ,(u'Saude', u'http://oglobo.globo.com/rss.xml?secao=saude&completo=true') + ,(u'Economia', u'http://oglobo.globo.com/rss.xml?secao=economia&completo=true') + ,(u'Tecnologia', u'http://oglobo.globo.com/rss.xml?secao=tecnologia&completo=true') ] + def print_version(self, url): + return url + '?service=print' + def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] return soup - language = 'pt' - + language = 'pt_BR' From 8156e13e837cf837161ab1209b8bf2f5e6cd3057 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 11:01:21 +0530 Subject: [PATCH 02/38] Finish up the UDisks2 support, however for the moment we continue to use UDisks1 as who knows what state UDisks2 is in all the distros out there --- src/calibre/devices/udisks.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py index 54a21cacff..112cd3d87f 100644 --- a/src/calibre/devices/udisks.py +++ b/src/calibre/devices/udisks.py @@ -5,10 +5,6 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -# First repeat after me: Linux desktop infrastructure is designed by a -# committee of rabid monkeys on crack. They would not know a decent desktop if -# it was driving the rabid monkey extermination truck that runs them over. - import os, dbus, re def node_mountpoint(node): @@ -67,6 +63,7 @@ class UDisks2(object): BLOCK = 'org.freedesktop.UDisks2.Block' FILESYSTEM = 'org.freedesktop.UDisks2.Filesystem' + DRIVE = 'org.freedesktop.UDisks2.Drive' def __init__(self): self.bus = dbus.SystemBus() @@ -131,6 +128,21 @@ class UDisks2(object): raise return mp + def unmount(self, device_node_path): + d = self.device(device_node_path) + d.Unmount({'force':True, 'auth.no_user_interaction':True}, + dbus_interface=self.FILESYSTEM) + + def drive_for_device(self, device): + drive = device.Get(self.BLOCK, 'Drive', + dbus_interface='org.freedesktop.DBus.Properties') + return self.bus.get_object('org.freedesktop.UDisks2', drive) + + def eject(self, device_node_path): + drive = self.drive_for_device(self.device(device_node_path)) + drive.Eject({'auth.no_user_interaction':True}, + dbus_interface=self.DRIVE) + def get_udisks(ver=None): if ver is None: try: @@ -140,7 +152,6 @@ def get_udisks(ver=None): return u return UDisks2() if ver == 2 else UDisks() - def mount(node_path): u = UDisks() u.mount(node_path) From 4a1ed673d7b7b0efb5675f9a484cb3c89dafd3e3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 11:02:18 +0530 Subject: [PATCH 03/38] ... --- recipes/bbc_brasil.recipe | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/recipes/bbc_brasil.recipe b/recipes/bbc_brasil.recipe index 947f51082f..a2d83944d1 100644 --- a/recipes/bbc_brasil.recipe +++ b/recipes/bbc_brasil.recipe @@ -149,8 +149,7 @@ class BBCBrasilRecipe(BasicNewsRecipe): # Author of this recipe. __author__ = 'Carlos Laviola' - # Specify English as the language of the RSS feeds (ISO-639 code). - language = 'en_GB' + language = 'pt_BR' # Set tags. tags = 'news, sport, blog' From 8ffeb0c2228e8d69c42c2203114d7f652a54d660 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 11:06:07 +0530 Subject: [PATCH 04/38] ... --- src/calibre/devices/udisks.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py index 112cd3d87f..18771dbeb2 100644 --- a/src/calibre/devices/udisks.py +++ b/src/calibre/devices/udisks.py @@ -19,13 +19,20 @@ def node_mountpoint(node): return de_mangle(line[1]) return None +class NoUDisks1(Exception): + pass class UDisks(object): def __init__(self): self.bus = dbus.SystemBus() - self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', + try: + self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks'), 'org.freedesktop.UDisks') + except dbus.exceptions.DBusException as e: + if getattr(e, '_dbus_error_name', None) == 'org.freedesktop.DBus.Error.ServiceUnknown': + raise NoUDisks1() + raise def device(self, device_node_path): devpath = self.main.FindDeviceByDeviceFile(device_node_path) From 313b4634d6697466ca984f623c0bc588fddee253 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 13:23:48 +0530 Subject: [PATCH 05/38] KF8 Output: Fix a couple of bugs that could lead to generation of invalid KF8 files. Fixes #1016672 (broken rtf to azw3 conversion) --- src/calibre/ebooks/mobi/writer8/skeleton.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer8/skeleton.py b/src/calibre/ebooks/mobi/writer8/skeleton.py index ae083163d2..5db6ee0b5c 100644 --- a/src/calibre/ebooks/mobi/writer8/skeleton.py +++ b/src/calibre/ebooks/mobi/writer8/skeleton.py @@ -111,7 +111,7 @@ class Skeleton(object): self.chunks = chunks self.skeleton = self.render(root) - self.body_offset = self.skeleton.find('')[0]) + len(text) + 1 end_length = len(raw.rpartition(b'<')[-1]) + 1 self.metrics[tag.get('aid')] = Metric(start_length, end_length) From 1c091c9bd892bf763f2fbafffe9f02ca28af9da1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 16:22:53 +0530 Subject: [PATCH 06/38] ... --- src/calibre/web/feeds/news.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 4a8d980fee..2a49804422 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -77,7 +77,8 @@ class BasicNewsRecipe(Recipe): delay = 0 #: Publication type - #: Set to newspaper, magazine or blog + #: Set to newspaper, magazine or blog. If set to None, no publication type + #: metadata will be written to the opf file. publication_type = 'unknown' #: Number of simultaneous downloads. Set to 1 if the server is picky. @@ -1264,7 +1265,8 @@ class BasicNewsRecipe(Recipe): mi = MetaInformation(title, [__appname__]) mi.publisher = __appname__ mi.author_sort = __appname__ - mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title() + if self.publication_type: + mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title() mi.timestamp = nowf() article_titles, aseen = [], set() for f in feeds: From c3f472e4ababafb3739b7dbf83656ba166512098 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 16:25:52 +0530 Subject: [PATCH 07/38] EPUB Output: Fix a bug that could cause corrupted output when doing an EPUB/OEB to EPUB conversion if the input EPUB had multiple files with the same name --- src/calibre/ebooks/oeb/transforms/filenames.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index 00b71ea8be..ddf4def30c 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -105,14 +105,14 @@ class UniqueFilenames(object): # {{{ base, ext = posixpath.splitext(item.href) nhref = base + suffix + ext nhref = oeb.manifest.generate(href=nhref)[1] + spine_pos = item.spine_position + oeb.manifest.remove(item) nitem = oeb.manifest.add(item.id, nhref, item.media_type, data=data, fallback=item.fallback) self.seen_filenames.add(posixpath.basename(nhref)) self.rename_map[item.href] = nhref - if item.spine_position is not None: - oeb.spine.insert(item.spine_position, nitem, item.linear) - oeb.spine.remove(item) - oeb.manifest.remove(item) + if spine_pos is not None: + oeb.spine.insert(spine_pos, nitem, item.linear) else: self.seen_filenames.add(fname) From bba964e9038f9fd384814df159738cf47d1bdcb8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 19:01:50 +0530 Subject: [PATCH 08/38] When generating cover center logo vertically --- src/calibre/utils/magick/draw.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index e4163743d9..0bfc806d42 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -279,6 +279,9 @@ def create_cover_page(top_lines, logo_path, width=590, height=750, logo.size = (lwidth, lheight) left = int(max(0, (width - lwidth)/2.)) top = bottom+10 + extra = int((available[1] - lheight)/2.0) + if extra > 0: + top += extra canvas.compose(logo, left, top) return canvas.export(output_format) From bef20cf124721d79160fe024e8dd8097edf226e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 19:09:05 +0530 Subject: [PATCH 09/38] Add option to metadata plugin tester to ignore failed fields --- src/calibre/ebooks/metadata/sources/test.py | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/test.py b/src/calibre/ebooks/metadata/sources/test.py index 4853035b27..9fa70aba07 100644 --- a/src/calibre/ebooks/metadata/sources/test.py +++ b/src/calibre/ebooks/metadata/sources/test.py @@ -167,7 +167,8 @@ def test_identify(tests): # {{{ # }}} -def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ +def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None, + fail_missing_meta=True): # {{{ ''' :param name: Plugin name :param tests: List of 2-tuples. Each two tuple is of the form (args, @@ -246,7 +247,8 @@ def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ None] if not good: prints('Failed to find', plugin.test_fields(possibles[0])) - raise SystemExit(1) + if fail_missing_meta: + raise SystemExit(1) if results[0] is not possibles[0]: prints('Most relevant result failed the tests') @@ -263,21 +265,22 @@ def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ results.append(rq.get_nowait()) except Empty: break - if not results: + if not results and fail_missing_meta: prints('Cover download failed') raise SystemExit(1) - cdata = results[0] - cover = os.path.join(tdir, plugin.name.replace(' ', - '')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ', - '_'))) - with open(cover, 'wb') as f: - f.write(cdata[-1]) + elif results: + cdata = results[0] + cover = os.path.join(tdir, plugin.name.replace(' ', + '')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ', + '_'))) + with open(cover, 'wb') as f: + f.write(cdata[-1]) - prints('Cover downloaded to:', cover) + prints('Cover downloaded to:', cover) - if len(cdata[-1]) < 10240: - prints('Downloaded cover too small') - raise SystemExit(1) + if len(cdata[-1]) < 10240: + prints('Downloaded cover too small') + raise SystemExit(1) prints('Average time per query', sum(times)/len(times)) From e32aae3b83cab2a18fa612e4deff4e431964e08a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 20:16:55 +0530 Subject: [PATCH 10/38] Support for amazon's new results page markup --- src/calibre/ebooks/metadata/sources/amazon.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index 6764e2f6f7..219a0e02ca 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -687,7 +687,11 @@ class Amazon(Source): return True for div in root.xpath(r'//div[starts-with(@id, "result_")]'): - for a in div.xpath(r'descendant::a[@class="title" and @href]'): + links = div.xpath(r'descendant::a[@class="title" and @href]') + if not links: + # New amazon markup + links = div.xpath('descendant::h3/a[@href]') + for a in links: title = tostring(a, method='text', encoding=unicode) if title_ok(title): matches.append(a.get('href')) From 5d982f24e570c9d1efecca8fdca6dbbf9cf3e5d8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 21:50:35 +0530 Subject: [PATCH 11/38] ... --- setup/installer/windows/notes.rst | 2 +- src/calibre/ebooks/oeb/display/paged.coffee | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 7fe978d30b..0a9c904ff7 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -115,7 +115,7 @@ PyQt4 Compiling instructions:: - python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose + python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose --confirm-license nmake nmake install diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 142fb5f887..263fe27f72 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -333,3 +333,4 @@ if window? # Indexing # Resizing of images # Full screen mode +# Highlight on jump_to_anchor From 957c80e02f221c65bd824d768a1de766513576f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 23:09:36 +0530 Subject: [PATCH 12/38] calibre -s now waits for calibre to shutdown --- manual/creating_plugins.rst | 2 +- src/calibre/gui2/main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/manual/creating_plugins.rst b/manual/creating_plugins.rst index 5963f4a890..f39b60dad9 100644 --- a/manual/creating_plugins.rst +++ b/manual/creating_plugins.rst @@ -195,7 +195,7 @@ It can get tiresome to keep re-adding a plugin to calibre to test small changes. Once you've located the zip file of your plugin you can then directly update it with your changes instead of re-adding it each time. To do so from the command line, in the directory that contains your plugin source code, use:: - calibre -s; sleep 4s; zip -R /path/to/plugin/zip/file.zip *; calibre + calibre -s; zip -R /path/to/plugin/zip/file.zip *; calibre This will shutdown a running calibre. Wait for the shutdown to complete, then update your plugin files and relaunch calibre. It relies on the freely available zip command line tool. diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 94c2ffa384..d787dbae58 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -323,6 +323,10 @@ def communicate(opts, args): if opts.shutdown_running_calibre: t.conn.send('shutdown:') + from calibre.utils.lock import singleinstance + prints(_('Shutdown command sent, waiting for shutdown...')) + while not singleinstance('calibre GUI'): + time.sleep(0.1) else: if len(args) > 1: args[1] = os.path.abspath(args[1]) From db855eda4a729f9a152590efb1e592c5a64e691d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 23 Jun 2012 23:15:26 +0530 Subject: [PATCH 13/38] Driver for HTC G2 --- src/calibre/devices/android/driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index c53cbd3812..9bed8f35d3 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -90,6 +90,7 @@ class ANDROID(USBMS): 0x4e22 : [0x0100, 0x226, 0x227, 0x231], 0xb058 : [0x0222, 0x226, 0x227], 0x0ff9 : [0x0226], + 0xc91 : HTC_BCDS, 0xdddd : [0x216], }, From f187d5f7e8a008066fa0527ed377f07cbd2f17c3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 07:34:42 +0530 Subject: [PATCH 14/38] Fix #1017010 (Calibre does not recognize my tablet) --- src/calibre/devices/android/driver.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 9bed8f35d3..ad599a9d42 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -166,7 +166,10 @@ class ANDROID(USBMS): 0x2237: { 0x2208 : [0x0226] }, # Lenovo - 0x17ef : { 0x7421 : [0x0216] }, + 0x17ef : { + 0x7421 : [0x0216], + 0x741b : [0x9999], + }, # Pantech 0x10a9 : { 0x6050 : [0x227] }, @@ -204,7 +207,8 @@ class ANDROID(USBMS): 'GT-I9003_CARD', 'XT912', 'FILE-CD_GADGET', 'RK29_SDK', 'MB855', 'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW', 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', - 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX'] + 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', + 'THINKPAD_TABLET'] 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', From a7fe71a54ccabeaf6ca98579cda1148f27ae4898 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 08:14:33 +0530 Subject: [PATCH 15/38] News download: Add support for images embedded in the HTML --- src/calibre/web/fetch/simple.py | 36 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index b8809147aa..d79ef6204a 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -12,6 +12,7 @@ from urllib import url2pathname, quote from httplib import responses from PIL import Image from cStringIO import StringIO +from base64 import b64decode from calibre import browser, relpath, unicode_path from calibre.constants import filesystem_encoding, iswindows @@ -346,22 +347,29 @@ class RecursiveFetcher(object): c = 0 for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')): iurl = tag['src'] - if callable(self.image_url_processor): - iurl = self.image_url_processor(baseurl, iurl) - if not urlparse.urlsplit(iurl).scheme: - iurl = urlparse.urljoin(baseurl, iurl, False) - with self.imagemap_lock: - if self.imagemap.has_key(iurl): - tag['src'] = self.imagemap[iurl] + if iurl.startswith('data:image/'): + try: + data = b64decode(iurl.partition(',')[-1]) + except: + self.log.exception('Failed to decode embedded image') continue - try: - data = self.fetch_url(iurl) - if data == 'GIF89a\x01': - # Skip empty GIF files as PIL errors on them anyway + else: + if callable(self.image_url_processor): + iurl = self.image_url_processor(baseurl, iurl) + if not urlparse.urlsplit(iurl).scheme: + iurl = urlparse.urljoin(baseurl, iurl, False) + with self.imagemap_lock: + if self.imagemap.has_key(iurl): + tag['src'] = self.imagemap[iurl] + continue + try: + data = self.fetch_url(iurl) + if data == 'GIF89a\x01': + # Skip empty GIF files as PIL errors on them anyway + continue + except Exception: + self.log.exception('Could not fetch image ', iurl) continue - except Exception: - self.log.exception('Could not fetch image ', iurl) - continue c += 1 fname = ascii_filename('img'+str(c)) if isinstance(fname, unicode): From dd712255a38cb8fb47eda44a4e9928a6136e6cf2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 08:22:38 +0530 Subject: [PATCH 16/38] Marketing Sensoriale by NotTaken --- recipes/marketing_sensoriale.recipe | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 recipes/marketing_sensoriale.recipe diff --git a/recipes/marketing_sensoriale.recipe b/recipes/marketing_sensoriale.recipe new file mode 100644 index 0000000000..214e0ea1b1 --- /dev/null +++ b/recipes/marketing_sensoriale.recipe @@ -0,0 +1,56 @@ +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.utils.ipc.simple_worker import fork_job +from calibre.ptempfile import PersistentTemporaryFile + +js_fetcher = ''' + +import calibre.web.jsbrowser.browser as jsbrowser + +def grab(url): + browser = jsbrowser.Browser() + #10 second timeout + browser.visit(url, 10) + browser.run_for_a_time(10) + html = browser.html + browser.close() + return html + + ''' +class MarketingSensoriale(BasicNewsRecipe): + + title = u'Marketing sensoriale' + __author__ = 'NotTaken' + description = 'Marketing Sensoriale, il Blog' + category = 'Blog' + oldest_article = 7 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = False + language = 'it' + remove_empty_feeds = True + recursions = 0 + requires_version = (0, 8, 58) + auto_cleanup = False + simultaneous_downloads = 1 + articles_are_obfuscated = True + + remove_tags_after = [dict(name='div', attrs={'class':['article-footer']})] + + + def get_article_url(self, article): + return article.get('feedburner_origlink', None) + + def get_obfuscated_article(self, url): + result = fork_job(js_fetcher, 'grab', (url,), module_is_source_code=True) + + html = result['result'] + if isinstance(html, type(u'')): + html = html.encode('utf-8') + pt = PersistentTemporaryFile('.html') + pt.write(html) + pt.close() + return pt.name + + feeds = [(u'Marketing sensoriale', u'http://feeds.feedburner.com/MarketingSensoriale?format=xml')] + From c7bc47a8055e03134302e027e565ac209ea7ecc4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 08:22:58 +0530 Subject: [PATCH 17/38] Fix #1017027 (Updated recipe for ABC Digital) --- recipes/abc_py.recipe | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/recipes/abc_py.recipe b/recipes/abc_py.recipe index 297129d269..41005c6844 100644 --- a/recipes/abc_py.recipe +++ b/recipes/abc_py.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010-2012, Darko Miletic ' ''' abc.com.py ''' @@ -7,7 +7,7 @@ abc.com.py from calibre.web.feeds.news import BasicNewsRecipe class ABC_py(BasicNewsRecipe): - title = 'ABC digital' + title = 'ABC Color' __author__ = 'Darko Miletic' description = 'Noticias de Paraguay y el resto del mundo' publisher = 'ABC' @@ -15,12 +15,16 @@ class ABC_py(BasicNewsRecipe): oldest_article = 2 max_articles_per_feed = 200 no_stylesheets = True - encoding = 'cp1252' + encoding = 'utf8' use_embedded_content = False language = 'es_PY' remove_empty_feeds = True + masthead_url = 'http://www.abc.com.py/plantillas/img/abc-logo.png' publication_type = 'newspaper' - extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} ' + extra_css = """ + body{font-family: UnitSlabProMedium,"Times New Roman",serif } + img{margin-bottom: 0.4em; display: block;} + """ conversion_options = { 'comment' : description @@ -29,21 +33,19 @@ class ABC_py(BasicNewsRecipe): , 'language' : language } - remove_tags = [dict(name=['form','iframe','embed','object','link','base','table']),dict(attrs={'class':'toolbox'})] - remove_tags_after = dict(attrs={'class':'date'}) - keep_only_tags = [dict(attrs={'class':'zcontent'})] + remove_tags = [ + dict(name=['form','iframe','embed','object','link','base','table']), + dict(attrs={'class':['es-carousel-wrapper']}), + dict(attrs={'id':['tools','article-banner-1']}) + ] + keep_only_tags = [dict(attrs={'id':'article'})] feeds = [ - (u'Ultimo momento' , u'http://www.abc.com.py/ultimo-momento.xml' ) - ,(u'Nacionales' , u'http://www.abc.com.py/nacionales.xml' ) - ,(u'Internacionales' , u'http://www.abc.com.py/internacionales.xml' ) - ,(u'Deportes' , u'http://www.abc.com.py/deportes.xml' ) - ,(u'Espectaculos' , u'http://www.abc.com.py/espectaculos.xml' ) - ,(u'Ciencia y Tecnologia', u'http://www.abc.com.py/ciencia-y-tecnologia.xml') + (u'Ultimo momento', u'http://www.abc.com.py/rss.xml' ) + ,(u'Nacionales' , u'http://www.abc.com.py/nacionales/rss.xml' ) + ,(u'Mundo' , u'http://www.abc.com.py/internacionales/rss.xml') + ,(u'Deportes' , u'http://www.abc.com.py/deportes/rss.xml' ) + ,(u'Espectaculos' , u'http://www.abc.com.py/espectaculos/rss.xml' ) + ,(u'TecnoCiencia' , u'http://www.abc.com.py/ciencia/rss.xml' ) ] - - def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - return soup From 3c4cb3f492864aa0cff8bf8e05f94367c98de79e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 10:29:20 +0530 Subject: [PATCH 18/38] Do not output unneccessary error messages from viewer when it is unintialized --- src/calibre/gui2/viewer/documentview.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 8c6da27d71..dc7b557f3c 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -205,6 +205,8 @@ class Document(QWebPage): # {{{ return self.anchor_positions def switch_to_paged_mode(self, onresize=False): + if onresize and not self.loaded_javascript: + return side_margin = self.javascript('window.paged_display.layout()', typ=int) # Setup the contents size to ensure that there is a right most margin. # Without this webkit renders the final column with no margin, as the @@ -294,6 +296,7 @@ class Document(QWebPage): # {{{ self.mainFrame().setScrollPosition(QPoint(x, y)) def jump_to_anchor(self, anchor): + if not self.loaded_javascript: return self.javascript('window.paged_display.jump_to_anchor("%s")'%anchor) def element_ypos(self, elem): @@ -352,7 +355,7 @@ class Document(QWebPage): # {{{ except ZeroDivisionError: return 0. def fset(self, val): - if self.in_paged_mode: + if self.in_paged_mode and self.loaded_javascript: self.javascript('paged_display.scroll_to_pos(%f)'%val) else: npos = val * (self.height - self.window_height) From 876ddf27b386128c4bf81d8bb398ac5747dff696 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 10:53:31 +0530 Subject: [PATCH 19/38] Paged display: Implement go to reference --- resources/viewer/referencing.js | 9 ++++- src/calibre/ebooks/oeb/display/paged.coffee | 43 +++++++++++++++------ src/calibre/gui2/viewer/main.py | 4 +- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/resources/viewer/referencing.js b/resources/viewer/referencing.js index eceea9f31d..7c2a9d0a3f 100644 --- a/resources/viewer/referencing.js +++ b/resources/viewer/referencing.js @@ -60,8 +60,13 @@ function goto_reference(ref) { if (num < 0) {alert("Invalid reference: "+ref); return;} var p = $("p"); if (num >= p.length) {alert("Reference not found: "+ref); return;} - $.scrollTo($(p[num]), 1000, - {onAfter:function(){window.py_bridge.animated_scroll_done()}}); + var dest = $(p[num]); + if (window.paged_display.in_paged_mode) { + var xpos = dest.offset().left; + window.paged_display.scroll_to_xpos(xpos, true, true, 1000); + } else + $.scrollTo(dest, 1000, + {onAfter:function(){window.py_bridge.animated_scroll_done()}}); } diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 263fe27f72..ff16c05085 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -50,14 +50,12 @@ absleft = (elem) -> # {{{ # }}} class PagedDisplay - ### - This class is a namespace to expose functions via the - window.paged_display object. The most important functions are: - - set_geometry(): sets the parameters used to layout text in paged mode - - layout(): causes the currently loaded document to be laid out in columns. - ### + # This class is a namespace to expose functions via the + # window.paged_display object. The most important functions are: + # + # set_geometry(): sets the parameters used to layout text in paged mode + # + # layout(): causes the currently loaded document to be laid out in columns. constructor: () -> if not this instanceof arguments.callee @@ -163,7 +161,7 @@ class PagedDisplay xpos = Math.floor(document.body.scrollWidth * frac) this.scroll_to_xpos(xpos) - scroll_to_xpos: (xpos) -> + scroll_to_xpos: (xpos, animated=false, notify=false, duration=1000) -> # Scroll so that the column containing xpos is the left most column in # the viewport if typeof(xpos) != 'number' @@ -177,7 +175,31 @@ class PagedDisplay pos += this.page_width limit = document.body.scrollWidth - this.screen_width pos = limit if pos > limit - window.scrollTo(pos, 0) + if animated + this.animated_scroll(pos, duration=1000, notify=notify) + else + window.scrollTo(pos, 0) + + animated_scroll: (pos, duration=1000, notify=true) -> + delta = pos - window.pageXOffset + interval = 50 + steps = Math.floor(duration/interval) + step_size = Math.floor(delta/steps) + this.current_scroll_animation = {target:pos, step_size:step_size, interval:interval, notify:notify, fn: () => + a = this.current_scroll_animation + npos = window.pageXOffset + a.step_size + completed = false + if Math.abs(npos - a.target) < Math.abs(a.step_size) + completed = true + npos = a.target + window.scrollTo(npos, 0) + if completed + if notify + window.py_bridge.animated_scroll_done() + else + setTimeout(a.fn, a.interval) + } + this.current_scroll_animation.fn() current_pos: (frac) -> # The current scroll position as a fraction between 0 and 1 @@ -329,7 +351,6 @@ if window? window.paged_display = new PagedDisplay() # TODO: -# Go to reference positions # Indexing # Resizing of images # Full screen mode diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index c74fd649de..8200169025 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -138,7 +138,9 @@ class Reference(QLineEdit): self.editingFinished.connect(self.editing_finished) def editing_finished(self): - self.goto.emit(unicode(self.text())) + text = unicode(self.text()) + self.setText('') + self.goto.emit(text) class RecentAction(QAction): From a82cfc7d7f6e2fe370008b8853a196c29b923ed1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 12:18:51 +0530 Subject: [PATCH 20/38] ... --- src/qtcurve/style/qtcurve.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/qtcurve/style/qtcurve.cpp b/src/qtcurve/style/qtcurve.cpp index dcb24f00f1..b48eb9d421 100644 --- a/src/qtcurve/style/qtcurve.cpp +++ b/src/qtcurve/style/qtcurve.cpp @@ -1075,9 +1075,7 @@ void Style::init(bool initial) #endif } - opts.contrast=QSettings(QLatin1String("Trolltech")).value("/Qt/KDE/contrast", DEFAULT_CONTRAST).toInt(); - if(opts.contrast<0 || opts.contrast>10) - opts.contrast=DEFAULT_CONTRAST; + opts.contrast=DEFAULT_CONTRAST; //Changed by Kovid shadeColors(QApplication::palette().color(QPalette::Active, QPalette::Highlight), itsHighlightCols); shadeColors(QApplication::palette().color(QPalette::Active, QPalette::Background), itsBackgroundCols); @@ -1522,7 +1520,7 @@ void Style::polish(QApplication *app) void Style::polish(QPalette &palette) { - int contrast(QSettings(QLatin1String("Trolltech")).value("/Qt/KDE/contrast", DEFAULT_CONTRAST).toInt()); + int contrast = DEFAULT_CONTRAST; // Changed by Kovid bool newContrast(false); if(contrast<0 || contrast>10) From a6df18b4d2fe4a606cab2b0309ef4b1794d6679b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 12:19:10 +0530 Subject: [PATCH 21/38] ... --- src/calibre/ebooks/oeb/display/paged.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index ff16c05085..17049fb99a 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -181,6 +181,9 @@ class PagedDisplay window.scrollTo(pos, 0) animated_scroll: (pos, duration=1000, notify=true) -> + # Scroll the window to X-position pos in an animated fashion over + # duration milliseconds. If notify is true, py_bridge.animated_scroll_done is + # called. delta = pos - window.pageXOffset interval = 50 steps = Math.floor(duration/interval) From e127d1a5e1faefb18cf4c4a42ae272a9dc25cafb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 12:34:28 +0530 Subject: [PATCH 22/38] ... --- src/calibre/gui2/widgets.py | 105 +++++++++++------------------------- 1 file changed, 30 insertions(+), 75 deletions(-) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 4332bb651b..385884d008 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -6,14 +6,12 @@ Miscellaneous widgets used in the GUI import re, traceback, os from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction, - QListWidgetItem, QTextCharFormat, QApplication, - QSyntaxHighlighter, QCursor, QColor, QWidget, - QPixmap, QSplitterHandle, QToolButton, - QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, - QRegExp, QSettings, QSize, QSplitter, - QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, - QMenu, QStringListModel, QCompleter, QStringList, - QTimer, QRect, QFontDatabase, QGraphicsView) + QListWidgetItem, QTextCharFormat, QApplication, QSyntaxHighlighter, + QCursor, QColor, QWidget, QPixmap, QSplitterHandle, QToolButton, + QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, QRegExp, QSize, + QSplitter, QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, QMenu, + QStringListModel, QCompleter, QStringList, QTimer, QRect, + QFontDatabase, QGraphicsView, QByteArray) from calibre.constants import iswindows from calibre.gui2 import (NONE, error_dialog, pixmap_to_data, gprefs, @@ -803,69 +801,29 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ @classmethod def loadConfig(cls): Config = cls.Config - settings = QSettings() - def setDefaultString(name, default): - value = settings.value(name).toString() - if value.isEmpty(): - value = default - Config[name] = value for name in ("window", "shell"): - Config["%swidth" % name] = settings.value("%swidth" % name, - QVariant(QApplication.desktop() \ - .availableGeometry().width() / 2)).toInt()[0] - Config["%sheight" % name] = settings.value("%sheight" % name, - QVariant(QApplication.desktop() \ - .availableGeometry().height() / 2)).toInt()[0] - Config["%sy" % name] = settings.value("%sy" % name, - QVariant(0)).toInt()[0] - Config["toolbars"] = settings.value("toolbars").toByteArray() - Config["splitter"] = settings.value("splitter").toByteArray() - Config["shellx"] = settings.value("shellx", QVariant(0)).toInt()[0] - Config["windowx"] = settings.value("windowx", QVariant(QApplication \ - .desktop().availableGeometry().width() / 2)).toInt()[0] - Config["remembergeometry"] = settings.value("remembergeometry", - QVariant(True)).toBool() - Config["startwithshell"] = settings.value("startwithshell", - QVariant(True)).toBool() - Config["showwindowinfo"] = settings.value("showwindowinfo", - QVariant(True)).toBool() - setDefaultString("shellstartup", """\ - from __future__ import division - import codecs - import sys - sys.stdin = codecs.getreader("UTF8")(sys.stdin) - sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""") - setDefaultString("newfile", """\ - #!/usr/bin/env python - - from __future__ import division - - import sys - """) - Config["backupsuffix"] = settings.value("backupsuffix", - QVariant(".bak")).toString() - setDefaultString("beforeinput", "#>>>") - setDefaultString("beforeoutput", "#---") - Config["cwd"] = settings.value("cwd", QVariant(".")).toString() - Config["tooltipsize"] = settings.value("tooltipsize", - QVariant(150)).toInt()[0] - Config["maxlinestoscan"] = settings.value("maxlinestoscan", - QVariant(5000)).toInt()[0] - Config["pythondocpath"] = settings.value("pythondocpath", - QVariant("http://docs.python.org")).toString() - Config["autohidefinddialog"] = settings.value("autohidefinddialog", - QVariant(True)).toBool() - Config["findcasesensitive"] = settings.value("findcasesensitive", - QVariant(False)).toBool() - Config["findwholewords"] = settings.value("findwholewords", - QVariant(False)).toBool() - Config["tabwidth"] = settings.value("tabwidth", - QVariant(4)).toInt()[0] - Config["fontfamily"] = settings.value("fontfamily", - QVariant("monospace")).toString() - Config["fontsize"] = settings.value("fontsize", - QVariant(10)).toInt()[0] + Config["%swidth" % name] = QVariant(QApplication.desktop().availableGeometry().width() / 2).toInt()[0] + Config["%sheight" % name] = QVariant(QApplication.desktop().availableGeometry().height() / 2).toInt()[0] + Config["%sy" % name] = QVariant(0).toInt()[0] + Config["toolbars"] = QByteArray(b'') + Config["splitter"] = QByteArray(b'') + Config["shellx"] = QVariant(0).toInt()[0] + Config["windowx"] = QVariant(QApplication.desktop().availableGeometry().width() / 2).toInt()[0] + Config["remembergeometry"] = QVariant(True).toBool() + Config["startwithshell"] = QVariant(True).toBool() + Config["showwindowinfo"] = QVariant(True).toBool() + Config["backupsuffix"] = QVariant(".bak").toString() + Config["cwd"] = QVariant(".").toString() + Config["tooltipsize"] = QVariant(150).toInt()[0] + Config["maxlinestoscan"] = QVariant(5000).toInt()[0] + Config["pythondocpath"] = QVariant("http://docs.python.org").toString() + Config["autohidefinddialog"] = QVariant(True).toBool() + Config["findcasesensitive"] = QVariant(False).toBool() + Config["findwholewords"] = QVariant(False).toBool() + Config["tabwidth"] = QVariant(4).toInt()[0] + Config["fontfamily"] = QVariant("monospace").toString() + Config["fontsize"] = QVariant(10).toInt()[0] for name, color, bold, italic in ( ("normal", "#000000", False, False), ("keyword", "#000080", True, False), @@ -877,12 +835,9 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ ("number", "#924900", False, False), ("error", "#FF0000", False, False), ("pyqt", "#50621A", False, False)): - Config["%sfontcolor" % name] = settings.value( - "%sfontcolor" % name, QVariant(color)).toString() - Config["%sfontbold" % name] = settings.value( - "%sfontbold" % name, QVariant(bold)).toBool() - Config["%sfontitalic" % name] = settings.value( - "%sfontitalic" % name, QVariant(italic)).toBool() + Config["%sfontcolor" % name] = QVariant(color).toString() + Config["%sfontbold" % name] = QVariant(bold).toBool() + Config["%sfontitalic" % name] = QVariant(italic).toBool() @classmethod From 3c6879cfa9050ba94228d507ef14c99a5f179419 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 14:11:08 +0530 Subject: [PATCH 23/38] More robust handling of EINTR during IPC --- src/calibre/utils/ipc/__init__.py | 11 ++++++++- src/calibre/utils/ipc/proxy.py | 11 ++++----- src/calibre/utils/ipc/server.py | 14 +++++------- src/calibre/utils/ipc/simple_worker.py | 31 +++++++------------------- src/calibre/utils/ipc/worker.py | 5 +++-- 5 files changed, 32 insertions(+), 40 deletions(-) diff --git a/src/calibre/utils/ipc/__init__.py b/src/calibre/utils/ipc/__init__.py index 93db2e9fc7..f0a8eb0cec 100644 --- a/src/calibre/utils/ipc/__init__.py +++ b/src/calibre/utils/ipc/__init__.py @@ -6,13 +6,22 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os +import os, errno from threading import Thread from calibre.constants import iswindows, get_windows_username ADDRESS = None +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except (OSError, IOError) as e: + if e.errno == errno.EINTR: + continue + raise + def gui_socket_address(): global ADDRESS if ADDRESS is None: diff --git a/src/calibre/utils/ipc/proxy.py b/src/calibre/utils/ipc/proxy.py index 3b037a42a6..897d79b423 100644 --- a/src/calibre/utils/ipc/proxy.py +++ b/src/calibre/utils/ipc/proxy.py @@ -15,6 +15,7 @@ from functools import partial from calibre import as_unicode, prints from calibre.constants import iswindows, DEBUG +from calibre.utils.ipc import eintr_retry_call def _encode(msg): raw = cPickle.dumps(msg, -1) @@ -68,7 +69,7 @@ class Writer(Thread): break try: self.data_written = True - self.conn.send_bytes(x) + eintr_retry_call(self.conn.send_bytes, x) except Exception as e: self.resultq.put(as_unicode(e)) else: @@ -112,7 +113,7 @@ class Server(Thread): def run(self): while self.keep_going: try: - conn = self.listener.accept() + conn = eintr_retry_call(self.listener.accept) self.handle_client(conn) except: pass @@ -125,7 +126,7 @@ class Server(Thread): def _handle_client(self, conn): while True: try: - func_name, args, kwargs = conn.recv() + func_name, args, kwargs = eintr_retry_call(conn.recv) except EOFError: try: conn.close() @@ -156,8 +157,8 @@ class Server(Thread): import traceback # Try to tell the client process what error happened try: - conn.send_bytes(_encode(('failed', (unicode(e), - as_unicode(traceback.format_exc()))))) + eintr_retry_call(conn.send_bytes, (_encode(('failed', (unicode(e), + as_unicode(traceback.format_exc())))))) except: pass raise diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index a4c5b5d881..2912971bf6 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -14,6 +14,7 @@ from multiprocessing.connection import Listener, arbitrary_address from collections import deque from binascii import hexlify +from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc.launch import Worker from calibre.utils.ipc.worker import PARALLEL_FUNCS from calibre import detect_ncpus as cpu_count @@ -38,7 +39,7 @@ class ConnectedWorker(Thread): def start_job(self, job): notification = PARALLEL_FUNCS[job.name][-1] is not None - self.conn.send((job.name, job.args, job.kwargs, job.description)) + eintr_retry_call(self.conn.send, (job.name, job.args, job.kwargs, job.description)) if notification: self.start() else: @@ -48,7 +49,7 @@ class ConnectedWorker(Thread): def run(self): while True: try: - x = self.conn.recv() + x = eintr_retry_call(self.conn.recv) self.notifications.put(x) except BaseException: break @@ -129,12 +130,7 @@ class Server(Thread): 'CALIBRE_WORKER_KEY' : hexlify(self.auth_key), 'CALIBRE_WORKER_RESULT' : hexlify(rfile.encode('utf-8')), } - for i in range(2): - # Try launch twice as occasionally on OS X - # Listener.accept fails with EINTR - cw = self.do_launch(env, gui, redirect_output, rfile) - if isinstance(cw, ConnectedWorker): - break + cw = self.do_launch(env, gui, redirect_output, rfile) if isinstance(cw, basestring): raise CriticalError('Failed to launch worker process:\n'+cw) if DEBUG: @@ -146,7 +142,7 @@ class Server(Thread): try: w(redirect_output=redirect_output) - conn = self.listener.accept() + conn = eintr_retry_call(self.listener.accept) if conn is None: raise Exception('Failed to launch worker process') except BaseException: diff --git a/src/calibre/utils/ipc/simple_worker.py b/src/calibre/utils/ipc/simple_worker.py index 68d879f8a8..ca84f9d84d 100644 --- a/src/calibre/utils/ipc/simple_worker.py +++ b/src/calibre/utils/ipc/simple_worker.py @@ -14,6 +14,7 @@ from threading import Thread from contextlib import closing from calibre.constants import iswindows +from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc.launch import Worker class WorkerError(Exception): @@ -35,30 +36,18 @@ class ConnectedWorker(Thread): def run(self): conn = tb = None - for i in range(2): - # On OS X an EINTR can interrupt the accept() call - try: - conn = self.listener.accept() - break - except: - tb = traceback.format_exc() - pass + try: + conn = eintr_retry_call(self.listener.accept) + except: + tb = traceback.format_exc() if conn is None: self.tb = tb return self.accepted = True with closing(conn): try: - try: - conn.send(self.args) - except: - # Maybe an EINTR - conn.send(self.args) - try: - self.res = conn.recv() - except: - # Maybe an EINTR - self.res = conn.recv() + eintr_retry_call(conn.send, self.args) + self.res = eintr_retry_call(conn.recv) except: self.tb = traceback.format_exc() @@ -202,11 +191,7 @@ def main(): address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS'])) key = unhexlify(os.environ['CALIBRE_WORKER_KEY']) with closing(Client(address, authkey=key)) as conn: - try: - args = conn.recv() - except: - # Maybe EINTR - args = conn.recv() + args = eintr_retry_call(conn.recv) try: mod, func, args, kwargs, module_is_source_code = args if module_is_source_code: diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 0c43f00dca..08374400ac 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -16,6 +16,7 @@ from zipimport import ZipImportError from calibre import prints from calibre.constants import iswindows, isosx +from calibre.utils.ipc import eintr_retry_call PARALLEL_FUNCS = { 'lrfviewer' : @@ -75,7 +76,7 @@ class Progress(Thread): if x is None: break try: - self.conn.send(x) + eintr_retry_call(self.conn.send, x) except: break @@ -178,7 +179,7 @@ def main(): key = unhexlify(os.environ['CALIBRE_WORKER_KEY']) resultf = unhexlify(os.environ['CALIBRE_WORKER_RESULT']).decode('utf-8') with closing(Client(address, authkey=key)) as conn: - name, args, kwargs, desc = conn.recv() + name, args, kwargs, desc = eintr_retry_call(conn.recv) if desc: prints(desc) sys.stdout.flush() From 1233206750f2c355060237abf3060fde8814724e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 24 Jun 2012 11:19:57 +0200 Subject: [PATCH 24/38] Add a find box to the tag category editor --- src/calibre/gui2/dialogs/tag_list_editor.py | 25 +++- src/calibre/gui2/dialogs/tag_list_editor.ui | 150 +++++++++++--------- 2 files changed, 109 insertions(+), 66 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index aae68c4966..136eeaff24 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -5,7 +5,7 @@ from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray, QString, QSize) from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor -from calibre.gui2 import question_dialog, error_dialog, gprefs +from calibre.gui2 import question_dialog, error_dialog, info_dialog, gprefs from calibre.utils.icu import sort_key class NameTableWidgetItem(QTableWidgetItem): @@ -149,6 +149,9 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.table.itemChanged.connect(self.finish_editing) self.buttonBox.accepted.connect(self.accepted) + self.search_box.initialize('tag_list_search_box_' + cat_name) + self.search_button.clicked.connect(self.search_clicked) + try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: @@ -158,6 +161,26 @@ class TagListEditor(QDialog, Ui_TagListEditor): except: pass + def search_clicked(self): + search_for = unicode(self.search_box.text()) + if not search_for: + error_dialog(self, _('Find'), _('You must enter some text to search for'), + show=True, show_copy_button=False) + return + row = self.table.currentRow() + if row < 0: + row = 0 + rows = self.table.rowCount() + for i in range(0, rows): + row += 1 + if row >= rows: + row = 0 + item = self.table.item(row, 0) + if search_for in unicode(item.text()): + self.table.setCurrentItem(item) + return + info_dialog(self, _('Find'), _('No tag found'), show=True, show_copy_button=False) + def table_column_resized(self, col, old, new): self.table_column_widths = [] for c in range(0, self.table.columnCount()): diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index ccc404bf9c..cd36d57044 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -18,75 +18,88 @@ :/images/chapters.png:/images/chapters.png - - + + - - - - - - - Delete item from database. This will unapply the item from all books and then remove it from the database. - - - ... - - - - :/images/trash.png:/images/trash.png - - - - 32 - 32 - - - - - - - - Rename the item in every book where it is used. - - - ... - - - - :/images/edit_input.png:/images/edit_input.png - - - - 32 - 32 - - - - Ctrl+S - - - - - - - - - true - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - - + + + Search for an item in the Tag column + + + + + + + Find + + + Copy the selected color name to the clipboard + + - + + + + + + Delete item from database. This will unapply the item from all books and then remove it from the database. + + + ... + + + + :/images/trash.png:/images/trash.png + + + + 32 + 32 + + + + + + + + Rename the item in every book where it is used. + + + ... + + + + :/images/edit_input.png:/images/edit_input.png + + + + 32 + 32 + + + + Ctrl+S + + + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + Qt::Horizontal @@ -101,6 +114,13 @@ + + + HistoryLineEdit + QLineEdit +
widgets.h
+
+
From 45df192a7718140d0f50d54ddb5e21861ba12992 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 24 Jun 2012 11:51:41 +0200 Subject: [PATCH 25/38] Tag category searches should be caseless. --- src/calibre/gui2/dialogs/tag_list_editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index 136eeaff24..298641a9df 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -162,7 +162,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): pass def search_clicked(self): - search_for = unicode(self.search_box.text()) + search_for = icu_lower(unicode(self.search_box.text())) if not search_for: error_dialog(self, _('Find'), _('You must enter some text to search for'), show=True, show_copy_button=False) @@ -176,7 +176,7 @@ class TagListEditor(QDialog, Ui_TagListEditor): if row >= rows: row = 0 item = self.table.item(row, 0) - if search_for in unicode(item.text()): + if search_for in icu_lower(unicode(item.text())): self.table.setCurrentItem(item) return info_dialog(self, _('Find'), _('No tag found'), show=True, show_copy_button=False) From 2b293865c3ccd1ee571ab10fc11bd9bfff47565a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 24 Jun 2012 18:48:26 +0530 Subject: [PATCH 26/38] ISBNDB metadata plugin: Return results even though they have no comments --- src/calibre/ebooks/metadata/sources/isbndb.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/metadata/sources/isbndb.py b/src/calibre/ebooks/metadata/sources/isbndb.py index 7e15ad275e..567673b704 100644 --- a/src/calibre/ebooks/metadata/sources/isbndb.py +++ b/src/calibre/ebooks/metadata/sources/isbndb.py @@ -154,10 +154,11 @@ class ISBNDB(Source): total_results = int(bl.get('total_results')) shown_results = int(bl.get('shown_results')) for bd in bl.xpath('.//BookData'): - isbn = check_isbn(bd.get('isbn13', bd.get('isbn', None))) - if not isbn: + isbn = check_isbn(bd.get('isbn', None)) + isbn13 = check_isbn(bd.get('isbn13', None)) + if not isbn and not isbn13: continue - if orig_isbn and isbn != orig_isbn: + if orig_isbn and orig_isbn not in {isbn, isbn13}: continue title = tostring(bd.find('Title')) if not title: @@ -173,10 +174,6 @@ class ISBNDB(Source): if not authors: continue comments = tostring(bd.find('Summary')) - if not comments: - # Require comments, since without them the result is useless - # anyway - continue id_ = (title, tuple(authors)) if id_ in seen: continue From e0bd0df98fcc96c8a801e2a6b131208add2b6ba6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 09:48:58 +0530 Subject: [PATCH 27/38] ... --- src/calibre/ebooks/oeb/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 7e76d4cc0d..a852cff031 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -469,6 +469,8 @@ class DirContainer(object): return f.write(data) def exists(self, path): + if not path: + return False try: path = os.path.join(self.rootdir, self._unquote(path)) except ValueError: #Happens if path contains quoted special chars From 8cab25887d7f1107043fbc7afcf7e2aec00281b4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 09:56:36 +0530 Subject: [PATCH 28/38] SmileZilla by Will --- recipes/smilezilla.recipe | 114 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 recipes/smilezilla.recipe diff --git a/recipes/smilezilla.recipe b/recipes/smilezilla.recipe new file mode 100644 index 0000000000..242ee8c42a --- /dev/null +++ b/recipes/smilezilla.recipe @@ -0,0 +1,114 @@ + +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import BeautifulSoup +from calibre.ptempfile import PersistentTemporaryFile + +class SmileZilla(BasicNewsRecipe): + + title = 'SmileZilla' + language = 'en' + __author__ = "Will" + JOKES_INDEX = 'http://www.smilezilla.com/joke.do' + STORIES_INDEX = 'http://www.smilezilla.com/story.do' + description = 'Daily Jokes and funny stoires' + oldest_article = 1 + remove_tags = [ + ] + keep_only_tags = [] + no_stylesheets = True + simultaneous_downloads = 1 + articles_are_obfuscated = True + encoding = 'utf-8' + + remove_tags = [dict(name='table')] + + counter = {JOKES_INDEX: 0, STORIES_INDEX: 0 } + cache = {} + + def cached_fetch(self, url): + cache = self.cache + + if url in cache: + f = open(cache[url]) + html = f.read() + f.close() + return BeautifulSoup(html, fromEncoding=self.encoding) + + br = BasicNewsRecipe.get_browser() + response = br.open(url) + html = response.read() + soup = BeautifulSoup(html, fromEncoding=self.encoding) + for img in soup.findAll('img',src=True): + if img['src'].startswith('/'): + img['src'] = 'http://www.smilezilla.com' + img['src'] + pt = PersistentTemporaryFile('.html') + pt.write(str(soup.html).encode(self.encoding)) + pt.close() + cache[url] = pt.name + return soup + + def _get_entry(self,soup): + return soup.find('form', attrs={'name':'contentForm'}) + + def _get_section_title(self, soup): + title_div = soup.find('div', attrs={'class':'title'}) + return self.tag_to_string(title_div).strip() + + def parse_index(self): + articles = [] + + soup = self.cached_fetch(self.JOKES_INDEX) + jokes_entry = self._get_entry(soup) + section_title = self._get_section_title(soup) + todays_jokes = [] + for hr in enumerate(jokes_entry.findAll('hr')): + title = 'Joke ' + str(hr[0] + 1) + url = self.JOKES_INDEX + todays_jokes.append({'title':title, 'url':url, + 'description':'', 'date':''}) + articles.append((section_title,todays_jokes)) + + soup = self.cached_fetch(self.STORIES_INDEX) + entry = self._get_entry(soup) + section_title = self._get_section_title(soup) + + todays_stories = [] + for hr in enumerate(entry.findAll('hr')): + title = 'Story ' + str(hr[0] + 1) + current = hr[1] + while True: + current = current.findPrevious() + if current is None: + break + elif current.name == 'hr': + break + elif current.name == 'b': + title = title + ': ' + self.tag_to_string(current) + break + url = self.STORIES_INDEX + todays_stories.append({'title':title, 'url':url, + 'description':'', 'date':''}) + articles.append((section_title,todays_stories)) + + + return articles + + def get_obfuscated_article(self, url): + return self.cache[url] + + + def preprocess_raw_html(self,raw_html, url): + url = self.JOKES_INDEX if (self.cache[self.JOKES_INDEX] in url) else self.STORIES_INDEX + count = self.counter[url] +1 + self.counter[url] = count + soup = self.index_to_soup(raw_html) + entry = self._get_entry(soup) + soup2 = BeautifulSoup('') + body = soup2.find('body') + entries = str(entry).split('
') + body.insert(0,entries[count -1]) + + return str(soup2) + + + From 93a370ef8cbd049566ad2c873111c2b4b30df690 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 16:25:08 +0530 Subject: [PATCH 29/38] Paged display: Indexing now (mostly) works --- .../ebooks/oeb/display/indexing.coffee | 59 ++++++++----- src/calibre/ebooks/oeb/display/paged.coffee | 20 +++-- src/calibre/gui2/viewer/documentview.py | 21 ++++- src/calibre/gui2/viewer/main.py | 11 ++- src/calibre/gui2/viewer/toc.py | 82 +++++++++++++++++-- 5 files changed, 152 insertions(+), 41 deletions(-) diff --git a/src/calibre/ebooks/oeb/display/indexing.coffee b/src/calibre/ebooks/oeb/display/indexing.coffee index 11f73c1504..48f0697506 100644 --- a/src/calibre/ebooks/oeb/display/indexing.coffee +++ b/src/calibre/ebooks/oeb/display/indexing.coffee @@ -6,20 +6,34 @@ Released under the GPLv3 License ### -body_height = () -> - db = document.body - dde = document.documentElement - if db? and dde? - return Math.max(db.scrollHeight, dde.scrollHeight, db.offsetHeight, - dde.offsetHeight, db.clientHeight, dde.clientHeight) - return 0 +window_scroll_pos = (win=window) -> # {{{ + if typeof(win.pageXOffset) == 'number' + x = win.pageXOffset + y = win.pageYOffset + else # IE < 9 + if document.body and ( document.body.scrollLeft or document.body.scrollTop ) + x = document.body.scrollLeft + y = document.body.scrollTop + else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop) + y = document.documentElement.scrollTop + x = document.documentElement.scrollLeft + return [x, y] +# }}} -abstop = (elem) -> - ans = elem.offsetTop - while elem.offsetParent - elem = elem.offsetParent - ans += elem.offsetTop - return ans +viewport_to_document = (x, y, doc=window?.document) -> # {{{ + until doc == window.document + # We are in a frame + frame = doc.defaultView.frameElement + rect = frame.getBoundingClientRect() + x += rect.left + y += rect.top + doc = frame.ownerDocument + win = doc.defaultView + [wx, wy] = window_scroll_pos(win) + x += wx + y += wy + return [x, y] +# }}} class BookIndexing ### @@ -33,7 +47,7 @@ class BookIndexing constructor: () -> this.cache = {} - this.body_height_at_last_check = null + this.last_check = [null, null] cache_valid: (anchors) -> for a in anchors @@ -45,7 +59,9 @@ class BookIndexing return true anchor_positions: (anchors, use_cache=false) -> - if use_cache and body_height() == this.body_height_at_last_check and this.cache_valid(anchors) + body = document.body + doc_constant = body.scrollHeight == this.last_check[1] and body.scrollWidth == this.last_check[0] + if use_cache and doc_constant and this.cache_valid(anchors) return this.cache ans = {} @@ -56,19 +72,24 @@ class BookIndexing try result = document.evaluate( ".//*[local-name() = 'a' and @name='#{ anchor }']", - document.body, null, + body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) elem = result.singleNodeValue catch error # The anchor had a ' or other invalid char elem = null if elem == null - pos = body_height() + 10000 + pos = [body.scrollWidth+1000, body.scrollHeight+1000] else - pos = abstop(elem) + br = elem.getBoundingClientRect() + pos = viewport_to_document(br.left, br.top, elem.ownerDocument) + + if window.paged_display?.in_paged_mode + pos[0] = window.paged_display.column_at(pos[0]) ans[anchor] = pos + this.cache = ans - this.body_height_at_last_check = body_height() + this.last_check = [body.scrollWidth, body.scrollHeight] return ans if window? diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 17049fb99a..ff750ff3ba 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -170,9 +170,7 @@ class PagedDisplay if this.is_full_screen_layout window.scrollTo(0, 0) return - pos = 0 - until (pos <= xpos < pos + this.page_width) - pos += this.page_width + pos = Math.floor(xpos/this.page_width) * this.page_width limit = document.body.scrollWidth - this.screen_width pos = limit if pos > limit if animated @@ -180,6 +178,16 @@ class PagedDisplay else window.scrollTo(pos, 0) + column_at: (xpos) -> + # Return the number of the column that contains xpos + return Math.floor(xpos/this.page_width) + + column_boundaries: () -> + # Return the column numbers at the left edge and after the right edge + # of the viewport + l = this.column_at(window.pageXOffset + 10) + return [l, l + this.cols_per_screen] + animated_scroll: (pos, duration=1000, notify=true) -> # Scroll the window to X-position pos in an animated fashion over # duration milliseconds. If notify is true, py_bridge.animated_scroll_done is @@ -217,10 +225,7 @@ class PagedDisplay if this.is_full_screen_layout return 0 x = window.pageXOffset + Math.max(10, this.current_margin_side) - edge = Math.floor(x/this.page_width) * this.page_width - while edge < x - edge += this.page_width - return edge - this.page_width + return Math.floor(x/this.page_width) * this.page_width next_screen_location: () -> # The position to scroll to for the next screen (which could contain @@ -354,7 +359,6 @@ if window? window.paged_display = new PagedDisplay() # TODO: -# Indexing # Resizing of images # Full screen mode # Highlight on jump_to_anchor diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index dc7b557f3c..a6a8616307 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -202,7 +202,7 @@ class Document(QWebPage): # {{{ if not isinstance(self.anchor_positions, dict): # Some weird javascript error happened self.anchor_positions = {} - return self.anchor_positions + return {k:tuple(v) for k, v in self.anchor_positions.iteritems()} def switch_to_paged_mode(self, onresize=False): if onresize and not self.loaded_javascript: @@ -217,6 +217,13 @@ class Document(QWebPage): # {{{ sz.setWidth(sz.width()+side_margin) self.setPreferredContentsSize(sz) + @property + def column_boundaries(self): + if not self.loaded_javascript: + return (0, 1) + self.javascript(u'py_bridge.value = paged_display.column_boundaries()') + return tuple(self.bridge_value) + def after_resize(self): if self.in_paged_mode: self.setPreferredContentsSize(QSize()) @@ -558,6 +565,18 @@ class DocumentView(QWebView): # {{{ return (self.document.ypos, self.document.ypos + self.document.window_height) + @property + def viewport_rect(self): + # (left, top, right, bottom) of the viewport in document co-ordinates + # When in paged mode, left and right are the numbers of the columns + # at the left edge and *after* the right edge of the viewport + d = self.document + if d.in_paged_mode: + l, r = d.column_boundaries + else: + l, r = d.xpos, d.xpos + d.window_width + return (l, d.ypos, r, d.ypos + d.window_height) + def link_hovered(self, link, text, context): link, text = unicode(link), unicode(text) if link: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 8200169025..08ab731e51 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -683,7 +683,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos) + self.view.viewport_rect, self.view.document.in_paged_mode) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, False) @@ -693,7 +693,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos, backwards=True) + self.view.viewport_rect, self.view.document.in_paged_mode, + backwards=True) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, True) @@ -705,7 +706,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if anchor_positions is None: anchor_positions = self.view.document.read_anchor_positions() items = self.toc_model.update_indexing_state(self.current_index, - self.view.scroll_pos, anchor_positions) + self.view.viewport_rect, anchor_positions, + self.view.document.in_paged_mode) if items: self.toc.scrollTo(items[-1].index()) if pgns is not None: @@ -714,7 +716,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if pgns[0] is self.toc_model.currently_viewed_entry: entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos, + self.view.viewport_rect, + self.view.document.in_paged_mode, backwards=pgns[2], current_entry=pgns[1]) if entry is not None: self.pending_goto_next_section = ( diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index ae03b3ed26..b0e97bea65 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -93,9 +93,19 @@ class TOCItem(QStandardItem): def type(cls): return QStandardItem.UserType+10 - def update_indexing_state(self, spine_index, scroll_pos, anchor_map): + def update_indexing_state(self, spine_index, viewport_rect, anchor_map, + in_paged_mode): + if in_paged_mode: + self.update_indexing_state_paged(spine_index, viewport_rect, + anchor_map) + else: + self.update_indexing_state_unpaged(spine_index, viewport_rect, + anchor_map) + + def update_indexing_state_unpaged(self, spine_index, viewport_rect, + anchor_map): is_being_viewed = False - top, bottom = scroll_pos + top, bottom = viewport_rect[1], viewport_rect[3] # We use bottom-25 in the checks below to account for the case where # the next entry has some invisible margin that just overlaps with the # bottom of the screen. In this case it will appear to the user that @@ -103,6 +113,9 @@ class TOCItem(QStandardItem): # be larger than 25, but that's a decent compromise. Also we dont want # to count a partial line as being visible. + # We only care about y position + anchor_map = {k:v[1] for k, v in anchor_map.iteritems()} + if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, 0) @@ -115,7 +128,7 @@ class TOCItem(QStandardItem): # ancestors of this entry. psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors] psp = [x for x in psp if x >= start_pos] - # The end position. The first anchor whose pos is >= self.start_pos + # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the bottom # of the window +1 end_pos = min(psp) if psp else (bottom+1 if self.ends_at >= @@ -141,6 +154,51 @@ class TOCItem(QStandardItem): if changed: self.setFont(self.bold_font if is_being_viewed else self.normal_font) + def update_indexing_state_paged(self, spine_index, viewport_rect, + anchor_map): + is_being_viewed = False + + left, right = viewport_rect[0], viewport_rect[2] + left, right = (left, 0), (right, -1) + + if spine_index >= self.starts_at and spine_index <= self.ends_at: + # The position at which this anchor is present in the document + start_pos = anchor_map.get(self.start_anchor, (0, 0)) + psp = [] + if self.ends_at == spine_index: + # Anchors that could possibly indicate the start of the next + # section and therefore the end of this section. + # self.possible_end_anchors is a set of anchors belonging to + # toc entries with depth <= self.depth that are also not + # ancestors of this entry. + psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors] + psp = [x for x in psp if x >= start_pos] + # The end position. The first anchor whose pos is >= start_pos + # or if the end is not in this spine item, we set it to the column + # after the right edge of the viewport + end_pos = min(psp) if psp else (right if self.ends_at >= + spine_index else (0, 0)) + if spine_index > self.starts_at and spine_index < self.ends_at: + # The entire spine item is contained in this entry + is_being_viewed = True + elif (spine_index == self.starts_at and right > start_pos and + # This spine item contains the start + # The start position is before the end of the viewport + (spine_index != self.ends_at or left < end_pos)): + # The end position is after the start of the viewport + is_being_viewed = True + elif (spine_index == self.ends_at and left < end_pos and + # This spine item contains the end + # The end position is after the start of the viewport + (spine_index != self.starts_at or right > start_pos)): + # The start position is before the end of the viewport + is_being_viewed = True + + changed = is_being_viewed != self.is_being_viewed + self.is_being_viewed = is_being_viewed + if changed: + self.setFont(self.bold_font if is_being_viewed else self.normal_font) + def __repr__(self): return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment) @@ -183,20 +241,26 @@ class TOC(QStandardItemModel): self.currently_viewed_entry = t return items_being_viewed - def next_entry(self, spine_pos, anchor_map, scroll_pos, backwards=False, - current_entry=None): + def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode, + backwards=False, current_entry=None): current_entry = (self.currently_viewed_entry if current_entry is None else current_entry) if current_entry is None: return items = reversed(self.all_items) if backwards else self.all_items found = False - top = scroll_pos[0] + + if in_paged_mode: + start = viewport_rect[0] + anchor_map = {k:v[0] for k, v in anchor_map.iteritems()} + else: + start = viewport_rect[1] + anchor_map = {k:v[1] for k, v in anchor_map.iteritems()} + for item in items: if found: start_pos = anchor_map.get(item.start_anchor, 0) - if backwards and item.is_being_viewed and start_pos >= top: - # Going to this item will either not move the scroll - # position or cause to to *increase* instead of descresing + if backwards and item.is_being_viewed and start_pos >= start: + # This item will not cause any scrolling continue if item.starts_at != spine_pos or item.start_anchor: return item From 303ec370904809ac55211368390f52933bc1a3f1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 16:39:19 +0530 Subject: [PATCH 30/38] Paged display: Fix right-most margin not being set for long HTML documents --- src/calibre/gui2/viewer/documentview.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index a6a8616307..a711442ba2 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -213,8 +213,11 @@ class Document(QWebPage): # {{{ # columns extend beyond the boundaries (and margin) of body mf = self.mainFrame() sz = mf.contentsSize() - if sz.width() > self.window_width: - sz.setWidth(sz.width()+side_margin) + scroll_width = self.javascript('document.body.scrollWidth', int) + # At this point sz.width() is not reliable, presumably because Qt + # has not yet been updated + if scroll_width > self.window_width: + sz.setWidth(scroll_width+side_margin) self.setPreferredContentsSize(sz) @property From 401aecc771d9db5bd329e59f171cfb204a229b4f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 19:30:20 +0530 Subject: [PATCH 31/38] Add some texture to calibre generated covers --- resources/images/cover_texture.png | Bin 0 -> 11650 bytes session.vim | 1 + src/calibre/ebooks/__init__.py | 4 ++- src/calibre/utils/magick/draw.py | 10 +++++-- src/calibre/utils/magick/magick.c | 43 ++++++++++++++++++++++++++++- 5 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 resources/images/cover_texture.png diff --git a/resources/images/cover_texture.png b/resources/images/cover_texture.png new file mode 100644 index 0000000000000000000000000000000000000000..3fbc804ea88742ca60eafa47c6056a6f5b18eeb5 GIT binary patch literal 11650 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, opacity)) return magick_set_exception(self->wand); + + Py_RETURN_NONE; +} +// }}} + // Image attr list {{{ static PyMethodDef magick_Image_methods[] = { {"destroy", (PyCFunction)magick_Image_destroy, METH_VARARGS, @@ -1145,6 +1161,14 @@ static PyMethodDef magick_Image_methods[] = { "compose(img, left, top, op) \n\n Compose img using operation op at (left, top)" }, + {"texture", (PyCFunction)magick_Image_texture, METH_VARARGS, + "texture(img)) \n\n Repeatedly tile img across and down the canvas." + }, + + {"set_opacity", (PyCFunction)magick_Image_set_opacity, METH_VARARGS, + "set_opacity(opacity)) \n\n Set the opacity of this image (between 0.0 - transparent and 1.0 - opaque)" + }, + {"copy", (PyCFunction)magick_Image_copy, METH_VARARGS, "copy(img) \n\n Copy img to self." }, @@ -1335,6 +1359,23 @@ magick_Image_copy(magick_Image *self, PyObject *args, PyObject *kwargs) } // }}} +// Image.texture {{{ +static PyObject * +magick_Image_texture(magick_Image *self, PyObject *args, PyObject *kwargs) { + PyObject *img; + magick_Image *texture; + + NULL_CHECK(NULL) + + if (!PyArg_ParseTuple(args, "O!", &magick_ImageType, &img)) return NULL; + texture = (magick_Image*)img; + if (!IsMagickWand(texture->wand)) {PyErr_SetString(PyExc_TypeError, "Not a valid ImageMagick wand"); return NULL;} + + self->wand = MagickTextureImage(self->wand, texture->wand); + + Py_RETURN_NONE; +} + // }}} // Module functions {{{ From b5f79cb81e8a1757411deae860993151b664edc1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 26 Jun 2012 19:45:35 +0530 Subject: [PATCH 32/38] New Statesman by NotTaken --- recipes/new_statesman.recipe | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 recipes/new_statesman.recipe diff --git a/recipes/new_statesman.recipe b/recipes/new_statesman.recipe new file mode 100644 index 0000000000..ac3455f8d1 --- /dev/null +++ b/recipes/new_statesman.recipe @@ -0,0 +1,66 @@ +__license__ = 'GPL v3' +''' +newstatesman.com +''' +from calibre.web.feeds.news import BasicNewsRecipe + +class NewStatesman(BasicNewsRecipe): + + title = 'New Statesman' + language = 'en_GB' + __author__ = "NotTaken" + description = "Britain's Current Affairs & Politics Magazine (Weekly)" + oldest_article = 7.0 + no_stylesheets = True + use_embedded_content = False + keep_only_tags = [dict(attrs={'class' : 'node'})] + remove_tags_after = [ + dict(attrs={'class' : lambda x: x and 'content123' in x}) + ] + remove_tags = [ + dict(attrs={'class' : lambda x: x and 'links_bookmark' in x}) + ] + + extra_css = ''' + .title-main {font-size: x-large;} + h2 { font-size: small; } + h1 { font-size: medium; } + .field-field-nodeimage-title { + font-size: small; + color: #3C3C3C; + } + .link_col { + font-size: x-small; + } + ''' + + + feeds = [ + (u'Politics', + u'http://www.newstatesman.com/politics.rss'), + (u'Business', + u'http://www.newstatesman.com/business.rss'), + (u'Economics', + u'http://www.newstatesman.com/economics.rss'), + (u'Culture', + u'http://www.newstatesman.com/culture.rss'), + (u'Media', + u'http://www.newstatesman.com/media.rss'), + (u'Books', + u'http://www.newstatesman.com/taxonomy/term/feed/27'), + (u'Life & Society', + u'http://www.newstatesman.com/taxonomyfeed/11'), + (u'World Affairs', + u'http://www.newstatesman.com/world-affairs.rss'), + (u'Sci-Tech', + u'http://www.newstatesman.com/feeds/topics/sci-tech.rss'), + ] + + + + + + + + + From 340303b9d6c6929bd7a4f032ad5e1dd96dc9ba7f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 09:32:43 +0530 Subject: [PATCH 33/38] ... --- src/calibre/gui2/dialogs/catalog.ui | 2 +- src/calibre/gui2/dialogs/confirm_delete_location.ui | 2 +- src/calibre/gui2/dialogs/conversion_error.ui | 2 +- src/calibre/gui2/layout.py | 2 +- src/calibre/gui2/main.py | 2 +- src/calibre/gui2/ui.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui index 7f3951b87e..cf51ac8848 100644 --- a/src/calibre/gui2/dialogs/catalog.ui +++ b/src/calibre/gui2/dialogs/catalog.ui @@ -15,7 +15,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/dialogs/confirm_delete_location.ui b/src/calibre/gui2/dialogs/confirm_delete_location.ui index 212d96584f..f375eb7207 100644 --- a/src/calibre/gui2/dialogs/confirm_delete_location.ui +++ b/src/calibre/gui2/dialogs/confirm_delete_location.ui @@ -54,7 +54,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/dialogs/conversion_error.ui b/src/calibre/gui2/dialogs/conversion_error.ui index c51c868d1b..0b21147fee 100644 --- a/src/calibre/gui2/dialogs/conversion_error.ui +++ b/src/calibre/gui2/dialogs/conversion_error.ui @@ -14,7 +14,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index eadf97191a..df277aa13c 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -255,7 +255,7 @@ class MainWindowMixin(object): # {{{ def __init__(self, db): self.setObjectName('MainWindow') - self.setWindowIcon(QIcon(I('library.png'))) + self.setWindowIcon(QIcon(I('lt.png'))) self.setWindowTitle(__appname__) self.setContextMenuPolicy(Qt.NoContextMenu) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index d787dbae58..9b80b7bddc 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -60,7 +60,7 @@ def init_qt(args): QCoreApplication.setApplicationName(APP_UID) app = Application(args) actions = tuple(Main.create_application_menubar()) - app.setWindowIcon(QIcon(I('library.png'))) + app.setWindowIcon(QIcon(I('lt.png'))) return app, opts, args, actions diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 713aa77e29..a597445f43 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -228,7 +228,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) self.viewers = collections.deque() - self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) + self.system_tray_icon = SystemTrayIcon(QIcon(I('lt.png')), self) self.system_tray_icon.setToolTip('calibre') self.system_tray_icon.tooltip_requested.connect( self.job_manager.show_tooltip) From 8459b0227617e710fa3db02d08a2c8c192078dc1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 09:55:25 +0530 Subject: [PATCH 34/38] ... --- recipes/new_statesman.recipe | 50 ++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/recipes/new_statesman.recipe b/recipes/new_statesman.recipe index ac3455f8d1..55524d09ff 100644 --- a/recipes/new_statesman.recipe +++ b/recipes/new_statesman.recipe @@ -9,22 +9,26 @@ class NewStatesman(BasicNewsRecipe): title = 'New Statesman' language = 'en_GB' __author__ = "NotTaken" - description = "Britain's Current Affairs & Politics Magazine (Weekly)" - oldest_article = 7.0 + description = "Britain's Current Affairs & Politics Magazine (bi-weekly)" + oldest_article = 4.0 no_stylesheets = True use_embedded_content = False + remove_empty_feeds = True + keep_only_tags = [dict(attrs={'class' : 'node'})] + remove_tags_after = [ dict(attrs={'class' : lambda x: x and 'content123' in x}) ] - remove_tags = [ - dict(attrs={'class' : lambda x: x and 'links_bookmark' in x}) - ] + remove_tags = [ + dict(attrs={'class' : lambda x: x and 'links_bookmark' in x}) + ] + extra_css = ''' .title-main {font-size: x-large;} h2 { font-size: small; } - h1 { font-size: medium; } + h1 { font-size: medium; } .field-field-nodeimage-title { font-size: small; color: #3C3C3C; @@ -34,6 +38,24 @@ class NewStatesman(BasicNewsRecipe): } ''' + processed_urls = [] + + def populate_article_metadata(self, article, soup, first): + if first and hasattr(self, 'add_toc_thumbnail'): + pic = soup.find('img') + if pic is not None: + self.add_toc_thumbnail(article,pic['src']) + + def get_article_url(self, article): + url = BasicNewsRecipe.get_article_url(self,article) + + if url in self.processed_urls: + self.log('skipping duplicate article: %s' %article.title ) + return None + + self.processed_urls.append(url) + return url + feeds = [ (u'Politics', @@ -54,13 +76,15 @@ class NewStatesman(BasicNewsRecipe): u'http://www.newstatesman.com/world-affairs.rss'), (u'Sci-Tech', u'http://www.newstatesman.com/feeds/topics/sci-tech.rss'), + (u'Others', + u'http://www.newstatesman.com/feeds_allsite/site_feed.php'), ] - - - - - - - + + + + + + + From 9b9799f5b1a5e016a18be0c796c1c5513f55bbbf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 10:31:43 +0530 Subject: [PATCH 35/38] CT24 by zoidozoido --- recipes/ct24.recipe | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 recipes/ct24.recipe diff --git a/recipes/ct24.recipe b/recipes/ct24.recipe new file mode 100644 index 0000000000..b9ccd2dd84 --- /dev/null +++ b/recipes/ct24.recipe @@ -0,0 +1,12 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1339974788(BasicNewsRecipe): + title = u'\u010cT24' + oldest_article = 1 + language = 'cs' + __author__ = 'zoidozoido' + max_articles_per_feed = 100 + auto_cleanup = True + + feeds = [(u'Hlavn\xed zpr\xe1vy', u'http://www.ceskatelevize.cz/ct24/rss/hlavni-zpravy/'), (u'Dom\xe1c\xed', u'http://www.ceskatelevize.cz/ct24/rss/domaci/'), (u'Sv\u011bt', u'http://www.ceskatelevize.cz/ct24/rss/svet/'), (u'Regiony', u'http://www.ceskatelevize.cz/ct24/rss/regiony/'), (u'Kultura', u'http://www.ceskatelevize.cz/ct24/rss/kultura/'), (u'Ekonomika', u'http://www.ceskatelevize.cz/ct24/rss/ekonomika/'), (u'Sport - hlavn\xed zpr\xe1vy', u'http://www.ceskatelevize.cz/ct4/rss/hlavni-zpravy/'), (u'OH 2012', u'http://www.ceskatelevize.cz/ct4/rss/oh-2012/')] + remove_tags = [dict(name='img')] From 8476de6507443d227de991344a2e2e8c01d37c78 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 13:44:06 +0530 Subject: [PATCH 36/38] ... --- src/calibre/utils/ipc/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/utils/ipc/__init__.py b/src/calibre/utils/ipc/__init__.py index f0a8eb0cec..78191a225b 100644 --- a/src/calibre/utils/ipc/__init__.py +++ b/src/calibre/utils/ipc/__init__.py @@ -17,8 +17,8 @@ def eintr_retry_call(func, *args, **kwargs): while True: try: return func(*args, **kwargs) - except (OSError, IOError) as e: - if e.errno == errno.EINTR: + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: continue raise From 08d2dbe8b733c54ccd3b84d7bbc99bf380212183 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 15:32:55 +0530 Subject: [PATCH 37/38] Windows: Disable the new UI style if the color depth of the desktop is less than 32 bits per pixel --- src/calibre/gui2/__init__.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 3ae1633230..d5e93c48c8 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -248,6 +248,18 @@ def available_width(): desktop = QCoreApplication.instance().desktop() return desktop.availableGeometry().width() +def get_windows_color_depth(): + import win32gui, win32con, win32print + hwin = win32gui.GetDesktopWindow() + hwindc = win32gui.GetWindowDC(hwin) + ans = win32print.GetDeviceCaps(hwindc, win32con.BITSPIXEL) + win32gui.ReleaseDC(hwin, hwindc) + return ans + +def get_screen_dpi(): + d = QApplication.desktop() + return (d.logicalDpiX(), d.logicalDpiY()) + _is_widescreen = None def is_widescreen(): @@ -791,7 +803,18 @@ class Application(QApplication): font.setStretch(s) QApplication.setFont(font) - if force_calibre_style or gprefs['ui_style'] != 'system': + depth_ok = True + if iswindows: + # There are some people that still run 16 bit winxp installs. The + # new style does not render well on 16bit machines. + try: + depth_ok = get_windows_color_depth() >= 32 + except: + import traceback + traceback.print_exc() + + if force_calibre_style or (depth_ok and gprefs['ui_style'] != + 'system'): self.load_calibre_style() else: st = self.style() From 8590ddce31f5e11c50a9ddd00afbe8dcfc176c1b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 27 Jun 2012 19:40:50 +0530 Subject: [PATCH 38/38] iTunes glue code --- src/calibre/devices/apple/itunes.py | 280 ++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 src/calibre/devices/apple/itunes.py diff --git a/src/calibre/devices/apple/itunes.py b/src/calibre/devices/apple/itunes.py new file mode 100644 index 0000000000..e80bc4c81f --- /dev/null +++ b/src/calibre/devices/apple/itunes.py @@ -0,0 +1,280 @@ +version = 1.1 +path = '/Applications/iTunes.app' + +classes = \ +[('print_settings', 'pset'), + ('application', 'capp'), + ('artwork', 'cArt'), + ('audio_CD_playlist', 'cCDP'), + ('audio_CD_track', 'cCDT'), + ('browser_window', 'cBrW'), + ('device_playlist', 'cDvP'), + ('device_track', 'cDvT'), + ('encoder', 'cEnc'), + ('EQ_preset', 'cEQP'), + ('EQ_window', 'cEQW'), + ('file_track', 'cFlT'), + ('folder_playlist', 'cFoP'), + ('item', 'cobj'), + ('library_playlist', 'cLiP'), + ('playlist', 'cPly'), + ('playlist_window', 'cPlW'), + ('radio_tuner_playlist', 'cRTP'), + ('shared_track', 'cShT'), + ('source', 'cSrc'), + ('track', 'cTrk'), + ('URL_track', 'cURT'), + ('user_playlist', 'cUsP'), + ('visual', 'cVis'), + ('window', 'cwin')] + +enums = \ +[('track_listing', 'kTrk'), + ('album_listing', 'kAlb'), + ('cd_insert', 'kCDi'), + ('standard', 'lwst'), + ('detailed', 'lwdt'), + ('stopped', 'kPSS'), + ('playing', 'kPSP'), + ('paused', 'kPSp'), + ('fast_forwarding', 'kPSF'), + ('rewinding', 'kPSR'), + ('off', 'kRpO'), + ('one', 'kRp1'), + ('all', 'kAll'), + ('small', 'kVSS'), + ('medium', 'kVSM'), + ('large', 'kVSL'), + ('library', 'kLib'), + ('iPod', 'kPod'), + ('audio_CD', 'kACD'), + ('MP3_CD', 'kMCD'), + ('device', 'kDev'), + ('radio_tuner', 'kTun'), + ('shared_library', 'kShd'), + ('unknown', 'kUnk'), + ('albums', 'kSrL'), + ('artists', 'kSrR'), + ('composers', 'kSrC'), + ('displayed', 'kSrV'), + ('songs', 'kSrS'), + ('none', 'kNon'), + ('Books', 'kSpA'), + ('folder', 'kSpF'), + ('Genius', 'kSpG'), + ('iTunes_U', 'kSpU'), + ('Library', 'kSpL'), + ('Movies', 'kSpI'), + ('Music', 'kSpZ'), + ('Party_Shuffle', 'kSpS'), + ('Podcasts', 'kSpP'), + ('Purchased_Music', 'kSpM'), + ('TV_Shows', 'kSpT'), + ('movie', 'kVdM'), + ('music_video', 'kVdV'), + ('TV_show', 'kVdT'), + ('user', 'kRtU'), + ('computed', 'kRtC')] + +properties = \ +[('copies', 'lwcp'), + ('collating', 'lwcl'), + ('starting_page', 'lwfp'), + ('ending_page', 'lwlp'), + ('pages_across', 'lwla'), + ('pages_down', 'lwld'), + ('error_handling', 'lweh'), + ('requested_print_time', 'lwqt'), + ('printer_features', 'lwpf'), + ('fax_number', 'faxn'), + ('target_printer', 'trpr'), + ('current_encoder', 'pEnc'), + ('current_EQ_preset', 'pEQP'), + ('current_playlist', 'pPla'), + ('current_stream_title', 'pStT'), + ('current_stream_URL', 'pStU'), + ('current_track', 'pTrk'), + ('current_visual', 'pVis'), + ('EQ_enabled', 'pEQ '), + ('fixed_indexing', 'pFix'), + ('frontmost', 'pisf'), + ('full_screen', 'pFSc'), + ('name', 'pnam'), + ('mute', 'pMut'), + ('player_position', 'pPos'), + ('player_state', 'pPlS'), + ('selection', 'sele'), + ('sound_volume', 'pVol'), + ('version', 'vers'), + ('visuals_enabled', 'pVsE'), + ('visual_size', 'pVSz'), + ('data', 'pPCT'), + ('description', 'pDes'), + ('downloaded', 'pDlA'), + ('format', 'pFmt'), + ('kind', 'pKnd'), + ('raw_data', 'pRaw'), + ('artist', 'pArt'), + ('compilation', 'pAnt'), + ('composer', 'pCmp'), + ('disc_count', 'pDsC'), + ('disc_number', 'pDsN'), + ('genre', 'pGen'), + ('year', 'pYr '), + ('location', 'pLoc'), + ('minimized', 'pMin'), + ('view', 'pPly'), + ('band_1', 'pEQ1'), + ('band_2', 'pEQ2'), + ('band_3', 'pEQ3'), + ('band_4', 'pEQ4'), + ('band_5', 'pEQ5'), + ('band_6', 'pEQ6'), + ('band_7', 'pEQ7'), + ('band_8', 'pEQ8'), + ('band_9', 'pEQ9'), + ('band_10', 'pEQ0'), + ('modifiable', 'pMod'), + ('preamp', 'pEQA'), + ('update_tracks', 'pUTC'), + ('container', 'ctnr'), + ('id', 'ID '), + ('index', 'pidx'), + ('persistent_ID', 'pPIS'), + ('duration', 'pDur'), + ('parent', 'pPlP'), + ('shuffle', 'pShf'), + ('size', 'pSiz'), + ('song_repeat', 'pRpt'), + ('special_kind', 'pSpK'), + ('time', 'pTim'), + ('visible', 'pvis'), + ('capacity', 'capa'), + ('free_space', 'frsp'), + ('album', 'pAlb'), + ('album_artist', 'pAlA'), + ('album_rating', 'pAlR'), + ('album_rating_kind', 'pARk'), + ('bit_rate', 'pBRt'), + ('bookmark', 'pBkt'), + ('bookmarkable', 'pBkm'), + ('bpm', 'pBPM'), + ('category', 'pCat'), + ('comment', 'pCmt'), + ('database_ID', 'pDID'), + ('date_added', 'pAdd'), + ('enabled', 'enbl'), + ('episode_ID', 'pEpD'), + ('episode_number', 'pEpN'), + ('EQ', 'pEQp'), + ('finish', 'pStp'), + ('gapless', 'pGpl'), + ('grouping', 'pGrp'), + ('long_description', 'pLds'), + ('lyrics', 'pLyr'), + ('modification_date', 'asmo'), + ('played_count', 'pPlC'), + ('played_date', 'pPlD'), + ('podcast', 'pTPc'), + ('rating', 'pRte'), + ('rating_kind', 'pRtk'), + ('release_date', 'pRlD'), + ('sample_rate', 'pSRt'), + ('season_number', 'pSeN'), + ('shufflable', 'pSfa'), + ('skipped_count', 'pSkC'), + ('skipped_date', 'pSkD'), + ('show', 'pShw'), + ('sort_album', 'pSAl'), + ('sort_artist', 'pSAr'), + ('sort_album_artist', 'pSAA'), + ('sort_name', 'pSNm'), + ('sort_composer', 'pSCm'), + ('sort_show', 'pSSN'), + ('start', 'pStr'), + ('track_count', 'pTrC'), + ('track_number', 'pTrN'), + ('unplayed', 'pUnp'), + ('video_kind', 'pVdK'), + ('volume_adjustment', 'pAdj'), + ('address', 'pURL'), + ('shared', 'pShr'), + ('smart', 'pSmt'), + ('bounds', 'pbnd'), + ('closeable', 'hclb'), + ('collapseable', 'pWSh'), + ('collapsed', 'wshd'), + ('position', 'ppos'), + ('resizable', 'prsz'), + ('zoomable', 'iszm'), + ('zoomed', 'pzum')] + +elements = \ +[('artworks', 'cArt'), + ('audio_CD_playlists', 'cCDP'), + ('audio_CD_tracks', 'cCDT'), + ('browser_windows', 'cBrW'), + ('device_playlists', 'cDvP'), + ('device_tracks', 'cDvT'), + ('encoders', 'cEnc'), + ('EQ_presets', 'cEQP'), + ('EQ_windows', 'cEQW'), + ('file_tracks', 'cFlT'), + ('folder_playlists', 'cFoP'), + ('items', 'cobj'), + ('library_playlists', 'cLiP'), + ('playlists', 'cPly'), + ('playlist_windows', 'cPlW'), + ('radio_tuner_playlists', 'cRTP'), + ('shared_tracks', 'cShT'), + ('sources', 'cSrc'), + ('tracks', 'cTrk'), + ('URL_tracks', 'cURT'), + ('user_playlists', 'cUsP'), + ('visuals', 'cVis'), + ('windows', 'cwin'), + ('application', 'capp'), + ('print_settings', 'pset')] + +commands = \ +[('set', 'coresetd', [('to', 'data')]), + ('exists', 'coredoex', []), + ('move', 'coremove', [('to', 'insh')]), + ('subscribe', 'hookpSub', []), + ('playpause', 'hookPlPs', []), + ('download', 'hookDwnl', []), + ('close', 'coreclos', []), + ('open', 'aevtodoc', []), + ('open_location', 'GURLGURL', []), + ('quit', 'aevtquit', []), + ('pause', 'hookPaus', []), + ('make', + 'corecrel', + [('new', 'kocl'), ('at', 'insh'), ('with_properties', 'prdt')]), + ('duplicate', 'coreclon', [('to', 'insh')]), + ('print_', + 'aevtpdoc', + [('print_dialog', 'pdlg'), + ('with_properties', 'prdt'), + ('kind', 'pKnd'), + ('theme', 'pThm')]), + ('add', 'hookAdd ', [('to', 'insh')]), + ('rewind', 'hookRwnd', []), + ('play', 'hookPlay', [('once', 'POne')]), + ('run', 'aevtoapp', []), + ('resume', 'hookResu', []), + ('updatePodcast', 'hookUpd1', []), + ('next_track', 'hookNext', []), + ('stop', 'hookStop', []), + ('search', 'hookSrch', [('for_', 'pTrm'), ('only', 'pAre')]), + ('updateAllPodcasts', 'hookUpdp', []), + ('update', 'hookUpdt', []), + ('previous_track', 'hookPrev', []), + ('fast_forward', 'hookFast', []), + ('count', 'corecnte', [('each', 'kocl')]), + ('reveal', 'hookRevl', []), + ('convert', 'hookConv', []), + ('eject', 'hookEjct', []), + ('back_track', 'hookBack', []), + ('refresh', 'hookRfrs', []), + ('delete', 'coredelo', [])]