diff --git a/resources/quick_start.epub b/resources/quick_start.epub index d5aeec5457..589fd1d0dc 100644 Binary files a/resources/quick_start.epub and b/resources/quick_start.epub differ diff --git a/resources/recipes/nytimes.recipe b/resources/recipes/nytimes.recipe index 9fbcf6d3d1..3e02363f68 100644 --- a/resources/recipes/nytimes.recipe +++ b/resources/recipes/nytimes.recipe @@ -17,7 +17,7 @@ class NYTimes(BasicNewsRecipe): title = 'New York Times Top Stories' __author__ = 'GRiker' language = 'en' - requires_version = (0, 7, 3) + requires_version = (0, 7, 5) description = 'Top Stories from the New York Times' # List of sections typically included in Top Stories. Use a keyword from the @@ -79,6 +79,7 @@ class NYTimes(BasicNewsRecipe): 'doubleRule', 'dottedLine', 'entry-meta', + 'entry-response module', 'icon enlargeThis', 'leftNavTabs', 'module box nav', @@ -110,6 +111,7 @@ class NYTimes(BasicNewsRecipe): 'navigation', 'portfolioInline', 'relatedArticles', + 'respond', 'side_search', 'side_index', 'side_tool', diff --git a/resources/recipes/nytimes_sub.recipe b/resources/recipes/nytimes_sub.recipe index bcec51ce97..f8ad12afe9 100644 --- a/resources/recipes/nytimes_sub.recipe +++ b/resources/recipes/nytimes_sub.recipe @@ -13,14 +13,14 @@ Story import re, string, time from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag +from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, NavigableString, Tag class NYTimes(BasicNewsRecipe): title = 'The New York Times' __author__ = 'GRiker' language = 'en' - requires_version = (0, 7, 3) + requires_version = (0, 7, 5) description = 'Daily news from the New York Times (subscription version)' allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials', @@ -66,6 +66,7 @@ class NYTimes(BasicNewsRecipe): 'doubleRule', 'dottedLine', 'entry-meta', + 'entry-response module', 'icon enlargeThis', 'leftNavTabs', 'module box nav', @@ -97,6 +98,7 @@ class NYTimes(BasicNewsRecipe): 'navigation', 'portfolioInline', 'relatedArticles', + 'respond', 'side_search', 'side_index', 'side_tool', @@ -417,12 +419,11 @@ class NYTimes(BasicNewsRecipe): return soup - def postprocess_book(self, oeb, opts, log) : - print "\npostprocess_book()\n" - - def extract_byline(href) : - # :' + articlebody = soup.find('div',attrs={'class':'articlebody'}) + if not articlebody: + print 'postprocess_book.extract_description(): Did not find
:' print soup.prettify() return None - paras = articleBody.findAll('p') + paras = articlebody.findAll('p') for p in paras: if p.renderContents() > '' : return self.massageNCXText(self.tag_to_string(p,use_alt=False)) return None - # Method entry point here - # Single section toc looks different than multi-section tocs - if oeb.toc.depth() == 2 : - for article in oeb.toc : - if article.author is None : - article.author = extract_byline(article.href) - if article.description is None : - article.description = extract_description(article.href).decode('utf-8') - elif oeb.toc.depth() == 3 : - for section in oeb.toc : - for article in section : - if article.author is None : - article.author = extract_byline(article.href) - if article.description is None : - article.description = extract_description(article.href) + article.author = extract_author(soup) + article.summary = article.text_summary = extract_description(soup) def strip_anchors(self,soup): paras = soup.findAll(True) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index c872c9df38..a16520410f 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -36,7 +36,7 @@ class Plugin(_Plugin): self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name) self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num) - +# Input profiles {{{ class InputProfile(Plugin): author = 'Kovid Goyal' @@ -218,6 +218,8 @@ input_profiles = [InputProfile, SonyReaderInput, SonyReader300Input, input_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower())) +# }}} + class OutputProfile(Plugin): author = 'Kovid Goyal' @@ -237,11 +239,12 @@ class OutputProfile(Plugin): # If True the MOBI renderer on the device supports MOBI indexing supports_mobi_indexing = False - # Device supports displaying a nested TOC - supports_nested_toc = True - # If True output should be optimized for a touchscreen interface touchscreen = False + touchscreen_news_css = '' + # A list of extra (beyond CSS 2.1) modules supported by the device + # Format is a cssutils profile dictionary (see iPad for example) + extra_css_modules = [] @classmethod def tags_to_string(cls, tags): @@ -256,8 +259,86 @@ class iPadOutput(OutputProfile): screen_size = (768, 1024) comic_screen_size = (768, 1024) dpi = 132.0 - supports_nested_toc = False + extra_css_modules = [ + { + 'name':'webkit', + 'props': { '-webkit-border-bottom-left-radius':'{length}', + '-webkit-border-bottom-right-radius':'{length}', + '-webkit-border-top-left-radius':'{length}', + '-webkit-border-top-right-radius':'{length}', + '-webkit-border-radius': r'{border-width}(\s+{border-width}){0,3}|inherit', + }, + 'macros': {'border-width': '{length}|medium|thick|thin'} + } + ] touchscreen = True + # touchscreen_news_css {{{ + touchscreen_news_css = u''' + /* hr used in articles */ + .caption_divider { + border:#ccc 1px solid; + } + + .touchscreen_navbar { + background:#ccc; + border:#ccc 1px solid; + border-collapse:separate; + border-spacing:1px; + margin-left: 5%; + margin-right: 5%; + width: 90%; + -webkit-border-radius:4px; + } + .touchscreen_navbar td { + background:#fff; + font-family:Helvetica; + font-size:80%; + padding: 5px; + text-align:center; + } + .touchscreen_navbar td:first-child { + -webkit-border-top-left-radius:4px; + -webkit-border-bottom-left-radius:4px; + } + .touchscreen_navbar td:last-child { + -webkit-border-top-right-radius:4px; + -webkit-border-bottom-right-radius:4px; + } + + .feed_link { + font-style: italic; + } + + + /* Feed summary formatting */ + .feed_title { + text-align: center; + font-size: 160%; + } + + .summary_headline { + font-weight:bold; + text-align:left; + } + + .summary_byline { + text-align:left; + font-family:monospace; + } + + .summary_text { + text-align:left; + } + + .feed { + font-family:sans-serif; + font-weight:bold; + font-size:larger; + } + + ''' + # }}} + class SonyReaderOutput(OutputProfile): diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index a994efb0f6..77f33ccf3d 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -10,12 +10,13 @@ from calibre.constants import __appname__, __version__, DEBUG from calibre import fit_image from calibre.constants import isosx, iswindows from calibre.devices.errors import UserFeedback +from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.interface import DevicePlugin from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata.epub import set_metadata from calibre.library.server.utils import strftime -from calibre.utils.config import Config, config_dir +from calibre.utils.config import config_dir from calibre.utils.date import isoformat, now, parse_date from calibre.utils.logging import Log from calibre.utils.zipfile import ZipFile @@ -33,8 +34,15 @@ if isosx: if iswindows: import pythoncom, win32com.client +class DriverBase(DeviceConfig, DevicePlugin): + # Needed for config_widget to work + FORMATS = ['epub', 'pdf'] -class ITUNES(DevicePlugin): + @classmethod + def _config_base_name(cls): + return 'iTunes' + +class ITUNES(DriverBase): ''' Calling sequences: Initialization: @@ -78,12 +86,11 @@ class ITUNES(DevicePlugin): supported_platforms = ['osx','windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (0,7,0) + version = (0,8,0) OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') - FORMATS = ['epub'] # Product IDs: # 0x1292:iPhone 3G @@ -141,6 +148,10 @@ class ITUNES(DevicePlugin): 'SongNames', ] + # Cover art size limits + MAX_COVER_WIDTH = 510 + MAX_COVER_HEIGHT = 680 + # Properties cached_books = {} cache_dir = os.path.join(config_dir, 'caches', 'itunes') @@ -159,7 +170,6 @@ class ITUNES(DevicePlugin): sources = None update_msg = None update_needed = False - use_series_data = True # Public methods def add_books_to_metadata(self, locations, metadata, booklists): @@ -173,16 +183,17 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' + if DEBUG: + self.log.info("ITUNES.add_books_to_metadata()") task_count = float(len(self.update_list)) # Delete any obsolete copies of the book from the booklist if self.update_list: - if True: - self.log.info("ITUNES.add_books_to_metadata()") - #self._dump_booklist(booklists[0], header='before',indent=2) - #self._dump_update_list(header='before',indent=2) - #self._dump_cached_books(header='before',indent=2) + if False: + self._dump_booklist(booklists[0], header='before',indent=2) + self._dump_update_list(header='before',indent=2) + self._dump_cached_books(header='before',indent=2) for (j,p_book) in enumerate(self.update_list): if False: @@ -230,12 +241,12 @@ class ITUNES(DevicePlugin): # Add new books to booklists[0] for new_book in locations[0]: - if False: + if DEBUG: self.log.info(" adding '%s' by '%s' to booklists[0]" % (new_book.title, new_book.author)) booklists[0].append(new_book) - if False: + if DEBUG: self._dump_booklist(booklists[0],header='after',indent=2) self._dump_cached_books(header='after',indent=2) @@ -329,7 +340,8 @@ class ITUNES(DevicePlugin): 'title':book.Name, 'author':book.Artist, 'lib_book':library_books[this_book.path] if this_book.path in library_books else None, - 'uuid': book.Composer + 'uuid': book.Composer, + 'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub' } if self.report_progress is not None: @@ -343,9 +355,9 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(1.0, _('finished')) self.cached_books = cached_books -# if DEBUG: -# self._dump_booklist(booklist, 'returning from books():') -# self._dump_cached_books('returning from books():') + if DEBUG: + self._dump_booklist(booklist, 'returning from books()', indent=2) + self._dump_cached_books('returning from books()',indent=2) return booklist else: return [] @@ -506,6 +518,19 @@ class ITUNES(DevicePlugin): ''' return (None,None) + @classmethod + def config_widget(cls): + ''' + Return a QWidget with settings for the device interface + ''' + cw = DriverBase.config_widget() + # Turn off the Save template + cw.opt_save_template.setVisible(False) + cw.label.setVisible(False) + # Repurpose the checkbox + cw.opt_read_metadata.setText(_("Use Series as Genre in iTunes/iBooks")) + return cw + def delete_books(self, paths, end_session=True): ''' Delete books at paths on device. @@ -685,6 +710,9 @@ class ITUNES(DevicePlugin): @param booklists: A tuple containing the result of calls to (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). + + NB: This will not find books that were added by a different installation of calibre + as uuids are different ''' if DEBUG: self.log.info("ITUNES.remove_books_from_metadata()") @@ -732,17 +760,6 @@ class ITUNES(DevicePlugin): ''' self.report_progress = report_progress - def settings(self): - ''' - Should return an opts object. The opts object should have one attribute - `format_map` which is an ordered list of formats for the device. - ''' - klass = self if isinstance(self, type) else self.__class__ - c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers')) - c.add_opt('format_map', default=self.FORMATS, - help=_('Ordered list of formats the device will accept')) - return c.parse() - def sync_booklists(self, booklists, end_session=True): ''' Update metadata on device. @@ -750,6 +767,10 @@ class ITUNES(DevicePlugin): (L{books}(oncard=None), L{books}(oncard='carda'), L{books}(oncard='cardb')). ''' + + if DEBUG: + self.log.info("ITUNES.sync_booklists()") + if self.update_needed: if DEBUG: self.log.info(' calling _update_device') @@ -812,29 +833,32 @@ class ITUNES(DevicePlugin): self.problem_msg = _("Some cover art could not be converted.\n" "Click 'Show Details' for a list.") - if False: + if DEBUG: self.log.info("ITUNES.upload_books()") self._dump_files(files, header='upload_books()',indent=2) self._dump_update_list(header='upload_books()',indent=2) if isosx: for (i,file) in enumerate(files): + format = file.rpartition('.')[2].lower() path = self.path_template % (metadata[i].title, metadata[i].author[0]) self._remove_existing_copy(path, metadata[i]) - fpath = self._get_fpath(file, metadata[i], update_md=True) + fpath = self._get_fpath(file, metadata[i], format, update_md=True) db_added, lb_added = self._add_new_copy(fpath, metadata[i]) - thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added) - this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb) + thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format) + this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format) new_booklist.append(this_book) self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book) # Add new_book to self.cached_paths self.cached_books[this_book.path] = { - 'title': metadata[i].title, 'author': metadata[i].author, - 'lib_book': lb_added, 'dev_book': db_added, - 'uuid': metadata[i].uuid} + 'format': format, + 'lib_book': lb_added, + 'title': metadata[i].title, + 'uuid': metadata[i].uuid } + # Report progress if self.report_progress is not None: @@ -846,9 +870,10 @@ class ITUNES(DevicePlugin): self.iTunes = win32com.client.Dispatch("iTunes.Application") for (i,file) in enumerate(files): + format = file.rpartition('.')[2].lower() path = self.path_template % (metadata[i].title, metadata[i].author[0]) self._remove_existing_copy(path, metadata[i]) - fpath = self._get_fpath(file, metadata[i], update_md=True) + fpath = self._get_fpath(file, metadata[i],format, update_md=True) db_added, lb_added = self._add_new_copy(fpath, metadata[i]) if self.manual_sync_mode and not db_added: @@ -857,17 +882,18 @@ class ITUNES(DevicePlugin): "Click 'Show Details...' for affected books.") self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0])) - thumb = self._cover_to_thumb(path, metadata[i], lb_added, db_added) - this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb) + thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format) + this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format) new_booklist.append(this_book) self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book) # Add new_book to self.cached_paths self.cached_books[this_book.path] = { - 'title': metadata[i].title, 'author': metadata[i].author[0], - 'lib_book': lb_added, 'dev_book': db_added, + 'format': format, + 'lib_book': lb_added, + 'title': metadata[i].title, 'uuid': metadata[i].uuid} # Report progress @@ -968,7 +994,8 @@ class ITUNES(DevicePlugin): db_added = self._find_device_book( {'title': metadata.title, 'author': metadata.authors[0], - 'uuid': metadata.uuid}) + 'uuid': metadata.uuid, + 'format': fpath.rpartition('.')[2].lower()}) return db_added @@ -1021,7 +1048,8 @@ class ITUNES(DevicePlugin): added = self._find_library_book( { 'title': metadata.title, 'author': metadata.author[0], - 'uuid': metadata.uuid}) + 'uuid': metadata.uuid, + 'format': file.rpartition('.')[2].lower()}) return added def _add_new_copy(self, fpath, metadata): @@ -1047,46 +1075,82 @@ class ITUNES(DevicePlugin): return db_added, lb_added - def _cover_to_thumb(self, path, metadata, db_added, lb_added): + def _cover_to_thumb(self, path, metadata, db_added, lb_added, format): ''' assumes pythoncom wrapper for db_added + as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation ''' self.log.info(" ITUNES._cover_to_thumb()") + thumb = None if metadata.cover: - if isosx: - cover_data = open(metadata.cover,'rb') - if lb_added: - lb_added.artworks[1].data_.set(cover_data.read()) - if db_added: - # The following command generates an error, but the artwork does in fact - # get sent to the device. Seems like a bug in Apple's automation interface - try: - db_added.artworks[1].data_.set(cover_data.read()) - except: + if (format == 'epub'): + # Pre-shrink cover + # self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT + try: + img = PILImage.open(metadata.cover) + width = img.size[0] + height = img.size[1] + scaled, nwidth, nheight = fit_image(width, height, self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT) + if scaled: if DEBUG: - self.log.warning(" iTunes automation interface reported an error" - " when adding artwork to '%s' on the iDevice" % metadata.title) - #import traceback - #traceback.print_exc() - #from calibre import ipython - #ipython(user_ns=locals()) - pass - - - elif iswindows: - if lb_added: - if lb_added.Artwork.Count: - lb_added.Artwork.Item(1).SetArtworkFromFile(metadata.cover) + self.log.info(" '%s' scaled from %sx%s to %sx%s" % + (metadata.cover,width,height,nwidth,nheight)) + img = img.resize((nwidth, nheight), PILImage.ANTIALIAS) + cd = cStringIO.StringIO() + img.convert('RGB').save(cd, 'JPEG') + cover_data = cd.getvalue() + cd.close() else: - lb_added.AddArtworkFromFile(metadata.cover) + with open(metadata.cover,'r+b') as cd: + cover_data = cd.read() + except: + self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0])) + self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title)) + return thumb - if db_added: - if db_added.Artwork.Count: - db_added.Artwork.Item(1).SetArtworkFromFile(metadata.cover) - else: - db_added.AddArtworkFromFile(metadata.cover) + if isosx: + if lb_added: + lb_added.artworks[1].data_.set(cover_data) + + if db_added: + # The following command generates an error, but the artwork does in fact + # get sent to the device. Seems like a bug in Apple's automation interface + try: + db_added.artworks[1].data_.set(cover_data) + except: + if DEBUG: + self.log.warning(" iTunes automation interface reported an error" + " when adding artwork to '%s' on the iDevice" % metadata.title) + #import traceback + #traceback.print_exc() + #from calibre import ipython + #ipython(user_ns=locals()) + pass + + + elif iswindows: + # Write the data to a real file for Windows iTunes + tc = os.path.join(tempfile.gettempdir(), "cover.jpg") + with open(tc,'wb') as tmp_cover: + tmp_cover.write(cover_data) + + if lb_added: + if lb_added.Artwork.Count: + lb_added.Artwork.Item(1).SetArtworkFromFile(tc) + else: + lb_added.AddArtworkFromFile(tc) + + if db_added: + if db_added.Artwork.Count: + db_added.Artwork.Item(1).SetArtworkFromFile(tc) + else: + db_added.AddArtworkFromFile(tc) + + elif format == 'pdf': + if DEBUG: + self.log.info(" unable to set PDF cover via automation interface") try: # Resize for thumb @@ -1097,6 +1161,7 @@ class ITUNES(DevicePlugin): of = cStringIO.StringIO() im.convert('RGB').save(of, 'JPEG') thumb = of.getvalue() + of.close() # Refresh the thumbnail cache if DEBUG: @@ -1105,14 +1170,15 @@ class ITUNES(DevicePlugin): zfw = ZipFile(archive_path, mode='a') thumb_path = path.rpartition('.')[0] + '.jpg' zfw.writestr(thumb_path, thumb) - zfw.close() except: self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0])) self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title)) + finally: + zfw.close() - return thumb + return thumb - def _create_new_book(self,fpath, metadata, path, db_added, lb_added, thumb): + def _create_new_book(self,fpath, metadata, path, db_added, lb_added, thumb, format): ''' ''' if DEBUG: @@ -1122,6 +1188,7 @@ class ITUNES(DevicePlugin): this_book.db_id = None this_book.device_collections = [] + this_book.format = format this_book.library_id = lb_added this_book.path = path this_book.thumbnail = thumb @@ -1319,10 +1386,11 @@ class ITUNES(DevicePlugin): self.cached_books[cb]['uuid'])) elif iswindows: for cb in self.cached_books.keys(): - self.log.info("%s%-40.40s %-30.30s %s" % + self.log.info("%s%-40.40s %-30.30s %-4.4s %s" % (' '*indent, self.cached_books[cb]['title'], self.cached_books[cb]['author'], + self.cached_books[cb]['format'], self.cached_books[cb]['uuid'])) self.log.info() @@ -1338,8 +1406,9 @@ class ITUNES(DevicePlugin): fnames = zf.namelist() opf = [x for x in fnames if '.opf' in x][0] if opf: - opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue() - soup = BeautifulSoup(opf_raw) + opf_raw = cStringIO.StringIO(zf.read(opf)) + soup = BeautifulSoup(opf_raw.getvalue()) + opf_raw.close() title = soup.find('dc:title').renderContents() author = soup.find('dc:creator').renderContents() ts = soup.find('meta',attrs={'name':'calibre:timestamp'}) @@ -1428,7 +1497,7 @@ class ITUNES(DevicePlugin): hits = dev_books.Search(search['uuid'],self.SearchField.index('All')) if hits: hit = hits[0] - self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit # Try by author - there could be multiple hits @@ -1437,9 +1506,25 @@ class ITUNES(DevicePlugin): for hit in hits: if hit.Name == search['title']: if DEBUG: - self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit + # PDF metadata was rewritten at export as 'safe(title) - safe(author)' + if search['format'] == 'pdf': + title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) + author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) + if DEBUG: + self.log.info(" searching by name: '%s - %s'" % (title,author)) + hits = dev_books.Search('%s - %s' % (title,author), + self.SearchField.index('All')) + if hits: + hit = hits[0] + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + return hit + else: + if DEBUG: + self.log.info(" no PDF hits") + attempts -= 1 time.sleep(0.5) if DEBUG: @@ -1496,7 +1581,7 @@ class ITUNES(DevicePlugin): if hits: hit = hits[0] if DEBUG: - self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit if DEBUG: @@ -1506,9 +1591,25 @@ class ITUNES(DevicePlugin): for hit in hits: if hit.Name == search['title']: if DEBUG: - self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) return hit + # PDF metadata was rewritten at export as 'safe(title) - safe(author)' + if search['format'] == 'pdf': + title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title']) + author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author']) + if DEBUG: + self.log.info(" searching by name: %s - %s" % (title,author)) + hits = lib_books.Search('%s - %s' % (title,author), + self.SearchField.index('All')) + if hits: + hit = hits[0] + self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer)) + return hit + else: + if DEBUG: + self.log.info(" no PDF hits") + attempts -= 1 time.sleep(0.5) if DEBUG: @@ -1523,10 +1624,12 @@ class ITUNES(DevicePlugin): Convert iTunes artwork to thumbnail Cache generated thumbnails cache_dir = os.path.join(config_dir, 'caches', 'itunes') + as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation ''' archive_path = os.path.join(self.cache_dir, "thumbs.zip") thumb_path = book_path.rpartition('.')[0] + '.jpg' + format = book_path.rpartition('.')[2].lower() try: zfr = ZipFile(archive_path) @@ -1539,77 +1642,99 @@ class ITUNES(DevicePlugin): self.log.info(" ITUNES._generate_thumbnail()") if isosx: - try: - # Resize the cover - data = book.artworks[1].raw_data().data - #self._dump_hex(data[:256]) - im = PILImage.open(cStringIO.StringIO(data)) - scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) - im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) - thumb = cStringIO.StringIO() - im.convert('RGB').save(thumb,'JPEG') - - # Cache the tagged thumb - if DEBUG: - self.log.info(" generated thumb for '%s', caching" % book.name()) - zfw.writestr(thumb_path, thumb.getvalue()) - zfw.close() - return thumb.getvalue() - except: - self.log.error(" error generating thumb for '%s'" % book.name()) + if format == 'epub': try: + if False: + self.log.info(" fetching artwork from %s\n %s" % (book_path,book)) + # Resize the cover + data = book.artworks[1].raw_data().data + #self._dump_hex(data[:256]) + img_data = cStringIO.StringIO(data) + im = PILImage.open(img_data) + scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) + im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) + img_data.close() + + thumb = cStringIO.StringIO() + im.convert('RGB').save(thumb,'JPEG') + thumb_data = thumb.getvalue() + thumb.close() + + # Cache the tagged thumb + if DEBUG: + self.log.info(" generated thumb for '%s', caching" % book.name()) + zfw.writestr(thumb_path, thumb_data) zfw.close() + return thumb_data except: - pass + self.log.error(" error generating thumb for '%s'" % book.name()) + try: + zfw.close() + except: + pass + return None + else: + if DEBUG: + self.log.info(" unable to generate PDF thumbs") return None elif iswindows: if not book.Artwork.Count: if DEBUG: - self.log.info(" no artwork available") + self.log.info(" no artwork available for '%s'" % book.Name) return None - # Save the cover from iTunes - try: - tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % self.ArtworkFormat[book.Artwork.Item(1).Format]) - book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb) - # Resize the cover - im = PILImage.open(tmp_thumb) - scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) - im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) - thumb = cStringIO.StringIO() - im.convert('RGB').save(thumb,'JPEG') - os.remove(tmp_thumb) - - # Cache the tagged thumb - if DEBUG: - self.log.info(" generated thumb for '%s', caching" % book.Name) - zfw.writestr(thumb_path, thumb.getvalue()) - zfw.close() - return thumb.getvalue() - except: - self.log.error(" error generating thumb for '%s'" % book.Name) + if format == 'epub': + # Save the cover from iTunes try: + tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % self.ArtworkFormat[book.Artwork.Item(1).Format]) + book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb) + # Resize the cover + im = PILImage.open(tmp_thumb) + scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80) + im = im.resize((int(width),int(height)), PILImage.ANTIALIAS) + thumb = cStringIO.StringIO() + im.convert('RGB').save(thumb,'JPEG') + thumb_data = thumb.getvalue() + os.remove(tmp_thumb) + thumb.close() + + # Cache the tagged thumb + if DEBUG: + self.log.info(" generated thumb for '%s', caching" % book.Name) + zfw.writestr(thumb_path, thumb_data) zfw.close() + return thumb_data except: - pass + self.log.error(" error generating thumb for '%s'" % book.Name) + try: + zfw.close() + except: + pass + return None + else: + if DEBUG: + self.log.info(" unable to generate PDF thumbs") return None def _get_device_book_size(self, file, compressed_size): ''' Calculate the exploded size of file ''' - myZip = ZipFile(file,'r') - myZipList = myZip.infolist() - exploded_file_size = 0 - for file in myZipList: - exploded_file_size += file.file_size - if False: - self.log.info(" ITUNES._get_device_book_size()") - self.log.info(" %d items in archive" % len(myZipList)) - self.log.info(" compressed: %d exploded: %d" % (compressed_size, exploded_file_size)) - myZip.close() + exploded_file_size = compressed_size + format = file.rpartition('.')[2].lower() + if format == 'epub': + myZip = ZipFile(file,'r') + myZipList = myZip.infolist() + exploded_file_size = 0 + for file in myZipList: + exploded_file_size += file.file_size + if False: + self.log.info(" ITUNES._get_device_book_size()") + self.log.info(" %d items in archive" % len(myZipList)) + self.log.info(" compressed: %d exploded: %d" % (compressed_size, exploded_file_size)) + myZip.close() return exploded_file_size def _get_device_books(self): @@ -1701,7 +1826,7 @@ class ITUNES(DevicePlugin): self.log.error(" no iPad|Books playlist found") return pl - def _get_fpath(self,file, metadata, update_md=False): + def _get_fpath(self,file, metadata, format, update_md=False): ''' If the database copy will be deleted after upload, we have to use file (the PersistentTemporaryFile), which will be around until @@ -1723,9 +1848,9 @@ class ITUNES(DevicePlugin): else: # Recipe - PTF if DEBUG: - self.log.info(" file will be deleted after upload") + self.log.info(" file will be deleted after upload") - if update_md: + if format == 'epub' and update_md: self._update_epub_metadata(fpath, metadata) return fpath @@ -1950,10 +2075,12 @@ class ITUNES(DevicePlugin): # Read the current storage path for iTunes media from the XML file with open(self.iTunes.LibraryXMLPath, 'r') as xml: - soup = BeautifulSoup(xml.read().decode('utf-8')) - mf = soup.find('key',text="Music Folder").parent - string = mf.findNext('string').renderContents() - media_dir = os.path.abspath(string[len('file://localhost/'):].replace('%20',' ')) + for line in xml: + if line.strip().startswith('Music Folder'): + soup = BeautifulSoup(line) + string = soup.find('string').renderContents() + media_dir = os.path.abspath(string[len('file://localhost/'):].replace('%20',' ')) + break if os.path.exists(media_dir): self.iTunes_media = media_dir else: @@ -2028,7 +2155,9 @@ class ITUNES(DevicePlugin): # Delete existing from Library|Books, add to self.update_list # for deletion from booklist[0] during add_books_to_metadata for book in self.cached_books: - if self.cached_books[book]['uuid'] == metadata.uuid: + if (self.cached_books[book]['uuid'] == metadata.uuid) or \ + (self.cached_books[book]['title'] == metadata.title and \ + self.cached_books[book]['author'] == metadata.authors[0]): self.update_list.append(self.cached_books[book]) self._remove_from_iTunes(self.cached_books[book]) if DEBUG: @@ -2036,7 +2165,7 @@ class ITUNES(DevicePlugin): break else: if DEBUG: - self.log.info(" '%s' not in cached_books" % metadata.title) + self.log.info(" '%s' not found in cached_books" % metadata.title) def _remove_from_device(self, cached_book): ''' @@ -2158,12 +2287,14 @@ class ITUNES(DevicePlugin): fnames = zf_opf.namelist() opf = [x for x in fnames if '.opf' in x][0] if opf: - opf_raw = cStringIO.StringIO(zf_opf.read(opf)).getvalue() - soup = BeautifulSoup(opf_raw) + opf_raw = cStringIO.StringIO(zf_opf.read(opf)) + soup = BeautifulSoup(opf_raw.getvalue()) + opf_raw.close() + + # Touch existing calibre timestamp md = soup.find('metadata') ts = md.find('meta',attrs={'name':'calibre:timestamp'}) if ts: - # Touch existing calibre timestamp timestamp = ts['content'] old_ts = parse_date(timestamp) metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour, @@ -2172,6 +2303,15 @@ class ITUNES(DevicePlugin): metadata.timestamp = isoformat(now()) if DEBUG: self.log.info(" add timestamp: %s" % metadata.timestamp) + + # Fix the language declaration for iBooks 1.1 + patched_language = 'en-US' + language = md.find('dc:language') + if language: + self.log.info(" changing from '%s' to '%s'" % + (language.renderContents(),patched_language)) + metadata.language = patched_language + zf_opf.close() # If 'News' in tags, tweak the title/author for friendlier display in iBooks @@ -2257,6 +2397,9 @@ class ITUNES(DevicePlugin): lb_added.enabled.set(True) lb_added.sort_artist.set(metadata.author_sort.title()) lb_added.sort_name.set(this_book.title_sorter) + if this_book.format == 'pdf': + lb_added.artist.set(metadata.authors[0]) + lb_added.name.set(metadata.title) if db_added: db_added.album.set(metadata.title) @@ -2265,6 +2408,9 @@ class ITUNES(DevicePlugin): db_added.enabled.set(True) db_added.sort_artist.set(metadata.author_sort.title()) db_added.sort_name.set(this_book.title_sorter) + if this_book.format == 'pdf': + db_added.artist.set(metadata.authors[0]) + db_added.name.set(metadata.title) if metadata.comments: if lb_added: @@ -2284,7 +2430,9 @@ class ITUNES(DevicePlugin): # Set genre from series if available, else first alpha tag # Otherwise iTunes grabs the first dc:subject from the opf metadata - if self.use_series_data and metadata.series: + if metadata.series and self.settings().read_metadata: + if DEBUG: + self.log.info(" using Series name as Genre") if lb_added: lb_added.sort_name.set("%s %03d" % (metadata.series, metadata.series_index)) lb_added.genre.set(metadata.series) @@ -2298,6 +2446,8 @@ class ITUNES(DevicePlugin): db_added.episode_number.set(metadata.series_index) elif metadata.tags: + if DEBUG: + self.log.info(" using Tag as Genre") for tag in metadata.tags: if self._is_alpha(tag[0]): if lb_added: @@ -2314,6 +2464,9 @@ class ITUNES(DevicePlugin): lb_added.Enabled = True lb_added.SortArtist = (metadata.author_sort.title()) lb_added.SortName = (this_book.title_sorter) + if this_book.format == 'pdf': + lb_added.Artist = metadata.authors[0] + lb_added.Name = metadata.title if db_added: db_added.Album = metadata.title @@ -2322,6 +2475,9 @@ class ITUNES(DevicePlugin): db_added.Enabled = True db_added.SortArtist = (metadata.author_sort.title()) db_added.SortName = (this_book.title_sorter) + if this_book.format == 'pdf': + db_added.Artist = metadata.authors[0] + db_added.Name = metadata.title if metadata.comments: if lb_added: @@ -2345,7 +2501,9 @@ class ITUNES(DevicePlugin): # Otherwise iBooks uses first from opf # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12) - if self.use_series_data and metadata.series: + if metadata.series and self.settings().read_metadata: + if DEBUG: + self.log.info(" using Series name as Genre") if lb_added: lb_added.SortName = "%s %03d" % (metadata.series, metadata.series_index) lb_added.Genre = metadata.series @@ -2365,6 +2523,8 @@ class ITUNES(DevicePlugin): self.log.warning(" iTunes automation interface reported an error" " setting EpisodeNumber on iDevice") elif metadata.tags: + if DEBUG: + self.log.info(" using Tag as Genre") for tag in metadata.tags: if self._is_alpha(tag[0]): if lb_added: diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index f860fc4720..c417c501f4 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -59,7 +59,7 @@ class DevicePlugin(Plugin): return cls.__name__ return cls.name - + # Device detection {{{ def test_bcd_windows(self, device_id, bcd): if bcd is None or len(bcd) == 0: return True @@ -152,6 +152,7 @@ class DevicePlugin(Plugin): return True, dev return False, None + # }}} def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None) : @@ -378,8 +379,6 @@ class DevicePlugin(Plugin): raise NotImplementedError() - - class BookList(list): ''' A list of books. Each Book object must have the fields: diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index cd56d210e1..c3e7bb190d 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -429,6 +429,7 @@ class Bookmark(): entries, = unpack('>I', data[9:13]) current_entry = 0 e_base = 0x0d + self.pdf_page_offset = 0 while current_entry < entries: ''' location, = unpack('>I', data[e_base+2:e_base+6]) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index d899c8e995..55790420f2 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -78,9 +78,6 @@ class Device(DeviceConfig, DevicePlugin): STORAGE_CARD_VOLUME_LABEL = '' STORAGE_CARD2_VOLUME_LABEL = None - SUPPORTS_SUB_DIRS = False - MUST_READ_METADATA = False - SUPPORTS_USE_AUTHOR_SORT = False EBOOK_DIR_MAIN = '' EBOOK_DIR_CARD_A = '' diff --git a/src/calibre/devices/usbms/deviceconfig.py b/src/calibre/devices/usbms/deviceconfig.py index 5edefff743..e074387175 100644 --- a/src/calibre/devices/usbms/deviceconfig.py +++ b/src/calibre/devices/usbms/deviceconfig.py @@ -13,6 +13,10 @@ class DeviceConfig(object): EXTRA_CUSTOMIZATION_MESSAGE = None EXTRA_CUSTOMIZATION_DEFAULT = None + SUPPORTS_SUB_DIRS = False + MUST_READ_METADATA = False + SUPPORTS_USE_AUTHOR_SORT = False + #: If None the default is used SAVE_TEMPLATE = None @@ -23,9 +27,14 @@ class DeviceConfig(object): config().parse().send_template @classmethod - def _config(cls): + def _config_base_name(cls): klass = cls if isinstance(cls, type) else cls.__class__ - c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers')) + return klass.__name__ + + @classmethod + def _config(cls): + name = cls._config_base_name() + c = Config('device_drivers_%s' % name, _('settings for device drivers')) c.add_opt('format_map', default=cls.FORMATS, help=_('Ordered list of formats the device will accept')) c.add_opt('use_subdirs', default=True, diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 0637dddfb6..3c84252ff4 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -126,6 +126,13 @@ class Stylizer(object): head = head[0] else: head = [] + + # Add cssutils parsing profiles from output_profile + for profile in self.opts.output_profile.extra_css_modules: + cssutils.profile.addProfile(profile['name'], + profile['props'], + profile['macros']) + parser = cssutils.CSSParser(fetcher=self._fetch_css_file, log=logging.getLogger('calibre.css')) self.font_face_rules = [] diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py index f838e9c1fe..a3f8442200 100644 --- a/src/calibre/gui2/actions.py +++ b/src/calibre/gui2/actions.py @@ -176,7 +176,8 @@ class AnnotationsAction(object): # {{{ def mark_book_as_read(self,id): read_tag = gprefs.get('catalog_epub_mobi_read_tag') - self.db.set_tags(id, [read_tag], append=True) + if read_tag: + self.db.set_tags(id, [read_tag], append=True) def canceled(self): self.pd.hide() diff --git a/src/calibre/gui2/convert/txt_input.ui b/src/calibre/gui2/convert/txt_input.ui index 5a9527ebc5..186783c277 100644 --- a/src/calibre/gui2/convert/txt_input.ui +++ b/src/calibre/gui2/convert/txt_input.ui @@ -43,6 +43,9 @@ true + + true + diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 73e0fae8e8..b1af210011 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -585,6 +585,8 @@ class BasicNewsRecipe(Recipe): self.lrf = options.lrf self.output_profile = options.output_profile self.touchscreen = getattr(self.output_profile, 'touchscreen', False) + if self.touchscreen: + self.template_css += self.output_profile.touchscreen_news_css self.output_dir = os.path.abspath(self.output_dir) if options.test: @@ -638,7 +640,8 @@ class BasicNewsRecipe(Recipe): if self.delay > 0: self.simultaneous_downloads = 1 - self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else templates.NavBarTemplate() + self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else \ + templates.NavBarTemplate() self.failed_downloads = [] self.partial_failures = [] @@ -726,7 +729,6 @@ class BasicNewsRecipe(Recipe): 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') @@ -752,7 +754,8 @@ class BasicNewsRecipe(Recipe): - def feed2index(self, feed): + def feed2index(self, f, feeds): + feed = feeds[f] if feed.image_url is not None: # Download feed image imgdir = os.path.join(self.output_dir, 'images') if not os.path.isdir(imgdir): @@ -782,33 +785,9 @@ class BasicNewsRecipe(Recipe): css = self.template_css + '\n\n' +(self.extra_css if self.extra_css else '') if self.touchscreen: - touchscreen_css = u''' - .summary_headline { - font-weight:bold; text-align:left; - } - - .summary_byline { - text-align:left; - font-family:monospace; - } - - .summary_text { - text-align:left; - } - - .feed { - font-family:sans-serif; font-weight:bold; font-size:larger; - } - - .calibre_navbar { - font-family:monospace; - } - - ''' - templ = templates.TouchscreenFeedTemplate() - css = touchscreen_css + '\n\n' + (self.extra_css if self.extra_css else '') - return templ.generate(feed, self.description_limiter, + + return templ.generate(f, feeds, self.description_limiter, extra_css=css).render(doctype='xhtml') @@ -951,7 +930,7 @@ class BasicNewsRecipe(Recipe): #feeds.restore_duplicates() for f, feed in enumerate(feeds): - html = self.feed2index(feed) + html = self.feed2index(f,feeds) feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi: fi.write(html) diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index 7ebf7294ae..26d4cbdc9d 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -3,9 +3,12 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' + +import copy + from lxml import html, etree from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \ - STRONG, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ + STRONG, EM, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ TABLE, TD, TR from calibre import preferred_encoding, strftime, isbytestring @@ -14,6 +17,7 @@ def CLASS(*args, **kwargs): # class is a reserved word in Python kwargs['class'] = ' '.join(args) return kwargs +# Regular templates class Template(object): IS_HTML = True @@ -44,105 +48,35 @@ class Template(object): return etree.tostring(self.root, encoding='utf-8', xml_declaration=True, pretty_print=True) -class NavBarTemplate(Template): +class EmbeddedContent(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')) + def _generate(self, article, style=None, extra_css=None): + content = article.content if article.content else '' + summary = article.summary if article.summary else '' + text = content if len(content) > len(summary) else summary + head = HEAD(TITLE(article.title)) 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_70', - 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(p) - 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 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(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) - 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(p) - 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(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) - - self.root = HTML(head, BODY(navbar)) + if isbytestring(text): + text = text.decode('utf-8', 'replace') + elements = html.fragments_fromstring(text) + self.root = HTML(head, + BODY(H2(article.title), DIV())) + div = self.root.find('body').find('div') + if elements and isinstance(elements[0], unicode): + div.text = elements[0] + elements = list(elements)[1:] + for elem in elements: + elem.getparent().remove(elem) + div.append(elem) class IndexTemplate(Template): def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): + self.IS_HTML = False if isinstance(datefmt, unicode): datefmt = datefmt.encode(preferred_encoding) date = strftime(datefmt) @@ -164,43 +98,10 @@ class IndexTemplate(Template): 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 = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y')) - masthead_p = etree.Element("p") - masthead_p.set("style","text-align:center") - masthead_img = etree.Element("img") - masthead_img.set("src",masthead) - masthead_img.set("alt","masthead") - masthead_p.append(masthead_img) - - 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('calibre_rescale_120'), A(feed.title, href='feed_%d/index.html'%i))) - tr.append(TD( '%s' % len(feed.articles), style="text-align:right")) - toc.append(tr) - div = DIV( - masthead_p, - PT(date, style='text-align:center'), - #DIV(style="border-color:gray;border-top-style:solid;border-width:thin"), - DIV(style="border-top:1px solid gray;border-bottom:1em solid white"), - toc) - self.root = HTML(head, BODY(div)) - class FeedTemplate(Template): - def _generate(self, feed, cutoff, extra_css=None, style=None): + def _generate(self, f, feeds, cutoff, extra_css=None, style=None): + feed = feeds[f] head = HEAD(TITLE(feed.title)) if style: head.append(STYLE(style, type='text/css')) @@ -248,9 +149,147 @@ class FeedTemplate(Template): self.root = HTML(head, body) +class NavBarTemplate(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_70', + 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(p) + 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)) + + +# Touchscreen templates +class TouchscreenIndexTemplate(Template): + + def _generate(self, title, masthead, datefmt, feeds, extra_css=None, style=None): + + self.IS_HTML = False + + if isinstance(datefmt, unicode): + datefmt = datefmt.encode(preferred_encoding) + date = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y')) + masthead_p = etree.Element("p") + masthead_p.set("style","text-align:center") + masthead_img = etree.Element("img") + masthead_img.set("src",masthead) + masthead_img.set("alt","masthead") + masthead_p.append(masthead_img) + + 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('calibre_rescale_120'), A(feed.title, href='feed_%d/index.html'%i))) + tr.append(TD( '%s' % len(feed.articles), style="text-align:right")) + toc.append(tr) + div = DIV( + masthead_p, + PT(date, style='text-align:center'), + #DIV(style="border-color:gray;border-top-style:solid;border-width:thin"), + DIV(style="border-top:1px solid gray;border-bottom:1em solid white"), + toc) + self.root = HTML(head, BODY(div)) + class TouchscreenFeedTemplate(Template): - def _generate(self, feed, cutoff, extra_css=None, style=None): + def _generate(self, f, feeds, cutoff, extra_css=None, style=None): + + def trim_title(title,clip=18): + if len(title)>clip: + tokens = title.split(' ') + new_title_tokens = [] + new_title_len = 0 + if len(tokens[0]) > clip: + return tokens[0][:clip] + '...' + for token in tokens: + if len(token) + new_title_len < clip: + new_title_tokens.append(token) + new_title_len += len(token) + else: + new_title_tokens.append('...') + title = ' '.join(new_title_tokens) + break + return title + + self.IS_HTML = False + feed = feeds[f] + + # Construct the navbar + navbar_t = TABLE(CLASS('touchscreen_navbar')) + navbar_tr = TR() + + # Previous Section + link = '' + if f > 0: + link = A(CLASS('feed_link'), + trim_title(feeds[f-1].title), + href = '../feed_%d/index.html' % int(f-1)) + navbar_tr.append(TD(link, width="40%", align="center")) + + # Up to Sections + link = A(STRONG('Sections'), href="../index.html") + navbar_tr.append(TD(link,width="20%",align="center")) + + # Next Section + link = '' + if f < len(feeds)-1: + link = A(CLASS('feed_link'), + trim_title(feeds[f+1].title), + href = '../feed_%d/index.html' % int(f+1)) + navbar_tr.append(TD(link, width="40%", align="center", )) + navbar_t.append(navbar_tr) + top_navbar = navbar_t + bottom_navbar = copy.copy(navbar_t) + #print "\n%s\n" % etree.tostring(navbar_t, pretty_print=True) + + + # Build the page head = HEAD(TITLE(feed.title)) if style: head.append(STYLE(style, type='text/css')) @@ -258,10 +297,11 @@ class TouchscreenFeedTemplate(Template): 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')), - DIV(style="border-top:1px solid gray;border-bottom:1em solid white") + top_navbar, + H2(feed.title, CLASS('feed_title')) ) body.append(div) + if getattr(feed, 'image', None): div.append(DIV(IMG( alt = feed.image_alt if feed.image_alt else '', @@ -280,65 +320,64 @@ class TouchscreenFeedTemplate(Template): continue tr = TR() - if True: - div_td = DIV( - A(article.title, CLASS('summary_headline','calibre_rescale_120', - href=article.url)), - style="display:inline-block") - if article.author: - div_td.append(DIV(article.author, - CLASS('summary_byline', 'calibre_rescale_100'))) - if article.summary: - div_td.append(DIV(cutoff(article.text_summary), - CLASS('summary_text', 'calibre_rescale_100'))) - tr.append(TD(div_td)) - else: - td = TD( - A(article.title, CLASS('summary_headline','calibre_rescale_120', - href=article.url)) - ) - if article.author: - td.append(DIV(article.author, - CLASS('summary_byline', 'calibre_rescale_100'))) - if article.summary: - td.append(DIV(cutoff(article.text_summary), - CLASS('summary_text', 'calibre_rescale_100'))) - - tr.append(td) - + div_td = DIV( + A(article.title, CLASS('summary_headline','calibre_rescale_120', + href=article.url)), + style="display:inline-block") + if article.author: + div_td.append(DIV(article.author, + CLASS('summary_byline', 'calibre_rescale_100'))) + if article.summary: + div_td.append(DIV(cutoff(article.text_summary), + CLASS('summary_text', 'calibre_rescale_100'))) + tr.append(TD(div_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) - + div.append(BR()) + div.append(bottom_navbar) self.root = HTML(head, body) -class EmbeddedContent(Template): +class TouchscreenNavBarTemplate(Template): - def _generate(self, article, style=None, extra_css=None): - content = article.content if article.content else '' - summary = article.summary if article.summary else '' - text = content if len(content) > len(summary) else summary - head = HEAD(TITLE(article.title)) + 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 isbytestring(text): - text = text.decode('utf-8', 'replace') - elements = html.fragments_fromstring(text) - self.root = HTML(head, - BODY(H2(article.title), DIV())) - div = self.root.find('body').find('div') - if elements and isinstance(elements[0], unicode): - div.text = elements[0] - elements = list(elements)[1:] - for elem in elements: - elem.getparent().remove(elem) - div.append(elem) + navbar = DIV() + navbar_t = TABLE(CLASS('touchscreen_navbar')) + navbar_tr = TR() + # | Previous + if art > 0: + href = '%s../article_%d/index.html'%(prefix, art-1) + navbar_tr.append(TD(A(EM('Previous'),href=href), + width="32%")) + else: + navbar_tr.append(TD('', width="32%")) + + # | Articles | Sections | + href = '%s../index.html#article_%d'%(prefix, art) + navbar_tr.append(TD(A(STRONG('Articles'), href=href),width="18%")) + + href = '%s../../index.html#feed_%d'%(prefix, feed) + navbar_tr.append(TD(A(STRONG('Sections'), href=href),width="18%")) + + # | Next + 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_tr.append(TD(A(EM('Next'),href=href), + width="32%")) + navbar_t.append(navbar_tr) + navbar.append(navbar_t) + #print "\n%s\n" % etree.tostring(navbar, pretty_print=True) + + self.root = HTML(head, BODY(navbar))