diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 98653a4d50..84dff6052b 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -254,6 +254,7 @@ class iPadOutput(OutputProfile): comic_screen_size = (768, 1024) dpi = 132.0 supports_nested_toc = False + touchscreen = True class SonyReaderOutput(OutputProfile): diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 26b3ad0593..efb24bcc9a 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -254,7 +254,7 @@ class BasicNewsRecipe(Recipe): #: will remove everythong from `` to ``. preprocess_regexps = [] - #: The CSS that is used to styles the templates, i.e., the navigation bars and + #: The CSS that is used to style the templates, i.e., the navigation bars and #: the Tables of Contents. Rather than overriding this variable, you should #: use :member:`extra_css` in your recipe to customize look and feel. template_css = u''' @@ -517,6 +517,21 @@ class BasicNewsRecipe(Recipe): ''' raise NotImplementedError + def extract_author(self, soup): + ''' + Parse downloaded articles for author, add to OEBBook object. + :param soup: + ''' + return None + + def extract_description(self, soup): + ''' + Parse downloaded articles for description, add to OEBBook object. + :param soup: + ''' + return None + + def postprocess_book(self, oeb, opts, log): ''' Run any needed post processing on the parsed downloaded e-book. @@ -544,6 +559,8 @@ class BasicNewsRecipe(Recipe): self.username = options.username self.password = options.password self.lrf = options.lrf + self.output_profile = options.output_profile.name + self.touchscreen = getattr(options.output_profile,'touchscreen',False) self.output_dir = os.path.abspath(self.output_dir) if options.test: @@ -597,7 +614,7 @@ class BasicNewsRecipe(Recipe): if self.delay > 0: self.simultaneous_downloads = 1 - self.navbar = templates.NavBarTemplate() + self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else templates.NavBarTemplate() self.failed_downloads = [] self.partial_failures = [] @@ -674,7 +691,11 @@ class BasicNewsRecipe(Recipe): def feeds2index(self, feeds): templ = templates.IndexTemplate() css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '') - return templ.generate(self.title, self.timefmt, feeds, + timefmt = self.timefmt + if self.touchscreen: + templ = templates.TouchscreenIndexTemplate() + timefmt = '%A, %d %b %Y' + return templ.generate(self.title, "mastheadImage.jpg", timefmt, feeds, extra_css=css).render(doctype='xhtml') @classmethod @@ -727,6 +748,44 @@ class BasicNewsRecipe(Recipe): templ = templates.FeedTemplate() css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '') + + if self.touchscreen: + touchscreen_css = u''' + .summary_headline { + font-size:large; font-weight:bold; margin-top:0px; margin-bottom:0px; + } + + .summary_byline { + font-size:small; margin-top:0px; margin-bottom:0px; + } + + .summary_text { + margin-top:0px; margin-bottom:0px; + } + + .feed { + font-family:sans-serif; font-weight:bold; font-size:larger; + } + + .calibre_navbar { + font-family:monospace; + } + hr { + border-color:gray; + border-style:solid; + border-width:thin; + } + + table.toc { + font-size:large; + } + td.article_count { + text-align:right; + } + ''' + + templ = templates.TouchscreenFeedTemplate() + css = touchscreen_css + '\n\n' + (self.extra_css if self.extra_css else '') return templ.generate(feed, self.description_limiter, extra_css=css).render(doctype='xhtml') @@ -868,9 +927,41 @@ class BasicNewsRecipe(Recipe): #feeds.restore_duplicates() + # GwR Populate any missing author/description fields in feed for f, feed in enumerate(feeds): - html = self.feed2index(feed) feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) + for article in feed.articles: + if article.summary == '' or article.author == '': + file = os.path.join(self.output_dir,feed_dir, article.url) + if os.path.exists(file): + with open(file, 'rb') as fi: + src = fi.read().decode('utf-8') + soup = BeautifulSoup(src) + if article.author == '': + author = self.extract_author(soup) + if author and not isinstance(author, unicode): + author = author.decode('utf-8', 'replace') + article.author = author + + if article.summary == '': + summary = article.summary = self.extract_description(soup) + if summary and not isinstance(summary, unicode): + summary = summary.decode('utf-8', 'replace') + if summary and '<' in summary: + try: + s = html.fragment_fromstring(summary, create_parent=True) + summary = html.tostring(s, method='text', encoding=unicode) + except: + print 'Failed to process article summary, deleting:' + print summary.encode('utf-8') + traceback.print_exc() + summary = u'' + article.text_summary = summary + + + for f, feed in enumerate(feeds): + feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) + html = self.feed2index(feed) with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi: fi.write(html) self.create_opf(feeds) @@ -949,13 +1040,47 @@ class BasicNewsRecipe(Recipe): Create a generic cover for recipes that dont have a cover ''' try: - from calibre.utils.magick_draw import create_cover_page, TextLine + try: + from PIL import Image, ImageDraw, ImageFont + Image, ImageDraw, ImageFont + except ImportError: + import Image, ImageDraw, ImageFont + font_path = P('fonts/liberation/LiberationSerif-Bold.ttf') title = self.title if isinstance(self.title, unicode) else \ self.title.decode(preferred_encoding, 'replace') date = strftime(self.timefmt) - lines = [TextLine(title, 44), TextLine(date, 32)] - img_data = create_cover_page(lines, I('library.png'), output_format='jpg') - cover_file.write(img_data) + app = '['+__appname__ +' '+__version__+']' + + COVER_WIDTH, COVER_HEIGHT = 590, 750 + img = Image.new('RGB', (COVER_WIDTH, COVER_HEIGHT), 'white') + draw = ImageDraw.Draw(img) + # Title + font = ImageFont.truetype(font_path, 44) + width, height = draw.textsize(title, font=font) + left = max(int((COVER_WIDTH - width)/2.), 0) + top = 15 + draw.text((left, top), title, fill=(0,0,0), font=font) + bottom = top + height + # Date + font = ImageFont.truetype(font_path, 32) + width, height = draw.textsize(date, font=font) + left = max(int((COVER_WIDTH - width)/2.), 0) + draw.text((left, bottom+15), date, fill=(0,0,0), font=font) + # Vanity + font = ImageFont.truetype(font_path, 28) + width, height = draw.textsize(app, font=font) + left = max(int((COVER_WIDTH - width)/2.), 0) + top = COVER_HEIGHT - height - 15 + draw.text((left, top), app, fill=(0,0,0), font=font) + # Logo + logo = Image.open(I('library.png'), 'r') + width, height = logo.size + left = max(int((COVER_WIDTH - width)/2.), 0) + top = max(int((COVER_HEIGHT - height)/2.), 0) + img.paste(logo, (left, top)) + img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE) + + img.convert('RGB').save(cover_file, 'JPEG') cover_file.flush() except: self.log.exception('Failed to generate default cover') @@ -1048,16 +1173,21 @@ class BasicNewsRecipe(Recipe): pw.DestroyMagickWand(x) def create_opf(self, feeds, dir=None): + if dir is None: dir = self.output_dir mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__]) - mi.publisher = __appname__ mi.author_sort = __appname__ + if self.output_profile == 'iPad': + mi = MetaInformation(self.short_title(), [strftime('%A, %d %B %Y')]) + mi.author_sort = strftime('%Y-%m-%d') + mi.publisher = __appname__ mi.publication_type = 'periodical:'+self.publication_type mi.timestamp = nowf() mi.comments = self.description if not isinstance(mi.comments, unicode): mi.comments = mi.comments.decode('utf-8', 'replace') + mi.tags = ['News'] mi.pubdate = nowf() opf_path = os.path.join(dir, 'index.opf') ncx_path = os.path.join(dir, 'index.ncx') @@ -1100,7 +1230,7 @@ class BasicNewsRecipe(Recipe): entries = ['index.html'] toc = TOC(base_path=dir) - self.play_order_counter = 0 + self.play_order_counter = 1 self.play_order_map = {} def feed_index(num, parent): @@ -1212,6 +1342,7 @@ class BasicNewsRecipe(Recipe): Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`. Return a list of :class:`Feed` objects. ''' + print "\nweb.feeds.news:parse_feeds()\n" feeds = self.get_feeds() parsed_feeds = [] for obj in feeds: diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index 4de7c42daa..af0c8da6b4 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -5,7 +5,8 @@ __copyright__ = '2008, Kovid Goyal ' from lxml import html, etree from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \ - STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT + STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ + TABLE, TD, TR from calibre import preferred_encoding, strftime, isbytestring @@ -89,12 +90,55 @@ class NavBarTemplate(Template): self.root = HTML(head, BODY(navbar)) +class TouchscreenNavBarTemplate(Template): + def _generate(self, bottom, feed, art, number_of_articles_in_feed, + two_levels, url, __appname__, prefix='', center=True, + extra_css=None, style=None): + head = HEAD(TITLE('navbar')) + if style: + head.append(STYLE(style, type='text/css')) + if extra_css: + head.append(STYLE(extra_css, type='text/css')) + if prefix and not prefix.endswith('/'): + prefix += '/' + align = 'center' if center else 'left' + navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100', + style='text-align:'+align)) + if bottom: + navbar.append(HR()) + text = 'This article was downloaded by ' + p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') + p[0].tail = ' from ' + navbar.append(BR()) + navbar.append(BR()) + else: + next = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \ + else 'article_%d'%(art+1) + up = '../..' if art == number_of_articles_in_feed - 1 else '..' + href = '%s%s/%s/index.html'%(prefix, up, next) + navbar.text = '| ' + navbar.append(A('Next', href=href)) + href = '%s../index.html#article_%d'%(prefix, art) + navbar.iterchildren(reversed=True).next().tail = ' | ' + navbar.append(A('Section Menu', href=href)) + href = '%s../../index.html#feed_%d'%(prefix, feed) + navbar.iterchildren(reversed=True).next().tail = ' | ' + navbar.append(A('Main Menu', href=href)) + if art > 0 and not bottom: + href = '%s../article_%d/index.html'%(prefix, art-1) + navbar.iterchildren(reversed=True).next().tail = ' | ' + navbar.append(A('Previous', href=href)) + navbar.iterchildren(reversed=True).next().tail = ' | ' + if not bottom: + navbar.append(HR()) + + self.root = HTML(head, BODY(navbar)) class IndexTemplate(Template): - def _generate(self, title, datefmt, feeds, extra_css=None, style=None): + def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): if isinstance(datefmt, unicode): datefmt = datefmt.encode(preferred_encoding) date = strftime(datefmt) @@ -110,12 +154,40 @@ class IndexTemplate(Template): href='feed_%d/index.html'%i)), id='feed_%d'%i) ul.append(li) div = DIV( - H1(title, CLASS('calibre_recipe_title', 'calibre_rescale_180')), + PT(IMG(src=masthead,alt="masthead"),style='text-align:center'), PT(date, style='text-align:right'), ul, CLASS('calibre_rescale_100')) self.root = HTML(head, BODY(div)) +class TouchscreenIndexTemplate(Template): + + def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): + if isinstance(datefmt, unicode): + datefmt = datefmt.encode(preferred_encoding) + date = strftime(datefmt) + masthead_img = IMG(src=masthead,alt="masthead") + head = HEAD(TITLE(title)) + if style: + head.append(STYLE(style, type='text/css')) + if extra_css: + head.append(STYLE(extra_css, type='text/css')) + + toc = TABLE(CLASS('toc'),width="100%",border="0",cellpadding="3px") + for i, feed in enumerate(feeds): + if feed: + tr = TR() + tr.append(TD( CLASS('toc_item'), A(feed.title, href='feed_%d/index.html'%i))) + tr.append(TD( CLASS('article_count'),'%d' % len(feed.articles))) + toc.append(tr) + + div = DIV( + PT(masthead_img,style='text-align:center'), + PT(date, style='text-align:center'), + toc, + CLASS('calibre_rescale_100')) + self.root = HTML(head, BODY(div)) + class FeedTemplate(Template): def _generate(self, feed, cutoff, extra_css=None, style=None): @@ -166,6 +238,56 @@ class FeedTemplate(Template): self.root = HTML(head, body) +class TouchscreenFeedTemplate(Template): + + def _generate(self, feed, cutoff, extra_css=None, style=None): + head = HEAD(TITLE(feed.title)) + if style: + head.append(STYLE(style, type='text/css')) + if extra_css: + head.append(STYLE(extra_css, type='text/css')) + body = BODY(style='page-break-before:always') + div = DIV( + H2(feed.title, + CLASS('calibre_feed_title', 'calibre_rescale_160')), + CLASS('calibre_rescale_100') + ) + body.append(div) + if getattr(feed, 'image', None): + div.append(DIV(IMG( + alt = feed.image_alt if feed.image_alt else '', + src = feed.image_url + ), + CLASS('calibre_feed_image'))) + if getattr(feed, 'description', None): + d = DIV(feed.description, CLASS('calibre_feed_description', + 'calibre_rescale_80')) + d.append(BR()) + div.append(d) + + toc = TABLE(CLASS('toc'),width="100%",border="0",cellpadding="3px") + for i, article in enumerate(feed.articles): + if not getattr(article, 'downloaded', False): + continue + tr = TR() + td = TD( + A(article.title, CLASS('article calibre_rescale_100', + href=article.url)) + ) + if article.summary: + td.append(DIV(cutoff(article.text_summary), + CLASS('article_description', 'calibre_rescale_80'))) + tr.append(td) + toc.append(tr) + div.append(toc) + + navbar = DIV('| ', CLASS('calibre_navbar', 'calibre_rescale_100'),style='text-align:center') + link = A('Up one level', href="../index.html") + link.tail = ' |' + navbar.append(link) + div.append(navbar) + + self.root = HTML(head, body) class EmbeddedContent(Template): diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index 93fb516f2d..37858268d4 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -328,6 +328,9 @@ class RecursiveFetcher(object): continue try: data = self.fetch_url(iurl) + if data == 'GIF89a\x01': + # Skip empty GIF files + continue except Exception: self.log.exception('Could not fetch image %s'% iurl) continue