diff --git a/resources/images/hotmail.png b/resources/images/hotmail.png new file mode 100644 index 0000000000..862b59c933 Binary files /dev/null and b/resources/images/hotmail.png differ diff --git a/resources/images/news/moscow_times.png b/resources/images/news/moscow_times.png new file mode 100644 index 0000000000..34c3117974 Binary files /dev/null and b/resources/images/news/moscow_times.png differ diff --git a/resources/recipes/globe_and_mail.recipe b/resources/recipes/globe_and_mail.recipe index b6e6b5c25b..4cc76688c1 100644 --- a/resources/recipes/globe_and_mail.recipe +++ b/resources/recipes/globe_and_mail.recipe @@ -1,7 +1,7 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__copyright__ = '2010, Szing' __docformat__ = 'restructuredtext en' ''' @@ -10,49 +10,52 @@ globeandmail.com from calibre.web.feeds.news import BasicNewsRecipe -class GlobeAndMail(BasicNewsRecipe): - title = u'Globe and Mail' - language = 'en_CA' - - __author__ = 'Kovid Goyal' +class AdvancedUserRecipe1287083651(BasicNewsRecipe): + title = u'Globe & Mail' + __license__ = 'GPL v3' + __author__ = 'Szing' oldest_article = 2 - max_articles_per_feed = 10 no_stylesheets = True - extra_css = ''' - h3 {font-size: 22pt; font-weight:bold; margin:0px; padding:0px 0px 8pt 0px;} - h4 {margin-top: 0px;} - #byline { font-family: monospace; font-weight:bold; } - #placeline {font-weight:bold;} - #credit {margin-top:0px;} - .tag {font-size: 22pt;}''' - description = 'Canada\'s national newspaper' - keep_only_tags = [dict(name='article')] - remove_tags = [dict(name='aside'), - dict(name='footer'), - dict(name='div', attrs={'class':(lambda x: isinstance(x, (str,unicode)) and 'articlecommentcountholder' in x.split(' '))}), - dict(name='ul', attrs={'class':(lambda x: isinstance(x, (str,unicode)) and 'articletoolbar' in x.split(' '))}), - ] - feeds = [ - (u'Latest headlines', u'http://www.theglobeandmail.com/?service=rss'), - (u'Top stories', u'http://www.theglobeandmail.com/?service=rss&feed=topstories'), - (u'National', u'http://www.theglobeandmail.com/news/national/?service=rss'), - (u'Politics', u'http://www.theglobeandmail.com/news/politics/?service=rss'), - (u'World', u'http://www.theglobeandmail.com/news/world/?service=rss'), - (u'Business', u'http://www.theglobeandmail.com/report-on-business/?service=rss'), - (u'Opinions', u'http://www.theglobeandmail.com/news/opinions/?service=rss'), - (u'Columnists', u'http://www.theglobeandmail.com/news/opinions/columnists/?service=rss'), - (u'Globe Investor', u'http://www.theglobeandmail.com/globe-investor/?service=rss'), - (u'Sports', u'http://www.theglobeandmail.com/sports/?service=rss'), - (u'Technology', u'http://www.theglobeandmail.com/news/technology/?service=rss'), - (u'Arts', u'http://www.theglobeandmail.com/news/arts/?service=rss'), - (u'Life', u'http://www.theglobeandmail.com/life/?service=rss'), - (u'Blogs', u'http://www.theglobeandmail.com/blogs/?service=rss'), - (u'Real Estate', u'http://www.theglobeandmail.com/real-estate/?service=rss'), - (u'Auto', u'http://www.theglobeandmail.com/auto/?service=rss') - ] + max_articles_per_feed = 100 + encoding = 'utf8' + publisher = 'Globe & Mail' + language = 'en_CA' + extra_css = 'p.meta {font-size:75%}\n .redtext {color: red;}\n .byline {font-size: 70%}' - def get_article_url(self, article): - url = BasicNewsRecipe.get_article_url(self, article) - if '/video/' not in url: - return url + feeds = [ + (u'Top National Stories', u'http://www.theglobeandmail.com/news/national/?service=rss'), + (u'Business', u'http://www.theglobeandmail.com/report-on-business/?service=rss'), + (u'Commentary', u'http://www.theglobeandmail.com/report-on-business/commentary/?service=rss'), + (u'Blogs', u'http://www.theglobeandmail.com/blogs/?service=rss'), + (u'Facts & Arguments', u'http://www.theglobeandmail.com/life/facts-and-arguments/?service=rss'), + (u'Technology', u'http://www.theglobeandmail.com/news/technology/?service=rss'), + (u'Investing', u'http://www.theglobeandmail.com/globe-investor/?service=rss'), + (u'Top Polical Stories', u'http://www.theglobeandmail.com/news/politics/?service=rss'), + (u'Arts', u'http://www.theglobeandmail.com/news/arts/?service=rss'), + (u'Life', u'http://www.theglobeandmail.com/life/?service=rss'), + (u'Real Estate', u'http://www.theglobeandmail.com/real-estate/?service=rss'), + (u'Auto', u'http://www.theglobeandmail.com/sports/?service=rss'), + (u'Sports', u'http://www.theglobeandmail.com/auto/?service=rss') + ] + + keep_only_tags = [ + dict(name='h1'), + dict(name='h2', attrs={'id':'articletitle'}), + dict(name='p', attrs={'class':['leadText', 'meta', 'leadImage', 'redtext byline', 'bodyText']}), + dict(name='div', attrs={'class':['news','articlemeta','articlecopy']}), + dict(name='id', attrs={'class':'article'}), + dict(name='table', attrs={'class':'todays-market'}), + dict(name='header', attrs={'id':'leadheader'}) + ] + + remove_tags = [ + dict(name='div', attrs={'id':['tabInside', 'ShareArticles', 'topStories']}) + ] + + #this has to be here or the text in the article appears twice. + remove_tags_after = [dict(id='article')] + + #Use the mobile version rather than the web version + def print_version(self, url): + return url + '&service=mobile' diff --git a/resources/recipes/moscow_times.recipe b/resources/recipes/moscow_times.recipe index 3105aba58e..9d178e8c53 100644 --- a/resources/recipes/moscow_times.recipe +++ b/resources/recipes/moscow_times.recipe @@ -1,31 +1,33 @@ -#!/usr/bin/env python - __license__ = 'GPL v3' -__copyright__ = '2008, Darko Miletic ' +__copyright__ = '2008-2010, Darko Miletic ' ''' -moscowtimes.ru +www.themoscowtimes.com ''' from calibre.web.feeds.news import BasicNewsRecipe class Moscowtimes(BasicNewsRecipe): - title = u'The Moscow Times' + title = 'The Moscow Times' __author__ = 'Darko Miletic and Sujata Raman' - description = 'News from Russia' - language = 'en' - lang = 'en' - oldest_article = 7 + description = 'The Moscow Times is a daily English-language newspaper featuring objective, reliable news on business, politics, sports and culture in Moscow, in Russia and the former Soviet Union (CIS).' + category = 'Russia, Moscow, Russian news, Moscow news, Russian newspaper, daily news, independent news, reliable news, USSR, Soviet Union, CIS, Russian politics, Russian business, Russian culture, Russian opinion, St Petersburg, Saint Petersburg' + publisher = 'The Moscow Times' + language = 'en' + oldest_article = 2 max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False - #encoding = 'utf-8' - encoding = 'cp1252' - remove_javascript = True + remove_empty_feeds = True + encoding = 'cp1251' + masthead_url = 'http://www.themoscowtimes.com/bitrix/templates/tmt/img/logo.gif' + publication_type = 'newspaper' conversion_options = { - 'comment' : description - , 'language' : lang - } + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } extra_css = ''' h1{ color:#0066B3; font-family: Georgia,serif ; font-size: large} @@ -35,39 +37,37 @@ class Moscowtimes(BasicNewsRecipe): .text{font-family:Arial,Tahoma,Verdana,Helvetica,sans-serif ; font-size:75%; } ''' feeds = [ - (u'The Moscow Times Top Stories' , u'http://www.themoscowtimes.com/rss/top'), - (u'The Moscow Times Current Issue' , u'http://www.themoscowtimes.com/rss/issue'), - (u'The Moscow Times News' , u'http://www.themoscowtimes.com/rss/news'), - (u'The Moscow Times Business' , u'http://www.themoscowtimes.com/rss/business'), - (u'The Moscow Times Art and Ideas' , u'http://www.themoscowtimes.com/rss/art'), - (u'The Moscow Times Opinion' , u'http://www.themoscowtimes.com/rss/opinion') + (u'Top Stories' , u'http://www.themoscowtimes.com/rss/top' ) + ,(u'Current Issue' , u'http://www.themoscowtimes.com/rss/issue' ) + ,(u'News' , u'http://www.themoscowtimes.com/rss/news' ) + ,(u'Business' , u'http://www.themoscowtimes.com/rss/business') + ,(u'Art and Ideas' , u'http://www.themoscowtimes.com/rss/art' ) + ,(u'Opinion' , u'http://www.themoscowtimes.com/rss/opinion' ) ] - keep_only_tags = [ - dict(name='div', attrs={'class':['newstextblock']}) - ] - + keep_only_tags = [dict(name='div', attrs={'id':'content'})] remove_tags = [ - dict(name='div', attrs={'class':['photo_nav']}) - ] - + dict(name='div', attrs={'class':['photo_nav','phototext']}) + ,dict(name=['iframe','meta','base','link','embed','object']) + ] + def preprocess_html(self, soup): - soup.html['xml:lang'] = self.lang - soup.html['lang'] = self.lang - mtag = '' - soup.head.insert(0,mtag) - - return self.adeify_images(soup) + for lnk in soup.findAll('a'): + if lnk.string is not None: + ind = self.tag_to_string(lnk) + lnk.replaceWith(ind) + return soup + def print_version(self, url): + return url.replace('.themoscowtimes.com/','.themoscowtimes.com/print/') def get_cover_url(self): - + cover_url = None href = 'http://www.themoscowtimes.com/pdf/' - - soup = self.index_to_soup(href) + soup = self.index_to_soup(href) div = soup.find('div',attrs={'class':'left'}) - a = div.find('a') - print a - if a : - cover_url = a.img['src'] + if div: + a = div.find('a') + if a : + cover_url = 'http://www.themoscowtimes.com' + a.img['src'] return cover_url diff --git a/resources/recipes/now_toronto.recipe b/resources/recipes/now_toronto.recipe new file mode 100644 index 0000000000..41741dbccb --- /dev/null +++ b/resources/recipes/now_toronto.recipe @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#Based on Lars Jacob's Taz Digiabo recipe + +__license__ = 'GPL v3' +__copyright__ = '2010, Starson17' + +import os, urllib2, zipfile +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ptempfile import PersistentTemporaryFile + +class NowToronto(BasicNewsRecipe): + title = u'Now Toronto' + description = u'Now Toronto' + __author__ = 'Starson17' + conversion_options = { + 'no_default_epub_cover' : True + } + + def build_index(self): + epub_feed = "http://feeds.feedburner.com/NowEpubEditions" + soup = self.index_to_soup(epub_feed) + url = soup.find(name = 'feedburner:origlink').string + f = urllib2.urlopen(url) + tmp = PersistentTemporaryFile(suffix='.epub') + self.report_progress(0,_('downloading epub')) + tmp.write(f.read()) + tmp.close() + zfile = zipfile.ZipFile(tmp.name, 'r') + self.report_progress(0,_('extracting epub')) + zfile.extractall(self.output_dir) + tmp.close() + index = os.path.join(self.output_dir, 'content.opf') + self.report_progress(1,_('epub downloaded and extracted')) + return index diff --git a/resources/recipes/pc_lab.recipe b/resources/recipes/pc_lab.recipe new file mode 100644 index 0000000000..c4b33b8416 --- /dev/null +++ b/resources/recipes/pc_lab.recipe @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +from calibre.web.feeds.recipes import BasicNewsRecipe + +class PCLab(BasicNewsRecipe): + cover_url = 'http://pclab.pl/img/logo.png' + title = u"PC Lab" + __author__ = 'ravcio - rlelusz[at]gmail.com' + description = u"Articles from PC Lab website" + language = 'pl' + oldest_article = 30.0 + max_articles_per_feed = 100 + recursions = 0 + encoding = 'iso-8859-2' + no_stylesheets = True + remove_javascript = True + use_embedded_content = False + + keep_only_tags = [ + dict(name='div', attrs={'class':['substance']}) + ] + + remove_tags = [ + dict(name='div', attrs={'class':['chapters']}) + ,dict(name='div', attrs={'id':['script_bxad_slot_display_list_bxad_slot']}) + ] + + remove_tags_after = [ + dict(name='div', attrs={'class':['navigation']}) + ] + + #links to RSS feeds + feeds = [ ('PCLab', u'http://pclab.pl/xml/artykuly.xml') ] + + #load second and subsequent page content + # in: soup - full page with 'next' button + # out: appendtag - tag to which new page is to be added + def append_page(self, soup, appendtag): + # find the 'Next' button + pager = soup.find('div', attrs={'class':'next'}) + + if pager: + #search for 'a' element with link to next page (exit if not found) + a = pager.find('a') + if a: + nexturl = a['href'] + + soup2 = self.index_to_soup('http://pclab.pl/' + nexturl) + + pagetext_substance = soup2.find('div', attrs={'class':'substance'}) + pagetext = pagetext_substance.find('div', attrs={'class':'data'}) + pagetext.extract() + + pos = len(appendtag.contents) + appendtag.insert(pos, pagetext) + pos = len(appendtag.contents) + + self.append_page(soup2, appendtag) + + + def preprocess_html(self, soup): + + # soup.body contains no title and no navigator, they are in soup + self.append_page(soup, soup.body) + + # finally remove some tags + tags = soup.findAll('div',attrs={'class':['tags', 'index', 'script_bxad_slot_display_list_bxad_slot', 'index first', 'zumi', 'navigation']}) + [tag.extract() for tag in tags] + + return soup diff --git a/resources/recipes/tagesan.recipe b/resources/recipes/tagesan.recipe index 8514162598..aac064645f 100644 --- a/resources/recipes/tagesan.recipe +++ b/resources/recipes/tagesan.recipe @@ -7,7 +7,7 @@ class AdvancedUserRecipe1284927619(BasicNewsRecipe): __author__ = 'noxxx' max_articles_per_feed = 100 description = 'tagesanzeiger.ch: Nichts verpassen' - category = 'News, Politik, Nachrichten, Schweiz, Zürich' + category = 'News, Politik, Nachrichten, Schweiz, Zuerich' language = 'de' conversion_options = { diff --git a/setup/build_environment.py b/setup/build_environment.py index b29ee88cc3..6c4cf04479 100644 --- a/setup/build_environment.py +++ b/setup/build_environment.py @@ -13,9 +13,9 @@ from PyQt4 import pyqtconfig from setup import isosx, iswindows, islinux -OSX_SDK = '/Developer/SDKs/MacOSX10.4u.sdk' +OSX_SDK = '/Developer/SDKs/MacOSX10.5.sdk' -os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.4' +os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5' NMAKE = RC = msvc = MT = win_inc = win_lib = win_ddk = None if iswindows: @@ -124,7 +124,7 @@ elif isosx: fc_inc = '/sw/include/fontconfig' fc_lib = '/sw/lib' poppler_inc_dirs = consolidate('POPPLER_INC_DIR', - '/sw/build/poppler-0.12.2/poppler:/sw/build/poppler-0.12.2') + '/sw/build/poppler-0.14.5/poppler:/sw/build/poppler-0.14.5') popplerqt4_inc_dirs = poppler_inc_dirs + [poppler_inc_dirs[0]+'/qt4'] poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', '/sw/lib') diff --git a/setup/commands.py b/setup/commands.py index 26af3d967a..06ab7b36f7 100644 --- a/setup/commands.py +++ b/setup/commands.py @@ -19,7 +19,7 @@ __all__ = [ 'upload_user_manual', 'upload_to_mobileread', 'upload_demo', 'upload_to_sourceforge', 'upload_to_google_code', 'linux32', 'linux64', 'linux', 'linux_freeze', 'linux_freeze2', - 'osx32_freeze', 'osx32', 'osx', 'rsync', 'push', + 'osx32_freeze', 'osx', 'rsync', 'push', 'win32_freeze', 'win32', 'win', 'stage1', 'stage2', 'stage3', 'stage4', 'publish' ] @@ -84,9 +84,8 @@ linux_freeze = LinuxFreeze() from setup.installer.linux.freeze2 import LinuxFreeze2 linux_freeze2 = LinuxFreeze2() -from setup.installer.osx import OSX, OSX32 +from setup.installer.osx import OSX osx = OSX() -osx32 = OSX32() from setup.installer.osx.app.main import OSX32_Freeze osx32_freeze = OSX32_Freeze() diff --git a/setup/extensions.py b/setup/extensions.py index 531107d3cb..f68a35974e 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -186,7 +186,7 @@ if isfreebsd: if isosx: - x, p = ('i386', 'ppc') + x, p = ('i386', 'x86_64') archs = ['-arch', x, '-arch', p, '-isysroot', OSX_SDK] cflags.append('-D_OSX') @@ -339,7 +339,7 @@ class Build(Command): obj_pat = 'release\\*.obj' if iswindows else '*.o' objects = glob.glob(obj_pat) if not objects or self.newer(objects, ext.sources+ext.headers): - archs = 'x86 ppc' + archs = 'x86 x86_64' pro = textwrap.dedent('''\ TARGET = %s TEMPLATE = lib diff --git a/setup/installer/osx/__init__.py b/setup/installer/osx/__init__.py index f68e984ef1..dfc129eab6 100644 --- a/setup/installer/osx/__init__.py +++ b/setup/installer/osx/__init__.py @@ -7,25 +7,14 @@ __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from setup import Command from setup.installer import VMInstaller -class OSX(Command): +class OSX(VMInstaller): - description = 'Build OS X binary installers' - - sub_commands = ['osx32'] - - def run(self, opts): - pass - - -class OSX32(VMInstaller): - - description = 'Build 32 bit OS X binary installer' + description = 'Build OS X binary installer' INSTALLER_EXT = 'dmg' - VM_NAME = 'leopard_build' + VM_NAME = 'osx_build' VM = '/vmware/bin/%s'%VM_NAME FREEZE_TEMPLATE = 'python -OO setup.py {freeze_command}' FREEZE_COMMAND = 'osx32_freeze' diff --git a/setup/installer/osx/app/main.py b/setup/installer/osx/app/main.py index 565b5dd07d..0c46994262 100644 --- a/setup/installer/osx/app/main.py +++ b/setup/installer/osx/app/main.py @@ -48,14 +48,14 @@ def compile_launcher_lib(contents_dir, gcc, base): fd = join(contents_dir, 'Frameworks') dest = join(fd, 'calibre-launcher.dylib') src = join(base, 'util.c') - cmd = [gcc] + '-Wall -arch i386 -arch ppc -dynamiclib -std=gnu99'.split() + [src] + \ + cmd = [gcc] + '-Wall -arch i386 -arch x86_64 -dynamiclib -std=gnu99'.split() + [src] + \ ['-I'+base] + \ - ['-I/Library/Frameworks/Python.framework/Versions/Current/Headers'] + \ + ['-I/sw/python/Python.framework/Versions/Current/Headers'] + \ '-current_version 1.0 -compatibility_version 1.0'.split() + \ '-fvisibility=hidden -o'.split() + [dest] + \ ['-install_name', '@executable_path/../Frameworks/'+os.path.basename(dest)] + \ - ['-framework', 'Python', '-framework', 'CoreFoundation', '-headerpad_max_install_names'] + ['-F/sw/python', '-framework', 'Python', '-framework', 'CoreFoundation', '-headerpad_max_install_names'] info('\t'+' '.join(cmd)) sys.stdout.flush() subprocess.check_call(cmd) @@ -88,7 +88,7 @@ def compile_launchers(contents_dir, xprograms, pyver): fsrc = '/tmp/%s.c'%program with open(fsrc, 'wb') as f: f.write(psrc) - cmd = [gcc, '-Wall', '-arch', 'ppc', '-arch', 'i386', + cmd = [gcc, '-Wall', '-arch', 'x86_64', '-arch', 'i386', '-I'+base, fsrc, lib, '-o', out, '-headerpad_max_install_names'] info('\t'+' '.join(cmd)) @@ -108,14 +108,6 @@ def flipwritable(fn, mode=None): os.chmod(fn, stat.S_IWRITE | old_mode) return old_mode -def thin(path): - try: - subprocess.check_call(['lipo', path, '-verify_arch', 'ppc64']) - info('\tThinning', path) - except: - return - else: - subprocess.check_call(['lipo', path, '-thin', 'x86_64', '-output', path]) STRIPCMD = ['/usr/bin/strip', '-x', '-S', '-'] def strip_files(files, argv_max=(256 * 1024)): @@ -200,7 +192,6 @@ class Py2App(object): self.copy_site() self.create_exe() if not test_launchers: - #self.thin_to_x86_64() self.strip_files() ret = self.makedmg(self.build_dir, APPNAME+'-'+VERSION) @@ -212,19 +203,6 @@ class Py2App(object): shutil.copytree('resources', os.path.join(self.resources_dir, 'resources')) - @flush - def thin_to_x86_64(self): - info('\nThinning to x86_64') - for y in (self.frameworks_dir, join(self.resources_dir, 'Python')): - for x in os.walk(y): - for f in x[-1]: - f = join(x[0], f) - if not os.path.isfile(f): continue - for t in ('.so', '.dylib', '/Python'): - if f.endswith(t): - thin(f) - break - @flush def strip_files(self): info('\nStripping files...') @@ -270,10 +248,10 @@ class Py2App(object): continue for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/', '/opt/local/lib/', - '/Library/Frameworks/Python.framework/', SW+'/freetype/lib/'): + SW+'/python/Python.framework/', SW+'/freetype/lib/'): if x.startswith(y): - if y == '/Library/Frameworks/Python.framework/': - y = '/Library/Frameworks/' + if y == SW+'/python/Python.framework/': + y = SW+'/python/' yield x, x[len(y):] break @@ -299,7 +277,7 @@ class Py2App(object): @flush def add_python_framework(self): info('\nAdding Python framework') - src = join('/Library/Frameworks', 'Python.framework') + src = join('/sw/python', 'Python.framework') x = join(self.frameworks_dir, 'Python.framework') curr = os.path.realpath(join(src, 'Versions', 'Current')) currd = join(x, 'Versions', basename(curr)) @@ -314,7 +292,7 @@ class Py2App(object): def add_qt_frameworks(self): info('\nAdding Qt Framework') for f in ('QtCore', 'QtGui', 'QtXml', 'QtNetwork', 'QtSvg', 'QtWebKit', - 'QtXmlPatterns', 'phonon'): + 'QtXmlPatterns'): self.add_qt_framework(f) for d in glob.glob(join(SW, 'qt', 'plugins', '*')): shutil.copytree(d, join(self.contents_dir, 'MacOS', basename(d))) @@ -353,8 +331,8 @@ class Py2App(object): shutil.copy2(f, dest) self.fix_dependencies_in_lib(join(dest, basename(f))) if 'podofo' in f: - self.change_dep('libpodofo.0.6.99.dylib', - self.FID+'/'+'libpodofo.0.6.99.dylib', join(dest, basename(f))) + self.change_dep('libpodofo.0.8.4.dylib', + self.FID+'/'+'libpodofo.0.8.4.dylib', join(dest, basename(f))) @flush @@ -401,25 +379,27 @@ class Py2App(object): @flush def add_podofo(self): info('\nAdding PoDoFo') - pdf = join(SW, 'lib', 'libpodofo.0.8.2.dylib') + pdf = join(SW, 'lib', 'libpodofo.0.8.4.dylib') self.install_dylib(pdf) @flush def add_poppler(self): info('\nAdding poppler') - for x in ('libpoppler.5.dylib', 'libpoppler-qt4.3.dylib'): + for x in ('libpoppler.7.dylib',): self.install_dylib(os.path.join(SW, 'lib', x)) self.install_dylib(os.path.join(SW, 'bin', 'pdftohtml'), False) @flush def add_libjpeg(self): info('\nAdding libjpeg') - self.install_dylib(os.path.join(SW, 'lib', 'libjpeg.7.dylib')) + self.install_dylib(os.path.join(SW, 'lib', 'libjpeg.8.dylib')) @flush def add_libpng(self): info('\nAdding libpng') self.install_dylib(os.path.join(SW, 'lib', 'libpng12.0.dylib')) + self.install_dylib(os.path.join(SW, 'lib', 'libpng.3.dylib')) + @flush def add_fontconfig(self): @@ -449,7 +429,7 @@ class Py2App(object): def add_imagemagick(self): info('\nAdding ImageMagick') for x in ('Wand', 'Core'): - self.install_dylib(os.path.join(SW, 'lib', 'libMagick%s.2.dylib'%x)) + self.install_dylib(os.path.join(SW, 'lib', 'libMagick%s.4.dylib'%x)) idir = glob.glob(os.path.join(SW, 'lib', 'ImageMagick-*'))[-1] dest = os.path.join(self.frameworks_dir, 'ImageMagick') if os.path.exists(dest): @@ -463,7 +443,8 @@ class Py2App(object): @flush def add_misc_libraries(self): - for x in ('usb', 'unrar', 'readline.6.0', 'wmflite-0.2.7', 'chm.0'): + for x in ('usb', 'unrar', 'readline.6.1', 'wmflite-0.2.7', 'chm.0', + 'sqlite3.0'): info('\nAdding', x) x = 'lib%s.dylib'%x shutil.copy2(join(SW, 'lib', x), self.frameworks_dir) @@ -551,7 +532,7 @@ class Py2App(object): @flush def add_stdlib(self): info('\nAdding python stdlib') - src = '/Library/Frameworks/Python.framework/Versions/Current/lib/python' + src = '/sw/python/Python.framework/Versions/Current/lib/python' src += self.version_info dest = join(self.resources_dir, 'Python', 'lib', 'python') dest += self.version_info diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 46aabecfa0..281cd8668e 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -28,7 +28,7 @@ If there are no windows binaries already compiled for the version of python you Run the following command to install python dependencies:: - easy_install --always-unzip -U ipython mechanize pyreadline python-dateutil dnspython + easy_install --always-unzip -U ipython mechanize pyreadline python-dateutil dnspython cssutils clientform Install BeautifulSoup 3.0.x manually into site-packages (3.1.x parses broken HTML very poorly) @@ -37,7 +37,7 @@ Qt Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make:: - configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license && nmake + configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs && nmake SIP ----- diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 3562da55d2..55ae40420c 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -503,7 +503,11 @@ class KOBO(USBMS): ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path) ContentID = self.contentid_from_path(book.path, ContentType) - datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + + t = (ContentID,) + cursor.execute('select DateLastRead from Content where BookID is Null and ContentID = ?', t) + result = cursor.fetchone() + datelastread = result[0] if result[0] is not None else '1970-01-01T00:00:00' t = (datelastread,ContentID,) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 3bcf7715a2..44ecd5cfd0 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -199,6 +199,8 @@ class PRS505(USBMS): thumbnail_dir = os.path.join(prefix, *thumbnail_dir.split('/')) relpath = os.path.relpath(filepath, prefix) + if relpath.startswith('..\\'): + relpath = relpath[3:] thumbnail_dir = os.path.join(thumbnail_dir, relpath) if not os.path.exists(thumbnail_dir): os.makedirs(thumbnail_dir) diff --git a/src/calibre/devices/usbobserver/usbobserver.c b/src/calibre/devices/usbobserver/usbobserver.c index 4b9b39d473..63923fabf4 100644 --- a/src/calibre/devices/usbobserver/usbobserver.c +++ b/src/calibre/devices/usbobserver/usbobserver.c @@ -53,8 +53,8 @@ #define NUKE(x) Py_XDECREF(x); x = NULL; -/* This function only works on 10.5 and later -static PyObject* send2trash(PyObject *self, PyObject *args) +/* This function only works on 10.5 and later. Pass in a unicode object as path */ +static PyObject* usbobserver_send2trash(PyObject *self, PyObject *args) { UInt8 *utf8_chars; FSRef fp; @@ -73,7 +73,7 @@ static PyObject* send2trash(PyObject *self, PyObject *args) } Py_RETURN_NONE; } -*/ + static PyObject* usbobserver_get_iokit_string_property(io_service_t dev, CFStringRef prop) { @@ -323,6 +323,9 @@ static PyMethodDef usbobserver_methods[] = { {"get_mounted_filesystems", usbobserver_get_mounted_filesystems, METH_VARARGS, "Get mapping of mounted filesystems. Mapping is from BSD name to mount point." }, + {"send2trash", usbobserver_send2trash, METH_VARARGS, + "send2trash(unicode object) -> Send specified file/dir to trash" + }, {NULL, NULL, 0, NULL} }; diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 7e4e2e4f57..98e7b6023c 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -363,11 +363,15 @@ class MobiMLizer(object): if value == getattr(self.profile, prop): result = '100%' else: + # Amazon's renderer does not support + # img sizes in units other than px + # See #7520 for test case try: - ems = int(round(float(value) / self.profile.fbase)) + pixs = int(round(float(value) / \ + (72./self.profile.dpi))) except: continue - result = "%dem" % ems + result = "%d"%pixs istate.attrib[prop] = result elif tag == 'hr' and asfloat(style['width']) > 0: prop = style['width'] / self.profile.width diff --git a/src/calibre/ebooks/pdf/reflow.cpp b/src/calibre/ebooks/pdf/reflow.cpp index 43efe2c103..c08d7e5507 100644 --- a/src/calibre/ebooks/pdf/reflow.cpp +++ b/src/calibre/ebooks/pdf/reflow.cpp @@ -620,6 +620,7 @@ static string get_link_dest(LinkAction *link, PDFDoc *doc) { case actionSound: break; case actionJavaScript: break; case actionUnknown: break; + default: break; } return oss.str(); } diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 2f39a548ef..f035c40cb4 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -223,7 +223,6 @@ class MessageBox(QMessageBox): if default_button is not None: self.setDefaultButton(default_button) - def copy_to_clipboard(self): QApplication.clipboard().setText('%s: %s\n\n%s' % (self.title, self.msg, self.det_msg)) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index e5711bb31a..e193fe10b2 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -212,9 +212,9 @@ class BookInfo(QWebView): def _show_data(self, rows, comments): f = QFontInfo(QApplication.font(self.parent())).pixelSize() p = unicode(QApplication.palette().color(QPalette.Normal, - QPalette.Base).name()) + QPalette.Window).name()) c = unicode(QApplication.palette().color(QPalette.Normal, - QPalette.Text).name()) + QPalette.WindowText).name()) templ = u'''\ diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py index 6ee9cd9a96..2c64219464 100644 --- a/src/calibre/gui2/dialogs/fetch_metadata.py +++ b/src/calibre/gui2/dialogs/fetch_metadata.py @@ -14,7 +14,7 @@ from PyQt4.QtGui import QDialog, QItemSelectionModel from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata from calibre.gui2 import error_dialog, NONE, info_dialog, config from calibre.gui2.widgets import ProgressIndicator -from calibre import strftime +from calibre import strftime, force_unicode from calibre.customize.ui import get_isbndb_key, set_isbndb_key _hung_fetchers = set([]) @@ -179,7 +179,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata): self.terminate() return self.queue_reject.emit() self.model = Matches(self.fetcher.results) - warnings = [(x[0], unicode(x[1])) for x in \ + warnings = [(x[0], force_unicode(x[1])) for x in \ self.fetcher.exceptions if x[1] is not None] if warnings: warnings='
'.join(['%s: %s'%(name, exc) for name,exc in warnings]) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 8e9fca718e..f8177b7680 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -98,7 +98,7 @@ class MyBlockingBusy(QDialog): return self.accept() def do_one(self, id): - remove, add, au, aus, do_aus, rating, pub, do_series, \ + remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, \ do_autonumber, do_remove_format, remove_format, do_swap_ta, \ do_remove_conv, do_auto_author, series, do_series_restart, \ series_start_value, do_title_case, clear_series = self.args @@ -168,6 +168,8 @@ class MyBlockingBusy(QDialog): # both of these are fast enough to just do them all for w in self.cc_widgets: w.commit(self.ids) + if remove_all: + self.db.remove_all_tags(self.ids) self.db.bulk_modify_tags(self.ids, add=add, remove=remove, notify=False) self.current_index = len(self.ids) @@ -640,9 +642,9 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): for w in getattr(self, 'custom_column_widgets', []): w.gui_val - if self.remove_all_tags.isChecked(): - remove = self.db.all_tags() - else: + remove_all = self.remove_all_tags.isChecked() + remove = [] + if not remove_all: remove = unicode(self.remove_tags.text()).strip().split(',') add = unicode(self.tags.text()).strip().split(',') au = unicode(self.authors.text()) @@ -663,7 +665,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): do_auto_author = self.auto_author_sort.isChecked() do_title_case = self.change_title_to_title_case.isChecked() - args = (remove, add, au, aus, do_aus, rating, pub, do_series, + args = (remove_all, remove, add, au, aus, do_aus, rating, pub, do_series, do_autonumber, do_remove_format, remove_format, do_swap_ta, do_remove_conv, do_auto_author, series, do_series_restart, series_start_value, do_title_case, clear_series) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index c6830c5d5f..44839bbacd 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -308,6 +308,9 @@ from the value in the box 1 + + 990000 + 1 @@ -660,8 +663,8 @@ nothing should be put between the original text and the inserted text 0 0 - 726 - 334 + 122 + 38 diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 61b9c9f934..c0830ddae4 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -8,13 +8,15 @@ __docformat__ = 'restructuredtext en' import cStringIO, sys from binascii import hexlify, unhexlify +from functools import partial -from PyQt4.Qt import QWidget, pyqtSignal, QDialog, Qt +from PyQt4.Qt import QWidget, pyqtSignal, QDialog, Qt, QLabel, \ + QLineEdit, QDialogButtonBox, QGridLayout, QCheckBox from calibre.gui2.wizard.send_email_ui import Ui_Form from calibre.utils.smtp import config as smtp_prefs from calibre.gui2.dialogs.test_email_ui import Ui_Dialog as TE_Dialog -from calibre.gui2 import error_dialog, info_dialog +from calibre.gui2 import error_dialog class TestEmail(QDialog, TE_Dialog): @@ -74,8 +76,9 @@ class SendEmail(QWidget, Ui_Form): (self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True) self.relay_tls.toggled.connect(self.changed) - self.relay_use_gmail.clicked.connect( - self.create_gmail_relay) + for x in ('gmail', 'hotmail'): + button = getattr(self, 'relay_use_'+x) + button.clicked.connect(partial(self.create_service_relay, x)) self.relay_show_password.stateChanged.connect( lambda state : self.relay_password.setEchoMode( self.relay_password.Password if @@ -114,19 +117,79 @@ class SendEmail(QWidget, Ui_Form): sys.stdout, sys.stderr = oout, oerr return tb - def create_gmail_relay(self, *args): - self.relay_username.setText('@gmail.com') - self.relay_password.setText('') - self.relay_host.setText('smtp.gmail.com') - self.relay_port.setValue(587) + def create_service_relay(self, service, *args): + service = { + 'gmail': { + 'name': 'Gmail', + 'relay': 'smtp.gmail.com', + 'port': 587, + 'username': '@gmail.com', + 'url': 'www.gmail.com', + 'extra': '' + }, + 'hotmail': { + 'name': 'Hotmail', + 'relay': 'smtp.live.com', + 'port': 587, + 'username': '', + 'url': 'www.hotmail.com', + 'extra': _('If you are setting up a new' + ' hotmail account, you must log in to it ' + ' once before you will be able to send mails.'), + } + }[service] + d = QDialog(self) + l = QGridLayout() + d.setLayout(l) + bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(d.accept) + bb.rejected.connect(d.reject) + d.tl = QLabel('

'+_('You can sign up for a free {name} email ' + 'account at http://{url}. {extra}').format( + **service)) + l.addWidget(d.tl, 0, 0, 3, 0) + d.tl.setWordWrap(True) + d.tl.setOpenExternalLinks(True) + for name, label in ( + ['from_', _('Your %s &email address:')], + ['username', _('Your %s &username:')], + ['password', _('Your %s &password:')], + ): + la = QLabel(label%service['name']) + le = QLineEdit(d) + setattr(d, name, le) + setattr(d, name+'_label', la) + r = l.rowCount() + l.addWidget(la, r, 0) + l.addWidget(le, r, 1) + la.setBuddy(le) + if name == 'password': + d.ptoggle = QCheckBox(_('&Show password'), d) + l.addWidget(d.ptoggle, r, 2) + d.ptoggle.stateChanged.connect( + lambda s: d.password.setEchoMode(d.password.Normal if s + == Qt.Checked else d.password.Password)) + d.username.setText(service['username']) + d.password.setEchoMode(d.password.Password) + d.bl = QLabel('

' + _( + 'If you plan to use email to send books to your Kindle, remember to' + ' add the your %s email address to the allowed email addresses in your ' + 'Amazon.com Kindle management page.')%service['name']) + d.bl.setWordWrap(True) + l.addWidget(d.bl, l.rowCount(), 0, 3, 0) + l.addWidget(bb, l.rowCount(), 0, 3, 0) + d.setWindowTitle(_('Setup') + ' ' + service['name']) + d.resize(d.sizeHint()) + bb.setVisible(True) + if d.exec_() != d.Accepted: + return + self.relay_username.setText(d.username.text()) + self.relay_password.setText(d.password.text()) + self.email_from.setText(d.from_.text()) + self.relay_host.setText(service['relay']) + self.relay_port.setValue(service['port']) self.relay_tls.setChecked(True) - info_dialog(self, _('Finish gmail setup'), - _('Dont forget to enter your gmail username and password. ' - 'You can sign up for a free gmail account at http://gmail.com')).exec_() - self.relay_username.setFocus(Qt.OtherFocusReason) - self.relay_username.setCursorPosition(0) - def set_email_settings(self, to_set): from_ = unicode(self.email_from.text()).strip() if to_set and not from_: diff --git a/src/calibre/gui2/wizard/send_email.ui b/src/calibre/gui2/wizard/send_email.ui index f248b8df89..ba6b6866e2 100644 --- a/src/calibre/gui2/wizard/send_email.ui +++ b/src/calibre/gui2/wizard/send_email.ui @@ -216,6 +216,26 @@ + + + + Use Hotmail + + + + :/images/hotmail.png:/images/hotmail.png + + + + 48 + 48 + + + + Qt::ToolButtonTextUnderIcon + + + diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index efe92d3c63..6d18a2d663 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -36,33 +36,8 @@ from calibre.utils.config import prefs, tweaks from calibre.utils.search_query_parser import saved_searches, set_saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.utils.magick.draw import save_cover_data_to +from calibre.utils.recycle_bin import delete_file, delete_tree -if iswindows: - import calibre.utils.winshell as winshell - -def delete_file(path): - try: - winshell.delete_file(path, silent=True, no_confirm=True) - except: - os.remove(path) - -def delete_tree(path, permanent=False): - if permanent: - try: - # For completely mysterious reasons, sometimes a file is left open - # leading to access errors. If we get an exception, wait and hope - # that whatever has the file (the O/S?) lets go of it. - shutil.rmtree(path) - except: - traceback.print_exc() - time.sleep(1) - shutil.rmtree(path) - else: - try: - if not permanent: - winshell.delete_file(path, silent=True, no_confirm=True) - except: - delete_tree(path, permanent=True) copyfile = os.link if hasattr(os, 'link') else shutil.copyfile @@ -983,10 +958,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): path = None self.data.remove(id) if path and os.path.exists(path): - try: - winshell.delete_file(path, no_confirm=True, silent=True) - except: - self.rmtree(path) + self.rmtree(path) parent = os.path.dirname(path) if len(os.listdir(parent)) == 0: self.rmtree(parent) @@ -1759,6 +1731,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ans.append(tag) return ans + def remove_all_tags(self, ids, notify=False, commit=True): + self.conn.executemany( + 'DELETE FROM books_tags_link WHERE book=?', [(x,) for x in ids]) + self.dirtied(ids, commit=False) + if commit: + self.conn.commit() + + for x in ids: + self.data.set(x, self.FIELD_MAP['tags'], '', row_is_id=True) + if notify: + self.notify('metadata', ids) + def bulk_modify_tags(self, ids, add=[], remove=[], notify=False): add = self.cleanup_tags(add) remove = self.cleanup_tags(remove) diff --git a/src/calibre/utils/recycle_bin.py b/src/calibre/utils/recycle_bin.py new file mode 100644 index 0000000000..df6016d796 --- /dev/null +++ b/src/calibre/utils/recycle_bin.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, shutil, time +from functools import partial + +from calibre import isbytestring +from calibre.constants import iswindows, isosx, plugins, filesystem_encoding + +recycle = None + +if iswindows: + import calibre.utils.winshell as winshell + recycle = partial(winshell.delete_file, silent=True, no_confirm=True) +elif isosx: + u = plugins['usbobserver'][0] + if hasattr(u, 'send2trash'): + def recycle(path): + if isbytestring(path): + path = path.decode(filesystem_encoding) + u.send2trash(path) + + +def delete_file(path): + if callable(recycle): + try: + recycle(path) + return + except: + import traceback + traceback.print_exc() + os.remove(path) + +def delete_tree(path, permanent=False): + if permanent: + try: + # For completely mysterious reasons, sometimes a file is left open + # leading to access errors. If we get an exception, wait and hope + # that whatever has the file (the O/S?) lets go of it. + shutil.rmtree(path) + except: + import traceback + traceback.print_exc() + time.sleep(1) + shutil.rmtree(path) + else: + if callable(recycle): + try: + recycle(path) + return + except: + import traceback + traceback.print_exc() + delete_tree(path, permanent=True) + diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py index b8b46a96cb..8af31b5d38 100644 --- a/src/calibre/utils/smtp.py +++ b/src/calibre/utils/smtp.py @@ -101,8 +101,12 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30, if encryption == 'SSL': s.sock = s.file.sslobj s.login(username, password) - s.sendmail(from_, to, msg) - return s.quit() + ret = None + try: + s.sendmail(from_, to, msg) + finally: + ret = s.quit() + return ret def option_parser(): try: