diff --git a/src/libprs500/__init__.py b/src/libprs500/__init__.py index 0b63eb6d91..9f0b041059 100644 --- a/src/libprs500/__init__.py +++ b/src/libprs500/__init__.py @@ -18,7 +18,7 @@ __docformat__ = "epytext" __author__ = "Kovid Goyal " __appname__ = 'libprs500' -import sys, os, logging +import sys, os, logging, mechanize iswindows = 'win32' in sys.platform.lower() isosx = 'darwin' in sys.platform.lower() @@ -75,3 +75,12 @@ def extract(path, dir): if not extractor: raise Exception('Unknown archive type') extractor(path, dir) + +def browser(): + opener = mechanize.Browser() + opener.set_handle_refresh(True) + opener.set_handle_robots(False) + opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')] + return opener + + \ No newline at end of file diff --git a/src/libprs500/ebooks/lrf/__init__.py b/src/libprs500/ebooks/lrf/__init__.py index 36b0f7424b..372631dcb2 100644 --- a/src/libprs500/ebooks/lrf/__init__.py +++ b/src/libprs500/ebooks/lrf/__init__.py @@ -72,14 +72,10 @@ def option_parser(usage): parser = OptionParser(usage=usage, version=__appname__+' '+__version__, epilog='Created by '+__author__) metadata = parser.add_option_group('METADATA OPTIONS') - metadata.add_option('--header', action='store_true', default=False, dest='header', - help='Add a header to all the pages with title and author.') - metadata.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string', - help='Set the format of the header. %a is replaced by the author and %t by the title. Default is %default') metadata.add_option("-t", "--title", action="store", type="string", default=None,\ dest="title", help="Set the title. Default: filename.") metadata.add_option("-a", "--author", action="store", type="string", \ - dest="author", help="Set the author. Default: %default", default='Unknown') + dest="author", help="Set the author(s). Multiple authors should be set as a comma separated list. Default: %default", default='Unknown') metadata.add_option("--comment", action="store", type="string", \ dest="freetext", help="Set the comment.", default=' ') metadata.add_option("--category", action="store", type="string", \ @@ -90,14 +86,14 @@ def option_parser(usage): help='Sort key for the author') metadata.add_option('--publisher', action='store', default='Unknown', dest='publisher', help='Publisher') + metadata.add_option('--cover', action='store', dest='cover', default=None, \ + help='Path to file containing image to be used as cover') profiles=['prs500'] parser.add_option('-o', '--output', action='store', default=None, \ help='Output file name. Default is derived from input filename') parser.add_option('--ignore-tables', action='store_true', default=False, dest='ignore_tables', help='Render HTML tables as blocks of text instead of actual tables. This is neccessary if the HTML contains very large or complex tables.') laf = parser.add_option_group('LOOK AND FEEL') - laf.add_option('--cover', action='store', dest='cover', default=None, \ - help='Path to file containing image to be used as cover') laf.add_option('--font-delta', action='store', type='float', default=0., \ help="""Increase the font size by 2 * FONT_DELTA pts and """ '''the line spacing by FONT_DELTA pts. FONT_DELTA can be a fraction.''' @@ -109,12 +105,19 @@ def option_parser(usage): help='Set the space between words in pts. Default is %default') laf.add_option('--blank-after-para', action='store_true', default=False, dest='blank_after_para', help='Separate paragraphs by blank lines.') + laf.add_option('--header', action='store_true', default=False, dest='header', + help='Add a header to all the pages with title and author.') + laf.add_option('--headerformat', default="%t by %a", dest='headerformat', type='string', + help='Set the format of the header. %a is replaced by the author and %t by the title. Default is %default') + page = parser.add_option_group('PAGE OPTIONS') page.add_option('-p', '--profile', default=PRS500_PROFILE, dest='profile', type='choice', choices=profiles, action='callback', callback=profile_from_string, help='''Profile of the target device for which this LRF is ''' - '''being generated. Default: ''' + profiles[0] + \ - ''' Supported profiles: '''+', '.join(profiles)) + '''being generated. The profile determines things like the ''' + '''resolution and screen size of the target device. ''' + '''Default: ''' + profiles[0] + ''' Supported profiles: '''+\ + ', '.join(profiles)) page.add_option('--left-margin', default=20, dest='left_margin', type='int', help='''Left margin of page. Default is %default px.''') page.add_option('--right-margin', default=20, dest='right_margin', type='int', @@ -134,14 +137,14 @@ def option_parser(usage): '''matches will be ignored. Defaults to %default''') chapter = parser.add_option_group('CHAPTER OPTIONS') chapter.add_option('--disable-chapter-detection', action='store_false', - default=True, dest='chapter_detection', + default=False, dest='disable_chapter_detection', help='''Prevent html2lrf from automatically inserting page breaks''' ''' before what it thinks are chapters.''') chapter.add_option('--chapter-regex', dest='chapter_regex', default='chapter|book|appendix', help='''The regular expression used to detect chapter titles.''' ''' It is searched for in heading tags. Defaults to %default''') - chapter.add_option('--page-break-before', dest='page_break', default='h[12]', + chapter.add_option('--page-break-before-tag', dest='page_break', default='h[12]', help='''If html2lrf does not find any page breaks in the ''' '''html file and cannot detect chapter headings, it will ''' '''automatically insert page-breaks before the tags whose ''' @@ -151,8 +154,8 @@ def option_parser(usage): '''there are no really long pages as this degrades the page ''' '''turn performance of the LRF. Thus this option is ignored ''' '''if the current page has only a few elements.''') - chapter.add_option('--force-page-break-before', dest='force_page_break', - default='$', help='Like --page-break-before, but page breaks are forced.') + chapter.add_option('--force-page-break-before-tag', dest='force_page_break', + default='$', help='Force a page break before tags whoose names match this regular expression.') chapter.add_option('--force-page-break-before-attr', dest='force_page_break_attr', default='$,,$', help='Force a page break before an element having the specified attribute. The format for this option is tagname regexp,attribute name,attribute value regexp. For example to match all heading tags that have the attribute class="chapter" you would use "h\d,class,chapter". Default is %default''') prepro = parser.add_option_group('PREPROCESSING OPTIONS') diff --git a/src/libprs500/gui2/Makefile b/src/libprs500/gui2/Makefile index 96620113f5..8dc331bead 100644 --- a/src/libprs500/gui2/Makefile +++ b/src/libprs500/gui2/Makefile @@ -1,4 +1,6 @@ -UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialogs/jobs_ui.py dialogs/conversion_error_ui.py +UI = main_ui.py dialogs/metadata_single_ui.py dialogs/metadata_bulk_ui.py dialogs/jobs_ui.py \ + dialogs/conversion_error_ui.py dialogs/lrf_single_ui.py dialogs/choose_format_ui.py \ + dialogs/password_ui.py RC = images_rc.pyc %_ui.py : %.ui diff --git a/src/libprs500/gui2/__init__.py b/src/libprs500/gui2/__init__.py index c317c6cc2f..51807b4bc3 100644 --- a/src/libprs500/gui2/__init__.py +++ b/src/libprs500/gui2/__init__.py @@ -41,7 +41,7 @@ def error_dialog(parent, title, msg): return d def qstring_to_unicode(q): - return unicode(q.toUtf8(), 'utf8') + return unicode(q) def human_readable(size): """ Convert a size in bytes into a human readable form """ diff --git a/src/libprs500/gui2/device.py b/src/libprs500/gui2/device.py index 381342d2a4..443623b3c2 100644 --- a/src/libprs500/gui2/device.py +++ b/src/libprs500/gui2/device.py @@ -102,6 +102,7 @@ class DeviceManager(QObject): '''Upload books to device''' def upload_books(updater, files, names, on_card=False): '''Upload books to device: ''' + self.device.set_progress_reporter(updater) return self.device.upload_books(files, names, on_card, end_session=False) return upload_books diff --git a/src/libprs500/gui2/dialogs/__init__.py b/src/libprs500/gui2/dialogs/__init__.py index 0a634b3b4e..c0f46e2f0b 100644 --- a/src/libprs500/gui2/dialogs/__init__.py +++ b/src/libprs500/gui2/dialogs/__init__.py @@ -13,20 +13,3 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''Various dialogs used in the GUI''' - -from PyQt4.QtCore import QObject -from PyQt4.QtGui import QDialog - -class Dialog(QObject): - def __init__(self, window): - QObject.__init__(self, window) - self.dialog = QDialog(window) - self.accept = self.dialog.accept - self.reject = self.dialog.reject - self._close_event = self.dialog.closeEvent - self.dialog.closeEvent = self.close_event - self.window = window - self.isVisible = self.dialog.isVisible - - def close_event(self, e): - e.accept() diff --git a/src/libprs500/gui2/dialogs/choose_format.py b/src/libprs500/gui2/dialogs/choose_format.py new file mode 100644 index 0000000000..69bf15bb96 --- /dev/null +++ b/src/libprs500/gui2/dialogs/choose_format.py @@ -0,0 +1,38 @@ +## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from PyQt4.QtGui import QDialog, QListWidgetItem + +from libprs500.gui2 import file_icon_provider +from libprs500.gui2.dialogs.choose_format_ui import Ui_ChooseFormatDialog + +class ChooseFormatDialog(QDialog, Ui_ChooseFormatDialog): + + def __init__(self, window, msg, formats): + QDialog.__init__(self, window) + Ui_ChooseFormatDialog.__init__(self) + self.setupUi(self) + + self.msg.setText(msg) + for format in formats: + self.formats.addItem(QListWidgetItem(file_icon_provider().icon_from_ext(format.lower()), + format.upper())) + self._formats = formats + self.formats.setCurrentRow(0) + + def format(self): + self._formats[self.formats.currentRow()] + + \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/choose_format.ui b/src/libprs500/gui2/dialogs/choose_format.ui new file mode 100644 index 0000000000..2a4073800f --- /dev/null +++ b/src/libprs500/gui2/dialogs/choose_format.ui @@ -0,0 +1,85 @@ + + ChooseFormatDialog + + + + 0 + 0 + 507 + 377 + + + + Choose Format + + + :/images/mimetypes/unknown.svg + + + + + + TextLabel + + + + + + + + 64 + 64 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + ChooseFormatDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ChooseFormatDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libprs500/gui2/dialogs/conversion_error.py b/src/libprs500/gui2/dialogs/conversion_error.py index 325481bf86..579f832102 100644 --- a/src/libprs500/gui2/dialogs/conversion_error.py +++ b/src/libprs500/gui2/dialogs/conversion_error.py @@ -14,16 +14,21 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. '''''' -from libprs500.gui2.dialogs import Dialog +from PyQt4.QtGui import QDialog + from libprs500.gui2.dialogs.conversion_error_ui import Ui_ConversionErrorDialog -class ConversionErrorDialog(Dialog, Ui_ConversionErrorDialog): +class ConversionErrorDialog(QDialog, Ui_ConversionErrorDialog): - def __init__(self, window, title, html): + def __init__(self, window, title, html, show=False): + QDialog.__init__(self, window) Ui_ConversionErrorDialog.__init__(self) - Dialog.__init__(self, window) - self.setupUi(self.dialog) - html = '' + html + '' - self.dialog.setWindowTitle(title) - self.text.setHtml(html) - self.dialog.show() \ No newline at end of file + self.setupUi(self) + self.setWindowTitle(title) + self.set_message(html) + if show: + self.show() + + def set_message(self, html): + self.text.setHtml('%s 1: + d = ChooseFormatDialog(self, 'Choose the format to convert into LRF', formats) + d.exec_() + if d.result() == QDialog.Accepted: + self.selected_format = d.format() + else: + self.selected_format = formats[0] + if self.selected_format: + self.setWindowTitle('Convert %s to LRF'%(self.selected_format,)) + + + def read_saved_options(self): + cmdline = self.db.conversion_options(self.id, 'lrf') + print 1, cmdline + if cmdline: + for opt in self.options(): + try: + i = cmdline.index(opt.get_opt_string()) + except ValueError: + continue + guiname = self.option_to_name(opt) + try: + obj = getattr(self, guiname) + except AttributeError: + continue + if isinstance(obj, QCheckBox): + obj.setCheckState(Qt.Checked) + elif isinstance(obj, QAbstractSpinBox): + obj.setValue(cmdline[i+1]) + elif isinstance(obj, QLineEdit): + obj.setText(cmdline[i+1]) + profile = cmdline[cmdline.index('--profile')+1] + self.gui_profile.setCurrentIndex(self.gui_profile.findText(profile)) + for prepro in self.PREPROCESS_OPTIONS: + ops = prepro.get_opt_string() + if ops in cmdline: + self.preprocess.setCurrentIndex(self.preprocess.findText(ops[2:])) + break + + + def select_cover(self, checked): + files = choose_images(self, 'change cover dialog', + u'Choose cover for ' + qstring_to_unicode(self.title.text())) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self.window, 'Cannot read', + 'You do not have permission to read the file: ' + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self.window, 'Error reading file', + "

There was an error reading from file:
" + _file + "


"+str(e)) + d.exec_() + if cover: + pix = QPixmap() + pix.loadFromData(cover) + if pix.isNull(): + d = error_dialog(self.window, _file + " is not a valid picture") + d.exec_() + else: + self.cover_path.setText(_file) + self.cover.setPixmap(pix) + self.cover_changed = True + self.cpixmap = pix + + def initialize_metadata(self): + db, row = self.db, self.row + self.id = self.db.id(row) + self.gui_title.setText(db.title(row)) + au = self.db.authors(row) + self.gui_author.setText(au if au else '') + aus = self.db.author_sort(row) + self.gui_author_sort.setText(aus if aus else '') + pub = self.db.publisher(row) + self.gui_publisher.setText(pub if pub else '') + tags = self.db.tags(row) + self.tags.setText(tags if tags else '') + comments = self.db.comments(row) + self.gui_comment.setPlainText(comments if comments else '') + cover = self.db.cover(row) + if cover: + pm = QPixmap() + pm.loadFromData(cover) + if not pm.isNull(): + self.cover.setPixmap(pm) + + def initialize_options(self): + '''Initialize non metadata options from the defaults.''' + for name in self.option_map.keys(): + default = self.option_map[name].default + obj = getattr(self, name) + if isinstance(obj, QAbstractSpinBox): + obj.setValue(default) + elif isinstance(obj, QLineEdit) and default: + obj.setText(default) + elif isinstance(obj, QCheckBox): + state = Qt.Checked if default else Qt.Unchecked + obj.setCheckState(state) + self.gui_headerformat.setDisabled(True) + self.preprocess.addItem('No preprocessing') + for opt in self.PREPROCESS_OPTIONS: + self.preprocess.addItem(opt.get_opt_string()[2:]) + ph = 'Preprocess the file before converting to LRF. This is useful if you know that the file is from a specific source. Known sources:' + ph += '
  1. baen - Books from BAEN Publishers
  2. ' + ph += '
  3. pdftohtml - HTML files that are the output of the program pdftohtml
  4. ' + self.preprocess.setToolTip(ph) + self.preprocess.setWhatsThis(ph) + for profile in self.PARSER.get_option('--profile').choices: + if self.gui_profile.findText(profile) < 0: + self.gui_profile.addItem(profile) + + def setup_tooltips(self): + def show_item_help(obj, event): + self.set_help(obj.toolTip()) + + self.option_map = {} + for opt in self.options(): + try: + help = opt.help.replace('%default', str(opt.default)) + except (ValueError, TypeError): + help = opt.help + + guiname = self.option_to_name(opt) + if hasattr(self, guiname): + obj = getattr(self, guiname) + obj.setToolTip(help) + obj.setWhatsThis(help) + self.option_map[guiname] = opt + obj.__class__.enterEvent = show_item_help + obj.leaveEvent = self.reset_help + self.preprocess.__class__.enterEvent = show_item_help + self.preprocess.leaveEvent = self.reset_help + + + def show_category_help(self, item): + text = qstring_to_unicode(item.text()) + help = { + u'Metadata' : 'Specify metadata such as title and author for the book.

    Metadata will be updated in the database as well as the generated LRF file.', + u'Look & Feel' : 'Adjust the look of the generated LRF file by specifying things like font sizes and the spacing between words.', + u'Page Setup' : 'Specify the page settings like margins and the screen size of the target device.', + u'Chapter Detection' : 'Fine tune the detection of chapter and section headings.', + } + self.set_help(help[text]) + + def set_help(self, msg): + self.help_view.setHtml('%s'%(msg,)) + + def reset_help(self, *args): + self.set_help('No help available') + if args: + args[0].accept() + + def build_commandline(self): + cmd = [] + for name in self.option_map.keys(): + opt = self.option_map[name].get_opt_string() + obj = getattr(self, name) + if isinstance(obj, QAbstractSpinBox): + cmd.extend([opt, obj.value()]) + elif isinstance(obj, QLineEdit): + val = qstring_to_unicode(obj.text()) + if val: + cmd.extend([opt, val]) + elif isinstance(obj, QCheckBox): + if obj.checkState() == Qt.Checked: + cmd.append(opt) + + text = qstring_to_unicode(self.preprocess.currentText()) + if text != 'No preprocessing': + cmd.append('--'+text) + cmd.extend(['--profile', qstring_to_unicode(self.gui_profile.currentText())]) + return cmd + + def write_metadata(self): + title = qstring_to_unicode(self.gui_title.text()) + self.db.set_title(self.id, title) + au = qstring_to_unicode(self.gui_author.text()).split(',') + if au: self.db.set_authors(self.id, au) + aus = qstring_to_unicode(self.gui_author_sort.text()) + if aus: self.db.set_author_sort(self.id, aus) + print self.db.author_sort(self.row) + self.db.set_publisher(self.id, qstring_to_unicode(self.gui_publisher.text())) + self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(',')) + self.db.set_series(self.id, qstring_to_unicode(self.series.currentText())) + self.db.set_series_index(self.id, self.series_index.value()) + if self.cover_changed: + self.db.set_cover(self.id, pixmap_to_data(self.cover.pixmap())) + + + def accept(self): + cmdline = self.build_commandline() + # TODO: put cover into tempfile + self.write_metadata() + self.db.set_conversion_options(self.id, 'lrf', cmdline) + QDialog.accept(self) + \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/lrf_single.ui b/src/libprs500/gui2/dialogs/lrf_single.ui new file mode 100644 index 0000000000..88516438c9 --- /dev/null +++ b/src/libprs500/gui2/dialogs/lrf_single.ui @@ -0,0 +1,948 @@ + + LRFSingleDialog + + + + 0 + 0 + 850 + 671 + + + + Convert to LRF + + + :/images/convert.svg + + + + + + + 0 + 0 + + + + Category + + + + + + + 0 + 0 + + + + + 75 + true + + + + true + + + false + + + QAbstractItemView::SelectRows + + + + 64 + 64 + + + + Qt::ElideLeft + + + QListView::Snap + + + QListView::TopToBottom + + + false + + + 40 + + + QListView::IconMode + + + true + + + + Metadata + + + :/images/metadata.svg + + + + + Look & Feel + + + :/images/lookfeel.svg + + + + + Page Setup + + + :/images/page.svg + + + + + Chapter Detection + + + :/images/chapters.svg + + + + + + + + + + + Options + + + + + + 0 + + + + + + + &Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + gui_title + + + + + + + Change the title of this book + + + + + + + &Author(s): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + gui_author + + + + + + + + 1 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + So&rt: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + gui_author_sort + + + + + + + + 0 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + &Publisher: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + gui_publisher + + + + + + + Change the publisher of this book + + + + + + + Ta&gs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + tags + + + + + + + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. + + + + + + + &Series: + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + series + + + + + + + + 2 + 0 + + + + List of known series. You can add new series. + + + List of known series. You can add new series. + + + true + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + false + + + Series index. + + + Series index. + + + Book + + + 1 + + + 10000 + + + + + + + Comments + + + + + + + + + + + + Book Cover + + + + + + + + Qt::Horizontal + + + + 81 + 181 + + + + + + + + + 0 + 0 + + + + + 250 + 180 + + + + + + + :/images/book.svg + + + true + + + + + + + Qt::Horizontal + + + + 71 + 181 + + + + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Change &cover image: + + + cover_path + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + Browse for an image to use as the cover of this book. + + + ... + + + :/images/document_open.svg + + + + + + + + + + + + + + + + + + &Font delta: + + + gui_font_delta + + + + + + + QAbstractSpinBox::PlusMinus + + + pts + + + 1 + + + -5.000000000000000 + + + 5.000000000000000 + + + 0.100000000000000 + + + + + + + &Word spacing: + + + Qt::PlainText + + + gui_wordspace + + + + + + + QAbstractSpinBox::PlusMinus + + + pts + + + 1 + + + 0.000000000000000 + + + 10.000000000000000 + + + 0.100000000000000 + + + 2.500000000000000 + + + + + + + &Preprocess: + + + preprocess + + + + + + + + + + + 0 + 0 + + + + Header + + + + + + &Show header + + + + + + + &Header format: + + + gui_headerformat + + + + + + + + + + + + + Disable auto &rotation of images + + + + + + + Insert &blank lines between paragraphs + + + + + + + Ignore &tables + + + + + + + Qt::Vertical + + + + 20 + 41 + + + + + + + + + + + + &Profile: + + + gui_profile + + + + + + + -1 + + + 1 + + + + + + + &Left Margin: + + + gui_left_margin + + + + + + + px + + + 100 + + + 20 + + + + + + + &Right Margin: + + + gui_right_margin + + + + + + + px + + + 100 + + + 20 + + + + + + + &Top Margin: + + + gui_top_margin + + + + + + + px + + + 100 + + + 10 + + + + + + + &Bottom Margin: + + + gui_bottom_margin + + + + + + + px + + + 100 + + + 0 + + + + + + + + + + + Title based detection + + + + + + &Disable chapter detection + + + + + + + &Regular expression: + + + gui_chapter_regex + + + + + + + + + + + + + Tag based detection + + + + + + &Page break before tag: + + + gui_page_break_before_tag + + + + + + + + + + &Force page break before tag: + + + gui_force_page_break_before_tag + + + + + + + + + + Force page break before &attribute: + + + gui_force_page_break_before_attr + + + + + + + + + + + + + + + + + + + + + Help on item + + + + + + + 0 + 0 + + + + + 0 + 150 + + + + + 16777215 + 170 + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p></body></html> + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + false + + + + + + + + + + + buttonBox + accepted() + LRFSingleDialog + accept() + + + 516 + 655 + + + 157 + 274 + + + + + buttonBox + rejected() + LRFSingleDialog + reject() + + + 584 + 661 + + + 286 + 274 + + + + + categoryList + currentRowChanged(int) + stack + setCurrentIndex(int) + + + 245 + 199 + + + 368 + 185 + + + + + gui_disable_chapter_detection + toggled(bool) + gui_chapter_regex + setDisabled(bool) + + + 439 + 97 + + + 539 + 149 + + + + + gui_header + toggled(bool) + gui_headerformat + setEnabled(bool) + + + 398 + 177 + + + 476 + 211 + + + + + diff --git a/src/libprs500/gui2/dialogs/metadata_bulk.py b/src/libprs500/gui2/dialogs/metadata_bulk.py index 6a31696344..910fe1fb0d 100644 --- a/src/libprs500/gui2/dialogs/metadata_bulk.py +++ b/src/libprs500/gui2/dialogs/metadata_bulk.py @@ -16,15 +16,15 @@ '''Dialog to edit metadata in bulk''' from PyQt4.QtCore import SIGNAL, QObject +from PyQt4.QtGui import QDialog from libprs500.gui2 import qstring_to_unicode -from libprs500.gui2.dialogs import Dialog from libprs500.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog -class MetadataBulkDialog(Ui_MetadataBulkDialog, Dialog): +class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): def __init__(self, window, rows, db): + QDialog.__init__(self, window) Ui_MetadataBulkDialog.__init__(self) - Dialog.__init__(self, window) self.setupUi(self.dialog) self.db = db self.ids = [ db.id(r) for r in rows] diff --git a/src/libprs500/gui2/dialogs/metadata_single.py b/src/libprs500/gui2/dialogs/metadata_single.py index fa022ef2c4..526dd28963 100644 --- a/src/libprs500/gui2/dialogs/metadata_single.py +++ b/src/libprs500/gui2/dialogs/metadata_single.py @@ -18,13 +18,12 @@ add/remove formats ''' import os -from PyQt4.QtCore import SIGNAL -from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage +from PyQt4.QtCore import SIGNAL, QObject +from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog from libprs500.gui2 import qstring_to_unicode, error_dialog, file_icon_provider, \ choose_files, pixmap_to_data, BOOK_EXTENSIONS, choose_images -from libprs500.gui2.dialogs import Dialog from libprs500.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog class Format(QListWidgetItem): @@ -34,10 +33,10 @@ class Format(QListWidgetItem): QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), ext.upper(), parent, QListWidgetItem.UserType) -class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): +class MetadataSingleDialog(QDialog, Ui_MetadataSingleDialog): def select_cover(self, checked): - files = choose_images(self.window, 'change cover dialog', + files = choose_images(self, 'change cover dialog', u'Choose cover for ' + qstring_to_unicode(self.title.text())) if not files: return @@ -71,7 +70,7 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): def add_format(self, x): - files = choose_files(self.window, 'add formats dialog', + files = choose_files(self, 'add formats dialog', "Choose formats for " + str(self.title.text()), [('Books', BOOK_EXTENSIONS)]) if not files: @@ -120,9 +119,9 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): self.db.remove_format(self.row, ext) def __init__(self, window, row, db): - Ui_MetadataSingleDialog.__init__(self) - Dialog.__init__(self, window) - self.setupUi(self.dialog) + QDialog.__init__(self, window) + Ui_MetadataSingleDialog.__init__(self) + self.setupUi(self) self.splitter.setStretchFactor(100, 1) self.db = db self.id = db.id(row) @@ -138,8 +137,6 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): self.add_format) QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \ self.remove_format) - QObject.connect(self.button_box, SIGNAL("accepted()"), \ - self.sync) self.title.setText(db.title(row)) au = self.db.authors(row) @@ -190,18 +187,18 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index) - self.dialog.exec_() + self.exec_() def enable_series_index(self, *args): self.series_index.setEnabled(True) - def sync(self): + def accept(self): if self.formats_changed: self.sync_formats() title = qstring_to_unicode(self.title.text()) self.db.set_title(self.id, title) au = qstring_to_unicode(self.authors.text()).split(',') - self.db.set_authors(self.id, au) + if au: self.db.set_authors(self.id, au) self.db.set_rating(self.id, 2*self.rating.value()) self.db.set_publisher(self.id, qstring_to_unicode(self.publisher.text())) self.db.set_tags(self.id, qstring_to_unicode(self.tags.text()).split(',')) @@ -211,6 +208,5 @@ class MetadataSingleDialog(Ui_MetadataSingleDialog, Dialog): if self.cover_changed: self.db.set_cover(self.id, pixmap_to_data(self.cover.pixmap())) self.changed = True - - def reject(self): - self.rejected = True + QDialog.accept(self) + \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/password.py b/src/libprs500/gui2/dialogs/password.py new file mode 100644 index 0000000000..3619f91016 --- /dev/null +++ b/src/libprs500/gui2/dialogs/password.py @@ -0,0 +1,47 @@ +## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from PyQt4.QtGui import QDialog +from PyQt4.QtCore import QSettings, QVariant + +from libprs500.gui2.dialogs.password_ui import Ui_Dialog +from libprs500.gui2 import qstring_to_unicode + +class PasswordDialog(QDialog, Ui_Dialog): + + def __init__(self, window, name, msg): + QDialog.__init__(self, window) + Ui_Dialog.__init__(self) + self.setupUi(self) + + settings = QSettings() + un = settings.value(name+': un', QVariant('')).toString() + pw = settings.value(name+': pw', QVariant('')).toString() + self.gui_username.setText(un) + self.gui_username.setText(pw) + self.sname = name + self.msg.setText(msg) + + def username(self): + return qstring_to_unicode(self.gui_username.text()) + + def password(self): + return qstring_to_unicode(self.gui_password.text()) + + def accept(self): + settings = QSettings() + settings.setValue(self.sname+': un', QVariant(self.gui_username.text())) + settings.setValue(self.sname+': pw', QVariant(self.gui_password.text())) + QDialog.accept(self) \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/password.ui b/src/libprs500/gui2/dialogs/password.ui new file mode 100644 index 0000000000..e90dafc947 --- /dev/null +++ b/src/libprs500/gui2/dialogs/password.ui @@ -0,0 +1,108 @@ + + Dialog + + + + 0 + 0 + 350 + 209 + + + + Password needed + + + :/images/mimetypes/unknown.svg + + + + + + TextLabel + + + true + + + + + + + &Username: + + + gui_username + + + + + + + + + + &Password: + + + gui_password + + + + + + + QLineEdit::Password + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/libprs500/gui2/dialogs/widgets.py b/src/libprs500/gui2/dialogs/widgets.py index 5b98bc9940..eaddcc1354 100644 --- a/src/libprs500/gui2/dialogs/widgets.py +++ b/src/libprs500/gui2/dialogs/widgets.py @@ -1,4 +1,21 @@ -from libprs500.gui2 import TableView +## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from PyQt4.QtGui import QStackedWidget + +from libprs500.gui2 import TableView, qstring_to_unicode class JobsView(TableView): - pass \ No newline at end of file + pass diff --git a/src/libprs500/gui2/images.qrc b/src/libprs500/gui2/images.qrc index 0d7f268364..151d2d68e4 100644 --- a/src/libprs500/gui2/images.qrc +++ b/src/libprs500/gui2/images.qrc @@ -1,8 +1,10 @@ images/book.svg - images/news.svg + images/chapters.svg images/clear_left.svg + images/config.svg + images/convert.svg images/dialog_error.svg images/dialog_warning.svg images/document_open.svg @@ -11,6 +13,8 @@ images/jobs.svg images/library.png images/list_remove.svg + images/lookfeel.svg + images/metadata.svg images/mimetypes/bmp.svg images/mimetypes/dir.svg images/mimetypes/gif.svg @@ -28,14 +32,16 @@ images/mimetypes/unknown.svg images/mimetypes/zero.svg images/mimetypes/zip.svg + images/news.svg + images/news/bbc.png + images/news/newsweek.png + images/news/nytimes.png + images/page.svg images/plus.svg images/reader.svg images/save.svg images/sd.svg images/sync.svg images/trash.svg - images/news/bbc.png - images/news/newsweek.png - images/news/nytimes.png diff --git a/src/libprs500/gui2/library.py b/src/libprs500/gui2/library.py index 76af53aa0e..e344392711 100644 --- a/src/libprs500/gui2/library.py +++ b/src/libprs500/gui2/library.py @@ -362,7 +362,7 @@ class BooksView(TableView): def migrate_database(self): - if self._model.database_needs_migration(): + if self.model().database_needs_migration(): print 'Migrating database from pre 0.4.0 version' path = os.path.abspath(os.path.expanduser('~/library.db')) progress = QProgressDialog('Upgrading database from pre 0.4.0 version.
    '+\ @@ -554,7 +554,7 @@ class DeviceBooksModel(BooksModel): text = self.db[self.map[row]].title if not text: text = self.unknown - return QVariant(BooksView.wrap(text, width=35)) + return QVariant(text) elif col == 1: au = self.db[self.map[row]].authors if not au: @@ -565,7 +565,7 @@ class DeviceBooksModel(BooksModel): authors = [] for i in au: authors += i.strip().split('&') - jau = [ BooksView.wrap(a.strip(), width=30).strip() for a in authors ] + jau = [ a.strip() for a in authors ] return QVariant("\n".join(jau)) elif col == 2: size = self.db[self.map[row]].size diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index 566e15a705..7c86977049 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -12,12 +12,13 @@ ## You should have received a copy of the GNU General Public License along ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.Warning +from libprs500.gui2.dialogs.password import PasswordDialog import os, sys, traceback, StringIO, textwrap from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \ QSettings, QVariant, QSize, QThread from PyQt4.QtGui import QPixmap, QColor, QPainter, QMenu, QIcon, QMessageBox, \ - QToolButton + QToolButton, QDialog from PyQt4.QtSvg import QSvgRenderer from libprs500 import __version__, __appname__ @@ -37,6 +38,7 @@ from libprs500.gui2.dialogs.metadata_single import MetadataSingleDialog from libprs500.gui2.dialogs.metadata_bulk import MetadataBulkDialog from libprs500.gui2.dialogs.jobs import JobsDialog from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog +from libprs500.gui2.dialogs.lrf_single import LRFSingleDialog class Main(QObject, Ui_MainWindow): @@ -64,7 +66,7 @@ class Main(QObject, Ui_MainWindow): self.conversion_jobs = {} self.persistent_files = [] self.default_thumbnail = None - self.device_error_dialog = error_dialog(self.window, 'Error communicating with device', ' ') + self.device_error_dialog = ConversionErrorDialog(self.window, 'Error communicating with device', ' ') self.device_error_dialog.setModal(Qt.NonModal) self.tb_wrapper = textwrap.TextWrapper(width=40) self.device_connected = False @@ -113,9 +115,18 @@ class Main(QObject, Ui_MainWindow): QObject.connect(nm.actions()[2], SIGNAL('triggered(bool)'), self.fetch_news_nytimes) self.news_menu = nm self.action_news.setMenu(nm) + cm = QMenu() + cm.addAction('Convert individually') + cm.addAction('Bulk convert') + self.action_convert.setMenu(cm) + QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single) + QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), self.convert_bulk) + QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single) + self.convert_menu = cm self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup) self.tool_bar.widgetForAction(self.action_edit).setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.widgetForAction(self.action_sync).setPopupMode(QToolButton.MenuButtonPopup) + self.tool_bar.widgetForAction(self.action_convert).setPopupMode(QToolButton.MenuButtonPopup) self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu) ####################### Library view ######################## self.library_view.set_database(self.database_path) @@ -470,10 +481,14 @@ class Main(QObject, Ui_MainWindow): ############################### Fetch news ################################# - def fetch_news(self, profile, pretty): + def fetch_news(self, profile, pretty, username=None, password=None): pt = PersistentTemporaryFile(suffix='.lrf') pt.close() args = ['web2lrf', '-o', pt.name, profile] + if username: + args.extend(['--username', username]) + if password: + args.extend(['--password', password]) id = self.job_manager.run_conversion_job(self.news_fetched, web2lrf, args=args, job_description='Fetch news from '+pretty) self.conversion_jobs[id] = (pt, 'lrf') @@ -497,7 +512,29 @@ class Main(QObject, Ui_MainWindow): self.fetch_news('newsweek', 'Newsweek') def fetch_news_nytimes(self, checked): - self.fetch_news('nytimes', 'New York Times') + d = PasswordDialog(self.window, 'nytimes info dialog', + '

    Please enter your username and password for nytimes.com
    If you do not have, you can register for free.
    Without a registration, some articles will not be downloaded correctly. Click OK to proceed.') + d.exec_() + if d.result() == QDialog.Accepted: + un, pw = d.username(), d.password() + self.fetch_news('nytimes', 'New York Times', username=un, password=pw) + + ############################################################################ + + ############################### Convert #################################### + def convert_bulk(self, checked): + pass + + def convert_single(self, checked): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self.window, 'Cannot convert', 'No books selected') + d.exec_() + + for row in rows: + d = LRFSingleDialog(self.window, self.library_view.model().db, row) + if d.selected_format: + d.exec_() ############################################################################ def location_selected(self, location): @@ -519,16 +556,12 @@ class Main(QObject, Ui_MainWindow): if self.device_connected: self.action_sync.setEnabled(True) self.action_edit.setEnabled(True) + self.action_convert.setEnabled(True) else: self.action_sync.setEnabled(False) self.action_edit.setEnabled(False) + self.action_convert.setEnabled(False) - - def wrap_traceback(self, tb): - tb = unicode(tb, 'utf8', 'replace') - tb = '\n'.join(self.tb_wrapper.wrap(tb)) - return tb - def device_job_exception(self, id, description, exception, formatted_traceback): ''' Handle exceptions in threaded jobs. @@ -541,8 +574,8 @@ class Main(QObject, Ui_MainWindow): msg += u'

    Failed to perform job: '+description msg += u'

    Further device related error messages will not be shown while this message is visible.' msg += u'

    Detailed traceback:

    '
    -            msg += self.wrap_traceback(formatted_traceback)
    -            self.device_error_dialog.setText(msg)
    +            msg += formatted_traceback
    +            self.device_error_dialog.set_message(msg)
                 self.device_error_dialog.show()
                 
         def conversion_job_exception(self, id, description, exception, formatted_traceback, log):
    @@ -556,7 +589,7 @@ class Main(QObject, Ui_MainWindow):
             msg += formatted_traceback + '
    ' msg += '

    Log:

    '
             msg += log
    -        ConversionErrorDialog(self.window, 'Conversion Error', msg)
    +        ConversionErrorDialog(self.window, 'Conversion Error', msg, show=True)
             
         
         def read_settings(self):
    @@ -604,8 +637,8 @@ class Main(QObject, Ui_MainWindow):
                 self.window.close()
                 self.window.thread().exit(0)
             msg = '

    ' + unicode(str(value), 'utf8', 'replace') + '

    ' - msg += '

    Detailed traceback:

    '+self.wrap_traceback(fe)+'
    ' - d = error_dialog(self.window, 'ERROR: Unhandled exception', msg) + msg += '

    Detailed traceback:

    '+fe+'
    ' + d = ConversionErrorDialog(self.window, 'ERROR: Unhandled exception', msg) d.exec_() def main(): diff --git a/src/libprs500/gui2/main.ui b/src/libprs500/gui2/main.ui index 39780763fe..484ad9dec6 100644 --- a/src/libprs500/gui2/main.ui +++ b/src/libprs500/gui2/main.ui @@ -6,7 +6,7 @@ 0 0 - 777 + 787 822 @@ -44,7 +44,7 @@ - + 0 0 @@ -61,11 +61,20 @@ Qt::ScrollBarAlwaysOff - - QAbstractItemView::DragDrop + + + 32 + 32 + + + + QListView::Static - QListView::TopToBottom + QListView::LeftToRight + + + false 20 @@ -73,6 +82,9 @@ QListView::IconMode + + true + @@ -325,6 +337,7 @@ + @@ -400,6 +413,14 @@ Fetch news + + + :/images/convert.svg + + + Convert E-books + + diff --git a/src/libprs500/library/database.py b/src/libprs500/library/database.py index 6cbcd73358..e6a570a9ab 100644 --- a/src/libprs500/library/database.py +++ b/src/libprs500/library/database.py @@ -13,11 +13,11 @@ ## with this program; if not, write to the Free Software Foundation, Inc., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from libprs500 import __appname__ -""" +''' Backend that implements storage of ebooks in an sqlite database. -""" +''' import sqlite3 as sqlite -import datetime, re, os +import datetime, re, os, cPickle from zlib import compress, decompress class Concatenate(object): @@ -88,12 +88,12 @@ class LibraryDatabase(object): def import_old_database(path, conn, progress=None): count = 0 for book, cover, formats in LibraryDatabase.books_in_old_database(path): - obj = conn.execute('INSERT INTO books(title, timestamp) VALUES (?,?)', - (book['title'], book['timestamp'])) - id = obj.lastrowid authors = book['authors'] if not authors: authors = 'Unknown' + obj = conn.execute('INSERT INTO books(title, timestamp, author_sort) VALUES (?,?,?)', + (book['title'], book['timestamp'], authors)) + id = obj.lastrowid authors = authors.split('&') for a in authors: author = conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone() @@ -559,16 +559,83 @@ class LibraryDatabase(object): ) conn.execute('pragma user_version=1') conn.commit() + + @staticmethod + def upgrade_version1(conn): + conn.executescript( +''' +/***** authors_sort table *****/ + ALTER TABLE books ADD COLUMN author_sort TEXT COLLATE NOCASE; + UPDATE books SET author_sort=(SELECT name FROM authors WHERE id=(SELECT author FROM books_authors_link WHERE book=books.id)) WHERE id IN (SELECT id FROM books ORDER BY id); + DROP INDEX authors_idx; + DROP TRIGGER authors_insert_trg; + DROP TRIGGER authors_update_trg; + CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE); + + CREATE TABLE conversion_options ( id INTEGER PRIMARY KEY, + format TEXT NOT NULL COLLATE NOCASE, + book INTEGER, + data BLOB NOT NULL, + UNIQUE(format,book) + ); + CREATE INDEX conversion_options_idx_a ON conversion_options (format COLLATE NOCASE); + CREATE INDEX conversion_options_idx_b ON conversion_options (book); + + DROP TRIGGER books_delete_trg; + CREATE TRIGGER books_delete_trg + AFTER DELETE ON books + BEGIN + DELETE FROM books_authors_link WHERE book=OLD.id; + DELETE FROM books_publishers_link WHERE book=OLD.id; + DELETE FROM books_ratings_link WHERE book=OLD.id; + DELETE FROM books_series_link WHERE book=OLD.id; + DELETE FROM books_tags_link WHERE book=OLD.id; + DELETE FROM data WHERE book=OLD.id; + DELETE FROM covers WHERE book=OLD.id; + DELETE FROM comments WHERE book=OLD.id; + DELETE FROM conversion_options WHERE book=OLD.id; + END; + + DROP VIEW meta; + CREATE VIEW meta AS + SELECT id, title, + (SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link WHERE book=books.id)) authors, + (SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher, + (SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating, + timestamp, + (SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size, + (SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags, + (SELECT text FROM comments WHERE book=books.id) comments, + (SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series, + sort, + author_sort + FROM books; + + DROP INDEX publishers_idx; + CREATE INDEX publishers_idx ON publishers (name COLLATE NOCASE); + DROP TRIGGER publishers_insert_trg; + DROP TRIGGER publishers_update_trg; +''' + ) + conn.execute('pragma user_version=2') + conn.commit() def __init__(self, dbpath): self.dbpath = dbpath self.conn = _connect(dbpath) - self.user_version = self.conn.execute('pragma user_version;').next()[0] self.cache = [] self.data = [] - if self.user_version == 0: + if self.user_version == 0: # No tables have been created LibraryDatabase.create_version1(self.conn) + if self.user_version == 1: # Upgrade to 2 + LibraryDatabase.upgrade_version1(self.conn) + @apply + def user_version(): + doc = 'The user version of this database' + def fget(self): + return self.conn.execute('pragma user_version;').next()[0] + return property(doc=doc, fget=fget) def is_empty(self): return not self.conn.execute('SELECT id FROM books LIMIT 1').fetchone() @@ -578,7 +645,7 @@ class LibraryDatabase(object): Rebuild self.data and self.cache. Filter results are lost. ''' FIELDS = {'title' : 'sort', - 'authors': 'authors_sort', + 'authors': 'author_sort', 'publisher': 'publisher_sort', 'size': 'size', 'date': 'timestamp', @@ -631,6 +698,10 @@ class LibraryDatabase(object): ''' Authors as a comman separated list or None''' return self.data[index][2] + def author_sort(self, index): + id = self.id(index) + return self.conn.execute('SELECT author_sort FROM books WHERE id=?', (id,)).fetchone()[0] + def publisher(self, index): return self.data[index][3] @@ -699,6 +770,13 @@ class LibraryDatabase(object): return [ (i[0], i[1]) for i in \ self.conn.execute('SELECT id, name FROM series').fetchall()] + def conversion_options(self, id, format): + data = self.conn.execute('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() + if data: + return cPickle.loads(str(data[0])) + return None + + def add_format(self, index, ext, stream): ''' Add the format specified by ext. If it already exists it is replaced. @@ -749,6 +827,16 @@ class LibraryDatabase(object): elif column == 'rating': self.set_rating(id, val) + def set_conversion_options(self, id, format, options): + data = sqlite.Binary(cPickle.dumps(options)) + oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() + if oid: + self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0])) + else: + self.conn.execute('INSERT INTO conversion_options(book,format,data) VALUES (?,?,?)', (id,format.upper(),data)) + self.conn.commit() + + def set_authors(self, id, authors): ''' @param authors: A list of authors. @@ -766,6 +854,10 @@ class LibraryDatabase(object): self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', (id, aid)) self.conn.commit() + def set_author_sort(self, id, sort): + self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id)) + self.conn.commit() + def set_title(self, id, title): if not title: return @@ -925,5 +1017,4 @@ class LibraryDatabase(object): if __name__ == '__main__': db = LibraryDatabase('/home/kovid/library1.db') - db.refresh('title', True) - db.export_to_dir('/tmp/test', range(1, 10)) \ No newline at end of file + \ No newline at end of file