diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index aaeb992151..40b3d2fa32 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -72,4 +72,9 @@ gui_pubdate_display_format = 'MMM yyyy' # without changing anything is sufficient to change the sort. title_series_sorting = 'library_order' - +# Apple iDevice +# Control whether Series name is used as Category/Genre in iTunes/iBooks +# If set to 'True', a Book's Series name will be used as the Category/Genre +# If set to 'False', the book's first tag beginning with an alpha character will +# be used as the Category/Genre +iDevice_use_series_as_category = True diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index c872c9df38..68345ca0da 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -237,9 +237,6 @@ 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 @@ -256,8 +253,82 @@ class iPadOutput(OutputProfile): screen_size = (768, 1024) comic_screen_size = (768, 1024) dpi = 132.0 - supports_nested_toc = False + timefmt = '%A, %d %b %Y' + cssutils_addProfile = { '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_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..2efe2b553f 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -15,7 +15,7 @@ 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, config_dir, tweaks from calibre.utils.date import isoformat, now, parse_date from calibre.utils.logging import Log from calibre.utils.zipfile import ZipFile @@ -78,12 +78,12 @@ 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'] + FORMATS = ['epub','pdf'] # Product IDs: # 0x1292:iPhone 3G @@ -141,6 +141,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 +163,7 @@ class ITUNES(DevicePlugin): sources = None update_msg = None update_needed = False - use_series_data = True + use_series_as_category = tweaks['iDevice_use_series_as_category'] # Public methods def add_books_to_metadata(self, locations, metadata, booklists): @@ -173,16 +177,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 +235,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 +334,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 +349,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 [] @@ -685,6 +691,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()") @@ -750,6 +759,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 +825,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 +862,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 +874,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 +986,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 +1040,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 +1067,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 +1153,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 +1162,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 +1180,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 +1378,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 +1398,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 +1489,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 +1498,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 +1573,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 +1583,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 +1616,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 +1634,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 = thmb.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 +1818,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 +1840,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 +2067,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 +2147,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 +2157,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 +2279,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 +2295,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 +2389,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 +2400,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 +2422,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 self.use_series_as_category and metadata.series: + 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 +2438,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 +2456,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 +2467,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 +2493,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 self.use_series_as_category and metadata.series: + 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 +2515,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/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index d6070efade..4560c608a5 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -127,15 +127,12 @@ class Stylizer(object): else: head = [] - # GwR : Add webkit profile to cssutils before validating - if True: - wk_macros = { - 'border-width': '{length}|thin|medium|thick' - } - wk_props = { - '-webkit-border-radius': r'{border-width}(\s+{border-width}){0,3}|inherit' - } - cssutils.profile.addProfile('webkit', wk_props, wk_macros) + # Add optional cssutils parsing profile from output_profile + if hasattr(self.opts.output_profile, 'cssutils_addProfile'): + profile = self.opts.output_profile.cssutils_addProfile + cssutils.profile.addProfile(profile['name'], + profile['props'], + profile['macros']) parser = cssutils.CSSParser(fetcher=self._fetch_css_file, log=logging.getLogger('calibre.css')) 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/web/feeds/news.py b/src/calibre/web/feeds/news.py index 29b581c361..78058a95ec 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -280,46 +280,6 @@ class BasicNewsRecipe(Recipe): } ''' - #: The CSS that is used to style the touchscreen elements, i.e., the navigation bars and - #: the Feed summaries. - touchscreen_css = u''' - .article_navbar { - -webkit-border-radius:4px; - background-color:#eee; - border:1px solid #888; - margin-left: 5%; - margin-right: 5%; - width: 90%; - } - - .feed_navbar { - -webkit-border-radius:4px; - background-color:#eee; - border:1px solid #888; - margin-left: 5%; - margin-right: 5%; - width: 90%; - } - - .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; - } - ''' - - #: By default, calibre will use a default image for the masthead (Kindle only). #: Override this in your recipe to provide a url to use as a masthead. masthead_url = None @@ -625,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 and getattr(self.output_profile, 'touchscreen_css',False): + self.extra_css += self.output_profile.touchscreen_css self.output_dir = os.path.abspath(self.output_dir) if options.test: @@ -678,10 +640,8 @@ class BasicNewsRecipe(Recipe): if self.delay > 0: self.simultaneous_downloads = 1 - if self.touchscreen: - self.extra_css += self.touchscreen_css - - 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 = [] @@ -768,7 +728,8 @@ class BasicNewsRecipe(Recipe): timefmt = self.timefmt if self.touchscreen: templ = templates.TouchscreenIndexTemplate() - timefmt = '%A, %d %b %Y' + if getattr(self.output_profile,'timefmt',False): + timefmt = self.output_profile.timefmt return templ.generate(self.title, "mastheadImage.jpg", timefmt, feeds, extra_css=css).render(doctype='xhtml') diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index 5b4704e766..26d4cbdc9d 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -3,6 +3,9 @@ __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, EM, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ @@ -73,6 +76,7 @@ class EmbeddedContent(Template): 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) @@ -198,6 +202,9 @@ class NavBarTemplate(Template): 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')) @@ -238,6 +245,8 @@ class TouchscreenFeedTemplate(Template): 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) @@ -248,29 +257,37 @@ class TouchscreenFeedTemplate(Template): break return title + self.IS_HTML = False feed = feeds[f] # Construct the navbar - navbar_t = TABLE(CLASS('feed_navbar')) + navbar_t = TABLE(CLASS('touchscreen_navbar')) navbar_tr = TR() + # Previous Section link = '' if f > 0: - link = A(EM( '< ' + trim_title(feeds[f-1].title)), + 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(EM(trim_title(feeds[f+1].title) + ' >'), + 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_tr.append(TD(link, width="40%", align="center", )) navbar_t.append(navbar_tr) - navbar = navbar_t + 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)) @@ -280,8 +297,8 @@ 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) @@ -317,9 +334,8 @@ class TouchscreenFeedTemplate(Template): toc.append(tr) div.append(toc) - #div.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) div.append(BR()) - div.append(navbar) + div.append(bottom_navbar) self.root = HTML(head, body) class TouchscreenNavBarTemplate(Template): @@ -334,24 +350,23 @@ class TouchscreenNavBarTemplate(Template): head.append(STYLE(extra_css, type='text/css')) navbar = DIV() - if bottom: - navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) - - navbar_t = TABLE(CLASS('article_navbar')) + 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%", align="center")) + navbar_tr.append(TD(A(EM('Previous'),href=href), + width="32%")) else: - navbar_tr.append(TD('', width="25%")) + 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%", align="center")) + 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%", align="center")) + 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 \ @@ -359,7 +374,8 @@ class TouchscreenNavBarTemplate(Template): 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%", align="center")) + 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)