diff --git a/resources/catalog/DefaultCover.jpg b/resources/catalog/DefaultCover.jpg new file mode 100644 index 0000000000..b3cc507bb7 Binary files /dev/null and b/resources/catalog/DefaultCover.jpg differ diff --git a/resources/catalog/mastheadImage.gif b/resources/catalog/mastheadImage.gif new file mode 100644 index 0000000000..1f2287a857 Binary files /dev/null and b/resources/catalog/mastheadImage.gif differ diff --git a/resources/catalog/stylesheet.css b/resources/catalog/stylesheet.css new file mode 100644 index 0000000000..07d13cab3b --- /dev/null +++ b/resources/catalog/stylesheet.css @@ -0,0 +1,62 @@ +p.title { + margin-top:0em; + margin-bottom:1em; + text-align:center; + font-style:italic; + font-size:xx-large; + height:0px; + } + +p.author { + margin-top:0em; + margin-bottom:0em; + text-align: left; + text-indent: 1em; + font-size:large; + } + +p.tags { + margin-top:0em; + margin-bottom:0em; + text-align: left; + text-indent: 1em; + font-size:small; + } + +p.description { + text-align:left; + font-style:italic; + margin-top: 0em; + } + +p.letter_index { + font-size:x-large; + text-align:left; + margin-top:0px; + margin-bottom:0px; + } + +p.author_index { + font-size:large; + text-align:left; + margin-top:0px; + margin-bottom:0px; + text-indent: 0em; + } + +p.read_book { + text-align:left; + margin-top:0px; + margin-bottom:0px; + margin-left:2em; + text-indent:-2em; + } + +p.unread_book { + text-align:left; + margin-top:0px; + margin-bottom:0px; + margin-left:2em; + text-indent:-2em; + } + diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 81e9021817..6f3bb1e875 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -421,8 +421,8 @@ from calibre.devices.binatone.driver import README from calibre.devices.hanvon.driver import N516 from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon -from calibre.library.catalog import CSV_XML -plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon, CSV_XML] +from calibre.library.catalog import CSV_XML, EPUB_MOBI +plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon, CSV_XML, EPUB_MOBI] plugins += [ ComicInput, EPUBInput, diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py new file mode 100644 index 0000000000..0e122d2f48 --- /dev/null +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2 import gprefs +from catalog_epub_mobi_ui import Ui_Form +from PyQt4.Qt import QWidget + +class PluginWidget(QWidget,Ui_Form): + + TITLE = _('EPUB/MOBI Options') + HELP = _('Options specific to')+' EPUB/MOBI '+_('output') + # Indicates whether this plugin wants its output synced to the connected device + sync_enabled = True + formats = set(['epub','mobi']) + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + self.setupUi(self) + + def initialize(self, name): + self.name = name + # Restore options from last use here + print "gui2.catalog.catalog_epub_mobi:initialize(): need to restore options" + + def options(self): + OPTION_FIELDS = ['exclude_genre','exclude_tags','read_tag','note_tag','output_profile'] + + # Save the current options + print "gui2.catalog.catalog_epub_mobi:options(): need to save options" + + # Return a dictionary with current options + print "gui2.catalog.catalog_epub_mobi:options(): need to return options" + print "gui2.catalog.catalog_epub_mobi:options(): using hard-coded options" + + opts_dict = {} + for opt in OPTION_FIELDS: + opts_dict[opt] = str(getattr(self,opt).text()).split(',') + + return opts_dict diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui new file mode 100644 index 0000000000..39e085d04f --- /dev/null +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -0,0 +1,170 @@ + + + Form + + + + 0 + 0 + 579 + 411 + + + + Form + + + + + 20 + 20 + 241 + 21 + + + + Tags to exclude as genres (regex): + + + Qt::LogText + + + true + + + + + + 20 + 50 + 241 + 17 + + + + 'Don't include this book' tag: + + + + + + 20 + 110 + 171 + 17 + + + + Additional note tag prefix: + + + + + + 20 + 140 + 181 + 17 + + + + Output profile: + + + + + + 300 + 20 + 231 + 22 + + + + + + + \[[\w ]*\] + + + + + + 300 + 50 + 231 + 22 + + + + + + + ~ + + + + + + 300 + 80 + 231 + 22 + + + + + + + + + + + + + + 20 + 80 + 241 + 17 + + + + 'Mark this book as read' tag: + + + + + + 300 + 110 + 231 + 22 + + + + + + + * + + + + + + 300 + 140 + 231 + 22 + + + + + + + kindle2 + + + + + + diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py index 07cfffbd84..4a1ce0d25b 100644 --- a/src/calibre/gui2/convert/gui_conversion.py +++ b/src/calibre/gui2/convert/gui_conversion.py @@ -42,6 +42,7 @@ def gui_catalog(fmt, title, dbspec, ids, out_file_name, fmt_options, opts, args = parser.parse_args() # Populate opts + opts.catalog_title = title opts.ids = ids opts.search_text = None opts.sort_by = None diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index d792707670..5dcce11746 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -17,7 +17,6 @@ from calibre.customize.ui import catalog_plugins class Catalog(QDialog, Ui_Dialog): ''' Catalog Dialog builder''' - widgets = [] def __init__(self, parent, dbspec, ids): import re, cStringIO @@ -50,11 +49,12 @@ class Catalog(QDialog, Ui_Dialog): name = plugin.name.lower().replace(' ', '_') if type(plugin) in builtin_plugins: - #info("Adding widget for builtin Catalog plugin %s" % plugin.name) + info("Adding widget for builtin Catalog plugin %s" % plugin.name) try: catalog_widget = __import__('calibre.gui2.catalog.'+name, fromlist=[1]) pw = catalog_widget.PluginWidget() + info("Initializing %s" % name) pw.initialize(name) pw.ICON = I('forward.svg') self.widgets.append(pw) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 796d58a3a5..889ad75645 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -1092,6 +1092,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.library_view.model().refresh_ids(ids) self.library_view.model().current_changed(self.library_view.currentIndex(), self.library_view.currentIndex()) + if ids: + self.tags_view.recount() def delete_all_but_selected_formats(self, *args): ids = self._get_selected_ids() @@ -1113,6 +1115,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.library_view.model().refresh_ids(ids) self.library_view.model().current_changed(self.library_view.currentIndex(), self.library_view.currentIndex()) + if ids: + self.tags_view.recount() def delete_covers(self, *args): @@ -1399,8 +1403,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.status_bar.showMessage(_('Catalog generated.'), 3000) self.sync_catalogs() if job.fmt in ['CSV','XML']: - export_dir = choose_dir(self, 'Export Catalog Directory', - 'Select destination for %s.%s' % (job.catalog_title, job.fmt.lower())) + export_dir = choose_dir(self, _('Export Catalog Directory'), + _('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower())) if export_dir: destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) shutil.copyfile(job.catalog_file_path, destination) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 19e00b6488..8dcb45da6c 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1,6 +1,11 @@ -import os +import pickle, os, re, shutil +from xml.sax.saxutils import escape + +from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString from calibre.customize import CatalogPlugin +from calibre.ptempfile import PersistentTemporaryDirectory + FIELDS = ['all', 'author_sort', 'authors', 'comments', 'cover', 'formats', 'id', 'isbn', 'pubdate', 'publisher', 'rating', @@ -211,3 +216,2365 @@ class CSV_XML(CatalogPlugin): return None +class EPUB_MOBI(CatalogPlugin): + 'ePub catalog generator' + + from collections import namedtuple + Option = namedtuple('Option', 'option, default, dest, help') + + name = 'Catalog_EPUB_MOBI' + description = 'EPUB/MOBI catalog generator' + supported_platforms = ['windows', 'osx', 'linux'] + minimum_calibre_version = (0, 6, 34) + author = 'Greg Riker' + version = (0, 0, 1) + file_types = set(['epub','mobi']) + + cli_options = [Option('--catalog-title', + default = 'My Catalog', + dest = 'catalog_title', + help = _('Title of generated catalog used as title in metadata.\n' + "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")), + Option('--exclude-genre', + default='\[[\w ]*\]', + dest='exclude_genre', + help=_("Regex describing tags to exclude as genres.\n" "Default: '%default' excludes bracketed tags, e.g. '[]'\n" + "Applies to: ePub, MOBI output formats")), + Option('--exclude-tags', + default='~', + dest='exclude_tags', + help=_("Comma-separated list of tag words indicating book should be excluded from output. Case-insensitive.\n" + "--exclude-tags=skip will match 'skip this book' and 'Skip will like this'.\n" + "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")), + Option('--read-tag', + default='+', + dest='read_tag', + help=_("Tag indicating book has been read.\n" "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")), + Option('--note-tag', + default='*', + dest='note_tag', + help=_("Tag prefix for user notes, e.g. '*Jeff might enjoy reading this'.\n" + "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")), + Option('--output-profile', + default=None, + dest='output_profile', + help=_("Specifies the output profile. In some cases, an output profile is required to optimize the catalog for the device. For example, 'kindle' or 'kindle_dx' creates a structured Table of Contents with Sections and Articles.\n" + "Default: '%default'\n" + "Applies to: ePub, MOBI output formats")) + ] + + + class NumberToText(object): + ''' + Converts numbers to text + 4.56 => four point fifty-six + 456 => four hundred fifty-six + 4:56 => four fifty-six + ''' + + lessThanTwenty = ["","one","two","three","four","five","six","seven","eight","nine", + "ten","eleven","twelve","thirteen","fourteen","fifteen","sixteen","seventeen", + "eighteen","nineteen"] + tens = ["","","twenty","thirty","forty","fifty","sixty","seventy","eighty","ninety"] + hundreds = ["","one","two","three","four","five","six","seven","eight","nine"] + + def __init__(self, number): + self.number = number + self.text = '' + self.numberTranslate() + + def stringFromInt(self, intToTranslate): + # Convert intToTranslate to string + # intToTranslate is a three-digit number + + tensComponentString = "" + hundredsComponenetString = "" + + hundredsComponent = intToTranslate - (intToTranslate % 100) + tensComponent = intToTranslate % 100 + + # Build the hundreds component + if hundredsComponent: + hundredsComponentString = "%s hundred" % self.hundreds[hundredsComponent/100] + else: + hundredsComponentString = "" + + # Build the tens component + if tensComponent < 20: + if tensComponent > 0: + tensComponentString = self.lessThanTwenty[tensComponent] + else: + tensPart = "" + onesPart = "" + + # Get the tens part + tensPart = self.tens[tensComponent / 10] + onesPart = self.lessThanTwenty[tensComponent % 10] + + if intToTranslate % 10: + tensComponentString = "%s-%s" % (tensPart, onesPart) + else: + tensComponentString = "%s" % tensPart + + # Concatenate the results + if hundredsComponent and not tensComponent: + result = hundredsComponentString + if not hundredsComponent and tensComponent: + result = tensComponentString + if hundredsComponent and tensComponent: + result = hundredsComponentString + " " + tensComponentString + + return result + + def numberTranslate(self): + hundredsNumber = 0 + thousandsNumber = 0 + hundredsString = "" + thousandsString = "" + resultString = "" + + # Test for time + if re.search(':',self.number): + time_strings = self.number.split(":") + hours = EPUB_MOBI.NumberToText(time_strings[0]).text + minutes = EPUB_MOBI.NumberToText(time_strings[1]).text + self.text = '%s-%s' % (hours.capitalize(), minutes) + + # Test for decimal + elif re.search('\.',self.number): + decimal_strings = self.number.split(".") + left = EPUB_MOBI.NumberToText(decimal_strings[0]).text + right = EPUB_MOBI.NumberToText(decimal_strings[1]).text + self.text = '%s point %s' % (left.capitalize(), right) + + # Test for hypenated + elif re.search('-', self.number): + strings = self.number.split('-') + if re.search('[0-9]+', strings[0]): + left = EPUB_MOBI.NumberToText(strings[0]).text + right = strings[1] + else: + left = strings[0] + right = EPUB_MOBI.NumberToText(strings[1]).text + self.text = '%s-%s' % (left, right) + + # Test for comma + elif re.search(',', self.number): + self.text = EPUB_MOBI.NumberToText(self.number.replace(',','')).text + + # Test for hybrid e.g., 'K2' + elif re.search('[\D]+', self.number): + result = [] + for char in self.number: + if re.search('[\d]+', char): + result.append(EPUB_MOBI.NumberToText(char).text) + else: + result.append(char) + self.text = ''.join(result) + + else: + number = int(self.number) + + if number > 1000000: + self.text = "%d out of range" % number + return + + if number == 1000000: + self.text = "one million" + else : + # Strip out the three-digit number groups + thousandsNumber = number/1000 + hundredsNumber = number - (thousandsNumber * 1000) + + # Convert the lower 3 numbers - hundredsNumber + if hundredsNumber : + hundredsString = self.stringFromInt(hundredsNumber) + + # Convert the upper 3 numbers - thousandsNumber + if thousandsNumber: + thousandsString = self.stringFromInt(thousandsNumber) + + # Concatenate the strings + if thousandsNumber and not hundredsNumber: + resultString = "%s thousand" % thousandsString + + if thousandsNumber and hundredsNumber: + resultString = "%s thousand %s" % (thousandsString, hundredsString) + + if not thousandsNumber and hundredsNumber: + resultString = "%s" % hundredsString + + if not thousandsNumber and not hundredsNumber: + resultString = "zero" + + self.text = resultString.strip().capitalize() + + class CatalogBuilder(object): + ''' + Generates catalog source files from calibre database + + Implementation notes + - 'Marker tags' in a book's metadata are used to flag special conditions: + '~' : Do not catalog this book + '+' : Mark this book as read (check mark) in lists + '*' : Display trailing text as 'Note: ' in top frame next to cover + '[] : Source of content (e.g., Amazon, Project Gutenberg). Do not create genre + + - Program flow + catalog = Catalog(notification=Reporter()) + catalog.createDirectoryStructure() + catalog.copyResources() + catalog.buildSources() + # At this point, catalog.catalogPath ($tmpdir/calibre_kindle_catalog) contains Catalog.opf + # and all source files necessary to build the catalog + + # After successful compilation, call catalog.cleanUp() to remove generated source files + catalog.cleanUp() + + - To do: + *** generateThumbnails() creates a default book image from book.svg, but the background + is black instead of white. This needs to be fixed (approx line #884) + + + ''' + + # Number of discrete steps to catalog creation + current_step = 0 + total_steps = 13 + + # Used to xlate pubdate to friendly format + MONTHS = ['January', 'February','March','April','May','June', + 'July','August','September','October','November','December'] + + # Tags starting with these characters will not be included in the genre list + REMOVE_TAGS = ['~','+','*','['] + + # Symbols used to show a book's read/unread status + NOT_READ_SYMBOL = '' + READ_SYMBOL = '' + + # basename output file basename + # creator dc:creator in OPF metadata + # dbs_fname stored catalog snapshot + # descriptionClip limits size of NCX descriptions (Kindle only) + # generateForMobigen Modifies OPF/NCX for Mobigen compilation + # includeSources Used in processSpecialTags to skip tags like '[SPL]' + # notification Used to check for cancel, report progress + # plugin_path Plugin zip file (resources) + # stylesheet CSS stylesheet + # title dc:title in OPF metadata, NCX periodical + # verbosity level of diagnostic printout + + class DummyReporter(object): + def __init__(self): + self.cancelRequested = False + + def __call__(self, percent, msg=''): + pass + + def __init__(self, db, opts, plugin, + generateForMobigen=False, + notification=DummyReporter(), + stylesheet="content/stylesheet.css"): + self.__authors = None + self.__basename = opts.basename + self.__booksByAuthor = None + self.__booksByTitle = None + self.__catalogPath = PersistentTemporaryDirectory("_epub_mobi_catalog", prefix='') + self.__contentDir = os.path.join(self.catalogPath, "content") + self.__creator = opts.creator + self.__db = db + self.__dbs_fname = opts.dbs_fname + self.__databaseSnapshot = self.fetchDatabaseSnapshot(self.__dbs_fname) + self.__descriptionClip = opts.descriptionClip + self.__error = None + self.__generateForMobigen = generateForMobigen + self.__genres = None + self.__htmlFileList = [] + self.__libraryPath = self.fetchLibraryPath() + self.__ncxSoup = None + self.__opts = opts + self.__playOrder = 1 + self.__plugin = plugin + self.__plugin_path = opts.plugin_path + self.__progressInt = 0 + self.__progressString = '' + self.__reporter = notification + self.__stylesheet = stylesheet + self.__thumbs = None + self.__title = opts.catalog_title + self.__verbose = opts.verbose + + # Accessors + def getauthors(self): + return self.__authors + def setauthors(self, value): + self.__authors = value + authors = property(getauthors, setauthors) + + def getbasename(self): + return self.__basename + def setbasename(self, value): + self.__authors = value + basename = property(getbasename, setbasename) + + def getbooksByAuthor(self): + return self.__booksByAuthor + def setbooksByAuthor(self, value): + self.__booksByAuthor = value + booksByAuthor = property(getbooksByAuthor, setbooksByAuthor) + + def getbooksByTitle(self): + return self.__booksByTitle + def setbooksByTitle(self, value): + self.__booksByTitle = value + booksByTitle = property(getbooksByTitle, setbooksByTitle) + + def getcatalogPath(self): + return self.__catalogPath + def setcatalogPath(self, value): + self.__catalogPath = value + catalogPath = property(getcatalogPath, setcatalogPath) + + def getcontentDir(self): + return self.__contentDir + def setcontentDir(self, value): + self.__contentDir = value + contentDir = property(getcontentDir, setcontentDir) + + def getcreator(self): + return self.__creator + def setcreator(self, value): + self.__creator = value + creator = property(getcreator, setcreator) + + def getdatabaseSnapshot(self): + return self.__databaseSnapshot + def setdatabaseSnapshot(self, value): + self.__databaseSnapshot = value + databaseSnapshot = property(getdatabaseSnapshot, setdatabaseSnapshot) + + def getdb(self): + return self.__db + db = property(getdb) + + def getdescriptionClip(self): + return self.__descriptionClip + def setdescriptionClip(self, value): + self.__descriptionClip = value + descriptionClip = property(getdescriptionClip, setdescriptionClip) + + def geterror(self): + return self.__error + error = property(geterror) + + def getgenerateForMobigen(self): + return self.__generateForMobigen + def setgenerateForMobigen(self, value): + self.__generateForMobigen = value + generateForMobigen = property(getgenerateForMobigen, setgenerateForMobigen) + + def getgenres(self): + return self.__genres + def setgenres(self, value): + self.__genres = value + genres = property(getgenres, setgenres) + + def gethtmlFileList(self): + return self.__htmlFileList + def sethtmlFileList(self, value): + self.__htmlFileList = value + htmlFileList = property(gethtmlFileList, sethtmlFileList) + + def getlibraryPath(self): + return self.__libraryPath + def setlibraryPath(self, value): + self.__libraryPath = value + libraryPath = property(getlibraryPath, setlibraryPath) + + def getncxSoup(self): + return self.__ncxSoup + def setncxSoup(self, value): + self.__ncxSoup = value + ncxSoup = property(getncxSoup, setncxSoup) + + def getopts(self): + return self.__opts + opts = property(getopts) + + def getplayOrder(self): + return self.__playOrder + def setplayOrder(self, value): + self.__playOrder = value + playOrder = property(getplayOrder, setplayOrder) + + def getplugin(self): + return self.__plugin + plugin = property(getplugin) + + def getpluginPath(self): + return self.__plugin_path + def setpluginPath(self, value): + self.__plugin_path = value + pluginPath = property(getpluginPath, setpluginPath) + + def getprogressInt(self): + return self.__progressInt + def setprogressInt(self, value): + self.__progressInt = value + progressInt = property(getprogressInt, setprogressInt) + + def getprogressString(self): + return self.__progressString + def setprogressString(self, value): + self.__progressString = value + progressString = property(getprogressString, setprogressString) + + def getreporter(self): + return self.__reporter + def setreporter(self, value): + self.__reporter = value + reporter = property(getreporter, setreporter) + + def getstylesheet(self): + return self.__stylesheet + def setstylesheet(self, value): + self.__stylesheet = value + stylesheet = property(getstylesheet, setstylesheet) + + def getthumbs(self): + return self.__thumbs + def setthumbs(self, value): + self.__thumbs = value + thumbs = property(getthumbs, setthumbs) + + def gettitle(self): + return self.__title + def settitle(self, value): + self.__title = value + title = property(gettitle, settitle) + + def getverbose(self): + return self.__verbose + def setverbose(self, value): + self.__verbose = value + verbose = property(getverbose, setverbose) + + # Methods + def buildSources(self): + if self.reporter.cancelRequested: return 1 + if not self.booksByTitle: + self.fetchBooksByTitle() + + if self.reporter.cancelRequested: return 1 + self.fetchBooksByAuthor() + + if self.reporter.cancelRequested: return 1 + self.generateHTMLDescriptions() + + if self.reporter.cancelRequested: return 1 + self.generateHTMLByTitle() + + if self.reporter.cancelRequested: return 1 + self.generateHTMLByAuthor() + + if self.reporter.cancelRequested: return 1 + self.generateHTMLByTags() + + if self.reporter.cancelRequested: return 1 + self.generateThumbnails() + + if self.reporter.cancelRequested: return 1 + self.generateOPF() + + if self.reporter.cancelRequested: return 1 + self.generateNCXHeader() + + if self.reporter.cancelRequested: return 1 + self.generateNCXDescriptions("Descriptions") + + if self.reporter.cancelRequested: return 1 + self.generateNCXByTitle("Titles", single_article_per_section=False) + + if self.reporter.cancelRequested: return 1 + self.generateNCXByAuthor("Authors", single_article_per_section=False) + + if self.reporter.cancelRequested: return 1 + self.generateNCXByTags("Genres") + + if self.reporter.cancelRequested: return 1 + self.writeNCX() + + return 0 + + def cleanUp(self): + pass + + def copyResources(self): + '''Move resource files to self.catalogPath''' + catalog_resources = P("catalog") + + files_to_copy = [('','DefaultCover.jpg'), + ('content','stylesheet.css'), + ('images','mastheadImage.gif')] + + for file in files_to_copy: + if file[0] == '': + shutil.copy(os.path.join(catalog_resources,file[1]), + self.catalogPath) + else: + shutil.copy(os.path.join(catalog_resources,file[1]), + os.path.join(self.catalogPath, file[0])) + + + def fetchBooksByTitle(self): + + if self.verbose: + print self.updateProgressFullStep("fetchBooksByTitle()") + + # Get the database as a dictionary + # Sort by title + # Search is a string like this: + # not tag: author:"Riker" + # So we need to merge opts.exclude_tag with opts.search_text + # not tag:"~" author:"Riker" + + self.opts.sort_by = 'title' + + # Merge opts.exclude_tag with opts.search_text + + # What if no exclude tags? + exclude_tags = self.opts.exclude_tags.split(',') + search_terms = [] + for tag in exclude_tags: + search_terms.append("tag:%s" % tag) + search_phrase = "not (%s)" % " or ".join(search_terms) + + # Allow for no search_text + if self.opts.search_text: + self.opts.search_text = self.opts.search_text + " " + search_phrase + else: + self.opts.search_text = search_phrase + + if self.verbose and False: + print "self.opts.search_text: %s" % self.opts.search_text + + # Fetch the database as a dictionary + data = self.plugin.search_sort_db(self.db, self.opts) + + # Populate this_title{} from data[{},{}] + titles = [] + for record in data: + this_title = {} + + title = this_title['title'] = self.convertHTMLEntities(record['title']) + this_title['title_sort'] = self.generateSortTitle(title) + this_title['author'] = " & ".join(record['authors']) + this_title['author_sort'] = record['author_sort'] + this_title['id'] = record['id'] + if record['publisher']: + this_title['publisher'] = re.sub('&', '&', record['publisher']) + + this_title['rating'] = record['rating'] if record['rating'] else 0 + # 2009-11-05 09:29:37 + date_strings = str(record['pubdate']).split("-") + this_title['date'] = '%s %s' % (self.MONTHS[int(date_strings[1])-1], date_strings[0]) + + if record['comments']: + this_title['description'] = re.sub('&', '&', record['comments']) + this_title['short_description'] = self.generateShortDescription(this_title['description']) + else: + this_title['description'] = None + this_title['short_description'] = None + + if record['cover']: + this_title['cover'] = re.sub('&', '&', record['cover']) + + # This may be updated in self.processSpecialTags() + this_title['read'] = False + + if record['tags']: + this_title['tags'] = self.processSpecialTags(record['tags'], + this_title, self.opts) + if record['formats']: + formats = [] + for format in record['formats']: + formats.append(self.convertHTMLEntities(format)) + this_title['formats'] = formats + + titles.append(this_title) + + # Re-sort based on title_sort + self.booksByTitle = sorted(titles, + key=lambda x:(x['title_sort'], x['title_sort'])) + + def fetchBooksByAuthor(self): + # Generate a list of titles sorted by author from the database + + if self.verbose: + print self.updateProgressFullStep("fetchBooksByAuthor()") + + self.booksByAuthor = sorted(self.booksByTitle, + key=lambda x:(x['author_sort'], x['author_sort'])) + + # Search_text already initialized + # Get the database sorted by author_sort + self.opts.sort_by = 'author_sort' + data = self.plugin.search_sort_db(self.db, self.opts) + + # Build the unique_authors set + authors = [] + for record in data: + # There may be multiple authors + author_list = [] + for author in record['authors']: + author_list.append(author) + authors_concatenated = ", ".join(author_list) + authors.append((authors_concatenated, record['author_sort'])) + + # authors[] contains a list of all book authors, with multiple entries for multiple books by author + # unique_authors : (([0]:friendly [1]:sort [2]:book_count)) + books_by_current_author = 0 + current_author = authors[0] + multiple_authors = False + unique_authors = [] + for (i,author) in enumerate(authors): + if author != current_author: + multiple_authors = True + + if author != current_author and i: + unique_authors.append((current_author[0], current_author[1], + books_by_current_author)) + current_author = author + books_by_current_author = 1 + elif i==0 and len(authors) == 1: + # Allow for single-book lists + unique_authors.append((current_author[0], current_author[1], + books_by_current_author)) + else: + books_by_current_author += 1 + + # Allow for single-author dataset + if not multiple_authors: + unique_authors.append((current_author[0], current_author[1], + books_by_current_author)) + + if self.verbose and False: + print "\nget_books_by_author(): %d unique authors" % len(unique_authors) + for author in unique_authors[0:3]: + print "%s" % author[0] + print " ... " + for author in unique_authors[-3:]: + print "%s" % author[0] + + self.authors = unique_authors + + def generateHTMLDescriptions(self): + # Write each title to a separate HTML file in contentdir + if self.verbose: + print self.updateProgressFullStep("generateHTMLDescriptions()") + + for (title_num, title) in enumerate(self.booksByTitle): + if False: + print "%3s: %s - %s" % (title['id'], title['title'], title['author']) + + self.updateProgressMicroStep("generating book descriptions ...", + 100*title_num/len(self.booksByTitle)) + + # Generate the header + soup = self.generateHTMLDescriptionHeader("%s" % title['title']) + body = soup.find('body') + + btc = 0 + + # Insert section tag if this is the section start - first article only + if not title_num and self.generateForMobigen: + aTag = Tag(soup,'a') + aTag['name'] = 'section_start' + body.insert(btc, aTag) + btc += 1 + + # Insert the anchor + aTag = Tag(soup, "a") + aTag['name'] = "book%d" % int(title['id']) + body.insert(btc, aTag) + btc += 1 + + # Insert section marker if this is the section head - first article only + if not title_num and self.generateForMobigen: + body.insert(btc, ' ') + btc += 1 + + # Insert the book title + #

Book Title

+ emTag = Tag(soup, "em") + emTag.insert(0, NavigableString(escape(title['title']))) + titleTag = body.find(attrs={'class':'title'}) + titleTag.insert(0,emTag) + + # Insert the author + authorTag = body.find(attrs={'class':'author'}) + aTag = Tag(soup, "a") + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(title['author'])) + aTag.insert(0, escape(title['author'])) + authorTag.insert(0, NavigableString("by ")) + authorTag.insert(1, aTag) + + ''' + # Insert the unlinked tags. Tags are not linked, just informative + if 'tags' in title: + tagsTag = body.find(attrs={'class':'tags'}) + emTag = Tag(soup,"em") + emTag.insert(0,NavigableString(', '.join(title['tags']))) + tagsTag.insert(0,emTag) + + ''' + # Insert tags with links to genre sections + if 'tags' in title: + tagsTag = body.find(attrs={'class':'tags'}) + ttc = 0 + + # Insert a spacer to match the author indent + fontTag = Tag(soup,"font") + fontTag['style'] = 'color:white;font-size:large' + fontTag.insert(0, NavigableString("by ")) + tagsTag.insert(ttc, fontTag) + ttc += 1 + + for tag in title['tags']: + aTag = Tag(soup,'a') + aTag['href'] = "Genre%s.html" % re.sub("\W","",self.convertHTMLEntities(tag)) + aTag.insert(0,escape(NavigableString(tag))) + emTag = Tag(soup, "em") + emTag.insert(0, aTag) + tagsTag.insert(ttc, emTag) + ttc += 1 + + # Insert the cover if available + imgTag = Tag(soup,"img") + if 'cover' in title: + imgTag['src'] = "../images/thumbnail_%d.jpg" % int(title['id']) + else: + imgTag['src'] = "../images/thumbnail_default.jpg" + imgTag['alt'] = "cover" + thumbnailTag = body.find(attrs={'class':'thumbnail'}) + thumbnailTag.insert(0,imgTag) + + # Insert the publisher + publisherTag = body.find(attrs={'class':'publisher'}) + if 'publisher' in title: + publisherTag.insert(0,NavigableString(title['publisher'] + '
' )) + else: + publisherTag.insert(0,NavigableString('(unknown)
')) + + # Insert the publication date + pubdateTag = body.find(attrs={'class':'date'}) + if title['date'] is not None: + pubdateTag.insert(0,NavigableString(title['date'] + '
')) + else: + pubdateTag.insert(0,NavigableString('(unknown)
')) + + # Insert the rating + # Render different ratings chars for epub/mobi + stars = int(title['rating']) / 2 + star_char = "★" if self.opts.fmt == 'mobi' else "*" + star_string = star_char * stars + + empty_star_char = "☆" if self.opts.fmt == 'mobi' else ' ' + empty_stars = empty_star_char * (5 - stars) + + ratingTag = body.find(attrs={'class':'rating'}) + ratingTag.insert(0,NavigableString('%s%s
' % (star_string,empty_stars))) + + # Insert user notes or remove Notes label. Notes > 1 line will push formatting down + if 'notes' in title: + notesTag = body.find(attrs={'class':'notes'}) + notesTag.insert(0,NavigableString(title['notes'] + '
')) + else: + notes_labelTag = body.find(attrs={'class':'notes_label'}) + empty_labelTag = Tag(soup, "td") + empty_labelTag.insert(0,NavigableString('
')) + notes_labelTag.replaceWith(empty_labelTag) + + # Insert the blurb + if 'description' in title and title['description'] > '': + blurbTag = body.find(attrs={'class':'description'}) + blurbTag.insert(0,NavigableString(title['description'])) + + # Write the book entry to contentdir + outfile = open("%s/book_%d.html" % (self.contentDir, int(title['id'])), 'w') + outfile.write(soup.prettify()) + outfile.close() + + def generateHTMLByTitle(self): + # Write books by title A-Z to HTML file + + if self.verbose: + print self.updateProgressFullStep("generateHTMLByTitle()") + + soup = self.generateHTMLEmptyHeader("Books By Alpha Title") + body = soup.find('body') + btc = 0 + + # Insert section tag + aTag = Tag(soup,'a') + aTag['name'] = 'section_start' + body.insert(btc, aTag) + btc += 1 + + # Insert the anchor + aTag = Tag(soup, "a") + aTag['name'] = "bytitle" + body.insert(btc, aTag) + btc += 1 + + # Insert section marker if this is the section head - first article only + if self.generateForMobigen: + body.insert(btc, ' ') + btc += 1 + + ''' + # We don't need this because the Kindle shows section titles + #

By Title

+ h2Tag = Tag(soup, "h2") + aTag = Tag(soup, "a") + aTag['name'] = "bytitle" + h2Tag.insert(0,aTag) + h2Tag.insert(1,NavigableString('By Title (%d)' % len(self.booksByTitle))) + body.insert(btc,h2Tag) + btc += 1 + ''' + + #

+ #

+ divTag = Tag(soup, "div") + dtc = 0 + current_letter = "" + + # Loop through the books by title + for book in self.booksByTitle: + if book['title_sort'][0] != current_letter : + # Start a new letter + current_letter = book['title_sort'][0] + pIndexTag = Tag(soup, "p") + pIndexTag['class'] = "letter_index" + aTag = Tag(soup, "a") + aTag['name'] = "%stitles" % book['title_sort'][0] + pIndexTag.insert(0,aTag) + pIndexTag.insert(1,NavigableString(book['title_sort'][0])) + divTag.insert(dtc,pIndexTag) + dtc += 1 + + # Add books + pBookTag = Tag(soup, "p") + ptc = 0 + + # Prefix book with read/unread symbol + if book['read']: + # check mark + pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) + pBookTag['class'] = "read_book" + ptc += 1 + else: + # hidden check mark + pBookTag['class'] = "unread_book" + pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) + ptc += 1 + + # Link to book + aTag = Tag(soup, "a") + aTag['href'] = "book_%d.html" % (int(float(book['id']))) + aTag.insert(0,escape(book['title'])) + pBookTag.insert(ptc, aTag) + ptc += 1 + + # Dot + pBookTag.insert(ptc, NavigableString(" · ")) + ptc += 1 + + # Link to author + emTag = Tag(soup, "em") + aTag = Tag(soup, "a") + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) + aTag.insert(0, escape(book['author'])) + emTag.insert(0,aTag) + pBookTag.insert(ptc, emTag) + ptc += 1 + + divTag.insert(dtc, pBookTag) + dtc += 1 + + # Add the divTag to the body + body.insert(btc, divTag) + btc += 1 + + # Write the volume to contentdir + outfile_spec = "%s/ByAlphaTitle.html" % (self.contentDir) + outfile = open(outfile_spec, 'w') + outfile.write(soup.prettify()) + outfile.close() + self.htmlFileList.append("content/ByAlphaTitle.html") + + def generateHTMLByAuthor(self): + # Write books by author A-Z + if self.verbose: + print self.updateProgressFullStep("generateHTMLByAuthor()") + friendly_name = "By Author" + + soup = self.generateHTMLEmptyHeader(friendly_name) + body = soup.find('body') + + btc = 0 + + # Insert section tag + aTag = Tag(soup,'a') + aTag['name'] = 'section_start' + body.insert(btc, aTag) + btc += 1 + + # Insert the anchor + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + body.insert(btc, aTag) + btc += 1 + + # Insert section marker if this is the section head - first article only + if self.generateForMobigen: + body.insert(btc, ' ') + btc += 1 + + ''' + # We don't need this because the kindle inserts section titles + #

By Author

+ h2Tag = Tag(soup, "h2") + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + h2Tag.insert(0,aTag) + h2Tag.insert(1,NavigableString('%s' % friendly_name)) + body.insert(btc,h2Tag) + btc += 1 + ''' + + #

+ #

+ divTag = Tag(soup, "div") + dtc = 0 + current_letter = "" + current_author = "" + + # Loop through books_by_author + book_count = 0 + for book in self.booksByAuthor: + book_count += 1 + if book['author_sort'][0] != current_letter : + ''' + # Start a new letter - anchor only, hidden + current_letter = book['author_sort'][0].upper() + aTag = Tag(soup, "a") + aTag['name'] = "%sauthors" % current_letter + divTag.insert(dtc, aTag) + dtc += 1 + ''' + # Start a new letter with Index letter + current_letter = book['author_sort'][0].upper() + pIndexTag = Tag(soup, "p") + pIndexTag['class'] = "letter_index" + aTag = Tag(soup, "a") + aTag['name'] = "%sauthors" % current_letter + pIndexTag.insert(0,aTag) + pIndexTag.insert(1,NavigableString(book['author_sort'][0].upper())) + divTag.insert(dtc,pIndexTag) + dtc += 1 + + if book['author'] != current_author: + # Start a new author + current_author = book['author'] + pAuthorTag = Tag(soup, "p") + pAuthorTag['class'] = "author_index" + emTag = Tag(soup, "em") + aTag = Tag(soup, "a") + aTag['name'] = "%s" % self.generateAuthorAnchor(current_author) + aTag.insert(0,NavigableString(current_author)) + emTag.insert(0,aTag) + pAuthorTag.insert(0,emTag) + divTag.insert(dtc,pAuthorTag) + dtc += 1 + + # Add books + pBookTag = Tag(soup, "p") + ptc = 0 + + # Prefix book with read/unread symbol + if book['read']: + # check mark + pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) + pBookTag['class'] = "read_book" + ptc += 1 + else: + # hidden check mark + pBookTag['class'] = "unread_book" + pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) + ptc += 1 + + aTag = Tag(soup, "a") + aTag['href'] = "book_%d.html" % (int(float(book['id']))) + aTag.insert(0,escape(book['title'])) + pBookTag.insert(ptc, aTag) + ptc += 1 + + divTag.insert(dtc, pBookTag) + dtc += 1 + + ''' + # Insert the

tag with book_count at the head + #

By Author

+ h2Tag = Tag(soup, "h2") + aTag = Tag(soup, "a") + anchor_name = friendly_name.lower() + aTag['name'] = anchor_name.replace(" ","") + h2Tag.insert(0,aTag) + h2Tag.insert(1,NavigableString('%s (%d)' % (friendly_name, book_count))) + body.insert(btc,h2Tag) + btc += 1 + ''' + + # Add the divTag to the body + body.insert(btc, divTag) + + + # Write the generated file to contentdir + outfile_spec = "%s/ByAlphaAuthor.html" % (self.contentDir) + outfile = open(outfile_spec, 'w') + outfile.write(soup.prettify()) + outfile.close() + self.htmlFileList.append("content/ByAlphaAuthor.html") + + def generateHTMLByTags(self): + # Generate individual HTML files for each tag, e.g. Fiction, Nonfiction ... + # Note that special tags - ~+*[] - have already been filtered from books[] + + if self.verbose: + print self.updateProgressFullStep("generateHTMLByTags()") + + # Fetch the tags using the database interface + from calibre.library.database2 import LibraryDatabase2 + db = LibraryDatabase2(self.libraryPath) + + # Filter out REMOVE_TAGS, sort + filtered_tags = self.filterDbTags(db.all_tags()) + + # Extract books matching filtered_tags + genre_list = [] + for tag in filtered_tags: + if False : print "searching for %s" % tag + tag_list = {} + tag_list['tag'] = tag + tag_list['books'] = [] + for book in self.booksByAuthor: + if 'tags' in book and tag in book['tags']: + if False: print "\t %s" % (book['title']) + this_book = {} + this_book['author'] = book['author'] + this_book['title'] = book['title'] + this_book['author_sort'] = book['author_sort'] + this_book['read'] = book['read'] + this_book['id'] = book['id'] + tag_list['books'].append(this_book) + + if len(tag_list['books']): + # Possible to have an empty tag list if the books were excluded + genre_list.append(tag_list) + + # Write the results + # genre_list = [ [tag_list], [tag_list] ...] + master_genre_list = [] + for (index, genre) in enumerate(genre_list): + if False : print "genre: %s" % genre['tag'] + + # Create sorted_authors[0] = friendly, [1] = author_sort for NCX creation + authors = [] + for book in genre['books']: + #print "\t %s - %s" % (book['title'], book['author']) + authors.append((book['author'],book['author_sort'])) + + # authors[] contains a list of all book authors, with multiple entries for multiple books by author + # Create unique_authors with a count of books per author as the third tuple element + books_by_current_author = 1 + current_author = authors[0] + unique_authors = [] + for (i,author) in enumerate(authors): + if author != current_author and i: + unique_authors.append((current_author[0], current_author[1], books_by_current_author)) + current_author = author + books_by_current_author = 1 + elif i==0 and len(authors) == 1: + # Allow for single-book lists + unique_authors.append((current_author[0], current_author[1], books_by_current_author)) + else: + books_by_current_author += 1 + ''' + # Extract the unique entries + unique_authors = [] + for author in authors: + if not author in unique_authors: + unique_authors.append(author) + ''' + + # Write the genre book list as an article + titles_spanned = self.generateHTMLByGenre(genre['tag'], True if index==0 else False, genre['books'], + "%s/Genre%s.html" % (self.contentDir, re.sub("\W","", self.convertHTMLEntities(genre['tag'])))) + + tag_file = "content/Genre%s.html" % (re.sub("\W","", self.convertHTMLEntities(genre['tag']))) + master_genre_list.append({'tag':genre['tag'], + 'file':tag_file, + 'authors':unique_authors, + 'books':genre['books'], + 'titles_spanned':titles_spanned}) + + self.genres = master_genre_list + + def generateThumbnails(self): + # Generate a thumbnail per cover. If a current thumbnail exists, skip + # If a cover doesn't exist, use default + # Return list of active thumbs + if self.verbose: + print self.updateProgressFullStep("generateThumbnails()") + + thumbs = ['thumbnail_default.jpg'] + + image_dir = "%s/images" % self.catalogPath + + for (i,title) in enumerate(self.booksByTitle): + # Update status + self.updateProgressMicroStep("generating thumbnails ...", + 100*i/len(self.booksByTitle)) + # Check to see if source file exists + if 'cover' in title and os.path.isfile(title['cover']): + # print "cover found for %s" % title['title'] + # Add the thumb spec to thumbs[] + thumbs.append("thumbnail_%d.jpg" % int(title['id'])) + + # Check to see if thumbnail exists + thumb_fp = "%s/thumbnail_%d.jpg" % (image_dir,int(title['id'])) + thumb_file = 'thumbnail_%d.jpg' % int(title['id']) + if os.path.isfile(thumb_fp): + # Check to see if cover is newer than thumbnail + # os.path.getmtime() = modified time + # os.path.ctime() = creation time + cover_timestamp = os.path.getmtime(cover) + thumb_timestamp = os.path.getmtime(thumb_fp) + if thumb_timestamp < cover_timestamp: + # if verbose: print "updating thumbnail for %s" % title['title'] + self.generateThumbnail(title, image_dir, thumb_file) + else: + #if verbose: print "generating new thumbnail for %s" % title['title'] + self.generateThumbnail(title, image_dir, thumb_file) + + else: + # Use default cover + if self.verbose: print "no cover available for %s, will use default" % \ + (title['title']) + # Check to make sure default is current + # Check to see if thumbnail exists + thumb_fp = "%s/thumbnail_default.jpg" % (image_dir) + cover = "%s/DefaultCover.png" % (self.catalogPath) + + # Init Qt for image conversion + from calibre.gui2 import is_ok_to_use_qt + is_ok_to_use_qt() + from PyQt4.QtGui import QImage + + # I() fetches path to resource, e.g. I('book.svg') returns: + # /Applications/calibre.app/Contents/Resources/resources/images/book.svg + # Convert .svg to .jpg + default_cover = I('book.svg') + cover_img = QImage() + cover_img.load(default_cover) + cover_img.save(cover, "PNG", -1) + + if os.path.isfile(thumb_fp): + # Check to see if default cover is newer than thumbnail + # os.path.getmtime() = modified time + # os.path.ctime() = creation time + cover_timestamp = os.path.getmtime(cover) + thumb_timestamp = os.path.getmtime(thumb_fp) + if thumb_timestamp < cover_timestamp: + if self.verbose: print "updating thumbnail_default for %s" % title['title'] + #title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath + title['cover'] = cover + self.generateThumbnail(title, image_dir, "thumbnail_default.jpg") + else: + if self.verbose: print "generating new thumbnail_default.jpg" + #title['cover'] = "%s/DefaultCover.jpg" % self.catalogPath + title['cover'] = cover + self.generateThumbnail(title, image_dir, "thumbnail_default.jpg") + + self.thumbs = thumbs + + def generateOPF(self): + + if self.verbose: + print self.updateProgressFullStep("generateOPF()") + + + if self.generateForMobigen: + header = ''' + + + + en-US + Gregory Riker 2009 + 2009-12-04 + + + + + + ''' + else: + header = ''' + + + + en-US + + + + + + + ''' + # Add the supplied metadata tags + soup = BeautifulStoneSoup(header, selfClosingTags=['item','itemref', 'reference']) + metadata = soup.find('metadata') + mtc = 0 + + titleTag = Tag(soup, "dc:title") + titleTag.insert(0,self.title + ' (M)' if self.generateForMobigen else self.title) + metadata.insert(mtc, titleTag) + mtc += 1 + + creatorTag = Tag(soup, "dc:creator") + creatorTag.insert(0, self.creator) + metadata.insert(mtc, creatorTag) + mtc += 1 + + # Create the OPF tags + manifest = soup.find('manifest') + mtc = 0 + spine = soup.find('spine') + stc = 0 + guide = soup.find('guide') + gtc = 0 + + itemTag = Tag(soup, "item") + itemTag['id'] = "ncx" + itemTag['href'] = '%s.ncx' % self.basename + itemTag['media-type'] = "application/x-dtbncx+xml" + manifest.insert(mtc, itemTag) + mtc += 1 + + itemTag = Tag(soup, "item") + itemTag['id'] = 'stylesheet' + itemTag['href'] = self.stylesheet + itemTag['media-type'] = 'text/css' + manifest.insert(mtc, itemTag) + mtc += 1 + + itemTag = Tag(soup, "item") + itemTag['id'] = 'mastheadimage-image' + itemTag['href'] = "images/mastheadImage.gif" + itemTag['media-type'] = 'image/gif' + manifest.insert(mtc, itemTag) + mtc += 1 + + # Write the thumbnail images to the manifest + for thumb in self.thumbs: + itemTag = Tag(soup, "item") + itemTag['href'] = "images/%s" % (thumb) + end = thumb.find('.jpg') + itemTag['id'] = "%s-image" % thumb[:end] + itemTag['media-type'] = 'image/jpeg' + manifest.insert(mtc, itemTag) + mtc += 1 + + # HTML files - add books to manifest and spine + for book in self.booksByTitle: + # manifest + itemTag = Tag(soup, "item") + itemTag['href'] = "content/book_%d.html" % int(book['id']) + itemTag['id'] = "book%d" % int(book['id']) + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 + + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = "book%d" % int(book['id']) + spine.insert(stc, itemrefTag) + stc += 1 + + # Add other html_files to manifest and spine + + for file in self.htmlFileList: + itemTag = Tag(soup, "item") + start = file.find('/') + 1 + end = file.find('.') + itemTag['href'] = file + itemTag['id'] = file[start:end].lower() + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 + + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = file[start:end].lower() + spine.insert(stc, itemrefTag) + stc += 1 + + # Add genre files to manifest and spine + for genre in self.genres: + if False: print "adding %s to manifest and spine" % genre['tag'] + itemTag = Tag(soup, "item") + start = genre['file'].find('/') + 1 + end = genre['file'].find('.') + itemTag['href'] = genre['file'] + itemTag['id'] = genre['file'][start:end].lower() + itemTag['media-type'] = "application/xhtml+xml" + manifest.insert(mtc, itemTag) + mtc += 1 + + # spine + itemrefTag = Tag(soup, "itemref") + itemrefTag['idref'] = genre['file'][start:end].lower() + spine.insert(stc, itemrefTag) + stc += 1 + + # Guide + referenceTag = Tag(soup, "reference") + referenceTag['type'] = 'masthead' + referenceTag['title'] = 'mastheadimage-image' + referenceTag['href'] = 'images/mastheadImage.gif' + guide.insert(0,referenceTag) + + # Write the OPF file + outfile = open("%s/%s.opf" % (self.catalogPath, self.basename), 'w') + outfile.write(soup.prettify()) + + def generateNCXHeader(self): + + if self.verbose: + print self.updateProgressFullStep("generateNCXHeader()") + + if self.generateForMobigen: + header = ''' + + + ''' + soup = BeautifulStoneSoup(header, selfClosingTags=['content','mbp:meta-img']) + + else: + header = ''' + + + + ''' + soup = BeautifulStoneSoup(header, selfClosingTags=['content','calibre:meta-img']) + + ncx = soup.find('ncx') + navMapTag = Tag(soup, 'navMap') + navPointTag = Tag(soup, 'navPoint') + navPointTag['class'] = "periodical" + navPointTag['id'] = "title" + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + textTag.insert(0, NavigableString(self.title)) + navLabelTag.insert(0, textTag) + navPointTag.insert(0, navLabelTag) + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id']) + navPointTag.insert(1, contentTag) + cmiTag = Tag(soup, '%s' % 'mbp:meta-img' if self.generateForMobigen else 'calibre:meta-img') + cmiTag['name'] = "mastheadImage" + cmiTag['src'] = "images/mastheadImage.gif" + navPointTag.insert(2,cmiTag) + navMapTag.insert(0,navPointTag) + + ncx.insert(0,navMapTag) + + self.ncxSoup = soup + + def generateNCXDescriptions(self, tocTitle): + + if self.verbose: + print self.updateProgressFullStep("generateNCXDescription()") + + # --- Construct the 'Books by Title' section --- + ncx_soup = self.ncxSoup + body = ncx_soup.find("navPoint") + btc = len(body.contents) + + # Add the section navPoint + navPointTag = Tag(ncx_soup, 'navPoint') + navPointTag['class'] = "section" + navPointTag['id'] = "bytitle-ID" + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(ncx_soup, 'navLabel') + textTag = Tag(ncx_soup, 'text') + textTag.insert(0, NavigableString(tocTitle)) + navLabelTag.insert(0, textTag) + nptc = 0 + navPointTag.insert(nptc, navLabelTag) + nptc += 1 + contentTag = Tag(ncx_soup,"content") + contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id']) + navPointTag.insert(nptc, contentTag) + nptc += 1 + + # Loop over the titles + for book in self.booksByTitle: + navPointVolumeTag = Tag(ncx_soup, 'navPoint') + navPointVolumeTag['class'] = "article" + navPointVolumeTag['id'] = "book%dID" % int(book['id']) + navPointVolumeTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(ncx_soup, "navLabel") + textTag = Tag(ncx_soup, "text") + textTag.insert(0, NavigableString(self.formatNCXText(book['title']))) + navLabelTag.insert(0,textTag) + navPointVolumeTag.insert(0,navLabelTag) + + contentTag = Tag(ncx_soup, "content") + contentTag['src'] = "content/book_%d.html#book%d" % (int(book['id']), int(book['id'])) + navPointVolumeTag.insert(1, contentTag) + + # Build the author tag + cmTag = Tag(ncx_soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "author" + cmTag.insert(0, NavigableString(self.formatNCXText(book['author']))) + navPointVolumeTag.insert(2, cmTag) + + # Build the description tag + if book['short_description']: + cmTag = Tag(ncx_soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "description" + cmTag.insert(0, NavigableString(self.formatNCXText(book['short_description']))) + navPointVolumeTag.insert(3, cmTag) + + # Add this volume to the section tag + navPointTag.insert(nptc, navPointVolumeTag) + nptc += 1 + + # Add this section to the body + body.insert(btc, navPointTag) + btc += 1 + + self.ncxSoup = ncx_soup + + def generateNCXByTitle(self, tocTitle, single_article_per_section=True): + + if self.verbose: + print self.updateProgressFullStep("generateNCXByTitle()") + + soup = self.ncxSoup + output = "ByAlphaTitle" + body = soup.find("navPoint") + btc = len(body.contents) + + # --- Construct the 'Books By Title' section --- + navPointTag = Tag(soup, 'navPoint') + navPointTag['class'] = "section" + navPointTag['id'] = "byalphatitle-ID" + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + # textTag.insert(0, NavigableString("By Title (%d)" % len(sorted_titles))) + textTag.insert(0, NavigableString(tocTitle)) + navLabelTag.insert(0, textTag) + nptc = 0 + navPointTag.insert(nptc, navLabelTag) + nptc += 1 + contentTag = Tag(soup,"content") + contentTag['src'] = "content/%s.html#section_start" % (output) + navPointTag.insert(nptc, contentTag) + nptc += 1 + + books_by_letter = [] + if single_article_per_section: + # Create a single article for all books in this section + single_list = [] + for book in self.booksByTitle: + single_list.append(book) + if len(single_list) > 1: + short_description = '%s -\n%s' % (single_list[0]['title'], single_list[-1]['title']) + else: + short_description = '%s' % (single_list[0]['title']) + + books_by_letter.append(short_description) + + else: + # Loop over the titles, find start of each letter, add description_preview_count books + current_letter = self.booksByTitle[0]['title_sort'][0] + title_letters = [current_letter] + current_book_list = [] + current_book = "" + for book in self.booksByTitle: + if book['title_sort'][0] != current_letter: + # Save the old list + book_list = " • ".join(current_book_list) + short_description = self.generateShortDescription(self.formatNCXText(book_list)) + books_by_letter.append(short_description) + + # Start the new list + current_letter = book['title_sort'][0] + title_letters.append(current_letter) + current_book = book['title'] + current_book_list = [book['title']] + else: + if len(current_book_list) < self.descriptionClip and \ + book['title'] != current_book : + current_book = book['title'] + current_book_list.append(book['title']) + + # Add the last book list + book_list = " • ".join(current_book_list) + short_description = self.generateShortDescription(self.formatNCXText(book_list)) + books_by_letter.append(short_description) + + + # Add *article* entries for each populated title letter + for (i,books) in enumerate(books_by_letter): + navPointByLetterTag = Tag(soup, 'navPoint') + navPointByLetterTag['class'] = "article" + if not single_article_per_section: + navPointByLetterTag['id'] = "%sTitles-ID" % (title_letters[i].upper()) + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + if single_article_per_section: + textTag.insert(0, NavigableString("All books sorted by title")) + else: + textTag.insert(0, NavigableString("Books beginning with '%s'" % (title_letters[i].upper()))) + navLabelTag.insert(0, textTag) + navPointByLetterTag.insert(0,navLabelTag) + contentTag = Tag(soup, 'content') + if single_article_per_section: + contentTag['src'] = "content/%s.html#bytitle" % (output) + else: + contentTag['src'] = "content/%s.html#%stitles" % (output, title_letters[i].upper()) + navPointByLetterTag.insert(1,contentTag) + cmTag = Tag(soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "description" + if single_article_per_section: + cmTag.insert(0, NavigableString(self.formatNCXText(books_by_letter[0]))) + else: + cmTag.insert(0, NavigableString(self.formatNCXText(books))) + navPointByLetterTag.insert(2, cmTag) + navPointTag.insert(nptc, navPointByLetterTag) + nptc += 1 + + # Add this section to the body + body.insert(btc, navPointTag) + btc += 1 + + self.ncxSoup = soup + + def generateNCXByAuthor(self, tocTitle, single_article_per_section=True): + + if self.verbose: + print self.updateProgressFullStep("generateNCXByAuthor()") + + soup = self.ncxSoup + output = "ByAlphaAuthor" + HTML_file = "content/ByAlphaAuthor.html" + body = soup.find("navPoint") + btc = len(body.contents) + + # --- Construct the 'Books By Author' *section* --- + navPointTag = Tag(soup, 'navPoint') + navPointTag['class'] = "section" + if single_article_per_section: + file_ID = "byauthor" + else: + file_ID = "%s" % tocTitle.lower() + file_ID = file_ID.replace(" ","") + navPointTag['id'] = "%s-ID" % file_ID + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + # textTag.insert(0, NavigableString('%s (%d)' % (section_title,len(sorted_authors)))) + textTag.insert(0, NavigableString('%s' % tocTitle)) + navLabelTag.insert(0, textTag) + nptc = 0 + navPointTag.insert(nptc, navLabelTag) + nptc += 1 + contentTag = Tag(soup,"content") + # contentTag['src'] = "%s#%s" % (HTML_file, file_ID) + contentTag['src'] = "%s#section_start" % HTML_file + navPointTag.insert(nptc, contentTag) + nptc += 1 + + if single_article_per_section: + # Create a single article for all authors in this section + + # Build a formatted author range for article preview + single_list = [] + for author in self.authors: + single_list.append(author[0]) + + if len(single_list) > 1: + author_list = '%s -\n%s' % (single_list[0], single_list[-1]) + else: + author_list = '%s' % (single_list[0]) + master_author_list=[(author_list, self.authors[0][1][0])] + + else: + # Create an article for each populated author index letter + # Loop over the sorted_authors list, find start of each letter, add description_preview_count artists + # sorted_authors[0]:friendly [1]:author_sort [2]:book_count + master_author_list = [] + current_letter = self.authors[0][1][0] + current_author_list = [] + for author in self.authors: + if author[1][0] != current_letter: + # Save the old list + author_list = " • ".join(current_author_list) + if len(current_author_list) == self.descriptionClip: + author_list += " …" + + author_list = self.formatNCXText(author_list) + master_author_list.append((author_list, current_letter)) + + # Start the new list + current_letter = author[1][0] + current_author_list = [author[0]] + else: + if len(current_author_list) < self.descriptionClip: + current_author_list.append(author[0]) + + # Add the last author list + author_list = " • ".join(current_author_list) + if len(current_author_list) == self.descriptionClip: + author_list += " …" + author_list = self.formatNCXText(author_list) + master_author_list.append((author_list, current_letter)) + + # Add *article* entries for each populated author initial letter + # master_author_list[0] = friendly, [1] = sort letter + for authors in master_author_list: + navPointByLetterTag = Tag(soup, 'navPoint') + navPointByLetterTag['class'] = "article" + navPointByLetterTag['id'] = "%sauthors-ID" % (authors[1]) + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(soup, 'navLabel') + textTag = Tag(soup, 'text') + if single_article_per_section: + textTag.insert(0, NavigableString("All books sorted by author")) + else: + textTag.insert(0, NavigableString("Authors beginning with '%s'" % (authors[1]))) + navLabelTag.insert(0, textTag) + navPointByLetterTag.insert(0,navLabelTag) + contentTag = Tag(soup, 'content') + + if single_article_per_section: + contentTag['src'] = "%s#byauthor" % HTML_file + else: + contentTag['src'] = "%s#%sauthors" % (HTML_file, authors[1]) + + navPointByLetterTag.insert(1,contentTag) + cmTag = Tag(soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "description" + cmTag.insert(0, NavigableString(authors[0])) + navPointByLetterTag.insert(2, cmTag) + navPointTag.insert(nptc, navPointByLetterTag) + nptc += 1 + + # Add this section to the body + body.insert(btc, navPointTag) + btc += 1 + + self.ncxSoup = soup + + def generateNCXByTags(self, tocTitle): + # Create an NCX section for 'By Genre' + # Add each genre as an article + # 'tag', 'file', 'authors' + + if self.verbose: + print self.updateProgressFullStep("generateNCXByTags()") + + ncx_soup = self.ncxSoup + body = ncx_soup.find("navPoint") + btc = len(body.contents) + + # --- Construct the 'Books By Genre' *section* --- + navPointTag = Tag(ncx_soup, 'navPoint') + navPointTag['class'] = "section" + file_ID = "%s" % tocTitle.lower() + file_ID = file_ID.replace(" ","") + navPointTag['id'] = "%s-ID" % file_ID + navPointTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(ncx_soup, 'navLabel') + textTag = Tag(ncx_soup, 'text') + # textTag.insert(0, NavigableString('%s (%d)' % (section_title, len(genre_list)))) + textTag.insert(0, NavigableString('%s' % tocTitle)) + navLabelTag.insert(0, textTag) + nptc = 0 + navPointTag.insert(nptc, navLabelTag) + nptc += 1 + contentTag = Tag(ncx_soup,"content") + contentTag['src'] = "content/Genre%s.html#section_start" % (re.sub("\W","", self.convertHTMLEntities(self.genres[0]['tag']))) + navPointTag.insert(nptc, contentTag) + nptc += 1 + + for genre in self.genres: + # Add an article for each genre + + navPointVolumeTag = Tag(ncx_soup, 'navPoint') + navPointVolumeTag['class'] = "article" + navPointVolumeTag['id'] = "genre-%s-ID" % genre['tag'] + navPointVolumeTag['playOrder'] = self.playOrder + self.playOrder += 1 + navLabelTag = Tag(ncx_soup, "navLabel") + textTag = Tag(ncx_soup, "text") + textTag.insert(0, self.formatNCXText(NavigableString(genre['tag']))) + navLabelTag.insert(0,textTag) + navPointVolumeTag.insert(0,navLabelTag) + + contentTag = Tag(ncx_soup, "content") + genre_name = re.sub("\W","", self.convertHTMLEntities(genre['tag'])) + contentTag['src'] = "content/Genre%s.html#Genre%s" % (genre_name, genre_name) + navPointVolumeTag.insert(1, contentTag) + + # Build the author tag + cmTag = Tag(ncx_soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "author" + # First - Last author + + if len(genre['titles_spanned']) > 1 : + author_range = "%s - %s" % (genre['titles_spanned'][0][0], genre['titles_spanned'][1][0]) + else : + author_range = "%s" % (genre['titles_spanned'][0][0]) + + cmTag.insert(0, NavigableString(author_range)) + navPointVolumeTag.insert(2, cmTag) + + # Build the description tag + cmTag = Tag(ncx_soup, '%s' % 'mbp:meta' if self.generateForMobigen else 'calibre:meta') + cmTag['name'] = "description" + + if False: + # Form 1: Titles spanned + if len(genre['titles_spanned']) > 1: + title_range = "%s -\n%s" % (genre['titles_spanned'][0][1], genre['titles_spanned'][1][1]) + else: + title_range = "%s" % (genre['titles_spanned'][0][1]) + cmTag.insert(0, NavigableString(self.formatNCXText(title_range))) + else: + # Form 2: title • title • title ... + titles = [] + for title in genre['books']: + titles.append(title['title']) + titles = sorted(titles, key=lambda x:(self.generateSortTitle(x),self.generateSortTitle(x))) + titles_list = self.generateShortDescription(" • ".join(titles)) + cmTag.insert(0, NavigableString(self.formatNCXText(titles_list))) + + navPointVolumeTag.insert(3, cmTag) + + # Add this volume to the section tag + navPointTag.insert(nptc, navPointVolumeTag) + nptc += 1 + + # Add this section to the body + body.insert(btc, navPointTag) + btc += 1 + + self.ncxSoup = ncx_soup + + def writeDatabaseSnapshot(self): + # Pickle the current databaseSnapshot + pickleFile = open(os.path.join(self.fetchLibraryPath(),self.__dbs_fname),'w') + pickle.dump(self.databaseSnapshot,pickleFile) + pickleFile.close() + + def writeNCX(self): + + if self.verbose: + print self.updateProgressFullStep("writeNCX()") + outfile = open("%s/%s.ncx" % (self.catalogPath, self.basename), 'w') + if self.generateForMobigen: + outfile.write(self.ncxSoup.renderContents()) + else: + outfile.write(self.ncxSoup.prettify()) + + # Helpers + def contents(self, element, title, key=None): + content = None + + if element is None: + return None + + # Some elements seem to have \n fields + for node in element: + if node == "\n": + continue + else: + content = node + # Special handling for '&' in 'cover' + if key == 'cover' and re.search('&',content): + hit = re.search('&',content) + content = re.sub('&','&',content) + + if content: + return unicode(content) + else: + return None + + def convertHTMLEntities(self, s): + matches = re.findall("&#\d+;", s) + if len(matches) > 0: + hits = set(matches) + for hit in hits: + name = hit[2:-1] + try: + entnum = int(name) + s = s.replace(hit, unichr(entnum)) + except ValueError: + pass + + matches = re.findall("&\w+;", s) + hits = set(matches) + amp = "&" + if amp in hits: + hits.remove(amp) + for hit in hits: + name = hit[1:-1] + if htmlentitydefs.name2codepoint.has_key(name): + s = s.replace(hit, unichr(htmlentitydefs.name2codepoint[name])) + s = s.replace(amp, "&") + return s + + def createDirectoryStructure(self): + catalogPath = self.catalogPath + self.cleanUp() + + if not os.path.isdir(catalogPath): + #if self.verbose: print " creating %s" % catalogPath + os.makedirs(catalogPath) + + # Create /content and /images + content_path = catalogPath + "/content" + if not os.path.isdir(content_path): + #if self.verbose: print " creating %s" % content_path + os.makedirs(content_path) + images_path = catalogPath + "/images" + if not os.path.isdir(images_path): + #if self.verbose: print " creating %s" % images_path + os.makedirs(images_path) + + def fetchDatabaseSnapshot(self,filename): + # Return the current database snapshot + fs = os.path.join(self.fetchLibraryPath(),filename) + if os.path.exists(fs): + return pickle.load(open(fs)) + else: + return {} + + def fetchLibraryPath(self): + # Return a path to the current library + from calibre.utils.config import prefs + return prefs['library_path'] + + def filterDbTags(self, tags): + # Remove the special marker tags from the list, return sorted list of filtered tags + + filtered_tags = [] + for tag in tags: + # Check the leading character + if not tag[0] in self.REMOVE_TAGS: + filtered_tags.append(tag) + + filtered_tags.sort() + + # Enable this code to force certain tags to the front of the genre list + if False: + for (i, tag) in enumerate(filtered_tags): + if tag == 'Fiction': + filtered_tags.insert(0, (filtered_tags.pop(i))) + elif tag == 'Nonfiction': + filtered_tags.insert(1, (filtered_tags.pop(i))) + else: + continue + + return filtered_tags + + def formatNCXText(self, description): + # Kindle TOC descriptions won't render certain characters + # Fix up + massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES)) + + # Replace '&' with '&' + massaged = re.sub("&","&", massaged) + + return massaged.strip() + + def generateAuthorAnchor(self, author): + # Strip white space to '' + return re.sub("\W","", author) + + def generateHTMLByGenre(self, genre, section_head, books, outfile): + # Write an HTML file of this genre's book list + # Return a list with [(first_author, first_book), (last_author, last_book)] + + soup = self.generateHTMLGenreHeader(genre) + body = soup.find('body') + + btc = 0 + + # Insert section tag if this is the section start - first article only + if section_head: + aTag = Tag(soup,'a') + aTag['name'] = 'section_start' + body.insert(btc, aTag) + btc += 1 + + # Insert the anchor with spaces stripped + aTag = Tag(soup, 'a') + aTag['name'] = "Genre%s" % re.sub("\W","", genre) + body.insert(btc,aTag) + btc += 1 + + # Insert section marker if this is the section start - first article only + if section_head and self.generateForMobigen: + # Insert a Mobigen section marker for Mobigen section + body.insert(btc,' ') + btc += 1 + + # Insert the genre title + titleTag = body.find(attrs={'class':'title'}) + titleTag.insert(0,NavigableString('%s' % escape(genre))) + + # Insert the books by author list + divTag = body.find(attrs={'class':'authors'}) + dtc = 0 + + current_author = '' + for book in books: + if book['author'] != current_author: + # Start a new author with link + current_author = book['author'] + pAuthorTag = Tag(soup, "p") + pAuthorTag['class'] = "author_index" + emTag = Tag(soup, "em") + aTag = Tag(soup, "a") + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) + aTag.insert(0, book['author']) + emTag.insert(0,aTag) + pAuthorTag.insert(0,emTag) + divTag.insert(dtc,pAuthorTag) + dtc += 1 + + # Add books + pBookTag = Tag(soup, "p") + ptc = 0 + + # Prefix book with read/unread symbol + if book['read']: + pBookTag.insert(ptc,NavigableString(self.READ_SYMBOL)) + pBookTag['class'] = "read_book" + else: + pBookTag['class'] = "unread_book" + pBookTag.insert(ptc,NavigableString(self.NOT_READ_SYMBOL)) + ptc += 1 + + # Add the book title + aTag = Tag(soup, "a") + aTag['href'] = "book_%d.html" % (int(float(book['id']))) + aTag.insert(0,escape(book['title'])) + pBookTag.insert(ptc, aTag) + ptc += 1 + + divTag.insert(dtc, pBookTag) + dtc += 1 + + # If Mobi, append to body + if self.generateForMobigen: + btc = len(body) + body.insert(btc, '') + + # Write the generated file to contentdir + outfile = open(outfile, 'w') + outfile.write(soup.prettify()) + outfile.close() + + if len(books) > 1: + titles_spanned = [(books[0]['author'],books[0]['title']), (books[-1]['author'],books[-1]['title'])] + else: + titles_spanned = [(books[0]['author'],books[0]['title'])] + + return titles_spanned + + def generateHTMLDescriptionHeader(self, title): + + if self.generateForMobigen: + header = ''' + + + + + + + + +

+

+

+

 

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  
  
Publisher
Published
Rating
Notes
  
+

+

+ + + + + + ''' + else: + header = ''' + + + + + + + + +

+

+

+

 

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  
  
Publisher
Published
Rating
Notes
  
+

+

+ + + + + ''' + + # Insert the supplied title + soup = BeautifulSoup(header, selfClosingTags=['mbp:pagebreak']) + titleTag = soup.find('title') + titleTag.insert(0,NavigableString(escape(title))) + return soup + + def generateHTMLEmptyHeader(self, title): + if self.generateForMobigen: + header = ''' + + + + + + + + + + + ''' + else: + header = ''' + + + + + + + + + + + ''' + # Insert the supplied title + soup = BeautifulSoup(header) + titleTag = soup.find('title') + titleTag.insert(0,NavigableString(title)) + return soup + + def generateHTMLGenreHeader(self, title): + if self.generateForMobigen: + header = ''' + + + + + + + + +

+

+
+ + + ''' + else: + header = ''' + + + + + + + + +

+

+
+ + + ''' + # Insert the supplied title + soup = BeautifulSoup(header) + titleTag = soup.find('title') + titleTag.insert(0,escape(NavigableString(title))) + return soup + + def generateShortDescription(self, description): + # Truncate the description to description_clip, on word boundaries if necessary + + if not description: + return None + + if not self.descriptionClip: + return description + + if len(description) < self.descriptionClip: + return description + + # Start adding words until we reach description_clip + short_description = "" + words = description.split(" ") + for word in words: + short_description += word + " " + if len(short_description) > self.descriptionClip: + short_description += "..." + return short_description + + return short_description + + def generateSortTitle(self, title): + # Convert the actual title to a string suitable for sorting. + # Convert numbers to strings, ignore leading stop words + # The 21-Day Consciousness Cleanse + + if False: print "generate_sort_title(%s)" % title + title_words = title.split(' ') + if title_words[0].lower() in ['the','a','an']: + stop_word = title_words.pop(0) + if False : print "removing stop word '%s'" % stop_word + + # Scan for numbers in each word clump + translated = [] + for (i,word) in enumerate(title_words): + hit = re.search('[0-9]+',word) + if hit : + translated.append(EPUB_MOBI.NumberToText(word).text) + else: + translated.append(word) + + return ' '.join(translated) + + def generateThumbnail(self, title, image_dir, thumb_file): + import calibre.utils.PythonMagickWand as pw + try: + img = pw.NewMagickWand() + if img < 0: + raise RuntimeError('generate_thumbnail(): Cannot create wand') + # Read the cover + if not pw.MagickReadImage(img, title['cover']): + print 'Failed to read cover image from: %s' % title['cover'] + raise IOError + thumb = pw.CloneMagickWand(img) + if thumb < 0: + print 'generate_thumbnail(): Cannot clone cover' + raise RuntimeError + # img, width, height + pw.MagickThumbnailImage(thumb, 75, 100) + pw.MagickWriteImage(thumb, os.path.join(image_dir, thumb_file)) + pw.DestroyMagickWand(thumb) + except IOError: + print "generate_thumbnail() IOError with %s" % title['title'] + except RuntimeError: + print "generate_thumbnail() RuntimeError with %s" % title['title'] + + def processSpecialTags(self, tags, this_title, opts): + tag_list = [] + for tag in tags: + tag = self.convertHTMLEntities(tag) + if tag.startswith(opts.note_tag): + # if opts.verbose: + # print "%s has a note: %s" % (this_title['title'], tag[1:]) + this_title['notes'] = tag[1:] + elif tag == opts.read_tag: + # if opts.verbose: + # print "%s marked as read" % this_title['title'] + this_title['read'] = True + elif re.search(opts.exclude_genre, tag): + # if opts.verbose: + # print "'%s' matches exclude_genre regex, skipping" % tag + continue + else: + tag_list.append(tag) + return tag_list + + class NotImplementedError: + def __init__(self, error): + self.error = error + + def logerror(self): + print '%s not implemented' % self.error + + def updateProgressFullStep(self, description): + + self.current_step += 1 + self.progressString = description + self.progressInt = ((self.current_step-1)*100)/self.total_steps + self.reporter(self.progressInt, self.progressString) + return "%d%% %s" % (self.progressInt, self.progressString) + + def updateProgressMicroStep(self, description, micro_step_pct): + step_range = 100/self.total_steps + self.progressString = description + self.progressInt = ((self.current_step-1)*100)/self.total_steps + (micro_step_pct*step_range)/100 + self.reporter(self.progressInt, self.progressString) + return "%d%% %s" % (self.progressInt, self.progressString) + + def run(self, path_to_output, opts, db): + from calibre.ebooks.conversion.cli import main as ebook_convert + from calibre.utils.logging import Log + + log = Log() + opts.fmt = self.fmt = path_to_output.rpartition('.')[2] + + # Add local options + opts.creator = "calibre" + opts.dbs_fname = "CatalogSnapshot.dat" + opts.descriptionClip = 250 + opts.basename = "Catalog" + opts.plugin_path = self.plugin_path + + if opts.verbose: + log("%s:run" % self.name) + log(" path_to_output: %s" % path_to_output) + log(" Output format: %s" % self.fmt) + + # Display opts + opts_dict = vars(opts) + keys = opts_dict.keys() + keys.sort() + log(" opts:") + for key in keys: + log(" %s: %s" % (key, opts_dict[key])) + + # Launch the Catalog builder + catalog = self.CatalogBuilder(db, opts, self) + catalog.createDirectoryStructure() + catalog.copyResources() + catalog.buildSources() + + cmd_line_args = ['ebook-convert', + os.path.join(catalog.catalogPath, + opts.basename + '.opf'), + path_to_output] + + if opts.fmt == 'mobi': + # options + if opts.output_profile.startswith("kindle"): + cmd_line_args.append("--output-profile=%s" % str(opts.output_profile)) + cmd_line_args.append("--no-inline-toc") + + + elif opts.fmt == 'epub': + pass + + # Run ebook-convert + ebook_convert(args=cmd_line_args) + + + return None diff --git a/src/calibre/manual/develop.rst b/src/calibre/manual/develop.rst index efab5a1cac..b9cebcab3d 100644 --- a/src/calibre/manual/develop.rst +++ b/src/calibre/manual/develop.rst @@ -194,7 +194,7 @@ You can insert the following two lines of code to start an interactive python se When running from the command line, this will start an interactive python interpreter with access to all locally defined variables (variables in the local scope). The interactive prompt even has TAB completion for object properties and you can use the various python facilities for introspection, such as -:function:`dir`, :function:`type`, :function:`repr`, etc. +:func:`dir`, :func:`type`, :func:`repr`, etc. Using print statements ^^^^^^^^^^^^^^^^^^^^^^^ @@ -204,4 +204,18 @@ terminal. For example, you can start the GUI from the terminal as:: calibre-debug -g +Executing arbitrary scripts in the calibre python environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :command:`calibre-debug` command provides a couple of handy switches to execute your own +code, with access to the calibre modules:: + + calibre-debug -c "some python code" + +is great for testing a little snippet of code on the command line. It works in the same way as the -c switch to the python interpreter:: + + calibre-debug -e myscript.py + +can be used to execute your own python script. It works in the same way as passing the script to the python interpreter, except +that the calibre environment is fully initialized, so you can use all the calibre code in your script. diff --git a/src/calibre/manual/images/bookmark.png b/src/calibre/manual/images/bookmark.png new file mode 100644 index 0000000000..c6671a2541 Binary files /dev/null and b/src/calibre/manual/images/bookmark.png differ diff --git a/src/calibre/manual/images/font_size.png b/src/calibre/manual/images/font_size.png new file mode 100644 index 0000000000..59ba041d83 Binary files /dev/null and b/src/calibre/manual/images/font_size.png differ diff --git a/src/calibre/manual/images/full_screen.png b/src/calibre/manual/images/full_screen.png new file mode 100644 index 0000000000..ed1e283194 Binary files /dev/null and b/src/calibre/manual/images/full_screen.png differ diff --git a/src/calibre/manual/images/nav_pos.png b/src/calibre/manual/images/nav_pos.png new file mode 100644 index 0000000000..2b99182176 Binary files /dev/null and b/src/calibre/manual/images/nav_pos.png differ diff --git a/src/calibre/manual/images/pref_button.png b/src/calibre/manual/images/pref_button.png new file mode 100644 index 0000000000..f43f2d7627 Binary files /dev/null and b/src/calibre/manual/images/pref_button.png differ diff --git a/src/calibre/manual/images/prev_next.png b/src/calibre/manual/images/prev_next.png new file mode 100644 index 0000000000..cc761704e7 Binary files /dev/null and b/src/calibre/manual/images/prev_next.png differ diff --git a/src/calibre/manual/images/ref_mode.png b/src/calibre/manual/images/ref_mode.png new file mode 100644 index 0000000000..67a75edf21 Binary files /dev/null and b/src/calibre/manual/images/ref_mode.png differ diff --git a/src/calibre/manual/images/ref_mode_button.png b/src/calibre/manual/images/ref_mode_button.png new file mode 100644 index 0000000000..efed1af26b Binary files /dev/null and b/src/calibre/manual/images/ref_mode_button.png differ diff --git a/src/calibre/manual/images/toc.png b/src/calibre/manual/images/toc.png new file mode 100644 index 0000000000..462b6b6c32 Binary files /dev/null and b/src/calibre/manual/images/toc.png differ diff --git a/src/calibre/manual/index.rst b/src/calibre/manual/index.rst index d736dbd020..827d848eb1 100644 --- a/src/calibre/manual/index.rst +++ b/src/calibre/manual/index.rst @@ -29,6 +29,7 @@ Sections gui news + viewer conversion metadata faq diff --git a/src/calibre/manual/viewer.rst b/src/calibre/manual/viewer.rst new file mode 100644 index 0000000000..70bf98412a --- /dev/null +++ b/src/calibre/manual/viewer.rst @@ -0,0 +1,105 @@ +.. include:: global.rst + +.. _gui: + +The E-book Viewer +============================= + +|app| includes a built-in E-book viewer that can view all the major e-book formats. +The viewer is highly customizable and has many advanced features. + +.. contents:: + :depth: 1 + :local: + +Starting the viewer +-------------------- + +You can view any of the books in your |app| library by selecting the book and pressing the View button. This +will open up the book in the e-book viewer. You can also launch the viewer by itself, from the Start menu in windows +or using the command :command:`ebook-viewer` in Linux and OS X (you have to install the command line tools on OS X +first by going to Preferences->Advanced). + +Navigating around an e-book +----------------------------- + +.. |pni| image:: images/prev_next.png + +.. |bookmi| image:: images/bookmark.png + +.. |toci| image:: images/toc.png + +.. |navposi| image:: images/nav_pos.png + +.. |refmi| image:: images/ref_mode_button.png + + +You can "turn pages" in a book by using the :guilabel:`Page Next` and :guilabel:`Page Previous` buttons |pni|, or by pressing +the Page Down/Page Up keys. Unlike most e-book viewers, |app| does not force you to view books in paged mode. You can +scroll by amounts less than a page by using the scroll bar or various customizable keyboard shortcuts. + +Bookmarks +^^^^^^^^^^^^ + +When you are in the middle of a book and close the viewer, it will remember where you stopped reading and return there +the next time you open the book. You can also set bookmarks in the book by using the Bookmark button |bookmi|. When viewing EPUB format +books, these bookmarks are actually saved in the EPUB file itself, so you can add bookmarks, then send the file to a friend and +when they open the file, they will be able to see your bookmarks. + +Table of Contents +^^^^^^^^^^^^^^^^^^^^ + +If the book you are reading defines a Table of Contents, you can access it by pressing the Table of Contents button |toci|. +This will bring up a list of sections in the book and you can click on any of them to jump to that portion of the book. + +Navigating by location +^^^^^^^^^^^^^^^^^^^^^^^^ + +E-books, unlike paper books have no concept of pages. Instead, +as you read through the book, you will notice that your position in the book is displayed in the upper left corner in a box +like this |navposi|. This is both your current position and the total length of the book. These numbers are independent of the screen size and font +size you are viewing the boko at, and they play a similar role to page numbers in paper books. +You can enter any number you like to go to the corresponding location in the book. + +|app| also has a very handy +reference mode. You can turn it on by clicking the Reference Mode button |refmi|. Once you do this, every time you move your +mouse over a paragraph, calibre will display a unique number made up of the section and paragraph numbers. + +.. image:: images/ref_mode.png + +You can use this number to unambiguously refer to parts of the books when discussing it with friends or referring to it +in other works. You can enter these numbers in the box marked Go to at the top of the window to go to a particular +reference location. + +If you click on links inside the e-book to take you to different parts of the book, like an endnote, you can use the back and forward buttons +in the top left corner to return to where you were. These button behave just like those in a web browser. + +Customizing the look and feel of your reading experience +------------------------------------------------------------ + +.. |fontsizei| image:: images/font_size.png + +.. |fsi| image:: images/full_screen.png + +.. |prefbi| image:: images/pref_button.png + +You can change font sizes on the fly by using the font size buttons |fontsizei|. You can also make the viewer full screen +by pressing the Full Screen button |fsi|. By clicking the Preferences button |prefbi|, you can change the default fonts used +by the viewer to ones you like as well as the default font size when the viewer starts up. + +More advanced customization can be achieved by the User Stylesheet setting. This is a stylesheet you can set that will be applied +to every book. Using it you can do things like have white text on a black background, change paragraph styles, text justification, etc. +For examples if custom stylesheets used by |app|'s users, see `the forums `_. + +Dictionary lookup +------------------- + +You can lookup the meaning of words in the current book by right clicking on a word. |app| uses the publicly available dictionary +server at ``dict.org`` to lookup words. The definition is displayed in a small box at the bottom of the screen. + +Copying text and images +------------------------- + +You can select text and images by dragging the content with your mouse and then right click to copy to the clipboard. +The copied material can be pasted into another application as plain text and images. +