diff --git a/imgsrc/trim.svg b/imgsrc/trim.svg new file mode 100644 index 0000000000..8c8810fc66 --- /dev/null +++ b/imgsrc/trim.svg @@ -0,0 +1,688 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Oxygen team + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/trim.png b/resources/images/trim.png new file mode 100644 index 0000000000..3cb93adfa6 Binary files /dev/null and b/resources/images/trim.png differ diff --git a/resources/recipes/danas.recipe b/resources/recipes/danas.recipe index 3543acd684..1e0e319334 100644 --- a/resources/recipes/danas.recipe +++ b/resources/recipes/danas.recipe @@ -49,7 +49,11 @@ class Danas(BasicNewsRecipe): , 'language' : language } - preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] + preprocess_regexps = [ + (re.compile(u'\u0110'), lambda match: u'\u00D0') + ,(re.compile(u'\u201c'), lambda match: '"') + ,(re.compile(u'\u201e'), lambda match: '"') + ] keep_only_tags = [dict(name='div', attrs={'id':'left'})] remove_tags = [ diff --git a/resources/recipes/novosti.recipe b/resources/recipes/novosti.recipe index 0465b59e17..d66e7d28b7 100644 --- a/resources/recipes/novosti.recipe +++ b/resources/recipes/novosti.recipe @@ -21,8 +21,8 @@ class Novosti(BasicNewsRecipe): encoding = 'utf-8' language = 'sr' publication_type = 'newspaper' - extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} - .article_description,body{font-family: Arial,Helvetica,sans1,sans-serif} + extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + .article_description,body{font-family: Arial,Helvetica,sans1,sans-serif} .author{font-size: small} .articleLead{font-size: large; font-weight: bold} """ @@ -47,6 +47,8 @@ class Novosti(BasicNewsRecipe): item.name='p' for item in soup.findAll('img'): if not item.has_key('alt'): - item['alt'] = 'image' + item['alt'] = 'image' return soup - \ No newline at end of file + + + diff --git a/resources/recipes/superesportes.recipe b/resources/recipes/superesportes.recipe new file mode 100644 index 0000000000..49289f188d --- /dev/null +++ b/resources/recipes/superesportes.recipe @@ -0,0 +1,79 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Luciano Furtado ' +''' +www.superesportes.com.br +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class SuperEsportesRecipe(BasicNewsRecipe): + + title = u'www.superesportes.com.br' + description = u'Superesportes - Not√≠cias do esporte no Brasil e no mundo' + __author__ = 'Luciano Furtado' + language = 'pt' + category = 'esportes, Brasil' + no_stylesheets = True + oldest_article = 7 + + use_embedded_content=0 + max_articles_per_feed = 10 + cover_url = 'http://imgs.mg.superesportes.com.br/superesportes_logo.png' + + extra_css = 'div.info_noticias h1 { font-size: 100% }' + + + + remove_tags = [ + dict(name='div',attrs={'class':'topo'}), + dict(name='div',attrs={'class':'rodape'}), + dict(name='div',attrs={'class':'navegacao'}), + dict(name='div',attrs={'class':'lateral2'}), + dict(name='div',attrs={'class':'leia_mais'}), + dict(name='div',attrs={'id':'comentar'}), + dict(name='div',attrs={'id':'vrumelc_noticia'}), + dict(name='div',attrs={'class':'compartilhe'}), + dict(name='div',attrs={'class':'linha_noticias'}), + dict(name='div',attrs={'class':'botoes_noticias'}), + dict(name='div',attrs={'class':'barra_time bg_time'}), + ] + + + + def parse_index(self): + feeds = [] + sections = [ + (u'Atletico', 'http://www.df.superesportes.com.br/futebol/atletico-mg/capa_atletico_mg/index.shtml'), + (u'Botafogo', 'http://www.df.superesportes.com.br/futebol/botafogo/capa_botafogo/index.shtml'), + (u'Corinthinas', 'http://www.df.superesportes.com.br/futebol/corinthians/capa_corinthians/index.shtml'), + (u'Cruzeiro', 'http://www.df.superesportes.com.br/futebol/cruzeiro/capa_cruzeiro/index.shtml'), + (u'Flamengo', 'http://www.df.superesportes.com.br/futebol/flamengo/capa_flamengo/index.shtml'), + (u'Fluminense', 'http://www.df.superesportes.com.br/futebol/fluminense/capa_fluminense/index.shtml'), + (u'Palmeiras', 'http://www.df.superesportes.com.br/futebol/palmeiras/capa_palmeiras/index.shtml'), + (u'Santos', 'http://www.df.superesportes.com.br/futebol/santos/capa_santos/index.shtml'), + (u'S√£o Paulo', 'http://www.df.superesportes.com.br/futebol/sao-paulo/capa_sao_paulo/index.shtml'), + (u'Vasco', 'http://www.df.superesportes.com.br/futebol/vasco/capa_vasco/index.shtml'), + ] + + + for section, url in sections: + current_articles = [] + + soup = self.index_to_soup(url) + latestNews = soup.find(name='ul',attrs={'class': 'lista_ultimas_noticias'}) + + for li_tag in latestNews.findAll(name='li'): + a_tag = li_tag.find('a', href= True) + if a_tag is None: + continue + title = self.tag_to_string(a_tag) + url = a_tag.get('href', False) + self.log("\n\nFound title: " + title + "\nUrl: " + url + "\nSection: " + section) + current_articles.append({'title': title, 'url': url, 'description': title, 'date':''}) + + if current_articles: + feeds.append((section, current_articles)) + + + return feeds + diff --git a/resources/recipes/tagesan.recipe b/resources/recipes/tagesan.recipe new file mode 100644 index 0000000000..8514162598 --- /dev/null +++ b/resources/recipes/tagesan.recipe @@ -0,0 +1,45 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1284927619(BasicNewsRecipe): + title = u'Tagesanzeiger' + publisher = u'Tamedia AG' + oldest_article = 2 + __author__ = 'noxxx' + max_articles_per_feed = 100 + description = 'tagesanzeiger.ch: Nichts verpassen' + category = 'News, Politik, Nachrichten, Schweiz, Zürich' + language = 'de' + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + remove_tags = [ + dict(name='img') + ,dict(name='div',attrs={'class':['swissquote ad','boxNews','centerAD','contentTabs2','sbsLabel']}) + ,dict(name='div',attrs={'id':['colRightAd','singleRight','singleSmallRight','MailInfo','metaLine','sidebarSky','contentFooter','commentInfo','commentInfo2','commentInfo3','footerBottom','clear','boxExclusiv','singleLogo','navSearch','headerLogin','headerBottomRight','horizontalNavigation','subnavigation','googleAdSense','footerAd','contentbox','articleGalleryNav']}) + ,dict(name='form',attrs={'id':['articleMailForm','commentform']}) + ,dict(name='div',attrs={'style':['position:absolute']}) + ,dict(name='script',attrs={'type':['text/javascript']}) + ,dict(name='p',attrs={'class':['schreiben','smallPrint','charCounter','caption']}) + ] + feeds = [ + (u'Front', u'http://www.tagesanzeiger.ch/rss.html') + ,(u'Zürich', u'http://www.tagesanzeiger.ch/zuerich/rss.html') + ,(u'Schweiz', u'http://www.tagesanzeiger.ch/schweiz/rss.html') + ,(u'Ausland', u'http://www.tagesanzeiger.ch/ausland/rss.html') + ,(u'Digital', u'http://www.tagesanzeiger.ch/digital/rss.html') + ,(u'Wissen', u'http://www.tagesanzeiger.ch/wissen/rss.html') + ,(u'Panorama', u'http://www.tagesanzeiger.ch/panorama/rss.html') + ,(u'Wirtschaft', u'http://www.tagesanzeiger.ch/wirtschaft/rss.html') + ,(u'Sport', u'http://www.tagesanzeiger.ch/sport/rss.html') + ,(u'Kultur', u'http://www.tagesanzeiger.ch/kultur/rss.html') + ,(u'Leben', u'http://www.tagesanzeiger.ch/leben/rss.html') + ,(u'Auto', u'http://www.tagesanzeiger.ch/auto/rss.html')] + + def print_version(self, url): + return url + '/print.html' + diff --git a/resources/recipes/the_marker.recipe b/resources/recipes/the_marker.recipe new file mode 100644 index 0000000000..e5f1ffc761 --- /dev/null +++ b/resources/recipes/the_marker.recipe @@ -0,0 +1,52 @@ +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1283848012(BasicNewsRecipe): + description = 'TheMarker Financial News in Hebrew' + __author__ = 'TonyTheBookworm, Marbs' + cover_url = 'http://static.ispot.co.il/wp-content/upload/2009/09/themarker.jpg' + title = u'TheMarker' + language = 'he' + simultaneous_downloads = 5 + remove_javascript = True + timefmt = '[%a, %d %b, %Y]' + oldest_article = 1 + remove_tags = [dict(name='tr', attrs={'bgcolor':['#738A94']}) ] + max_articles_per_feed = 10 + extra_css='body{direction: rtl;} .article_description{direction: rtl; } a.article{direction: rtl; } .calibre_feed_description{direction: rtl; }' + feeds = [(u'Head Lines', u'http://www.themarker.com/tmc/content/xml/rss/hpfeed.xml'), + (u'TA Market', u'http://www.themarker.com/tmc/content/xml/rss/sections/marketfeed.xml'), + (u'Real Estate', u'http://www.themarker.com/tmc/content/xml/rss/sections/realEstaterfeed.xml'), + (u'Wall Street & Global', u'http://www.themarker.com/tmc/content/xml/rss/sections/wallsfeed.xml'), + (u'Law', u'http://www.themarker.com/tmc/content/xml/rss/sections/lawfeed.xml'), + (u'Media', u'http://www.themarker.com/tmc/content/xml/rss/sections/mediafeed.xml'), + (u'Consumer', u'http://www.themarker.com/tmc/content/xml/rss/sections/consumerfeed.xml'), + (u'Career', u'http://www.themarker.com/tmc/content/xml/rss/sections/careerfeed.xml'), + (u'Car', u'http://www.themarker.com/tmc/content/xml/rss/sections/carfeed.xml'), + (u'High Tech', u'http://www.themarker.com/tmc/content/xml/rss/sections/hightechfeed.xml'), + (u'Investor Guide', u'http://www.themarker.com/tmc/content/xml/rss/sections/investorGuidefeed.xml')] + + def print_version(self, url): + split1 = url.split("=") + weblinks = url + + if weblinks is not None: + for link in weblinks: + #--------------------------------------------------------- + #here we need some help with some regexpressions + #we are trying to find it.themarker.com in a url + #----------------------------------------------------------- + re1='.*?' # Non-greedy match on filler + re2='(it\\.themarker\\.com)' # Fully Qualified Domain Name 1 + rg = re.compile(re1+re2,re.IGNORECASE|re.DOTALL) + m = rg.search(url) + + + if m: + split2 = url.split("article/") + print_url = 'http://it.themarker.com/tmit/PrintArticle/' + split2[1] + + else: + print_url = 'http://www.themarker.com/ibo/misc/printFriendly.jhtml?ElementId=%2Fibo%2Frepositories%2Fstories%2Fm1_2000%2F' + split1[1]+'.xml' + + return print_url diff --git a/resources/recipes/wsj.recipe b/resources/recipes/wsj.recipe index fd5e977d10..88e07bcea3 100644 --- a/resources/recipes/wsj.recipe +++ b/resources/recipes/wsj.recipe @@ -70,13 +70,16 @@ class WallStreetJournal(BasicNewsRecipe): def wsj_add_feed(self,feeds,title,url): self.log('Found section:', title) - if url.endswith('whatsnews'): - articles = self.wsj_find_wn_articles(url) - else: - articles = self.wsj_find_articles(url) + try: + if url.endswith('whatsnews'): + articles = self.wsj_find_wn_articles(url) + else: + articles = self.wsj_find_articles(url) + except: + articles = [] if articles: feeds.append((title, articles)) - return feeds + return feeds def parse_index(self): soup = self.wsj_get_index() @@ -99,7 +102,7 @@ class WallStreetJournal(BasicNewsRecipe): url = 'http://online.wsj.com' + a['href'] feeds = self.wsj_add_feed(feeds,title,url) title = 'What''s News' - url = url.replace('pageone','whatsnews') + url = url.replace('pageone','whatsnews') feeds = self.wsj_add_feed(feeds,title,url) else: title = self.tag_to_string(a) @@ -141,7 +144,7 @@ class WallStreetJournal(BasicNewsRecipe): articles = [] flavorarea = soup.find('div', attrs={'class':lambda x: x and 'ahed' in x}) - if flavorarea is not None: + if flavorarea is not None: flavorstory = flavorarea.find('a', href=lambda x: x and x.startswith('/article')) if flavorstory is not None: flavorstory['class'] = 'mjLinkItem' diff --git a/resources/recipes/wsj_free.recipe b/resources/recipes/wsj_free.recipe index 7f3664f1c4..df8234e8e2 100644 --- a/resources/recipes/wsj_free.recipe +++ b/resources/recipes/wsj_free.recipe @@ -54,10 +54,13 @@ class WallStreetJournal(BasicNewsRecipe): def wsj_add_feed(self,feeds,title,url): self.log('Found section:', title) - if url.endswith('whatsnews'): - articles = self.wsj_find_wn_articles(url) - else: - articles = self.wsj_find_articles(url) + try: + if url.endswith('whatsnews'): + articles = self.wsj_find_wn_articles(url) + else: + articles = self.wsj_find_articles(url) + except: + articles = [] if articles: feeds.append((title, articles)) return feeds diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 68df832048..ec9f7e2bc2 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -666,13 +666,17 @@ class ActionCopyToLibrary(InterfaceActionBase): name = 'Copy To Library' actual_plugin = 'calibre.gui2.actions.copy_to_library:CopyToLibraryAction' +class ActionTweakEpub(InterfaceActionBase): + name = 'Tweak ePub' + actual_plugin = 'calibre.gui2.actions.tweak_epub:TweakEpubAction' + plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionConvert, ActionDelete, ActionEditMetadata, ActionView, ActionFetchNews, ActionSaveToDisk, ActionShowBookDetails, ActionRestart, ActionOpenFolder, ActionConnectShare, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, - ActionCopyToLibrary] + ActionCopyToLibrary, ActionTweakEpub] # }}} diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 7a451112c0..c9c0827759 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -29,7 +29,9 @@ class ANDROID(USBMS): # Sony Ericsson 0xfce : { 0xd12e : [0x0100]}, - 0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]}, + # Google + 0x18d1 : { 0x4e11 : [0x0100, 0x226, 0x227], 0x4e12: [0x0100, 0x226, + 0x227]}, # Samsung 0x04e8 : { 0x681d : [0x0222, 0x0400], diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 5fe36faf75..0946bd2f51 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -739,7 +739,7 @@ class ITUNES(DriverBase): # Purge the booklist, self.cached_books, thumb cache for i,bl_book in enumerate(booklists[0]): - if DEBUG: + if False: self.log.info(" evaluating '%s' by '%s' uuid:%s" % (bl_book.title, bl_book.author,bl_book.uuid)) diff --git a/src/calibre/devices/hanvon/driver.py b/src/calibre/devices/hanvon/driver.py index 75728a94ea..6291864b86 100644 --- a/src/calibre/devices/hanvon/driver.py +++ b/src/calibre/devices/hanvon/driver.py @@ -11,6 +11,10 @@ import re from calibre.devices.usbms.driver import USBMS +def is_alex(device_info): + return device_info[3] == u'Linux 2.6.28 with pxa3xx_u2d' and \ + device_info[4] == u'Seleucia Disk' + class N516(USBMS): name = 'N516 driver' @@ -34,6 +38,9 @@ class N516(USBMS): EBOOK_DIR_MAIN = 'e_book' SUPPORTS_SUB_DIRS = True + def can_handle(self, device_info, debug=False): + return not is_alex(device_info) + class THEBOOK(N516): name = 'The Book driver' gui_name = 'The Book' @@ -61,6 +68,9 @@ class ALEX(N516): EBOOK_DIR_MAIN = 'eBooks' SUPPORTS_SUB_DIRS = True + def can_handle(self, device_info, debug=False): + return is_alex(device_info) + class AZBOOKA(ALEX): name = 'Azbooka driver' @@ -74,6 +84,9 @@ class AZBOOKA(ALEX): EBOOK_DIR_MAIN = '' + def can_handle(self, device_info, debug=False): + return not is_alex(device_info) + class EB511(USBMS): name = 'Elonex EB 511 driver' diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 762a05d193..104553b675 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -98,6 +98,8 @@ class KOBO(USBMS): if readstatus == 1: playlist_map[lpath]= "Im_Reading" + elif readstatus == 2: + playlist_map[lpath]= "Read" path = self.normalize_path(path) # print "Normalized FileName: " + path @@ -441,43 +443,99 @@ class KOBO(USBMS): connection = sqlite.connect(self._main_prefix + '.kobo/KoboReader.sqlite') cursor = connection.cursor() - # Reset Im_Reading list in the database - if oncard == 'carda': - query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID like \'file:///mnt/sd/%\'' - elif oncard != 'carda' and oncard != 'cardb': - query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID not like \'file:///mnt/sd/%\'' + + if collections: + # Process any collections that exist + for category, books in collections.items(): + if category == 'Im_Reading': + # Reset Im_Reading list in the database + if oncard == 'carda': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 1 and ContentID like \'file:///mnt/sd/%\'' + elif oncard != 'carda' and oncard != 'cardb': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 1 and ContentID not like \'file:///mnt/sd/%\'' - try: - cursor.execute (query) - except: - debug_print('Database Exception: Unable to reset Im_Reading list') - raise - else: -# debug_print('Commit: Reset Im_Reading list') - connection.commit() - - for category, books in collections.items(): - if category == 'Im_Reading': - for book in books: -# debug_print('Title:', book.title, 'lpath:', book.path) - book.device_collections = ['Im_Reading'] - - extension = os.path.splitext(book.path)[1] - ContentType = self.get_content_type_from_extension(extension) - - ContentID = self.contentid_from_path(book.path, ContentType) - datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) - - t = (datelastread,ContentID,) - try: - cursor.execute('update content set ReadStatus=1,FirstTimeReading=\'false\',DateLastRead=? where BookID is Null and ContentID = ?', t) + cursor.execute (query) except: - debug_print('Database Exception: Unable create Im_Reading list') + debug_print('Database Exception: Unable to reset Im_Reading list') raise else: +# debug_print('Commit: Reset Im_Reading list') connection.commit() - # debug_print('Database: Commit create Im_Reading list') + + for book in books: +# debug_print('Title:', book.title, 'lpath:', book.path) + book.device_collections = ['Im_Reading'] + + extension = os.path.splitext(book.path)[1] + ContentType = self.get_content_type_from_extension(extension) + + ContentID = self.contentid_from_path(book.path, ContentType) + datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + + t = (datelastread,ContentID,) + + try: + cursor.execute('update content set ReadStatus=1,FirstTimeReading=\'false\',DateLastRead=? where BookID is Null and ContentID = ?', t) + except: + debug_print('Database Exception: Unable create Im_Reading list') + raise + else: + connection.commit() + # debug_print('Database: Commit create Im_Reading list') + if category == 'Read': + # Reset Im_Reading list in the database + if oncard == 'carda': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 2 and ContentID like \'file:///mnt/sd/%\'' + elif oncard != 'carda' and oncard != 'cardb': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ReadStatus = 2 and ContentID not like \'file:///mnt/sd/%\'' + + try: + cursor.execute (query) + except: + debug_print('Database Exception: Unable to reset Im_Reading list') + raise + else: +# debug_print('Commit: Reset Im_Reading list') + connection.commit() + + for book in books: +# debug_print('Title:', book.title, 'lpath:', book.path) + book.device_collections = ['Read'] + + extension = os.path.splitext(book.path)[1] + ContentType = self.get_content_type_from_extension(extension) + + ContentID = self.contentid_from_path(book.path, ContentType) +# datelastread = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()) + + t = (ContentID,) + + try: + cursor.execute('update content set ReadStatus=2,FirstTimeReading=\'true\' where BookID is Null and ContentID = ?', t) + except: + debug_print('Database Exception: Unable set book as Rinished') + raise + else: + connection.commit() +# debug_print('Database: Commit set ReadStatus as Finished') + else: # No collections + # Since no collections exist the ReadStatus needs to be reset to 0 (Unread) + print "Reseting ReadStatus to 0" + # Reset Im_Reading list in the database + if oncard == 'carda': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID like \'file:///mnt/sd/%\'' + elif oncard != 'carda' and oncard != 'cardb': + query= 'update content set ReadStatus=0, FirstTimeReading = \'true\' where BookID is Null and ContentID not like \'file:///mnt/sd/%\'' + + try: + cursor.execute (query) + except: + debug_print('Database Exception: Unable to reset Im_Reading list') + raise + else: +# debug_print('Commit: Reset Im_Reading list') + connection.commit() cursor.close() connection.close() diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 3ea2926461..395447edba 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -241,7 +241,7 @@ OptionRecommendation(name='toc_filter', OptionRecommendation(name='chapter', recommended_value="//*[((name()='h1' or name()='h2') and " - r"re:test(., 'chapter|book|section|part\s+', 'i')) or @class " + r"re:test(., 'chapter|book|section|part|prologue|epilogue\s+', 'i')) or @class " "= 'chapter']", level=OptionRecommendation.LOW, help=_('An XPath expression to detect chapter titles. The default ' 'is to consider

or

tags that contain the words ' diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 5d5de7b153..23f92d1fd2 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -1574,14 +1574,15 @@ class MobiWriter(object): id = unicode(oeb.metadata.cover[0]) item = oeb.manifest.ids[id] href = item.href - index = self._images[href] - 1 - exth.write(pack('>III', 0xc9, 0x0c, index)) - exth.write(pack('>III', 0xcb, 0x0c, 0)) - nrecs += 2 - index = self._add_thumbnail(item) - if index is not None: - exth.write(pack('>III', 0xca, 0x0c, index - 1)) - nrecs += 1 + if href in self._images: + index = self._images[href] - 1 + exth.write(pack('>III', 0xc9, 0x0c, index)) + exth.write(pack('>III', 0xcb, 0x0c, 0)) + nrecs += 2 + index = self._add_thumbnail(item) + if index is not None: + exth.write(pack('>III', 0xca, 0x0c, index - 1)) + nrecs += 1 exth = exth.getvalue() trail = len(exth) % 4 diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index e58dce5559..c0c7b0a9ed 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1,7 +1,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' """ The GUI """ -import os, sys +import os, sys, Queue from threading import RLock from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \ @@ -39,7 +39,7 @@ gprefs.defaults['action-layout-context-menu'] = ( 'Edit Metadata', 'Send To Device', 'Save To Disk', 'Connect Share', 'Copy To Library', None, 'Convert Books', 'View', 'Open Folder', 'Show Book Details', - 'Similar Books', None, 'Remove Books', + 'Similar Books', 'Tweak ePub', None, 'Remove Books', ) gprefs.defaults['action-layout-context-menu-device'] = ( @@ -296,6 +296,34 @@ class Dispatcher(QObject): def dispatch(self, args, kwargs): self.func(*args, **kwargs) +class FunctionDispatcher(QObject): + ''' + Convenience class to use Qt signals with arbitrary python functions. + By default, ensures that a function call always happens in the + thread this Dispatcher was created in. + ''' + dispatch_signal = pyqtSignal(object, object, object) + + def __init__(self, func, queued=True, parent=None): + QObject.__init__(self, parent) + self.func = func + typ = Qt.QueuedConnection + if not queued: + typ = Qt.AutoConnection if queued is None else Qt.DirectConnection + self.dispatch_signal.connect(self.dispatch, type=typ) + + def __call__(self, *args, **kwargs): + q = Queue.Queue() + self.dispatch_signal.emit(q, args, kwargs) + return q.get() + + def dispatch(self, q, args, kwargs): + try: + res = self.func(*args, **kwargs) + except: + res = None + q.put(res) + class GetMetadata(QObject): ''' Convenience class to ensure that metadata readers are used only in the @@ -575,18 +603,6 @@ class Application(QApplication): self._file_open_paths = [] self._file_open_lock = RLock() - if islinux: - self.setStyleSheet(''' - QToolTip { - border: 2px solid black; - padding: 5px; - border-radius: 10px; - opacity: 200; - background-color: #e1e1ff; - color: black; - } - ''') - def _send_file_open_events(self): with self._file_open_lock: if self._file_open_paths: diff --git a/src/calibre/gui2/actions/tweak_epub.py b/src/calibre/gui2/actions/tweak_epub.py new file mode 100755 index 0000000000..212aff8019 --- /dev/null +++ b/src/calibre/gui2/actions/tweak_epub.py @@ -0,0 +1,55 @@ +#!/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' + +from calibre.gui2 import error_dialog +from calibre.gui2.actions import InterfaceAction +from calibre.gui2.dialogs.tweak_epub import TweakEpub + +class TweakEpubAction(InterfaceAction): + + name = 'Tweak ePub' + action_spec = (_('Tweak ePub'), 'trim.png', + _('Make small changes to ePub format books'), + _('T')) + dont_add_to = frozenset(['toolbar-device', 'context-menu-device']) + action_type = 'current' + + def genesis(self): + self.qaction.triggered.connect(self.edit_epub_in_situ) + + def edit_epub_in_situ(self, *args): + row = self.gui.library_view.currentIndex() + if not row.isValid(): + return error_dialog(self.gui, _('Cannot tweak ePub'), + _('No book selected'), show=True) + + # Confirm 'EPUB' in formats + book_id = self.gui.library_view.model().id(row) + try: + path_to_epub = self.gui.library_view.model().db.format_abspath( + book_id, 'EPUB', index_is_id=True) + except: + path_to_epub = None + + if not path_to_epub: + return error_dialog(self.gui, _('Cannot tweak ePub'), + _('No ePub available. First convert the book to ePub.'), + show=True) + + # Launch modal dialog waiting for user to tweak or cancel + dlg = TweakEpub(self.gui, path_to_epub) + if dlg.exec_() == dlg.Accepted: + self.update_db(book_id, dlg._output) + dlg.cleanup() + + def update_db(self, book_id, rebuilt): + ''' + Update the calibre db with the tweaked epub + ''' + self.gui.library_view.model().db.add_format(book_id, 'EPUB', + open(rebuilt, 'rb'), index_is_id=True) + diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index ae3141db56..a7e55c4619 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -800,7 +800,7 @@ class DeviceMixin(object): # {{{ # if set_books_in_library did not. if not self.set_books_in_library(self.booklists(), reset=True): self.upload_booklists() - self.book_on_device(None, None, reset=True) + self.book_on_device(None, reset=True) # We need to reset the ondevice flags in the library. Use a big hammer, # so we don't need to worry about whether some succeeded or not. self.refresh_ondevice_info(device_connected=True, reset_only=False) @@ -1309,7 +1309,7 @@ class DeviceMixin(object): # {{{ for f in files: getattr(f, 'close', lambda : True)() - def book_on_device(self, id, format=None, reset=False): + def book_on_device(self, id, reset=False): ''' Return an indication of whether the given book represented by its db id is on the currently connected device. It returns a 5 element list. The @@ -1338,8 +1338,6 @@ class DeviceMixin(object): # {{{ self.book_db_id_cache.append(set()) for book in l: db_id = getattr(book, 'application_id', None) - if db_id is None: - db_id = book.db_id if db_id is not None: # increment the count of books on the device with this # db_id. diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 26dbda6ca4..53788809b6 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -300,6 +300,24 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.cpixmap = pix self.cover_data = cdata + def trim_cover(self, *args): + from calibre.utils.magick import Image + cdata = self.cover_data + if not cdata: + return + im = Image() + im.load(cdata) + im.trim(10) + cdata = im.export('jpg') + pix = QPixmap() + pix.loadFromData(cdata) + self.cover.setPixmap(pix) + self.cover_changed = True + self.cpixmap = pix + self.cover_data = cdata + + + def sync_formats(self): old_extensions, new_extensions, paths = set(), set(), {} for row in range(self.formats.count()): @@ -380,6 +398,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.remove_unused_series) QObject.connect(self.auto_author_sort, SIGNAL('clicked()'), self.deduce_author_sort) + self.trim_cover_button.clicked.connect(self.trim_cover) self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'), self.author_sort_box_changed) self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'), diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index 74febf9c29..dbf825e706 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -625,6 +625,17 @@ Using this button to create author sort will change author sort from red to gree + + + + Remove border (if any) from cover + + + + :/images/trim.png:/images/trim.png + + + diff --git a/src/calibre/gui2/dialogs/tweak_epub.py b/src/calibre/gui2/dialogs/tweak_epub.py new file mode 100755 index 0000000000..db6e93fd7a --- /dev/null +++ b/src/calibre/gui2/dialogs/tweak_epub.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, shutil +from contextlib import closing +from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED + +from PyQt4.Qt import QDialog + +from calibre.gui2 import open_local_file +from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog +from calibre.libunzip import extract as zipextract +from calibre.ptempfile import PersistentTemporaryDirectory + +class TweakEpub(QDialog, Ui_Dialog): + ''' + Display controls for tweaking ePubs + + ''' + + def __init__(self, parent, epub): + QDialog.__init__(self, parent) + + self._epub = epub + self._exploded = None + self._output = None + + # Run the dialog setup generated from tweak_epub.ui + self.setupUi(self) + + self.cancel_button.clicked.connect(self.reject) + self.explode_button.clicked.connect(self.explode) + self.rebuild_button.clicked.connect(self.rebuild) + + # Position update dialog overlaying top left of app window + parent_loc = parent.pos() + self.move(parent_loc.x(),parent_loc.y()) + + def cleanup(self): + # Delete directory containing exploded ePub + if self._exploded is not None: + shutil.rmtree(self._exploded, ignore_errors=True) + + + def display_exploded(self): + ''' + Generic subprocess launch of native file browser + User can use right-click to 'Open with ...' + ''' + open_local_file(self._exploded) + + def explode(self, *args): + if self._exploded is None: + self._exploded = PersistentTemporaryDirectory("_exploded", prefix='') + zipextract(self._epub, self._exploded) + self.display_exploded() + self.rebuild_button.setEnabled(True) + self.explode_button.setEnabled(False) + + def rebuild(self, *args): + self._output = os.path.join(self._exploded, 'rebuilt.epub') + with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf: + # Write mimetype + zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED) + # Write everything else + exclude_files = ['.DS_Store','mimetype','iTunesMetadata.plist','rebuilt.epub'] + for root, dirs, files in os.walk(self._exploded): + for fn in files: + if fn in exclude_files: + continue + absfn = os.path.join(root, fn) + zfn = os.path.relpath(absfn, + self._exploded).replace(os.sep, '/') + zf.write(absfn, zfn) + return QDialog.accept(self) + diff --git a/src/calibre/gui2/dialogs/tweak_epub.ui b/src/calibre/gui2/dialogs/tweak_epub.ui new file mode 100644 index 0000000000..ccd33f44ab --- /dev/null +++ b/src/calibre/gui2/dialogs/tweak_epub.ui @@ -0,0 +1,87 @@ + + + Dialog + + + Qt::NonModal + + + + 0 + 0 + 382 + 242 + + + + Tweak ePub + + + false + + + false + + + + + + Display contents of exploded ePub + + + &Explode ePub + + + + :/images/wizard.png:/images/wizard.png + + + + + + + false + + + Rebuild ePub from exploded contents + + + &Rebuild ePub + + + + :/images/exec.png:/images/exec.png + + + + + + + Discard changes + + + &Cancel + + + + :/images/window-close.png:/images/window-close.png + + + + + + + Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window. Rebuild the ePub, updating your calibre library. + + + true + + + + + + + + + + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 3370fd4b75..53f701386b 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -12,7 +12,7 @@ from operator import attrgetter from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ QModelIndex, QVariant, QDate -from calibre.gui2 import NONE, config, UNDEFINED_QDATE +from calibre.gui2 import NONE, config, UNDEFINED_QDATE, FunctionDispatcher from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors from calibre.ptempfile import PersistentTemporaryFile @@ -151,7 +151,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.database_changed.emit(db) if self.cover_cache is not None: self.cover_cache.stop() - self.cover_cache = CoverCache(db) + self.cover_cache = CoverCache(db, FunctionDispatcher(self.db.cover)) self.cover_cache.start() def refresh_cover(event, ids): if event == 'cover' and self.cover_cache is not None: diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 7a516bb4ff..2f0452a773 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -217,6 +217,10 @@ def fetch_scheduled_recipe(arg): if 'output_profile' in ps: recs.append(('output_profile', ps['output_profile'], OptionRecommendation.HIGH)) + if ps['output_profile'] == 'kindle': + recs.append(('no_inline_toc', True, + OptionRecommendation.HIGH)) + lf = load_defaults('look_and_feel') if lf.get('base_font_size', 0.0) != 0.0: recs.append(('base_font_size', lf['base_font_size'], diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 60224aefc7..5efce74c08 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -863,11 +863,11 @@ class SplitterHandle(QSplitterHandle): self.update() def paintEvent(self, ev): - QSplitterHandle.paintEvent(self, ev) if self.highlight: painter = QPainter(self) painter.setClipRect(ev.rect()) painter.fillRect(self.rect(), Qt.yellow) + QSplitterHandle.paintEvent(self, ev) def mouseDoubleClickEvent(self, ev): self.double_clicked.emit(self) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 211baeb634..58edd89cb2 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, itertools +import re, itertools, time from itertools import repeat from datetime import timedelta from threading import Thread, RLock @@ -23,10 +23,11 @@ from calibre import fit_image class CoverCache(Thread): - def __init__(self, db): + def __init__(self, db, cover_func): Thread.__init__(self) self.daemon = True self.db = db + self.cover_func = cover_func self.load_queue = Queue() self.keep_running = True self.cache = {} @@ -37,7 +38,8 @@ class CoverCache(Thread): self.keep_running = False def _image_for_id(self, id_): - img = self.db.cover(id_, index_is_id=True, as_image=True) + time.sleep(0.050) # Limit 20/second to not overwhelm the GUI + img = self.cover_func(id_, index_is_id=True, as_image=True) if img is None: img = QImage() if not img.isNull(): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index eb6e8336f9..1a2eef2c81 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -402,7 +402,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): path = path.lower() return path - def set_path(self, index, index_is_id=False, commit=True): + def set_path(self, index, index_is_id=False): ''' Set the path to the directory containing this books files based on its current title and author. If there was a previous directory, its contents @@ -432,7 +432,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if current_path and os.path.exists(spath): # Migrate existing files cdata = self.cover(id, index_is_id=True) if cdata is not None: - open(os.path.join(tpath, 'cover.jpg'), 'wb').write(cdata) + with open(os.path.join(tpath, 'cover.jpg'), 'wb') as f: + f.write(cdata) for format in formats: # Get data as string (can't use file as source and target files may be the same) f = self.format(id, format, index_is_id=True, as_file=False) @@ -442,8 +443,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.add_format(id, format, stream, index_is_id=True, path=tpath, notify=False) self.conn.execute('UPDATE books SET path=? WHERE id=?', (path, id)) - if commit: - self.conn.commit() self.data.set(id, self.FIELD_MAP['path'], path, row_is_id=True) # Delete not needed directories if current_path and os.path.exists(spath): @@ -1131,7 +1130,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def set_authors(self, id, authors, notify=True, commit=True): ''' - `authors`: A list of authors. + Note that even if commit is False, the db will still be committed to + because this causes the location of files to change + + :param authors: A list of authors. ''' if not authors: authors = [_('Unknown')] @@ -1163,11 +1165,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ','.join([a.replace(',', '|') for a in authors]), row_is_id=True) self.data.set(id, self.FIELD_MAP['author_sort'], ss, row_is_id=True) - self.set_path(id, index_is_id=True, commit=commit) + self.set_path(id, index_is_id=True) if notify: self.notify('metadata', [id]) def set_title(self, id, title, notify=True, commit=True): + ''' + Note that even if commit is False, the db will still be committed to + because this causes the location of files to change + ''' if not title: return if not isinstance(title, unicode): @@ -1178,7 +1184,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.data.set(id, self.FIELD_MAP['sort'], title_sort(title), row_is_id=True) else: self.data.set(id, self.FIELD_MAP['sort'], title, row_is_id=True) - self.set_path(id, index_is_id=True, commit=commit) + self.set_path(id, index_is_id=True) if commit: self.conn.commit() if notify: diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index ed9e3d3d83..dcf9d7b671 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -60,15 +60,15 @@ def identify(path): data = open(path, 'rb').read() return identify_data(data) -def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0, - border_color='#ffffff'): +def add_borders_to_image(img_data, left=0, top=0, right=0, bottom=0, + border_color='#ffffff', fmt='jpg'): img = Image() - img.open(path_to_image) + img.load(img_data) lwidth, lheight = img.size canvas = create_canvas(lwidth+left+right, lheight+top+bottom, border_color) canvas.compose(img, left, top) - canvas.save(path_to_image) + return canvas.export(fmt) def create_text_wand(font_size, font_path=None): if font_path is None: diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index a140dfbf05..d1e7866198 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -7,7 +7,7 @@ Defines various abstract base classes that can be subclassed to create powerful __docformat__ = "restructuredtext en" -import os, time, traceback, re, urlparse, sys +import os, time, traceback, re, urlparse, sys, cStringIO from collections import defaultdict from functools import partial from contextlib import nested, closing @@ -27,6 +27,7 @@ from calibre.web.fetch.simple import RecursiveFetcher from calibre.utils.threadpool import WorkRequest, ThreadPool, NoResultsPending from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.date import now as nowf +from calibre.utils.magick.draw import save_cover_data_to, add_borders_to_image class LoginFailed(ValueError): pass @@ -948,38 +949,36 @@ class BasicNewsRecipe(Recipe): try: cu = self.get_cover_url() except Exception, err: - cu = None self.log.error(_('Could not download cover: %s')%str(err)) self.log.debug(traceback.format_exc()) - if cu is not None: - ext = cu.split('/')[-1].rpartition('.')[-1] - if '?' in ext: - ext = '' - ext = ext.lower() if ext and '/' not in ext else 'jpg' - cpath = os.path.join(self.output_dir, 'cover.'+ext) + else: + cdata = None if os.access(cu, os.R_OK): - with open(cpath, 'wb') as cfile: - cfile.write(open(cu, 'rb').read()) + cdata = open(cu, 'rb').read() else: self.report_progress(1, _('Downloading cover from %s')%cu) - with nested(open(cpath, 'wb'), closing(self.browser.open(cu))) as (cfile, r): - cfile.write(r.read()) - if self.cover_margins[0] or self.cover_margins[1]: - from calibre.utils.magick.draw import add_borders_to_image - add_borders_to_image(cpath, - left=self.cover_margins[0],right=self.cover_margins[0], - top=self.cover_margins[1],bottom=self.cover_margins[1], - border_color=self.cover_margins[2]) - if ext.lower() == 'pdf': + with closing(self.browser.open(cu)) as r: + cdata = r.read() + if not cdata: + return + ext = cu.split('/')[-1].rpartition('.')[-1].lower().strip() + if ext == 'pdf': from calibre.ebooks.metadata.pdf import get_metadata - stream = open(cpath, 'rb') + stream = cStringIO.StringIO(cdata) + cdata = None mi = get_metadata(stream) - cpath = None if mi.cover_data and mi.cover_data[1]: - cpath = os.path.join(self.output_dir, - 'cover.'+mi.cover_data[0]) - with open(cpath, 'wb') as f: - f.write(mi.cover_data[1]) + cdata = mi.cover_data[1] + if not cdata: + return + if self.cover_margins[0] or self.cover_margins[1]: + cdata = add_borders_to_image(cdata, + left=self.cover_margins[0],right=self.cover_margins[0], + top=self.cover_margins[1],bottom=self.cover_margins[1], + border_color=self.cover_margins[2]) + + cpath = os.path.join(self.output_dir, 'cover.jpg') + save_cover_data_to(cdata, cpath) self.cover_path = cpath def download_cover(self): @@ -1422,7 +1421,6 @@ class CalibrePeriodical(BasicNewsRecipe): return br def download(self): - import cStringIO self.log('Fetching downloaded recipe') try: raw = self.browser.open_novisit(