diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index f4256c4c14..6e21c60d1b 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -24,7 +24,7 @@ class File(object): path = path[:-1] self.path = path self.name = os.path.basename(path) - + class PRS505(Device): VENDOR_ID = 0x054c #: SONY Vendor Id @@ -33,17 +33,17 @@ class PRS505(Device): PRODUCT_NAME = 'PRS-505' VENDOR_NAME = 'SONY' FORMATS = ['epub', 'lrf', 'lrx', 'rtf', 'pdf', 'txt'] - + MEDIA_XML = 'database/cache/media.xml' CACHE_XML = 'Sony Reader/database/cache.xml' - + MAIN_MEMORY_VOLUME_LABEL = 'Sony Reader Main Memory' STORAGE_CARD_VOLUME_LABEL = 'Sony Reader Storage Card' - + OSX_NAME = 'Sony PRS-505' - + CARD_PATH_PREFIX = __appname__ - + FDI_TEMPLATE = \ ''' @@ -75,11 +75,11 @@ class PRS505(Device): '''.replace('%(app)s', __appname__) - - + + def __init__(self, log_packets=False): self._main_prefix = self._card_prefix = None - + @classmethod def get_fdi(cls): return cls.FDI_TEMPLATE%dict( @@ -90,7 +90,7 @@ class PRS505(Device): main_memory=cls.MAIN_MEMORY_VOLUME_LABEL, storage_card=cls.STORAGE_CARD_VOLUME_LABEL, ) - + @classmethod def is_device(cls, device_id): device_id = device_id.upper() @@ -104,7 +104,7 @@ class PRS505(Device): 'PID_'+pid in device_id: return True return False - + @classmethod def get_osx_mountpoints(cls, raw=None): if raw is None: @@ -112,7 +112,7 @@ class PRS505(Device): if not os.access(ioreg, os.X_OK): ioreg = 'ioreg' raw = subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), - stdout=subprocess.PIPE).stdout.read() + stdout=subprocess.PIPE).communicate()[0] lines = raw.splitlines() names = {} for i, line in enumerate(lines): @@ -130,9 +130,9 @@ class PRS505(Device): break return names - + def open_osx(self): - mount = subprocess.Popen('mount', shell=True, + mount = subprocess.Popen('mount', shell=True, stdout=subprocess.PIPE).stdout.read() names = self.get_osx_mountpoints() dev_pat = r'/dev/%s(\w*)\s+on\s+([^\(]+)\s+' @@ -144,12 +144,12 @@ class PRS505(Device): if card_pat is not None: card_pat = dev_pat%card_pat self._card_prefix = re.search(card_pat, mount).group(2) + os.sep - - + + def open_windows(self): time.sleep(6) drives = [] - wmi = __import__('wmi', globals(), locals(), [], -1) + wmi = __import__('wmi', globals(), locals(), [], -1) c = wmi.WMI() for drive in c.Win32_DiskDrive(): if self.__class__.is_device(str(drive.PNPDeviceID)): @@ -162,22 +162,22 @@ class PRS505(Device): drives.append((drive.Index, prefix)) except IndexError: continue - - + + if not drives: raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__) - + drives.sort(cmp=lambda a, b: cmp(a[0], b[0])) self._main_prefix = drives[0][1] if len(drives) > 1: self._card_prefix = drives[1][1] - - + + def open_linux(self): import dbus - bus = dbus.SystemBus() + bus = dbus.SystemBus() hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager") - + def conditional_mount(dev, main_mem=True): mmo = bus.get_object("org.freedesktop.Hal", dev) label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device') @@ -186,11 +186,11 @@ class PRS505(Device): fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device') if is_mounted: return str(mount_point) - mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], + mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], dbus_interface='org.freedesktop.Hal.Device.Volume') return os.path.normpath('/media/'+label)+'/' - - + + mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__) if not mm: raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,)) @@ -201,21 +201,21 @@ class PRS505(Device): break except dbus.exceptions.DBusException: continue - - + + if not self._main_prefix: raise DeviceError('Could not open device for reading. Try a reboot.') - + self._card_prefix = None cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__) keys = [] for card in cards: keys.append(int('UC_SD' in bus.get_object("org.freedesktop.Hal", card).GetPropertyString('info.parent', dbus_interface='org.freedesktop.Hal.Device'))) - + cards = zip(cards, keys) cards.sort(cmp=lambda x, y: cmp(x[1], y[1])) cards = [i[0] for i in cards] - + for dev in cards: try: self._card_prefix = conditional_mount(dev, False)+os.sep @@ -224,8 +224,8 @@ class PRS505(Device): import traceback print traceback continue - - + + def open(self): time.sleep(5) self._main_prefix = self._card_prefix = None @@ -262,16 +262,16 @@ class PRS505(Device): self._card_prefix = None import traceback traceback.print_exc() - + def set_progress_reporter(self, pr): self.report_progress = pr - + def get_device_information(self, end_session=True): return (self.__class__.__name__, '', '', '') - + def card_prefix(self, end_session=True): return self._card_prefix - + @classmethod def _windows_space(cls, prefix): if prefix is None: @@ -288,7 +288,7 @@ class PRS505(Device): else: raise mult = sectors_per_cluster * bytes_per_sector return total_clusters * mult, free_clusters * mult - + def total_space(self, end_session=True): msz = csz = 0 if not iswindows: @@ -301,9 +301,9 @@ class PRS505(Device): else: msz = self._windows_space(self._main_prefix)[0] csz = self._windows_space(self._card_prefix)[0] - + return (msz, 0, csz) - + def free_space(self, end_session=True): msz = csz = 0 if not iswindows: @@ -316,9 +316,9 @@ class PRS505(Device): else: msz = self._windows_space(self._main_prefix)[1] csz = self._windows_space(self._card_prefix)[1] - + return (msz, 0, csz) - + def books(self, oncard=False, end_session=True): if oncard and self._card_prefix is None: return [] @@ -331,7 +331,7 @@ class PRS505(Device): if os.path.exists(path): os.unlink(path) return bl - + def munge_path(self, path): if path.startswith('/') and not (path.startswith(self._main_prefix) or \ (self._card_prefix and path.startswith(self._card_prefix))): @@ -339,12 +339,12 @@ class PRS505(Device): elif path.startswith('card:'): path = path.replace('card:', self._card_prefix[:-1]) return path - + def mkdir(self, path, end_session=True): """ Make directory """ path = self.munge_path(path) os.mkdir(path) - + def list(self, path, recurse=False, end_session=True, munge=True): if munge: path = self.munge_path(path) @@ -356,12 +356,12 @@ class PRS505(Device): if recurse and _file.is_dir: dirs[len(dirs):] = self.list(_file.path, recurse=True, munge=False) return dirs - + def get_file(self, path, outfile, end_session=True): path = self.munge_path(path) src = open(path, 'rb') shutil.copyfileobj(src, outfile, 10*1024*1024) - + def put_file(self, infile, path, replace_file=False, end_session=True): path = self.munge_path(path) if os.path.isdir(path): @@ -372,25 +372,25 @@ class PRS505(Device): shutil.copyfileobj(infile, dest, 10*1024*1024) dest.flush() dest.close() - + def rm(self, path, end_session=True): path = self.munge_path(path) os.unlink(path) - + def touch(self, path, end_session=True): path = self.munge_path(path) if not os.path.exists(path): open(path, 'w').close() if not os.path.isdir(path): os.utime(path, None) - - def upload_books(self, files, names, on_card=False, end_session=True, + + def upload_books(self, files, names, on_card=False, end_session=True, metadata=None): if on_card and not self._card_prefix: raise ValueError(_('The reader has no storage card connected.')) path = os.path.join(self._card_prefix, self.CARD_PATH_PREFIX) if on_card \ else os.path.join(self._main_prefix, 'database', 'media', 'books') - + def get_size(obj): if hasattr(obj, 'seek'): obj.seek(0, 2) @@ -398,27 +398,27 @@ class PRS505(Device): obj.seek(0) return size return os.path.getsize(obj) - + sizes = map(get_size, files) size = sum(sizes) space = self.free_space() mspace = space[0] cspace = space[2] - if on_card and size > cspace - 1024*1024: + if on_card and size > cspace - 1024*1024: raise FreeSpaceError("There is insufficient free space "+\ "on the storage card") - if not on_card and size > mspace - 2*1024*1024: + if not on_card and size > mspace - 2*1024*1024: raise FreeSpaceError("There is insufficient free space " +\ "in main memory") - + paths, ctimes = [], [] - + names = iter(names) for infile in files: close = False if not hasattr(infile, 'read'): infile, close = open(infile, 'rb'), True - infile.seek(0) + infile.seek(0) name = names.next() paths.append(os.path.join(path, name)) if not os.path.exists(os.path.dirname(paths[-1])): @@ -428,7 +428,7 @@ class PRS505(Device): infile.close() ctimes.append(os.path.getctime(paths[-1])) return zip(paths, sizes, ctimes, cycle([on_card])) - + @classmethod def add_books_to_metadata(cls, locations, metadata, booklists): metadata = iter(metadata) @@ -441,12 +441,12 @@ class PRS505(Device): name = name.replace('//', '/') booklists[on_card].add_book(info, name, *location[1:-1]) fix_ids(*booklists) - + def delete_books(self, paths, end_session=True): for path in paths: if os.path.exists(path): os.unlink(path) - + @classmethod def remove_books_from_metadata(cls, paths, booklists): for path in paths: @@ -454,7 +454,7 @@ class PRS505(Device): if hasattr(bl, 'remove_book'): bl.remove_book(path) fix_ids(*booklists) - + def sync_booklists(self, booklists, end_session=True): fix_ids(*booklists) if not os.path.exists(self._main_prefix): @@ -468,9 +468,9 @@ class PRS505(Device): f = open(self._card_prefix + self.__class__.CACHE_XML, 'wb') booklists[1].write(f) f.close() - - - + + + def main(args=sys.argv): return 0 diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 5943e2e13f..eb86cb7edd 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -190,7 +190,7 @@ class Device(_Device): self._main_prefix = drives.get('main') self._card_prefix = drives.get('card') - + if not self._main_prefix: raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.') % self.__class__.__name__) @@ -200,7 +200,7 @@ class Device(_Device): if not os.access(ioreg, os.X_OK): ioreg = 'ioreg' raw = subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), - stdout=subprocess.PIPE).stdout.read() + stdout=subprocess.PIPE).communicate()[0] lines = raw.splitlines() names = {} diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index a723959214..24e5dd83fc 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -9,7 +9,7 @@ lxml based OPF parser. import sys, unittest, functools, os, mimetypes, uuid, glob, cStringIO from urllib import unquote -from urlparse import urlparse, urldefrag +from urlparse import urlparse from lxml import etree from dateutil import parser @@ -258,6 +258,11 @@ class Manifest(ResourceCollection): if i.id == id: return i.path + def type_for_id(self, id): + for i in self: + if i.id == id: + return i.mime_type + class Spine(ResourceCollection): class Item(Resource): @@ -487,7 +492,10 @@ class OPF(object): if toc is None: return self.toc = TOC(base_path=self.base_dir) - if toc.lower() in ('ncx', 'ncxtoc'): + is_ncx = getattr(self, 'manifest', None) is not None and \ + self.manifest.type_for_id(toc) is not None and \ + 'dtbncx' in self.manifest.type_for_id(toc) + if is_ncx or toc.lower() in ('ncx', 'ncxtoc'): path = self.manifest.path_for_id(toc) if path: self.toc.read_ncx_toc(path) diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 850fe20429..18f53317e0 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -79,7 +79,7 @@ class FormatState(object): class MobiMLizer(object): def __init__(self, ignore_tables=False): self.ignore_tables = ignore_tables - + @classmethod def config(cls, cfg): group = cfg.add_group('mobiml', _('Mobipocket markup options.')) @@ -92,7 +92,7 @@ class MobiMLizer(object): @classmethod def generate(cls, opts): return cls(ignore_tables=opts.ignore_tables) - + def __call__(self, oeb, context): oeb.logger.info('Converting XHTML to Mobipocket markup...') self.oeb = oeb @@ -111,10 +111,10 @@ class MobiMLizer(object): del oeb.guide['cover'] item = oeb.manifest.hrefs[href] if item.spine_position is not None: - oeb.spine.remove(item) + oeb.spine.remove(item) if item.media_type in OEB_DOCS: self.oeb.manifest.remove(item) - + def mobimlize_spine(self): for item in self.oeb.spine: stylizer = Stylizer(item.data, item.href, self.oeb, self.profile) @@ -147,7 +147,7 @@ class MobiMLizer(object): if line: result.append(line) return result - + def mobimlize_content(self, tag, text, bstate, istates): if text or tag != 'br': bstate.content = True @@ -252,7 +252,7 @@ class MobiMLizer(object): last.tail = (last.tail or '') + item else: inline.append(item) - + def mobimlize_elem(self, elem, stylizer, bstate, istates): if not isinstance(elem.tag, basestring) \ or namespace(elem.tag) != XHTML_NS: diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 5798276165..32e0126b12 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Read data from .mobi files ''' -import struct, os, cStringIO, re, functools +import struct, os, cStringIO, re, functools, datetime try: from PIL import Image as PILImage @@ -53,6 +53,12 @@ class EXTHHeader(object): self.cover_offset = co elif id == 202: self.thumbnail_offset, = struct.unpack('>L', content) + elif id == 501: + # cdetype + pass + elif id == 502: + # last update time + pass elif id == 503 and (not title or title == _('Unknown')): title = content #else: @@ -75,8 +81,14 @@ class EXTHHeader(object): if not self.mi.tags: self.mi.tags = [] self.mi.tags.append(content.decode(codec, 'ignore')) + elif id == 106: + try: + self.mi.publish_date = datetime.datetime.strptime( + content, '%Y-%m-%d',).date() + except: + pass #else: - # print 'unhandled metadata record', id, repr(content), codec + # print 'unhandled metadata record', id, repr(content) class BookHeader(object): @@ -327,8 +339,8 @@ class MobiReader(object): mobi_version = self.book_header.mobi_version for i, tag in enumerate(root.iter(etree.Element)): if tag.tag in ('country-region', 'place', 'placetype', 'placename', - 'state', 'city', 'street', 'address'): - tag.tag = 'span' + 'state', 'city', 'street', 'address', 'content'): + tag.tag = 'div' if tag.tag == 'content' else 'span' for key in tag.attrib.keys(): tag.attrib.pop(key) continue diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index fdcae00e8b..39aea3fa30 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -211,12 +211,14 @@ class Serializer(object): def serialize_item(self, item): buffer = self.buffer + #buffer.write('') if not item.linear: self.breaks.append(buffer.tell() - 1) self.id_offsets[item.href] = buffer.tell() for elem in item.data.find(XHTML('body')): self.serialize_elem(elem, item) - buffer.write('') + #buffer.write('') + buffer.write('') def serialize_elem(self, elem, item, nsrmap=NSRMAP): buffer = self.buffer diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 209212a6be..91bc988a22 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -1,6 +1,6 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import os, re, time, textwrap +import os, re, time, textwrap, sys, cStringIO from binascii import hexlify, unhexlify from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \ @@ -11,6 +11,7 @@ from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \ from calibre.constants import islinux, iswindows from calibre.gui2.dialogs.config_ui import Ui_Dialog +from calibre.gui2.dialogs.test_email_ui import Ui_Dialog as TE_Dialog from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \ ALL_COLUMNS, NONE, info_dialog, choose_files from calibre.utils.config import prefs @@ -134,6 +135,33 @@ class CategoryModel(QStringListModel): return self.icons[index.row()] return QStringListModel.data(self, index, role) +class TestEmail(QDialog, TE_Dialog): + + def __init__(self, accounts, parent): + QDialog.__init__(self, parent) + TE_Dialog.__init__(self) + self.setupUi(self) + opts = smtp_prefs().parse() + self.test_func = parent.test_email_settings + self.connect(self.test_button, SIGNAL('clicked(bool)'), self.test) + self.from_.setText(unicode(self.from_.text())%opts.from_) + if accounts: + self.to.setText(list(accounts.keys())[0]) + if opts.relay_host: + self.label.setText(_('Using: %s:%s@%s:%s and %s encryption')% + (opts.relay_username, unhexlify(opts.relay_password), + opts.relay_host, opts.relay_port, opts.encryption)) + + def test(self): + self.log.setPlainText(_('Sending...')) + self.test_button.setEnabled(False) + try: + tb = self.test_func(unicode(self.to.text())) + if not tb: + tb = _('Mail successfully sent') + self.log.setPlainText(tb) + finally: + self.test_button.setEnabled(True) class EmailAccounts(QAbstractTableModel): @@ -395,6 +423,8 @@ class ConfigDialog(QDialog, Ui_Dialog): self.connect(self.email_make_default, SIGNAL('clicked(bool)'), lambda c: self._email_accounts.make_default(self.email_view.currentIndex())) self.email_view.resizeColumnsToContents() + self.connect(self.test_email_button, SIGNAL('clicked(bool)'), + self.test_email) def add_email_account(self, checked): index = self._email_accounts.add() @@ -438,6 +468,33 @@ class ConfigDialog(QDialog, Ui_Dialog): conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL') return True + def test_email(self, *args): + if self.set_email_settings(): + TestEmail(self._email_accounts.accounts, self).exec_() + + def test_email_settings(self, to): + opts = smtp_prefs().parse() + from calibre.utils.smtp import sendmail, create_mail + buf = cStringIO.StringIO() + oout, oerr = sys.stdout, sys.stderr + sys.stdout = sys.stderr = buf + tb = None + try: + msg = create_mail(opts.from_, to, 'Test mail from calibre', + 'Test mail from calibre') + sendmail(msg, from_=opts.from_, to=[to], + verbose=3, timeout=30, relay=opts.relay_host, + username=opts.relay_username, + password=unhexlify(opts.relay_password), + encryption=opts.encryption, port=opts.relay_port) + except: + import traceback + tb = traceback.format_exc() + tb += '\n\nLog:\n' + buf.getvalue() + finally: + sys.stdout, sys.stderr = oout, oerr + return tb + def add_plugin(self): path = unicode(self.plugin_path.text()) if path and os.access(path, os.R_OK) and path.lower().endswith('.zip'): diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 3f02ffc7f4..a75a6f0a8d 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -1,117 +1,118 @@ - + + Kovid Goyal Dialog - - + + 0 0 - 789 + 800 557 - + Configuration - - + + :/images/config.svg:/images/config.svg - - - + + + - - - + + + 1 0 - + 75 true - + true - + false - + 48 48 - + QAbstractItemView::ScrollPerItem - + QAbstractItemView::ScrollPerPixel - + QListView::TopToBottom - + 20 - + QListView::ListMode - - - + + + 100 0 - + 0 - - + + - + - - + + 16777215 70 - + &Location of ebooks (The ebooks are stored in folders sorted by author and metadata is stored in the file metadata.db) - + true - + location - + - + - - + + Browse for the new database location - + ... - - + + :/images/mimetypes/dir.svg:/images/mimetypes/dir.svg @@ -121,107 +122,107 @@ - - + + Show notification when &new version is available - - + + If you disable this setting, metadata is guessed from the filename instead. This can be configured in the Advanced section. - + Read &metadata from files - + true - - - - + + + + Format for &single file save: - + single_format - - + + - - - + + + Default network &timeout: - + timeout - - - + + + Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information) - + seconds - + 2 - + 120 - + 5 - - + + - - - + + + Choose &language (requires restart): - + language - - + + - + Normal - + High - + Low - - - + + + Job &priority: - + priority @@ -229,19 +230,19 @@ - - + + Frequently used directories - - - + + + - - + + true - + 22 22 @@ -250,13 +251,13 @@ - + - + Qt::Vertical - + 20 40 @@ -265,25 +266,25 @@ - - + + Add a directory to the frequently used directories list - + ... - - + + :/images/plus.svg:/images/plus.svg - + Qt::Vertical - + 20 40 @@ -292,25 +293,25 @@ - - + + Remove a directory from the frequently used directories list - + ... - - + + :/images/list_remove.svg:/images/list_remove.svg - + Qt::Vertical - + 20 40 @@ -327,111 +328,111 @@ - - + + - - + + Use &Roman numerals for series number - + true - - + + Enable system &tray icon (needs restart) - - + + Show &notifications in system tray - - + + Show cover &browser in a separate window (needs restart) - - + + Automatically send downloaded &news to ebook reader - - + + &Delete news from library when it is sent to reader - + - - + + &Number of covers to show in browse mode (needs restart): - + cover_browse - + - - + + Toolbar - - - + + + - + Large - + Medium - + Small - - - + + + &Button size in toolbar - + toolbar_button_size - - - + + + Show &text in toolbar buttons - + true @@ -440,44 +441,44 @@ - + - - + + Select visible &columns in library view - + - + - - + + true - + QAbstractItemView::SelectRows - + - - + + ... - - + + :/images/arrow-up.svg:/images/arrow-up.svg - - + + Qt::Vertical - + 20 40 @@ -486,12 +487,12 @@ - - + + ... - - + + :/images/arrow-down.svg:/images/arrow-down.svg @@ -504,17 +505,17 @@ - - + + Use internal &viewer for: - - - - + + + + true - + QAbstractItemView::NoSelection @@ -535,99 +536,99 @@ - - - - - + + + + + calibre can send your books to you (or your reader) by email - + true - - + + - - + + Send email &from: - + email_from - - - <p>This is what will be present in the From: field of emails sent by calibre.<br> Set it to your email address + + + <p>This is what will be present in the From: field of emails sent by calibre.<br> Set it to your email address - - + + - - + + QAbstractItemView::SingleSelection - + QAbstractItemView::SelectRows - + - - + + Add an email address to which to send books - + &Add email - - + + :/images/plus.svg:/images/plus.svg - + 24 24 - + Qt::ToolButtonTextUnderIcon - - + + Make &default - - + + &Remove email - - + + :/images/minus.svg:/images/minus.svg - + 24 24 - + Qt::ToolButtonTextUnderIcon @@ -636,188 +637,208 @@ - - - - - - <p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services. - - - Mail &Server - - - - - - calibre can <b>optionally</b> use a server to send mail + + + + <p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services. + + + Mail &Server + + + + + + calibre can <b>optionally</b> use a server to send mail + + + true + + + + + + + &Hostname: + + + relay_host + + + + + + + The hostname of your mail server. For e.g. smtp.gmail.com + + + + + + + + + &Port: - - true + + relay_port - - - - &Hostname: + + + + The port your mail server listens for connections on. The default is 25 - - relay_host + + 1 - - - - - - The hostname of your mail server. For e.g. smtp.gmail.com + + 65555 - - - - - - - - &Port: - - - relay_port - - - - - - - The port your mail server listens for connections on. The default is 25 - - - 1 - - - 65555 - - - 25 - - - - - - - - - &Username: - - - relay_username - - - - - - - Your username on the mail server - - - - - - - &Password: - - - relay_password - - - - - - - Your password on the mail server - - - QLineEdit::Password - - - - - - - &Show - - - - - - - &Encryption: - - - relay_tls - - - - - - - Use TLS encryption when connecting to the mail server. This is the most common. - - - &TLS - - - true - - - - - - - Use SSL encryption when connecting to the mail server. - - - &SSL + + 25 - - + + + + + &Username: + + + relay_username + + + + + + + Your username on the mail server + + + + + + + &Password: + + + relay_password + + + + + + + Your password on the mail server + + + QLineEdit::Password + + + + + + + &Show + + + + + + + &Encryption: + + + relay_tls + + + + + + + Use TLS encryption when connecting to the mail server. This is the most common. + + + &TLS + + + true + + + + + + + Use SSL encryption when connecting to the mail server. + + + &SSL + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + - - + + Use Gmail - - + + :/images/gmail_logo.png:/images/gmail_logo.png - + 48 48 - + Qt::ToolButtonTextUnderIcon + + + + &Test email + + + - - + + - + - + Qt::Horizontal - + 40 20 @@ -826,21 +847,21 @@ - - + + Free unused diskspace from the database - + &Compact database - + Qt::Horizontal - + 40 20 @@ -851,17 +872,17 @@ - - + + &Metadata from file name - + - + Qt::Vertical - + 20 40 @@ -874,96 +895,96 @@ - - + + - - + + calibre contains a network server that allows you to access your book collection using a browser from anywhere in the world. Any changes to the settings will only take effect after a server restart. - + true - - - - + + + + Server &port: - + port - - - + + + 1025 - + 16000 - + 8080 - - - + + + &Username: - + username - - + + - - - + + + &Password: - + password - - - + + + If you leave the password blank, anyone will be able to access your book collection using the web interface. - - - + + + &Show password - - - + + + The maximum size (widthxheight) for displayed covers. Larger covers are resized. - + - - - + + + Max. &cover size: - + max_cover_size @@ -971,27 +992,27 @@ - + - - + + &Start Server - - + + St&op Server - - + + Qt::Horizontal - + 40 20 @@ -1000,8 +1021,8 @@ - - + + &Test Server @@ -1009,25 +1030,25 @@ - - + + Run server &automatically on startup - - + + View &server logs - - + + Qt::Vertical - + 20 40 @@ -1036,21 +1057,21 @@ - - + + If you want to use the content server to access your ebook collection on your iphone with Stanza, you will need to add the URL http://myhostname:8080/stanza as a new catalog in the stanza reader on your iphone. Here myhostname should be the fully qualified hostname or the IP address of this computer. - + true - - + + Qt::Vertical - + 20 40 @@ -1060,53 +1081,53 @@ - - + + - - + + Here you can customize the behavior of Calibre by controlling what plugins it uses. - + true - - + + 32 32 - + true - + true - + - - + + Enable/&Disable plugin - - + + &Customize plugin - - + + &Remove plugin @@ -1114,33 +1135,33 @@ - - + + Add new plugin - + - + - - + + Plugin &file: - + plugin_path - + - - + + ... - - + + :/images/document_open.svg:/images/document_open.svg @@ -1148,13 +1169,13 @@ - + - - + + Qt::Horizontal - + 40 20 @@ -1163,8 +1184,8 @@ - - + + &Add @@ -1180,12 +1201,12 @@ - - - + + + Qt::Horizontal - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -1193,7 +1214,7 @@ - + @@ -1202,11 +1223,11 @@ Dialog accept() - + 239 558 - + 157 274 @@ -1218,11 +1239,11 @@ Dialog reject() - + 307 558 - + 286 274 diff --git a/src/calibre/gui2/dialogs/search.py b/src/calibre/gui2/dialogs/search.py index 403bb7f287..f41a80e620 100644 --- a/src/calibre/gui2/dialogs/search.py +++ b/src/calibre/gui2/dialogs/search.py @@ -6,13 +6,13 @@ from PyQt4.QtGui import QDialog from calibre.gui2.dialogs.search_ui import Ui_Dialog from calibre.gui2 import qstring_to_unicode - + class SearchDialog(QDialog, Ui_Dialog): - + def __init__(self, *args): QDialog.__init__(self, *args) self.setupUi(self) - + def tokens(self, raw): phrases = re.findall(r'\s+".*?"\s+', raw) for f in phrases: @@ -20,7 +20,8 @@ class SearchDialog(QDialog, Ui_Dialog): return [t.strip() for t in phrases + raw.split()] def search_string(self): - all, any, phrase, none = map(lambda x: unicode(x.text()), (self.all, self.any, self.phrase, self.none)) + all, any, phrase, none = map(lambda x: unicode(x.text()), + (self.all, self.any, self.phrase, self.none)) all, any, none = map(self.tokens, (all, any, none)) phrase = phrase.strip() all = ' and '.join(all) @@ -32,11 +33,11 @@ class SearchDialog(QDialog, Ui_Dialog): if all: ans += (' and ' if ans else '') + all if none: - ans += (' and not ' if ans else '') + none + ans += (' and not ' if ans else 'not ') + none if any: ans += (' or ' if ans else '') + any return ans - + def token(self): txt = qstring_to_unicode(self.text.text()).strip() if txt: @@ -46,4 +47,4 @@ class SearchDialog(QDialog, Ui_Dialog): if re.search(r'\s', tok): tok = '"%s"'%tok return tok - + diff --git a/src/calibre/gui2/dialogs/test_email.ui b/src/calibre/gui2/dialogs/test_email.ui new file mode 100644 index 0000000000..f1d5568c03 --- /dev/null +++ b/src/calibre/gui2/dialogs/test_email.ui @@ -0,0 +1,103 @@ + + + Dialog + + + + 0 + 0 + 542 + 418 + + + + Test email settings + + + + :/images/config.svg:/images/config.svg + + + + + + Send test mail from %s to: + + + true + + + + + + + + + + + + + true + + + + + + + &Test + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/images/news/krstarica.png b/src/calibre/gui2/images/news/krstarica.png new file mode 100644 index 0000000000..92eecfc4e0 Binary files /dev/null and b/src/calibre/gui2/images/news/krstarica.png differ diff --git a/src/calibre/gui2/images/news/krstarica_en.png b/src/calibre/gui2/images/news/krstarica_en.png new file mode 100644 index 0000000000..92eecfc4e0 Binary files /dev/null and b/src/calibre/gui2/images/news/krstarica_en.png differ diff --git a/src/calibre/gui2/images/news/tanjug.png b/src/calibre/gui2/images/news/tanjug.png new file mode 100644 index 0000000000..be1128073c Binary files /dev/null and b/src/calibre/gui2/images/news/tanjug.png differ diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index a97c667ddf..a5bda4d3ef 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -93,7 +93,7 @@ class DateDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): qde = QStyledItemDelegate.createEditor(self, parent, option, index) - qde.setDisplayFormat('MM/dd/yyyy') + qde.setDisplayFormat(unicode(qde.displayFormat()).replace('yy', 'yyyy')) qde.setMinimumDate(QDate(101,1,1)) qde.setCalendarPopup(True) return qde @@ -637,7 +637,8 @@ class BooksView(TableView): def columns_sorted(self, rating_col, timestamp_col): for i in range(self.model().columnCount(None)): - if self.itemDelegateForColumn(i) == self.rating_delegate: + if self.itemDelegateForColumn(i) in (self.rating_delegate, + self.timestamp_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) if rating_col > -1: self.setItemDelegateForColumn(rating_col, self.rating_delegate) @@ -708,7 +709,7 @@ class BooksView(TableView): def close(self): self._model.close() - + def set_editable(self, editable): self._model.set_editable(editable) @@ -1001,10 +1002,10 @@ class DeviceBooksModel(BooksModel): self.sort(col, self.sorted_on[1]) done = True return done - + def set_editable(self, editable): self.editable = editable - + class SearchBox(QLineEdit): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 28f861ae3a..fd80a722e5 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -226,7 +226,11 @@ class ResultCache(SearchQueryParser): Returns a list of affected rows or None if the rows are filtered. ''' for id in ids: - self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0] + try: + self._data[id] = conn.get('SELECT * from meta WHERE id=?', + (id,))[0] + except IndexError: + return None try: return map(self.row, ids) except ValueError: @@ -269,7 +273,7 @@ class ResultCache(SearchQueryParser): ans = cmp(self._data[x][9], self._data[y][9]) if ans != 0: return ans return cmp(self._data[x][10], self._data[y][10]) - + def cmp(self, loc, x, y, str=True, subsort=False): try: ans = cmp(self._data[x][loc].lower(), self._data[y][loc].lower()) if str else\ @@ -279,7 +283,7 @@ class ResultCache(SearchQueryParser): if subsort and ans == 0: return cmp(self._data[x][11].lower(), self._data[y][11].lower()) return ans - + def sort(self, field, ascending, subsort=False): field = field.lower().strip() if field in ('author', 'tag', 'comment'): @@ -733,7 +737,7 @@ class LibraryDatabase2(LibraryDatabase): self.refresh_ids([id]) if notify: self.notify('metadata', [id]) - + def delete_book(self, id, notify=True): ''' Removes book from the result cache and the underlying database. @@ -751,7 +755,7 @@ class LibraryDatabase2(LibraryDatabase): self.data.books_deleted([id]) if notify: self.notify('delete', [id]) - + def remove_format(self, index, format, index_is_id=False, notify=True): id = index if index_is_id else self.id(index) path = os.path.join(self.library_path, *self.path(id, index_is_id=True).split(os.sep)) @@ -925,14 +929,14 @@ class LibraryDatabase2(LibraryDatabase): (id, aid)) except IntegrityError: # Sometimes books specify the same author twice in their metadata pass - ss = authors_to_sort_string(authors) + ss = authors_to_sort_string(authors) self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) self.conn.commit() self.data.set(id, FIELD_MAP['authors'], - ','.join([a.replace(',', '|') for a in authors]), + ','.join([a.replace(',', '|') for a in authors]), row_is_id=True) - self.data.set(id, FIELD_MAP['author_sort'], ss, row_is_id=True) + self.data.set(id, FIELD_MAP['author_sort'], ss, row_is_id=True) self.set_path(id, True) if notify: self.notify('metadata', [id]) @@ -1159,7 +1163,7 @@ class LibraryDatabase2(LibraryDatabase): else: path = path_or_stream return run_plugins_on_import(path, format) - + def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): ''' Add a book to the database. The result cache is not updated. @@ -1219,7 +1223,7 @@ class LibraryDatabase2(LibraryDatabase): aus = aus.decode(preferred_encoding, 'replace') title = mi.title if isinstance(mi.title, unicode) else \ mi.title.decode(preferred_encoding, 'replace') - obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', + obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', (title, None, series_index, aus)) id = obj.lastrowid self.data.books_added([id], self.conn) @@ -1568,3 +1572,4 @@ books_series_link feeds return duplicates + diff --git a/src/calibre/utils/smtp.py b/src/calibre/utils/smtp.py index 79e46c03c3..0234e27c55 100644 --- a/src/calibre/utils/smtp.py +++ b/src/calibre/utils/smtp.py @@ -45,15 +45,17 @@ def create_mail(from_, to, subject, text=None, attachment_data=None, return outer.as_string() -def get_mx(host): +def get_mx(host, verbose=0): import dns.resolver + if verbose: + print 'Find mail exchanger for', host answers = list(dns.resolver.query(host, 'MX')) answers.sort(cmp=lambda x, y: cmp(int(x.preference), int(y.preference))) return [str(x.exchange) for x in answers] def sendmail_direct(from_, to, msg, timeout, localhost, verbose): import smtplib - hosts = get_mx(to.split('@')[-1].strip()) + hosts = get_mx(to.split('@')[-1].strip(), verbose) timeout=None # Non blocking sockets sometimes don't work s = smtplib.SMTP(timeout=timeout, local_hostname=localhost) s.set_debuglevel(verbose) diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py index 0cb80ec192..d407378ce5 100644 --- a/src/calibre/web/feeds/recipes/__init__.py +++ b/src/calibre/web/feeds/recipes/__init__.py @@ -37,6 +37,7 @@ recipe_modules = ['recipe_' + r for r in ( 'new_york_review_of_books_no_sub', 'politico', 'adventuregamers', 'mondedurable', 'instapaper', 'dnevnik_cro', 'vecernji_list', 'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs', + 'krstarica', 'krstarica_en', 'tanjug', )] import re, imp, inspect, time, os diff --git a/src/calibre/web/feeds/recipes/recipe_krstarica.py b/src/calibre/web/feeds/recipes/recipe_krstarica.py new file mode 100644 index 0000000000..fb25ae8d84 --- /dev/null +++ b/src/calibre/web/feeds/recipes/recipe_krstarica.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +vesti.krstarica.com +''' +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class Krstarica(BasicNewsRecipe): + title = 'Krstarica - Vesti' + __author__ = 'Darko Miletic' + description = 'Dnevne vesti iz Srbije i sveta' + publisher = 'Krstarica' + category = 'news, politics, Serbia' + oldest_article = 1 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + remove_javascript = True + encoding = 'utf-8' + language = _('Serbian') + extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}' + + html2lrf_options = [ + '--comment', description + , '--category', category + , '--publisher', publisher + ] + + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"' + + preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] + + feeds = [ + (u'Vesti dana' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=aktuelno&lang=0' ) + ,(u'Srbija' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=scg&lang=0' ) + ,(u'Svet' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=svet&lang=0' ) + ,(u'Politika' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=politika&lang=0' ) + ,(u'Ekonomija' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=ekonomija&lang=0' ) + ,(u'Drustvo' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=drustvo&lang=0' ) + ,(u'Kultura' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=kultura&lang=0' ) + ,(u'Nauka i Tehnologija', u'http://vesti.krstarica.com/index.php?rss=1&rubrika=nauka&lang=0' ) + ,(u'Medicina' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=medicina&lang=0' ) + ,(u'Sport' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=sport&lang=0' ) + ,(u'Zanimljivosti' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=zanimljivosti&lang=0') + ] + + def preprocess_html(self, soup): + mtag = '' + soup.head.insert(0,mtag) + titletag = soup.find('h4') + if titletag: + realtag = titletag.parent.parent + realtag.extract() + for item in soup.findAll(['table','center']): + item.extract() + soup.body.insert(1,realtag) + realtag.name = 'div' + for item in soup.findAll(style=True): + del item['style'] + for item in soup.findAll(align=True): + del item['align'] + return soup diff --git a/src/calibre/web/feeds/recipes/recipe_krstarica_en.py b/src/calibre/web/feeds/recipes/recipe_krstarica_en.py new file mode 100644 index 0000000000..e426e7d807 --- /dev/null +++ b/src/calibre/web/feeds/recipes/recipe_krstarica_en.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +vesti.krstarica.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class Krstarica_en(BasicNewsRecipe): + title = 'Krstarica - news in english' + __author__ = 'Darko Miletic' + description = 'News from Serbia and world' + publisher = 'Krstarica' + category = 'news, politics, Serbia' + oldest_article = 1 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + remove_javascript = True + encoding = 'utf-8' + language = _('English') + + html2lrf_options = [ + '--comment', description + , '--category', category + , '--publisher', publisher + ] + + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"' + + feeds = [ + (u'Daily news', u'http://vesti.krstarica.com/index.php?rss=1&rubrika=aktuelno&lang=1' ) + ,(u'Serbia' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=scg&lang=1' ) + ,(u'Politics' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=politika&lang=1' ) + ,(u'Economy' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=ekonomija&lang=1' ) + ,(u'Culture' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=kultura&lang=1' ) + ,(u'Sports' , u'http://vesti.krstarica.com/index.php?rss=1&rubrika=sport&lang=1' ) + ] + + def preprocess_html(self, soup): + mtag = '' + soup.head.insert(0,mtag) + titletag = soup.find('h4') + if titletag: + realtag = titletag.parent.parent + realtag.extract() + for item in soup.findAll(['table','center']): + item.extract() + soup.body.insert(1,realtag) + realtag.name = 'div' + for item in soup.findAll(style=True): + del item['style'] + for item in soup.findAll(align=True): + del item['align'] + return soup diff --git a/src/calibre/web/feeds/recipes/recipe_tanjug.py b/src/calibre/web/feeds/recipes/recipe_tanjug.py new file mode 100644 index 0000000000..9a8acfaca7 --- /dev/null +++ b/src/calibre/web/feeds/recipes/recipe_tanjug.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = '2009, Darko Miletic ' +''' +tanjug.rs +''' +import re +from calibre.web.feeds.news import BasicNewsRecipe + +class Tanjug(BasicNewsRecipe): + title = 'Tanjug' + __author__ = 'Darko Miletic' + description = 'Novinska agencija TANJUG - Dnevne vesti iz Srbije i sveta' + publisher = 'Tanjug' + category = 'news, politics, Serbia' + oldest_article = 1 + max_articles_per_feed = 100 + use_embedded_content = True + encoding = 'utf-8' + lang = 'sr-Latn-RS' + language = _('Serbian') + extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: serif1, serif}' + + html2lrf_options = [ + '--comment', description + , '--category', category + , '--publisher', publisher + ] + + html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"' + + preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] + + feeds = [(u'Vesti', u'http://www.tanjug.rs/StaticPages/RssTanjug.aspx')] + + def preprocess_html(self, soup): + soup.html['xml:lang'] = self.lang + soup.html['lang' ] = self.lang + soup.html['dir' ] = "ltr" + mtag = '' + soup.head.insert(0,mtag) + return soup diff --git a/src/calibre/www/apps/feedjack/fjlib.py b/src/calibre/www/apps/feedjack/fjlib.py index e13fd5e5af..2801d59a70 100644 --- a/src/calibre/www/apps/feedjack/fjlib.py +++ b/src/calibre/www/apps/feedjack/fjlib.py @@ -128,7 +128,7 @@ def get_extra_content(site, sfeeds_ids, ctx): def get_posts_tags(object_list, sfeeds_obj, user_id, tag_name): """ Adds a qtags property in every post object in a page. - Use "qtags" instead of "tags" in templates to avoid innecesary DB hits. + Use "qtags" instead of "tags" in templates to avoid unnecessary DB hits. """ tagd = {} user_obj = None