Pull from trunk

This commit is contained in:
Kovid Goyal 2009-03-28 12:18:03 -07:00
commit 83dedd68fc
177 changed files with 44667 additions and 18758 deletions

View File

@ -29,3 +29,4 @@ src/cssutils/scripts/
src/cssutils/css/.svn/
src/cssutils/stylesheets/.svn/
src/odf/.svn
tags

View File

@ -9,7 +9,7 @@ Create linux binary.
'''
def freeze():
import glob, sys, subprocess, tarfile, os, re, textwrap, shutil, cStringIO, bz2, codecs
import glob, sys, tarfile, os, textwrap, shutil
from contextlib import closing
from cx_Freeze import Executable, setup
from calibre.constants import __version__, __appname__
@ -41,7 +41,6 @@ def freeze():
'/usr/lib/libxslt.so.1',
'/usr/lib/libxslt.so.1',
'/usr/lib/libgthread-2.0.so.0',
'/usr/lib/libglib-2.0.so.0',
'/usr/lib/gcc/i686-pc-linux-gnu/4.3.3/libstdc++.so.6',
'/usr/lib/libpng12.so.0',
'/usr/lib/libexslt.so.0',
@ -81,6 +80,8 @@ def freeze():
includes = [x[0] for x in executables.values()]
includes += ['calibre.ebooks.lrf.fonts.prs500.'+x for x in FONT_MAP.values()]
includes += ['email.iterators', 'email.generator']
excludes = ['matplotlib', "Tkconstants", "Tkinter", "tcl", "_imagingtk",
"ImageTk", "FixTk", 'wx', 'PyQt4.QtAssistant', 'PyQt4.QtOpenGL.so',
@ -88,7 +89,7 @@ def freeze():
'glib', 'gobject']
packages = ['calibre', 'encodings', 'cherrypy', 'cssutils', 'xdg',
'dateutil']
'dateutil', 'dns', 'email']
includes += ['calibre.web.feeds.recipes.'+r for r in recipe_modules]

View File

@ -270,7 +270,7 @@ _check_symlinks_prescript()
print 'Adding ImageMagick'
dest = os.path.join(frameworks_dir, 'ImageMagick')
if os.path.exists(dest):
sutil.rmtree(dest)
shutil.rmtree(dest)
shutil.copytree(os.path.expanduser('~/ImageMagick'), dest, True)
shutil.copyfile('/usr/local/lib/libpng12.0.dylib', os.path.join(dest, 'lib', 'libpng12.0.dylib'))
@ -343,9 +343,10 @@ def main():
'calibre.ebooks.lrf.any.*', 'calibre.ebooks.lrf.feeds.*',
'keyword', 'codeop', 'pydoc', 'readline',
'BeautifulSoup', 'calibre.ebooks.lrf.fonts.prs500.*',
'dateutil',
'dateutil', 'email.iterators',
'email.generator',
],
'packages' : ['PIL', 'Authorization', 'lxml'],
'packages' : ['PIL', 'Authorization', 'lxml', 'dns'],
'excludes' : ['IPython'],
'plist' : { 'CFBundleGetInfoString' : '''calibre, an E-book management application.'''
''' Visit http://calibre.kovidgoyal.net for details.''',

View File

@ -273,7 +273,6 @@ File ::C49805D2-C0B8-01C4-DF6F-674D9C0BFD15 -name IM_MOD_RL_viff_.dll -parent 8E
File ::1B9F2F00-20A5-B207-5A80-8F75470286AD -name txt2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::826F1915-9F97-59DD-6637-3EEC0744A79C -name IM_MOD_RL_ps2_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::519A6618-8A1F-93A5-93B4-6EEF5A4A3DE9 -name comic2pdf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::B0CEAA35-52BF-0DE0-BAC7-7B23157E29BD -name isbndb.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::A5F23791-BCDC-A997-4941-5D1F2F227E6D -name type.xml -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::0A1C107A-C0AA-3ED6-4F37-A6894386DCBE -name IM_MOD_RL_ps3_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::EEBA64E7-6509-EBAF-3E23-1A203216F39A -name epub2lrf.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
@ -284,7 +283,6 @@ File ::EA37C1C2-57BB-4E7A-C004-0010D79142C2 -name IM_MOD_RL_fits_.dll -parent 8E
File ::05F5C10D-6988-F1F4-A486-86C96DB20302 -name pywintypes26.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::0137A2B1-EB94-EB26-7295-0C7CD941A1DF -name IM_MOD_RL_histogram_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::7F199A1F-4FA4-2ABA-DED3-36ECF3C089CA -name epub2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::F9F112C9-B61B-E041-1A9D-47641B047135 -name isbndb.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::CF6398D8-2140-53CF-1DA6-421A82E92621 -name any2epub.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::8DFA6C69-360D-FA63-7FF9-860E3DB00B19 -name any2lrf.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::5BB7579D-9183-412C-81F8-B411B07C57B3 -name IM_MOD_RL_pnm_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
@ -544,6 +542,8 @@ File ::325F545D-30A8-08DA-74F0-AC1244F6C1D9 -name IM_MOD_RL_vid_.dll -parent 8E5
File ::24238371-77D0-0A8F-35D1-498A5FCC1B0D -name IM_MOD_RL_rla_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::6F5D62F3-5E63-0753-364C-01CAAF1002E0 -name IM_MOD_RL_magick_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::9FDAC308-5D4F-A865-A09A-9FBF48162A47 -name IM_MOD_RL_djvu_.dll -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::5D748040-5973-EFF1-41FC-B424636C642E -name fetch-ebook-metadata.exe.local -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
File ::8B8655B8-3823-AA02-1CDA-02F5AD4677C0 -name fetch-ebook-metadata.exe -parent 8E5D85A4-7608-47A1-CF7C-309060D5FF40
Component ::F6829AB7-9F66-4CEE-CA0E-21F54C6D3609 -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Main -parent Components
SetupType ::D9ADE41C-B744-690C-2CED-CF826BF03D2E -setup Install -active Yes -platforms {AIX-ppc FreeBSD-4-x86 FreeBSD-x86 HPUX-hppa Linux-x86 Solaris-sparc Windows} -name Typical -parent SetupTypes

View File

@ -14,12 +14,11 @@ IMAGEMAGICK_DIR = 'C:\\ImageMagick'
FONTCONFIG_DIR = 'C:\\fontconfig'
VC90 = r'C:\VC90.CRT'
import sys, os, py2exe, shutil, zipfile, glob, subprocess, re
import sys, os, py2exe, shutil, zipfile, glob, re
from distutils.core import setup
from distutils.filelist import FileList
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, BASE_DIR)
from setup import VERSION, APPNAME, entry_points, scripts, basenames
from setup import VERSION, APPNAME, scripts, basenames
sys.path.remove(BASE_DIR)
ICONS = [os.path.abspath(os.path.join(BASE_DIR, 'icons', i)) for i in ('library.ico', 'viewer.ico')]
@ -145,6 +144,8 @@ def main(args=sys.argv):
'sip', 'pkg_resources', 'PyQt4.QtSvg',
'mechanize', 'ClientForm', 'wmi',
'win32file', 'pythoncom',
'email.iterators',
'email.generator',
'win32process', 'win32api', 'msvcrt',
'win32event', 'calibre.ebooks.lrf.any.*',
'calibre.ebooks.lrf.feeds.*',
@ -155,7 +156,7 @@ def main(args=sys.argv):
'PyQt4.QtWebKit', 'PyQt4.QtNetwork',
],
'packages' : ['PIL', 'lxml', 'cherrypy',
'dateutil'],
'dateutil', 'dns'],
'excludes' : ["Tkconstants", "Tkinter", "tcl",
"_imagingtk", "ImageTk", "FixTk"
],

16
session.vim Normal file
View File

@ -0,0 +1,16 @@
" Project wide builtins
let g:pyflakes_builtins += ["dynamic_property"]
python << EOFPY
import os
import vipy
source_file = vipy.vipy.eval('expand("<sfile>")')
project_dir = os.path.dirname(source_file)
src_dir = os.path.abspath(os.path.join(project_dir, 'src'))
base_dir = os.path.join(src_dir, 'calibre')
vipy.session.initialize(project_name='calibre', src_dir=src_dir,
project_dir=project_dir, base_dir=base_dir)
EOFPY

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.5.2'
__version__ = '0.5.3'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
'''
Various run time constants.

View File

@ -17,8 +17,10 @@ def option_parser():
Run an embedded python interpreter.
''')
parser.add_option('--update-module', help='Update the specified module in the frozen library. '+
'Module specifications are of the form full.name.of.module,path_to_module.py', default=None
parser.add_option('--update-module',
help='Update the specified module in the frozen library. '+
'Module specifications are of the form full.name.of.module,path_to_module.py',
default=None
)
parser.add_option('-c', '--command', help='Run python code.', default=None)
parser.add_option('-e', '--exec-file', default=None, help='Run the python code in file.')
@ -27,7 +29,8 @@ Run an embedded python interpreter.
parser.add_option('-g', '--gui', default=False, action='store_true',
help='Run the GUI',)
parser.add_option('--migrate', action='store_true', default=False,
help='Migrate old database. Needs two arguments. Path to library1.db and path to new library folder.')
help='Migrate old database. Needs two arguments. Path '
'to library1.db and path to new library folder.')
return parser
def update_zipfile(zipfile, mod, path):

View File

@ -24,6 +24,8 @@ class Device(object):
# it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
# Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = True
def __init__(self, key='-1', log_packets=False, report_progress=None) :
"""

View File

@ -4,7 +4,7 @@ __copyright__ = '2009, John Schember <john at nachtimwald.com>'
Device driver for Amazon's Kindle
'''
import os, re
import os, re, sys
from calibre.devices.usbms.driver import USBMS, metadata_from_formats
@ -51,6 +51,9 @@ class KINDLE(USBMS):
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
if match is not None:
mi.title = match.group('title')
if not isinstance(mi.title, unicode):
mi.title = mi.title.decode(sys.getfilesystemencoding(),
'replace')
return mi

View File

@ -35,6 +35,7 @@ class USBMS(Device):
EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD = ''
SUPPORTS_SUB_DIRS = False
CAN_SET_METADATA = False
def __init__(self, key='-1', log_packets=False, report_progress=None):
Device.__init__(self, key=key, log_packets=log_packets,

View File

@ -38,6 +38,8 @@ class UnsupportedFormatError(Exception):
class SpineItem(unicode):
def __new__(cls, *args):
args = list(args)
args[0] = args[0].partition('#')[0]
obj = super(SpineItem, cls).__new__(cls, *args)
path = args[0]
raw = open(path, 'rb').read()
@ -67,6 +69,7 @@ class EbookIterator(object):
CHARACTERS_PER_PAGE = 1000
def __init__(self, pathtoebook):
pathtoebook = pathtoebook.strip()
self.pathtoebook = os.path.abspath(pathtoebook)
self.config = DynamicConfig(name='iterator')
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()

View File

@ -354,7 +354,10 @@ class PreProcessor(object):
(re.compile(r'-\n\r?'), lambda match: ''),
# Remove gray background
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>')
(re.compile(r'<BODY[^<>]+>'), lambda match : '<BODY>'),
# Remove non breaking spaces
(re.compile(ur'\u00a0'), lambda match : ' '),
]

View File

@ -59,6 +59,8 @@ class FetchISBNDB(Thread):
args.extend(['--author', self.author])
if self.publisher:
args.extend(['--publisher', self.publisher])
if self.verbose:
args.extend(['--verbose'])
args.append(self.key)
try:
opts, args = option_parser().parse_args(args)

View File

@ -60,10 +60,12 @@ class Query(object):
if title is not None:
q += build_term('title', title.split())
if author is not None:
q += build_term('author', author.split())
q += ('+' if q else '')+build_term('author', author.split())
if publisher is not None:
q += build_term('publisher', publisher.split())
q += ('+' if q else '')+build_term('publisher', publisher.split())
if isinstance(q, unicode):
q = q.encode('utf-8')
self.url = self.BASE_URL+urlencode({
'q':q,
'max-results':max_results,

View File

@ -8,7 +8,7 @@ import sys, re, socket
from urllib import urlopen, quote
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata import MetaInformation, authors_to_sort_string
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&'
@ -28,7 +28,8 @@ def fetch_metadata(url, max=100, timeout=5.):
raw = urlopen(url).read()
except Exception, err:
raise ISBNDBError('Could not fetch ISBNDB metadata. Error: '+str(err))
soup = BeautifulStoneSoup(raw)
soup = BeautifulStoneSoup(raw,
convertEntities=BeautifulStoneSoup.XML_ENTITIES)
book_list = soup.find('booklist')
if book_list is None:
errmsg = soup.find('errormessage').string

View File

@ -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
from urlparse import urlparse, urldefrag
from lxml import etree
from dateutil import parser
@ -444,7 +444,7 @@ class OPF(object):
if not hasattr(stream, 'read'):
stream = open(stream, 'rb')
self.basedir = self.base_dir = basedir
self.path_to_html_toc = None
self.path_to_html_toc = self.html_toc_fragment = None
raw, self.encoding = xml_to_unicode(stream.read(), strip_encoding_pats=True, resolve_entities=True)
raw = raw[raw.find('<'):]
self.root = etree.fromstring(raw, self.PARSER)
@ -496,7 +496,8 @@ class OPF(object):
if f:
self.toc.read_ncx_toc(f[0])
else:
self.path_to_html_toc = toc
self.path_to_html_toc, self.html_toc_fragment = \
toc.partition('#')[0], toc.partition('#')[-1]
self.toc.read_html_toc(toc)
except:
pass

View File

@ -9,6 +9,7 @@ import struct, os, cStringIO, re, functools
try:
from PIL import Image as PILImage
PILImage
except ImportError:
import Image as PILImage
@ -52,6 +53,8 @@ class EXTHHeader(object):
self.cover_offset = co
elif id == 202:
self.thumbnail_offset, = struct.unpack('>L', content)
elif id == 503 and (not title or title == _('Unknown')):
title = content
#else:
# print 'unknown record', id, repr(content)
if title:
@ -110,7 +113,6 @@ class BookHeader(object):
self.codec = 'cp1252' if user_encoding is None else user_encoding
log.warn('Unknown codepage %d. Assuming %s'%(self.codepage,
self.codec))
if ident == 'TEXTREAD' or self.length < 0xE4 or 0xE8 < self.length:
self.extra_flags = 0
else:
@ -267,6 +269,17 @@ class MobiReader(object):
rule = rule.encode('utf-8')
s.write(rule+'\n\n')
if self.book_header.exth is not None or self.embedded_mi is not None:
if self.verbose:
print 'Creating OPF...'
ncx = cStringIO.StringIO()
opf = self.create_opf(htmlfile, guide, root)
opf.render(open(os.path.splitext(htmlfile)[0]+'.opf', 'wb'), ncx)
ncx = ncx.getvalue()
if ncx:
open(os.path.splitext(htmlfile)[0]+'.ncx', 'wb').write(ncx)
def read_embedded_metadata(self, root, elem, guide):
raw = '<package>'+html.tostring(elem, encoding='utf-8')+'</package>'
stream = cStringIO.StringIO(raw)
@ -369,19 +382,19 @@ class MobiReader(object):
if 'filepos-id' in attrib:
attrib['id'] = attrib.pop('filepos-id')
if 'name' in attrib and attrib['name'] != attrib['id']:
attrib['name'] = attrib['id']
if 'filepos' in attrib:
filepos = attrib.pop('filepos')
try:
attrib['href'] = "#filepos%d" % int(filepos)
except ValueError:
pass
if styles:
attrib['id'] = attrib.get('id', 'calibre_mr_gid%d'%i)
self.tag_css_rules.append('#%s {%s}'%(attrib['id'],
'; '.join(styles)))
def create_opf(self, htmlfile, guide=None, root=None):
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
if mi is None:
@ -583,3 +596,4 @@ def get_metadata(stream):
log.exception()
return mi

View File

@ -17,7 +17,6 @@ import logging
from lxml import etree, html
import calibre
from cssutils import CSSParser
from cssutils.css import CSSStyleSheet
from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
@ -239,7 +238,7 @@ class DirContainer(object):
for path in self.namelist():
ext = os.path.splitext(path)[1].lower()
if ext == '.opf':
self.opfname = fname
self.opfname = path
return
self.opfname = None
@ -289,6 +288,9 @@ class Metadata(object):
OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'),
'scheme': OPF('scheme'), 'event': OPF('event'),
'type': XSI('type'), 'lang': XML('lang'), 'id': 'id'}
OPF1_NSMAP = {'dc': DC11_NS, 'oebpackage': OPF1_NS}
OPF2_NSMAP = {'opf': OPF2_NS, 'dc': DC11_NS, 'dcterms': DCTERMS_NS,
'xsi': XSI_NS, 'calibre': CALIBRE_NS}
class Item(object):
"""An item of OEB data model metadata.
@ -609,7 +611,11 @@ class Manifest(object):
elif not namespace(data.tag):
data.attrib['xmlns'] = XHTML_NS
data = etree.tostring(data, encoding=unicode)
data = etree.fromstring(data)
try:
data = etree.fromstring(data)
except:
data=data.replace(':=', '=').replace(':>', '>')
data = etree.fromstring(data)
elif namespace(data.tag) != XHTML_NS:
# OEB_DOC_NS, but possibly others
ns = namespace(data.tag)
@ -1246,6 +1252,9 @@ class PageList(object):
class OEBBook(object):
"""Representation of a book in the IDPF OEB data model."""
COVER_SVG_XP = XPath('h:body//svg:svg[position() = 1]')
COVER_OBJECT_XP = XPath('h:body//h:object[@data][position() = 1]')
def __init__(self, logger, parse_cache={}, encoding='utf-8',
pretty_print=False):
"""Create empty book. Optional arguments:
@ -1275,6 +1284,7 @@ class OEBBook(object):
:attr:`pages`: List of "pages," such as indexed to a print edition of
the same text.
"""
self.encoding = encoding
self.pretty_print = pretty_print
self.logger = self.log = logger

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,15 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import sys, os, re, StringIO, traceback, time
import os
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QLocale, QUrl, QTranslator, QCoreApplication, \
QModelIndex
QByteArray, QUrl, QTranslator, QCoreApplication
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QTableView, QDialogButtonBox, QApplication, QDialog
QIcon, QTableView, QApplication, QDialog
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
from calibre import __author__, islinux, iswindows, isosx
from calibre import islinux, iswindows
from calibre.startup import get_lang
from calibre.utils.config import Config, ConfigProxy, dynamic
import calibre.resources as resources
@ -65,6 +64,9 @@ def _config():
help=_('Show the cover flow in a separate window instead of in the main calibre window'))
c.add_opt('disable_tray_notification', default=False,
help=_('Disable notifications from the system tray icon'))
c.add_opt('default_send_to_device_action', default=None,
help=_('Default action to perform when send to device button is '
'clicked'))
return ConfigProxy(c)
config = _config()
@ -139,15 +141,15 @@ def human_readable(size):
class Dispatcher(QObject):
'''Convenience class to ensure that a function call always happens in the GUI thread'''
SIGNAL = SIGNAL('dispatcher(PyQt_PyObject,PyQt_PyObject)')
def __init__(self, func):
QObject.__init__(self)
self.func = func
self.connect(self, SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'),
self.dispatch, Qt.QueuedConnection)
self.connect(self, self.SIGNAL, self.dispatch, Qt.QueuedConnection)
def __call__(self, *args, **kwargs):
self.emit(SIGNAL('edispatch(PyQt_PyObject, PyQt_PyObject)'), args, kwargs)
self.emit(self.SIGNAL, args, kwargs)
def dispatch(self, args, kwargs):
self.func(*args, **kwargs)
@ -447,6 +449,7 @@ class ResizableDialog(QDialog):
try:
from calibre.utils.single_qt_application import SingleApplication
SingleApplication
except:
SingleApplication = None

View File

@ -1,12 +1,29 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, traceback, Queue, time
from threading import Thread
import os, traceback, Queue, time, socket
from threading import Thread, RLock
from itertools import repeat
from functools import partial
from binascii import unhexlify
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
Qt
from calibre.devices import devices
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.parallel import Job
from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog
from calibre.ebooks.metadata import authors_to_string
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.devices.interface import Device
from calibre import sanitize_file_name, preferred_encoding
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
config as email_config
class DeviceJob(Job):
@ -26,11 +43,7 @@ class DeviceJob(Job):
class DeviceManager(Thread):
'''
Worker thread that polls the USB ports for devices. Emits the
signal connected(PyQt_PyObject, PyQt_PyObject) on connection and
disconnection events.
'''
def __init__(self, connected_slot, job_manager, sleep_time=2):
'''
@param sleep_time: Time to sleep between device probes in millisecs
@ -104,6 +117,12 @@ class DeviceManager(Thread):
self.jobs.put(job)
return job
def has_card(self):
try:
return bool(self.device.card_prefix())
except:
return False
def _get_device_information(self):
info = self.device.get_device_information(end_session=False)
info = [i.replace('\x00', '').replace('\x01', '') for i in info]
@ -116,7 +135,6 @@ class DeviceManager(Thread):
return self.create_job(self._get_device_information, done,
description=_('Get device information'))
def _books(self):
'''Get metadata from device'''
mainlist = self.device.books(oncard=False, end_session=False)
@ -185,3 +203,487 @@ class DeviceManager(Thread):
return self.create_job(self._view_book, done, args=[path, target],
description=_('View book on device'))
class DeviceAction(QAction):
def __init__(self, dest, delete, specific, icon_path, text, parent=None):
if delete:
text += ' ' + _('and delete from library')
QAction.__init__(self, QIcon(icon_path), text, parent)
self.dest = dest
self.delete = delete
self.specific = specific
self.connect(self, SIGNAL('triggered(bool)'),
lambda x : self.emit(SIGNAL('a_s(QAction)'), self))
def __repr__(self):
return self.__class__.__name__ + ':%s:%s:%s'%(self.dest, self.delete,
self.specific)
class DeviceMenu(QMenu):
def __init__(self, parent=None):
QMenu.__init__(self, parent)
self.group = QActionGroup(self)
self.actions = []
self._memory = []
self.set_default_menu = self.addMenu(_('Set default send to device'
' action'))
opts = email_config().parse()
default_account = None
if opts.accounts:
self.email_to_menu = self.addMenu(_('Email to')+'...')
keys = sorted(opts.accounts.keys())
for account in keys:
formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats
if default:
default_account = (dest, False, False, ':/images/mail.svg',
_('Email to')+' '+account)
action1 = DeviceAction(dest, False, False, ':/images/mail.svg',
_('Email to')+' '+account, self)
action2 = DeviceAction(dest, True, False, ':/images/mail.svg',
_('Email to')+' '+account, self)
map(self.email_to_menu.addAction, (action1, action2))
map(self._memory.append, (action1, action2))
self.email_to_menu.addSeparator()
self.connect(action1, SIGNAL('a_s(QAction)'),
self.action_triggered)
self.connect(action2, SIGNAL('a_s(QAction)'),
self.action_triggered)
_actions = [
('main:', False, False, ':/images/reader.svg',
_('Send to main memory')),
('card:0', False, False, ':/images/sd.svg',
_('Send to storage card')),
'-----',
('main:', True, False, ':/images/reader.svg',
_('Send to main memory')),
('card:0', True, False, ':/images/sd.svg',
_('Send to storage card')),
'-----',
('main:', False, True, ':/images/reader.svg',
_('Send specific format to main memory')),
('card:0', False, True, ':/images/sd.svg',
_('Send specific format to storage card')),
]
if default_account is not None:
_actions.insert(2, default_account)
_actions.insert(6, list(default_account))
_actions[6][1] = True
for round in (0, 1):
for dest, delete, specific, icon, text in _actions:
if dest == '-':
(self.set_default_menu if round else self).addSeparator()
continue
action = DeviceAction(dest, delete, specific, icon, text, self)
self._memory.append(action)
if round == 1:
action.setCheckable(True)
action.setText(action.text())
self.group.addAction(action)
self.set_default_menu.addAction(action)
else:
self.connect(action, SIGNAL('a_s(QAction)'),
self.action_triggered)
self.actions.append(action)
self.addAction(action)
da = config['default_send_to_device_action']
done = False
for action in self.group.actions():
if repr(action) == da:
action.setChecked(True)
done = True
break
if not done:
action = list(self.group.actions())[0]
action.setChecked(True)
config['default_send_to_device_action'] = repr(action)
self.connect(self.group, SIGNAL('triggered(QAction*)'),
self.change_default_action)
self.enable_device_actions(False)
if opts.accounts:
self.addSeparator()
self.addMenu(self.email_to_menu)
def change_default_action(self, action):
config['default_send_to_device_action'] = repr(action)
action.setChecked(True)
def action_triggered(self, action):
self.emit(SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
action.dest, action.delete, action.specific)
def trigger_default(self, *args):
r = config['default_send_to_device_action']
for action in self.actions:
if repr(action) == r:
self.action_triggered(action)
break
def enable_device_actions(self, enable):
for action in self.actions:
if action.dest[:4] in ('main', 'card'):
action.setEnabled(enable)
class Emailer(Thread):
def __init__(self, timeout=60):
Thread.__init__(self)
self.setDaemon(True)
self.job_lock = RLock()
self.jobs = []
self._run = True
self.timeout = timeout
def run(self):
while self._run:
job = None
with self.job_lock:
if self.jobs:
job = self.jobs[0]
self.jobs = self.jobs[1:]
if job is not None:
self._send_mails(*job)
time.sleep(1)
def stop(self):
self._run = False
def send_mails(self, jobnames, callback, attachments, to_s, subjects,
texts, attachment_names):
job = (jobnames, callback, attachments, to_s, subjects, texts,
attachment_names)
with self.job_lock:
self.jobs.append(job)
def _send_mails(self, jobnames, callback, attachments,
to_s, subjects, texts, attachment_names):
opts = email_config().parse()
opts.verbose = 3 if os.environ.get('CALIBRE_DEBUG_EMAIL', False) else 0
from_ = opts.from_
if not from_:
from_ = 'calibre <calibre@'+socket.getfqdn()+'>'
results = []
for i, jobname in enumerate(jobnames):
try:
msg = compose_mail(from_, to_s[i], texts[i], subjects[i],
open(attachments[i], 'rb'),
attachment_name = attachment_names[i])
efrom, eto = map(extract_email_address, (from_, to_s[i]))
eto = [eto]
sendmail(msg, efrom, eto, localhost=None,
verbose=opts.verbose,
timeout=self.timeout, relay=opts.relay_host,
username=opts.relay_username,
password=unhexlify(opts.relay_password), port=opts.relay_port,
encryption=opts.encryption)
results.append([jobname, None, None])
except Exception, e:
results.append([jobname, e, traceback.format_exc()])
callback(results)
class DeviceGUI(object):
def dispatch_sync_event(self, dest, delete, specific):
rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
error_dialog(self, _('No books'), _('No books')+' '+\
_('selected to send')).exec_()
return
fmt = None
if specific:
d = ChooseFormatDialog(self, _('Choose format to send to device'),
self.device_manager.device_class.FORMATS)
d.exec_()
fmt = d.format().lower()
dest, sub_dest = dest.split(':')
if dest in ('main', 'card'):
if not self.device_connected or not self.device_manager:
error_dialog(self, _('No device'),
_('Cannot send: No device is connected')).exec_()
return
on_card = dest == 'card'
if on_card and not self.device_manager.has_card():
error_dialog(self, _('No card'),
_('Cannot send: Device has no storage card')).exec_()
return
self.sync_to_device(on_card, delete, fmt)
elif dest == 'mail':
to, fmts = sub_dest.split(';')
fmts = [x.strip().lower() for x in fmts.split(',')]
self.send_by_mail(to, fmts, delete)
def send_by_mail(self, to, fmts, delete_from_library):
rows = self.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return
ids = iter(self.library_view.model().id(r) for r in rows)
full_metadata = self.library_view.model().get_metadata(
rows, full_metadata=True)[-1]
files = self.library_view.model().get_preferred_formats(rows,
fmts, paths=True, set_metadata=True)
files = [getattr(f, 'name', None) for f in files]
bad, remove_ids, jobnames = [], [], []
texts, subjects, attachments, attachment_names = [], [], [], []
for f, mi, id in zip(files, full_metadata, ids):
t = mi.title
if not t:
t = _('Unknown')
if f is None:
bad.append(t)
else:
remove_ids.append(id)
jobnames.append(u'%s:%s'%(id, t))
attachments.append(f)
subjects.append(_('E-book:')+ ' '+t)
a = authors_to_string(mi.authors if mi.authors else \
[_('Unknown')])
texts.append(_('Attached, you will find the e-book') + \
'\n\n' + t + '\n\t' + _('by') + ' ' + a + '\n\n' + \
_('in the %s format.') %
os.path.splitext(f)[1][1:].upper())
prefix = sanitize_file_name(t+' - '+a)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
attachment_names.append(prefix + os.path.splitext(f)[1])
remove = remove_ids if delete_from_library else []
to_s = list(repeat(to, len(attachments)))
if attachments:
self.emailer.send_mails(jobnames,
Dispatcher(partial(self.emails_sent, remove=remove)),
attachments, to_s, subjects, texts, attachment_names)
self.status_bar.showMessage(_('Sending email to')+' '+to, 3000)
if bad:
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
d = warning_dialog(self, _('No suitable formats'),
'<p>'+ _('Could not email the following books '
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
d.exec_()
def emails_sent(self, results, remove=[]):
errors, good = [], []
for jobname, exception, tb in results:
id = jobname.partition(':')[0]
title = jobname.partition(':')[-1]
if exception is not None:
errors.append([title, exception, tb])
else:
good.append(title)
if errors:
errors = '\n'.join([
'<li><b>%s</b><br>%s<br>%s<br></li>' %
(title, e, tb.replace('\n', '<br>')) for \
title, e, tb in errors
])
ConversionErrorDialog(self, _('Failed to email books'),
'<p>'+_('Failed to email the following books:')+\
'<ul>%s</ul>'%errors,
show=True)
else:
self.status_bar.showMessage(_('Sent by email:') + ', '.join(good),
5000)
def cover_to_thumbnail(self, data):
p = QPixmap()
p.loadFromData(data)
if not p.isNull():
ht = self.device_manager.device_class.THUMBNAIL_HEIGHT \
if self.device_manager else Device.THUMBNAIL_HEIGHT
p = p.scaledToHeight(ht, Qt.SmoothTransformation)
return (p.width(), p.height(), pixmap_to_data(p))
def email_news(self, id):
opts = email_config().parse()
accounts = [(account, [x.strip().lower() for x in x[0].split(',')])
for account, x in opts.accounts.items() if x[1]]
sent_mails = []
for account, fmts in accounts:
files = self.library_view.model().\
get_preferred_formats_from_ids([id], fmts)
files = [f.name for f in files if f is not None]
if not files:
continue
attachment = files[0]
mi = self.library_view.model().db.get_metadata(id,
index_is_id=True)
to_s = [account]
subjects = [_('News:')+' '+mi.title]
texts = [_('Attached is the')+' '+mi.title]
attachment_names = [mi.title+os.path.splitext(attachment)[1]]
attachments = [attachment]
jobnames = ['%s:%s'%(id, mi.title)]
remove = [id] if config['delete_news_from_library_on_upload']\
else []
self.emailer.send_mails(jobnames,
Dispatcher(partial(self.emails_sent, remove=remove)),
attachments, to_s, subjects, texts, attachment_names)
sent_mails.append(to_s[0])
if sent_mails:
self.status_bar.showMessage(_('Sent news to')+' '+\
', '.join(sent_mails), 3000)
def sync_news(self):
if self.device_connected:
ids = list(dynamic.get('news_to_be_synced', set([])))
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
files = self.library_view.model().get_preferred_formats_from_ids(
ids, self.device_manager.device_class.FORMATS)
files = [f for f in files if f is not None]
if not files:
dynamic.set('news_to_be_synced', set([]))
return
metadata = self.library_view.model().get_metadata(ids,
rows_are_ids=True)
names = []
for mi in metadata:
prefix = sanitize_file_name(mi['title'])
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id,
os.path.splitext(f.name)[1]))
cdata = mi['cover']
if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata)
dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files:
remove = ids if \
config['delete_news_from_library_on_upload'] else []
on_card = self.location_view.model().free[0] < \
self.location_view.model().free[1]
self.upload_books(files, names, metadata,
on_card=on_card,
memory=[[f.name for f in files], remove])
self.status_bar.showMessage(_('Sending news to device.'), 5000)
def sync_to_device(self, on_card, delete_from_library,
specific_format=None):
rows = self.library_view.selectionModel().selectedRows()
if not self.device_manager or not rows or len(rows) == 0:
return
ids = iter(self.library_view.model().id(r) for r in rows)
metadata = self.library_view.model().get_metadata(rows)
for mi in metadata:
cdata = mi['cover']
if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata)
metadata = iter(metadata)
_files = self.library_view.model().get_preferred_formats(rows,
self.device_manager.device_class.FORMATS,
paths=True, set_metadata=True,
specific_format=specific_format)
files = [getattr(f, 'name', None) for f in _files]
bad, good, gf, names, remove_ids = [], [], [], [], []
for f in files:
mi = metadata.next()
id = ids.next()
if f is None:
bad.append(mi['title'])
else:
remove_ids.append(id)
good.append(mi)
gf.append(f)
t = mi['title']
if not t:
t = _('Unknown')
a = mi['authors']
if not a:
a = _('Unknown')
prefix = sanitize_file_name(t+' - '+a)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
remove = remove_ids if delete_from_library else []
self.upload_books(gf, names, good, on_card, memory=(_files, remove))
self.status_bar.showMessage(_('Sending books to device.'), 5000)
if bad:
bad = '\n'.join('<li>%s</li>'%(i,) for i in bad)
d = warning_dialog(self, _('No suitable formats'),
_('Could not upload the following books to the device, '
'as no suitable formats were found:<br><ul>%s</ul>')%(bad,))
d.exec_()
def upload_booklists(self):
'''
Upload metadata to device.
'''
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
self.booklists())
def metadata_synced(self, job):
'''
Called once metadata has been uploaded.
'''
if job.exception is not None:
self.device_job_exception(job)
return
cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
def upload_books(self, files, names, metadata, on_card=False, memory=None):
'''
Upload books to device.
:param files: List of either paths to files or file like objects
'''
titles = [i['title'] for i in metadata]
job = self.device_manager.upload_books(
Dispatcher(self.books_uploaded),
files, names, on_card=on_card,
metadata=metadata, titles=titles
)
self.upload_memory[job] = (metadata, on_card, memory, files)
def books_uploaded(self, job):
'''
Called once books have been uploaded.
'''
metadata, on_card, memory, files = self.upload_memory.pop(job)
if job.exception is not None:
if isinstance(job.exception, FreeSpaceError):
where = 'in main memory.' if 'memory' in str(job.exception) \
else 'on the storage card.'
titles = '\n'.join(['<li>'+mi['title']+'</li>' \
for mi in metadata])
d = error_dialog(self, _('No space on device'),
_('<p>Cannot upload books to device there '
'is no more free space available ')+where+
'</p>\n<ul>%s</ul>'%(titles,))
d.exec_()
else:
self.device_job_exception(job)
return
self.device_manager.add_books_to_metadata(job.result,
metadata, self.booklists())
self.upload_booklists()
view = self.card_view if on_card else self.memory_view
view.model().resort(reset=False)
view.model().research()
for f in files:
getattr(f, 'close', lambda : True)()
if memory and memory[1]:
self.library_view.model().delete_books_by_id(memory[1])

View File

@ -1,12 +1,13 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, time, textwrap
from binascii import hexlify, unhexlify
from PyQt4.Qt import QDialog, QMessageBox, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, \
QStringListModel, QAbstractItemModel, QFont, \
SIGNAL, QTimer, Qt, QSize, QVariant, QUrl, \
QModelIndex, QInputDialog
QModelIndex, QInputDialog, QAbstractTableModel
from calibre.constants import islinux, iswindows
from calibre.gui2.dialogs.config_ui import Ui_Dialog
@ -21,6 +22,7 @@ from calibre.library import server_config
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, customize_plugin, \
plugin_customization, add_plugin, remove_plugin
from calibre.utils.smtp import config as smtp_prefs
class PluginModel(QAbstractItemModel):
@ -120,12 +122,12 @@ class CategoryModel(QStringListModel):
def __init__(self, *args):
QStringListModel.__init__(self, *args)
self.setStringList([_('General'), _('Interface'), _('Advanced'),
_('Content\nServer'), _('Plugins')])
self.setStringList([_('General'), _('Interface'), _('Email\nDelivery'),
_('Advanced'), _('Content\nServer'), _('Plugins')])
self.icons = list(map(QVariant, map(QIcon,
[':/images/dialog_information.svg', ':/images/lookfeel.svg',
':/images/view.svg', ':/images/network-server.svg',
':/images/plugins.svg'])))
':/images/mail.svg', ':/images/view.svg',
':/images/network-server.svg', ':/images/plugins.svg'])))
def data(self, index, role):
if role == Qt.DecorationRole:
@ -133,6 +135,121 @@ class CategoryModel(QStringListModel):
return QStringListModel.data(self, index, role)
class EmailAccounts(QAbstractTableModel):
def __init__(self, accounts):
QAbstractTableModel.__init__(self)
self.accounts = accounts
self.account_order = sorted(self.accounts.keys())
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')])
self.default_font = QFont()
self.default_font.setBold(True)
self.default_font = QVariant(self.default_font)
self.tooltips =[NONE] + map(QVariant,
[_('Formats to email. The first matching format will be sent.'),
'<p>'+_('If checked, downloaded news will be automatically '
'mailed <br>to this email address '
'(provided it is in one of the listed formats).')])
def rowCount(self, *args):
return len(self.account_order)
def columnCount(self, *args):
return 3
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
return NONE
def data(self, index, role):
row, col = index.row(), index.column()
if row < 0 or row >= self.rowCount():
return NONE
account = self.account_order[row]
if role == Qt.UserRole:
return (account, self.accounts[account])
if role == Qt.ToolTipRole:
return self.tooltips[col]
if role == Qt.DisplayRole:
if col == 0:
return QVariant(account)
if col == 1:
return QVariant(self.accounts[account][0])
if role == Qt.FontRole and self.accounts[account][2]:
return self.default_font
if role == Qt.CheckStateRole and col == 2:
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
return NONE
def flags(self, index):
if index.column() == 2:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
else:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
def setData(self, index, value, role):
if not index.isValid():
return False
row, col = index.row(), index.column()
account = self.account_order[row]
if col == 2:
self.accounts[account][1] ^= True
elif col == 1:
self.accounts[account][0] = unicode(value.toString()).upper()
else:
na = unicode(value.toString())
from email.utils import parseaddr
addr = parseaddr(na)[-1]
if not addr:
return False
self.accounts[na] = self.accounts.pop(account)
self.account_order[row] = na
if '@kindle.com' in addr:
self.accounts[na][0] = 'AZW, MOBI, TPZ, PRC, AZW1'
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.index(index.row(), 0), self.index(index.row(), 2))
return True
def make_default(self, index):
if index.isValid():
row = index.row()
for x in self.accounts.values():
x[2] = False
self.accounts[self.account_order[row]][2] = True
self.reset()
def add(self):
x = _('new email address')
y = x
c = 0
while y in self.accounts:
c += 1
y = x + str(c)
self.accounts[y] = ['MOBI, EPUB', True,
len(self.account_order) == 0]
self.account_order = sorted(self.accounts.keys())
self.reset()
return self.index(self.account_order.index(y), 0)
def remove(self, index):
if index.isValid():
row = self.index.row()
account = self.account_order[row]
self.accounts.pop(account)
self.account_order = sorted(self.accounts.keys())
has_default = False
for account in self.account_order:
if self.accounts[account][2]:
has_default = True
break
if not has_default and self.account_order:
self.accounts[self.account_order[0]][2] = True
self.reset()
class ConfigDialog(QDialog, Ui_Dialog):
def __init__(self, window, db, server=None):
@ -142,8 +259,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.setupUi(self)
self._category_model = CategoryModel()
self.connect(self.category_view, SIGNAL('activated(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
self.connect(self.category_view, SIGNAL('clicked(QModelIndex)'), lambda i: self.stackedWidget.setCurrentIndex(i.row()))
self.category_view.currentChanged = \
lambda n, p: self.stackedWidget.setCurrentIndex(n.row())
self.category_view.setModel(self._category_model)
self.db = db
self.server = server
@ -242,7 +359,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.priority.setCurrentIndex(p)
self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
self.category_view.setCurrentIndex(self._category_model.index(0))
self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model)
self.connect(self.toggle_plugin, SIGNAL('clicked()'), lambda : self.modify_plugin(op='toggle'))
@ -251,6 +367,76 @@ class ConfigDialog(QDialog, Ui_Dialog):
self.connect(self.button_plugin_browse, SIGNAL('clicked()'), self.find_plugin)
self.connect(self.button_plugin_add, SIGNAL('clicked()'), self.add_plugin)
self.separate_cover_flow.setChecked(config['separate_cover_flow'])
self.setup_email_page()
self.category_view.setCurrentIndex(self.category_view.model().index(0))
def setup_email_page(self):
opts = smtp_prefs().parse()
if opts.from_:
self.email_from.setText(opts.from_)
self._email_accounts = EmailAccounts(opts.accounts)
self.email_view.setModel(self._email_accounts)
if opts.relay_host:
self.relay_host.setText(opts.relay_host)
self.relay_port.setValue(opts.relay_port)
if opts.relay_username:
self.relay_username.setText(opts.relay_username)
if opts.relay_password:
self.relay_password.setText(unhexlify(opts.relay_password))
(self.relay_tls if opts.encryption == 'TLS' else self.relay_ssl).setChecked(True)
self.connect(self.relay_use_gmail, SIGNAL('clicked(bool)'),
self.create_gmail_relay)
self.connect(self.relay_show_password, SIGNAL('stateChanged(int)'),
lambda
state:self.relay_password.setEchoMode(self.relay_password.Password if
state == 0 else self.relay_password.Normal))
self.connect(self.email_add, SIGNAL('clicked(bool)'),
self.add_email_account)
self.connect(self.email_make_default, SIGNAL('clicked(bool)'),
lambda c: self._email_accounts.make_default(self.email_view.currentIndex()))
self.email_view.resizeColumnsToContents()
def add_email_account(self, checked):
index = self._email_accounts.add()
self.email_view.setCurrentIndex(index)
self.email_view.resizeColumnsToContents()
self.email_view.edit(index)
def create_gmail_relay(self, *args):
self.relay_username.setText('@gmail.com')
self.relay_password.setText('')
self.relay_host.setText('smtp.gmail.com')
self.relay_port.setValue(587)
self.relay_tls.setChecked(True)
info_dialog(self, _('Finish gmail setup'),
_('Dont forget to enter your gmail username and password')).exec_()
self.relay_username.setFocus(Qt.OtherFocusReason)
self.relay_username.setCursorPosition(0)
def set_email_settings(self):
from_ = unicode(self.email_from.text()).strip()
if self._email_accounts.accounts and not from_:
error_dialog(self, _('Bad configuration'),
_('You must set the From email address')).exec_()
return False
username = unicode(self.relay_username.text()).strip()
password = unicode(self.relay_password.text()).strip()
host = unicode(self.relay_host.text()).strip()
if host and not (username and password):
error_dialog(self, _('Bad configuration'),
_('You must set the username and password for '
'the mail server.')).exec_()
return False
conf = smtp_prefs()
conf.set('from_', from_)
conf.set('accounts', self._email_accounts.accounts)
conf.set('relay_host', host if host else None)
conf.set('relay_port', self.relay_port.value())
conf.set('relay_username', username if username else None)
conf.set('relay_password', hexlify(password))
conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL')
return True
def add_plugin(self):
path = unicode(self.plugin_path.text())
@ -286,7 +472,7 @@ class ConfigDialog(QDialog, Ui_Dialog):
if op == 'customize':
if not plugin.is_customizable():
info_dialog(self, _('Plugin not customizable'),
_('Plugin: %s does not need customization')%plugin.name).exec_()
_('Plugin: %s does not need customization')%plugin.name).exec_()
return
help = plugin.customization_help()
text, ok = QInputDialog.getText(self, _('Customize %s')%plugin.name,
@ -300,7 +486,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
self._plugin_model.reset()
else:
error_dialog(self, _('Cannot remove builtin plugin'),
plugin.name + _(' cannot be removed. It is a builtin plugin. Try disabling it instead.')).exec_()
plugin.name + _(' cannot be removed. It is a '
'builtin plugin. Try disabling it instead.')).exec_()
def up_column(self):
@ -376,7 +563,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
d.exec_()
def browse(self):
dir = choose_dir(self, 'database location dialog', 'Select database location')
dir = choose_dir(self, 'database location dialog',
_('Select database location'))
if dir:
self.location.setText(dir)
@ -393,7 +581,10 @@ class ConfigDialog(QDialog, Ui_Dialog):
def accept(self):
mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs):
error_dialog(self, _('Invalid size'), _('The size %s is invalid. must be of the form widthxheight')%mcs).exec_()
error_dialog(self, _('Invalid size'),
_('The size %s is invalid. must be of the form widthxheight')%mcs).exec_()
return
if not self.set_email_settings():
return
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
@ -432,15 +623,18 @@ class ConfigDialog(QDialog, Ui_Dialog):
if not path or not os.path.exists(path) or not os.path.isdir(path):
d = error_dialog(self, _('Invalid database location'),
_('Invalid database location ')+path+_('<br>Must be a directory.'))
_('Invalid database location ')+path+
_('<br>Must be a directory.'))
d.exec_()
elif not os.access(path, os.W_OK):
d = error_dialog(self, _('Invalid database location'),
_('Invalid database location.<br>Cannot write to ')+path)
_('Invalid database location.<br>Cannot write to ')+path)
d.exec_()
else:
self.database_location = os.path.abspath(path)
self.directories = [qstring_to_unicode(self.directory_list.item(i).text()) for i in range(self.directory_list.count())]
self.directories = [
qstring_to_unicode(self.directory_list.item(i).text()) for i in \
range(self.directory_list.count())]
config['frequently_used_directories'] = self.directories
QDialog.accept(self)
@ -448,7 +642,8 @@ class Vacuum(QMessageBox):
def __init__(self, parent, db):
self.db = db
QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'), _('Compacting database. This may take a while.'),
QMessageBox.__init__(self, QMessageBox.Information, _('Compacting...'),
_('Compacting database. This may take a while.'),
QMessageBox.NoButton, parent)
QTimer.singleShot(200, self.vacuum)
@ -456,3 +651,11 @@ class Vacuum(QMessageBox):
self.db.vacuum()
self.accept()
if __name__ == '__main__':
from calibre.library.database2 import LibraryDatabase2
from PyQt4.Qt import QApplication
app = QApplication([])
d=ConfigDialog(None, LibraryDatabase2('/tmp'))
d.category_view.setCurrentIndex(d.category_view.model().index(2))
d.show()
app.exec_()

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>755</width>
<width>789</width>
<height>557</height>
</rect>
</property>
@ -437,12 +437,6 @@
</widget>
</item>
</layout>
<zorder>toolbar_button_size</zorder>
<zorder>label_4</zorder>
<zorder>show_toolbar_text</zorder>
<zorder>columns</zorder>
<zorder></zorder>
<zorder>groupBox_3</zorder>
</widget>
</item>
<item>
@ -507,7 +501,6 @@
</layout>
</item>
</layout>
<zorder>columns</zorder>
</widget>
</item>
<item>
@ -534,16 +527,287 @@
</layout>
<zorder>roman_numerals</zorder>
<zorder>groupBox_2</zorder>
<zorder>groupBox</zorder>
<zorder>systray_icon</zorder>
<zorder>sync_news</zorder>
<zorder>delete_news</zorder>
<zorder>separate_cover_flow</zorder>
<zorder>systray_notifications</zorder>
<zorder>groupBox_3</zorder>
<zorder></zorder>
<zorder></zorder>
</widget>
<widget class="QWidget" name="page_6" >
<layout class="QVBoxLayout" name="verticalLayout_9" >
<item>
<widget class="QLabel" name="label_22" >
<property name="text" >
<string>calibre can send your books to you (or your reader) by email</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9" >
<item>
<widget class="QLabel" name="label_15" >
<property name="text" >
<string>Send email &amp;from:</string>
</property>
<property name="buddy" >
<cstring>email_from</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="email_from" >
<property name="toolTip" >
<string>&lt;p>This is what will be present in the From: field of emails sent by calibre.&lt;br> Set it to your email address</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8" >
<item>
<widget class="QTableView" name="email_view" >
<property name="selectionMode" >
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_8" >
<item>
<widget class="QToolButton" name="email_add" >
<property name="toolTip" >
<string>Add an email address to which to send books</string>
</property>
<property name="text" >
<string>&amp;Add email</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
<property name="iconSize" >
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="email_make_default" >
<property name="text" >
<string>Make &amp;default</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="email_remove" >
<property name="text" >
<string>&amp;Remove email</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
<property name="iconSize" >
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10" >
<item>
<widget class="QGroupBox" name="groupBox_5" >
<property name="toolTip" >
<string>&lt;p>A mail server is useful if the service you are sending mail to only accepts email from well know mail services.</string>
</property>
<property name="title" >
<string>Mail &amp;Server</string>
</property>
<layout class="QGridLayout" name="gridLayout_3" >
<item row="0" column="0" colspan="4" >
<widget class="QLabel" name="label_16" >
<property name="text" >
<string>calibre can &lt;b>optionally&lt;/b> use a server to send mail</string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_17" >
<property name="text" >
<string>&amp;Hostname:</string>
</property>
<property name="buddy" >
<cstring>relay_host</cstring>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_host" >
<property name="toolTip" >
<string>The hostname of your mail server. For e.g. smtp.gmail.com</string>
</property>
</widget>
</item>
<item row="1" column="3" >
<layout class="QHBoxLayout" name="horizontalLayout_11" >
<item>
<widget class="QLabel" name="label_18" >
<property name="text" >
<string>&amp;Port:</string>
</property>
<property name="buddy" >
<cstring>relay_port</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="relay_port" >
<property name="toolTip" >
<string>The port your mail server listens for connections on. The default is 25</string>
</property>
<property name="minimum" >
<number>1</number>
</property>
<property name="maximum" >
<number>65555</number>
</property>
<property name="value" >
<number>25</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_19" >
<property name="text" >
<string>&amp;Username:</string>
</property>
<property name="buddy" >
<cstring>relay_username</cstring>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_username" >
<property name="toolTip" >
<string>Your username on the mail server</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_20" >
<property name="text" >
<string>&amp;Password:</string>
</property>
<property name="buddy" >
<cstring>relay_password</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2" >
<widget class="QLineEdit" name="relay_password" >
<property name="toolTip" >
<string>Your password on the mail server</string>
</property>
<property name="echoMode" >
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="3" column="3" >
<widget class="QCheckBox" name="relay_show_password" >
<property name="text" >
<string>&amp;Show</string>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_21" >
<property name="text" >
<string>&amp;Encryption:</string>
</property>
<property name="buddy" >
<cstring>relay_tls</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QRadioButton" name="relay_tls" >
<property name="toolTip" >
<string>Use TLS encryption when connecting to the mail server. This is the most common.</string>
</property>
<property name="text" >
<string>&amp;TLS</string>
</property>
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="2" colspan="2" >
<widget class="QRadioButton" name="relay_ssl" >
<property name="toolTip" >
<string>Use SSL encryption when connecting to the mail server.</string>
</property>
<property name="text" >
<string>&amp;SSL</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QToolButton" name="relay_use_gmail" >
<property name="text" >
<string>Use Gmail</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >
<normaloff>:/images/gmail_logo.png</normaloff>:/images/gmail_logo.png</iconset>
</property>
<property name="iconSize" >
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2" >
<layout class="QVBoxLayout" >
<item>

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
'''
The GUI for conversion to EPUB.
'''
import os
import os, uuid
from PyQt4.Qt import QDialog, QSpinBox, QDoubleSpinBox, QComboBox, QLineEdit, \
QTextEdit, QCheckBox, Qt, QPixmap, QIcon, QListWidgetItem, SIGNAL
@ -272,6 +272,7 @@ class Config(ResizableDialog, Ui_Dialog):
if self.row is not None:
self.db.set_metadata(self.id, mi)
self.mi = self.db.get_metadata(self.id, index_is_id=True)
self.mi.application_id = uuid.uuid4()
opf = OPFCreator(os.getcwdu(), self.mi)
self.opf_file = PersistentTemporaryFile('.opf')
opf.render(self.opf_file)

View File

@ -8,10 +8,11 @@ import time
from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, QThread, \
QAbstractTableModel, QCoreApplication, QTimer
from PyQt4.QtGui import QDialog, QItemSelectionModel, QWidget, QLabel, QMovie
from PyQt4.QtGui import QDialog, QItemSelectionModel
from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
from calibre.gui2 import error_dialog, NONE, info_dialog, warning_dialog
from calibre.gui2 import error_dialog, NONE, info_dialog
from calibre.gui2.widgets import ProgressIndicator
from calibre.utils.config import prefs
class Fetcher(QThread):
@ -30,40 +31,6 @@ class Fetcher(QThread):
self.publisher, self.isbn,
self.key if self.key else None)
class ProgressIndicator(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.setGeometry(0, 0, 300, 350)
self.movie = QMovie(':/images/jobs-animated.mng')
self.ml = QLabel(self)
self.ml.setMovie(self.movie)
self.movie.start()
self.movie.setPaused(True)
self.status = QLabel(self)
self.status.setWordWrap(True)
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.status.font().setBold(True)
self.status.font().setPointSize(self.font().pointSize()+6)
self.setVisible(False)
def start(self, msg=''):
view = self.parent()
pwidth, pheight = view.size().width(), view.size().height()
self.resize(pwidth, min(pheight, 250))
self.move(0, (pheight-self.size().height())/2.)
self.ml.resize(self.ml.sizeHint())
self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0)
self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10)
self.status.move(0, self.ml.size().height()+10)
self.status.setText(msg)
self.setVisible(True)
self.movie.setPaused(False)
def stop(self):
if self.movie.state() == self.movie.Running:
self.movie.setPaused(True)
self.setVisible(False)
class Matches(QAbstractTableModel):
@ -137,14 +104,15 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.author = author.strip()
self.publisher = publisher
self.previous_row = None
self.warning.setVisible(False)
self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
lambda index:self.matches.setCurrentIndex(index))
self.show_summary)
self.matches.setMouseTracking(True)
self.fetch_metadata()
def show_summary(self, current, previous):
def show_summary(self, current, *args):
row = current.row()
if row != self.previous_row:
summ = self.model.summary(row)
@ -152,6 +120,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.previous_row = row
def fetch_metadata(self):
self.warning.setVisible(False)
key = str(self.key.text())
if key:
prefs['isbndb_com_key'] = key
@ -173,7 +142,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.start_time = time.time()
self._hangcheck.start()
self._hangcheck.start(100)
def hangcheck(self):
if not (self.fetcher.isFinished() or time.time() - self.start_time > 75):
@ -191,14 +160,14 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.fetcher.exceptions if x[1] is not None]
if warnings:
warnings='<br>'.join(['<b>%s</b>: %s'%(name, exc) for name,exc in warnings])
warning_dialog(self, _('Warning'),
'<p>'+_('Could not fetch metadata from:')+\
'<br><br>'+warnings+'</p>').exec_()
self.warning.setText('<p><b>'+ _('Warning')+':</b>'+\
_('Could not fetch metadata from:')+\
'<br>'+warnings+'</p>')
self.warning.setVisible(True)
if self.model.rowCount() < 1:
info_dialog(self, _('No metadata found'),
_('No metadata found, try adjusting the title and author '
'or the ISBN key.')).exec_()
self.reject()
return
self.matches.setModel(self.model)
@ -215,6 +184,16 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
self.matches.resizeColumnsToContents()
self.pi.stop()
def terminate(self):
if hasattr(self, 'fetcher') and self.fetcher.isRunning():
self.fetcher.terminate()
def __enter__(self, *args):
return self
def __exit__(self, *args):
self.terminate()
def selected_book(self):
try:

View File

@ -23,7 +23,7 @@
<item>
<widget class="QLabel" name="tlabel" >
<property name="text" >
<string>&lt;p>calibre can find metadata for your books from two locations: &lt;b>Google Books&lt;/b> and &lt;b>isbndb.com&lt;/b>. &lt;p>To use isbndb.com you must sign up for a &lt;a href="http://www.isbndb.com">free account&lt;/a> and exter you access key below.</string>
<string>&lt;p>calibre can find metadata for your books from two locations: &lt;b>Google Books&lt;/b> and &lt;b>isbndb.com&lt;/b>. &lt;p>To use isbndb.com you must sign up for a &lt;a href="http://www.isbndb.com">free account&lt;/a> and enter your access key below.</string>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>
@ -60,6 +60,16 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="warning" >
<property name="text" >
<string/>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >

View File

@ -50,7 +50,10 @@ class JobsDialog(QDialog, Ui_JobsDialog):
self.running_time_timer.start(1000)
def update_running_time(self, *args):
self.model.running_time_updated()
try:
self.model.running_time_updated()
except: # Raises random exceptions on OS X
pass
def kill_job(self):
for index in self.jobs_view.selectedIndexes():

View File

@ -70,14 +70,14 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
pub = qstring_to_unicode(self.publisher.text())
if pub:
self.db.set_publisher(id, pub, notify=False)
tags = qstring_to_unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
remove_tags = qstring_to_unicode(self.remove_tags.text()).strip()
if remove_tags:
remove_tags = [i.strip() for i in remove_tags.split(',')]
self.db.unapply_tags(id, remove_tags, notify=False)
tags = qstring_to_unicode(self.tags.text()).strip()
if tags:
tags = map(lambda x: x.strip(), tags.split(','))
self.db.set_tags(id, tags, append=True, notify=False)
if self.write_series:
self.db.set_series(id, qstring_to_unicode(self.series.currentText()), notify=False)

View File

@ -1,12 +1,13 @@
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
The dialog used to edit meta information for a book as well as
add/remove formats
'''
import os
import os, time, traceback
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt
from PyQt4.QtCore import SIGNAL, QObject, QCoreApplication, Qt, QTimer, QThread
from PyQt4.QtGui import QPixmap, QListWidgetItem, QErrorMessage, QDialog, QCompleter
@ -16,14 +17,35 @@ from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.dialogs.password import PasswordDialog
from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, authors_to_string
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn, LibraryThingError
from calibre.ebooks.metadata.library_thing import login, cover_from_isbn
from calibre import islinux
from calibre.ebooks.metadata.meta import get_metadata
from calibre.utils.config import prefs
from calibre.customize.ui import run_plugins_on_import
class CoverFetcher(QThread):
def __init__(self, username, password, isbn, timeout):
self.username = username
self.password = password
self.timeout = timeout
self.isbn = isbn
QThread.__init__(self)
self.exception = self.traceback = self.cover_data = None
def run(self):
try:
login(self.username, self.password, force=False)
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout)[0]
except Exception, e:
self.exception = e
self.traceback = traceback.format_exc()
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
self.path = path
@ -172,6 +194,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.splitter.setStretchFactor(100, 1)
self.db = db
self.pi = ProgressIndicator(self)
self.accepted_callback = accepted_callback
self.id = db.id(row)
self.row = row
@ -338,30 +361,49 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
return
self.fetch_cover_button.setEnabled(False)
self.setCursor(Qt.WaitCursor)
QCoreApplication.instance().processEvents()
try:
login(d.username(), d.password(), force=False)
cover_data = cover_from_isbn(isbn, timeout=self.timeout)[0]
pix = QPixmap()
pix.loadFromData(cover_data)
if pix.isNull():
error_dialog(self.window, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
except LibraryThingError, err:
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
self.cover_fetcher = CoverFetcher(d.username(), d.password(), isbn,
self.timeout)
self.cover_fetcher.start()
self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100)
else:
error_dialog(self, _('Cannot fetch cover'),
_('You must specify the ISBN identifier for this book.')).exec_()
_('You must specify the ISBN identifier for this book.')).exec_()
def hangcheck(self):
if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150):
return
self._hangcheck.stop()
try:
if self.cover_fetcher.isRunning():
self.cover_fetcher.terminate()
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_()
return
if self.cover_fetcher.exception is not None:
err = self.cover_fetcher.exception
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+repr(err)).exec_()
return
pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data)
if pix.isNull():
error_dialog(self.window, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
self.pi.stop()
def fetch_metadata(self):
@ -371,7 +413,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
publisher = qstring_to_unicode(self.publisher.currentText())
if isbn or title or author or publisher:
d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
d.exec_()
with d:
d.exec_()
if d.result() == QDialog.Accepted:
book = d.selected_book()
if book:
@ -387,7 +430,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
prefix += '\n'
self.comments.setText(prefix + summ)
else:
error_dialog(self, 'Cannot fetch metadata', 'You must specify at least one of ISBN, Title, Authors or Publisher')
error_dialog(self, _('Cannot fetch metadata'),
_('You must specify at least one of ISBN, Title, '
'Authors or Publisher'))
def enable_series_index(self, *args):
self.series_index.setEnabled(True)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
id="svg3007"
sodipodi:version="0.32"
inkscape:version="0.45.1"
version="1.0"
sodipodi:docbase="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions"
sodipodi:docname="mail.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
inkscape:export-filename="/home/david/Documents/Projects/KDE/Oxygen/kdelibs/scalable/actions/mail.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs3009">
<linearGradient
id="polygon3293_1_"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-584.6021"
x2="615.5"
y2="-595.8521"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop2997" />
<stop
offset="1"
style="stop-color:#EEEEEE"
id="stop2999" />
</linearGradient>
<linearGradient
id="polygon3286_1_"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-589.8511"
x2="615.5"
y2="-580.6011"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop2991" />
<stop
offset="1"
style="stop-color:#D1DFF1"
id="stop2993" />
</linearGradient>
<linearGradient
id="rect3244_1_"
gradientUnits="userSpaceOnUse"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.0005"
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)">
<stop
offset="0"
style="stop-color:#A4C0E4"
id="stop2983" />
<stop
offset="0.25"
style="stop-color:#D1DFF1"
id="stop2985" />
<stop
offset="0.85"
style="stop-color:#FFFFFF"
id="stop2987" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#rect3244_1_"
id="linearGradient2212"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,1.0588235,0,-0.2352941)"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.0005" />
<linearGradient
inkscape:collect="always"
xlink:href="#polygon3286_1_"
id="linearGradient2214"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
x1="615.5"
y1="-589.8511"
x2="615.5"
y2="-580.6011" />
<linearGradient
inkscape:collect="always"
xlink:href="#polygon3293_1_"
id="linearGradient2216"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)"
x1="615.5"
y1="-584.6021"
x2="615.5"
y2="-595.8521" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="64"
inkscape:cy="64"
inkscape:document-units="px"
inkscape:current-layer="g2972"
width="128px"
height="128px"
inkscape:showpageshadow="false"
inkscape:window-width="794"
inkscape:window-height="731"
inkscape:window-x="0"
inkscape:window-y="0"
showgrid="true"
gridspacingx="4px"
gridspacingy="4px"
gridempspacing="2"
showborder="false" />
<metadata
id="metadata3012">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<switch
id="switch2966"
transform="translate(4,12)">
<foreignObject
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
x="0"
y="0"
width="1"
height="1"
id="foreignObject2968">
<i:pgfRef
xlink:href="#adobe_illustrator_pgf" />
</foreignObject>
<g
i:extraneous="self"
id="g2970">
<g
id="g2972">
<linearGradient
id="linearGradient3033"
gradientUnits="userSpaceOnUse"
x1="59.9995"
y1="4"
x2="59.9995"
y2="72.000504">
<stop
offset="0"
style="stop-color:#A4C0E4"
id="stop3035" />
<stop
offset="0.25"
style="stop-color:#D1DFF1"
id="stop3037" />
<stop
offset="0.85"
style="stop-color:#FFFFFF"
id="stop3039" />
</linearGradient>
<linearGradient
id="linearGradient3042"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-589.85107"
x2="615.5"
y2="-580.60107"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop3044" />
<stop
offset="1"
style="stop-color:#D1DFF1"
id="stop3046" />
</linearGradient>
<linearGradient
id="linearGradient3049"
gradientUnits="userSpaceOnUse"
x1="615.5"
y1="-584.60211"
x2="615.5"
y2="-595.85211"
gradientTransform="matrix(4,0,0,-4,-2402,-2314.406)">
<stop
offset="0"
style="stop-color:#6193CF"
id="stop3051" />
<stop
offset="1"
style="stop-color:#EEEEEE"
id="stop3053" />
</linearGradient>
<g
id="g2202"
transform="translate(0,8)">
<path
style="opacity:0.1"
id="path2974"
d="M 4,0 C 1.794,0 0,1.8884211 0,4.2105263 L 0,75.789474 C 0,78.111579 1.794,80 4,80 L 116,80 C 118.206,80 120,78.111579 120,75.789474 L 120,4.2105263 C 120,1.8884211 118.206,0 116,0 L 4,0 z " />
<path
style="opacity:0.15"
id="path2976"
d="M 4,1 C 2.346,1 1,2.4187568 1,4.1621622 L 1,75.837838 C 1,77.581243 2.346,79 4,79 L 116,79 C 117.654,79 119,77.581243 119,75.837838 L 119,4.1621622 C 119,2.4187568 117.654,1 116,1 L 4,1 z " />
<path
style="opacity:0.2"
id="path2978"
d="M 4,2 C 2.897,2 2,2.9468333 2,4.1111111 L 2,75.888889 C 2,77.053167 2.897,78 4,78 L 116,78 C 117.103,78 118,77.053167 118,75.888889 L 118,4.1111111 C 118,2.9468333 117.103,2 116,2 L 4,2 z " />
<path
style="opacity:0.25"
id="path2980"
d="M 4,3 C 3.448,3 3,3.4736 3,4.0571428 L 3,75.942857 C 3,76.527457 3.448,77 4,77 L 116,77 C 116.553,77 117,76.527457 117,75.942857 L 117,4.0571428 C 117,3.4736 116.553,3 116,3 L 4,3 z " />
<rect
style="fill:url(#linearGradient2212)"
height="72"
width="112"
y="4"
x="4"
id="rect3244_9_" />
<polygon
style="fill:url(#linearGradient2214)"
points="4,8 4,12 60,45 116,12 116,8 60,41 4,8 "
id="polygon3286_9_" />
<polygon
style="fill:url(#linearGradient2216)"
points="116,69 116,65 59.997,24 4,65 4,69 59.997,28 116,69 "
id="polygon3293_9_" />
<polygon
style="fill:#ffffff"
id="polygon3002"
points="4,8 60.004,40.967 116,8 116,4 4,4 4,8 " />
</g>
</g>
</g>
</switch>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

View File

@ -135,13 +135,13 @@ class JobManager(QAbstractTableModel):
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 0), self.index(row, 3))
def running_time_updated(self):
def running_time_updated(self, *args):
for job in self.jobs:
if not job.is_running:
continue
row = self.jobs.index(job)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
self.index(row, 3), self.index(row, 3))
self.index(row, 3), self.index(row, 3))
def has_device_jobs(self):
for job in self.jobs:

View File

@ -94,7 +94,7 @@ class DateDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat('MM/dd/yyyy')
qde.setMinimumDate(QDate(-4000,1,1))
qde.setMinimumDate(QDate(101,1,1))
qde.setCalendarPopup(True)
return qde
@ -709,6 +709,9 @@ class BooksView(TableView):
def close(self):
self._model.close()
def set_editable(self, editable):
self._model.set_editable(editable)
def connect_to_search_box(self, sb):
QObject.connect(sb, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'),
self._model.search)
@ -785,7 +788,7 @@ class DeviceBooksModel(BooksModel):
self.unknown = str(self.trUtf8('Unknown'))
self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self)
self.editable = True
def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows)
@ -793,7 +796,6 @@ class DeviceBooksModel(BooksModel):
indices = self.row_indices(row)
self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'), indices[0], indices[-1])
def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job):
return
@ -818,7 +820,7 @@ class DeviceBooksModel(BooksModel):
if self.map[index.row()] in self.indices_to_be_deleted():
return Qt.ItemIsUserCheckable # Can't figure out how to get the disabled flag in python
flags = QAbstractTableModel.flags(self, index)
if index.isValid():
if index.isValid() and self.editable:
if index.column() in [0, 1] or (index.column() == 4 and self.db.supports_tags()):
flags |= Qt.ItemIsEditable
return flags
@ -959,7 +961,7 @@ class DeviceBooksModel(BooksModel):
return QVariant('Marked for deletion')
col = index.column()
if col in [0, 1] or (col == 4 and self.db.supports_tags()):
return QVariant("Double click to <b>edit</b> me<br><br>")
return QVariant(_("Double click to <b>edit</b> me<br><br>"))
return NONE
def headerData(self, section, orientation, role):
@ -1000,6 +1002,10 @@ class DeviceBooksModel(BooksModel):
done = True
return done
def set_editable(self, editable):
self.editable = editable
class SearchBox(QLineEdit):
INTERVAL = 1000 #: Time to wait before emitting search signal

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<ui version="4.0" >
<author>Kovid Goyal</author>
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<widget class="QMainWindow" name="MainWindow" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
@ -11,140 +10,149 @@
<height>822</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<property name="sizePolicy" >
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="contextMenuPolicy">
<property name="contextMenuPolicy" >
<enum>Qt::NoContextMenu</enum>
</property>
<property name="windowTitle">
<property name="windowTitle" >
<string>__appname__</string>
</property>
<property name="windowIcon">
<iconset resource="images.qrc">
<property name="windowIcon" >
<iconset resource="images.qrc" >
<normaloff>:/library</normaloff>:/library</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<widget class="QWidget" name="centralwidget" >
<layout class="QGridLayout" name="gridLayout" >
<item row="0" column="0" >
<layout class="QHBoxLayout" name="horizontalLayout_3" >
<item>
<widget class="LocationView" name="location_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="LocationView" name="location_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="verticalScrollBarPolicy">
<property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="tabKeyNavigation">
<property name="editTriggers" >
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0">
<property name="showDropIndicator" stdset="0" >
<bool>true</bool>
</property>
<property name="iconSize">
<property name="selectionMode" >
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize" >
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="movement">
<property name="movement" >
<enum>QListView::Static</enum>
</property>
<property name="flow">
<property name="flow" >
<enum>QListView::LeftToRight</enum>
</property>
<property name="gridSize">
<property name="gridSize" >
<size>
<width>175</width>
<height>90</height>
</size>
</property>
<property name="viewMode">
<property name="viewMode" >
<enum>QListView::ListMode</enum>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="donate_button">
<property name="cursor">
<widget class="QToolButton" name="donate_button" >
<property name="cursor" >
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
</property>
<property name="iconSize">
<property name="iconSize" >
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="autoRaise">
<property name="autoRaise" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_3" >
<item>
<widget class="QLabel" name="vanity">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<widget class="QLabel" name="vanity" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<property name="maximumSize" >
<size>
<width>16777215</width>
<height>90</height>
</size>
</property>
<property name="text">
<property name="text" >
<string/>
</property>
<property name="textFormat">
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<property name="openExternalLinks" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="horizontalLayout_2" >
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>Output:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="output_format">
<property name="toolTip">
<widget class="QComboBox" name="output_format" >
<property name="toolTip" >
<string>Set the output format that is used when converting ebooks and downloading news</string>
</property>
</widget>
@ -155,99 +163,99 @@
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout">
<property name="spacing">
<item row="1" column="0" >
<layout class="QHBoxLayout" >
<property name="spacing" >
<number>6</number>
</property>
<property name="margin">
<property name="margin" >
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="advanced_search_button">
<property name="toolTip">
<widget class="QToolButton" name="advanced_search_button" >
<property name="toolTip" >
<string>Advanced search</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/search.svg</normaloff>:/images/search.svg</iconset>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Alt+S</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Search:</string>
</property>
<property name="buddy">
<property name="buddy" >
<cstring>search</cstring>
</property>
</widget>
</item>
<item>
<widget class="SearchBox" name="search">
<property name="enabled">
<widget class="SearchBox" name="search" >
<property name="enabled" >
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<property name="sizePolicy" >
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>false</bool>
</property>
<property name="toolTip">
<string>Search the list of books by title or author&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
<property name="toolTip" >
<string>Search the list of books by title or author&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property>
<property name="whatsThis">
<string>Search the list of books by title, author, publisher, tags and comments&lt;br&gt;&lt;br&gt;Words separated by spaces are ANDed</string>
<property name="whatsThis" >
<string>Search the list of books by title, author, publisher, tags and comments&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property>
<property name="autoFillBackground">
<property name="autoFillBackground" >
<bool>false</bool>
</property>
<property name="text">
<property name="text" >
<string/>
</property>
<property name="frame">
<property name="frame" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear_button">
<property name="toolTip">
<widget class="QToolButton" name="clear_button" >
<property name="toolTip" >
<string>Reset Quick Search</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/clear_left.svg</normaloff>:/images/clear_left.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<widget class="Line" name="line" >
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<property name="sizeHint" stdset="0" >
<size>
<width>20</width>
<height>20</height>
@ -256,77 +264,77 @@
</spacer>
</item>
<item>
<widget class="QToolButton" name="config_button">
<property name="toolTip">
<widget class="QToolButton" name="config_button" >
<property name="toolTip" >
<string>Configuration</string>
</property>
<property name="text">
<property name="text" >
<string>...</string>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<item row="2" column="0" >
<widget class="QStackedWidget" name="stack" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<property name="currentIndex" >
<number>0</number>
</property>
<widget class="QWidget" name="library">
<layout class="QVBoxLayout" name="verticalLayout_2">
<widget class="QWidget" name="library" >
<layout class="QVBoxLayout" name="verticalLayout_2" >
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="horizontalLayout" >
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout" >
<item>
<widget class="QRadioButton" name="match_any">
<property name="text">
<widget class="QRadioButton" name="match_any" >
<property name="text" >
<string>Match any</string>
</property>
<property name="checked">
<property name="checked" >
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="match_all">
<property name="text">
<widget class="QRadioButton" name="match_all" >
<property name="text" >
<string>Match all</string>
</property>
<property name="checked">
<property name="checked" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="popularity">
<property name="text">
<widget class="QCheckBox" name="popularity" >
<property name="text" >
<string>Sort by &amp;popularity</string>
</property>
</widget>
</item>
<item>
<widget class="TagsView" name="tags_view">
<property name="tabKeyNavigation">
<widget class="TagsView" name="tags_view" >
<property name="tabKeyNavigation" >
<bool>true</bool>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="animated">
<property name="animated" >
<bool>true</bool>
</property>
<property name="headerHidden">
<property name="headerHidden" >
<bool>true</bool>
</property>
</widget>
@ -334,35 +342,35 @@
</layout>
</item>
<item>
<widget class="BooksView" name="library_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="BooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
@ -371,76 +379,76 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="main_memory">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="memory_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<widget class="QWidget" name="main_memory" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="memory_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
<horstretch>100</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page">
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="DeviceBooksView" name="card_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<widget class="QWidget" name="page" >
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="DeviceBooksView" name="card_view" >
<property name="sizePolicy" >
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
<horstretch>10</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled">
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode">
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors">
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior">
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid">
<property name="showGrid" >
<bool>false</bool>
</property>
<property name="wordWrap">
<property name="wordWrap" >
<bool>false</bool>
</property>
</widget>
@ -451,234 +459,225 @@
</item>
</layout>
</widget>
<widget class="QToolBar" name="tool_bar">
<property name="minimumSize">
<widget class="QToolBar" name="tool_bar" >
<property name="minimumSize" >
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="contextMenuPolicy">
<property name="contextMenuPolicy" >
<enum>Qt::PreventContextMenu</enum>
</property>
<property name="movable">
<property name="movable" >
<bool>false</bool>
</property>
<property name="orientation">
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="iconSize">
<property name="iconSize" >
<size>
<width>48</width>
<height>48</height>
</size>
</property>
<property name="toolButtonStyle">
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
<attribute name="toolBarArea">
<attribute name="toolBarArea" >
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<attribute name="toolBarBreak" >
<bool>false</bool>
</attribute>
<addaction name="action_add"/>
<addaction name="action_edit"/>
<addaction name="action_convert"/>
<addaction name="action_view"/>
<addaction name="action_news"/>
<addaction name="separator"/>
<addaction name="action_sync"/>
<addaction name="action_save"/>
<addaction name="action_del"/>
<addaction name="separator"/>
<addaction name="action_preferences"/>
<addaction name="action_add" />
<addaction name="action_edit" />
<addaction name="action_convert" />
<addaction name="action_view" />
<addaction name="action_news" />
<addaction name="separator" />
<addaction name="action_sync" />
<addaction name="action_save" />
<addaction name="action_del" />
<addaction name="separator" />
<addaction name="action_preferences" />
</widget>
<widget class="QStatusBar" name="statusBar">
<property name="mouseTracking">
<widget class="QStatusBar" name="statusBar" >
<property name="mouseTracking" >
<bool>true</bool>
</property>
</widget>
<action name="action_add">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_add" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/add_book.svg</normaloff>:/images/add_book.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Add books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>A</string>
</property>
<property name="autoRepeat">
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
<action name="action_del">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_del" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/trash.svg</normaloff>:/images/trash.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Remove books</string>
</property>
<property name="toolTip">
<property name="toolTip" >
<string>Remove books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Del</string>
</property>
</action>
<action name="action_edit">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_edit" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Edit meta information</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>E</string>
</property>
<property name="autoRepeat">
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
<action name="action_sync">
<property name="enabled">
<action name="action_sync" >
<property name="enabled" >
<bool>false</bool>
</property>
<property name="icon">
<iconset resource="images.qrc">
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/sync.svg</normaloff>:/images/sync.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Send to device</string>
</property>
</action>
<action name="action_save">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_save" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/save.svg</normaloff>:/images/save.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Save to disk</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>S</string>
</property>
</action>
<action name="action_news">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_news" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/news.svg</normaloff>:/images/news.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Fetch news</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>F</string>
</property>
</action>
<action name="action_convert">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_convert" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Convert E-books</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>C</string>
</property>
</action>
<action name="action_view">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_view" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/view.svg</normaloff>:/images/view.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>View</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>V</string>
</property>
</action>
<action name="action_open_containing_folder">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_open_containing_folder" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/document_open.svg</normaloff>:/images/document_open.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Open containing folder</string>
</property>
</action>
<action name="action_show_book_details">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_show_book_details" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/dialog_information.svg</normaloff>:/images/dialog_information.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Show book details</string>
</property>
</action>
<action name="action_books_by_same_author">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_by_same_author" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/user_profile.svg</normaloff>:/images/user_profile.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books by same author</string>
</property>
</action>
<action name="action_books_in_this_series">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_in_this_series" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/books_in_series.svg</normaloff>:/images/books_in_series.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books in this series</string>
</property>
</action>
<action name="action_books_by_this_publisher">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_by_this_publisher" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/publisher.png</normaloff>:/images/publisher.png</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books by this publisher</string>
</property>
</action>
<action name="action_books_with_the_same_tags">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_books_with_the_same_tags" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/tags.svg</normaloff>:/images/tags.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Books with the same tags</string>
</property>
</action>
<action name="action_send_specific_format_to_device">
<property name="icon">
<iconset resource="images.qrc">
<normaloff>:/images/book.svg</normaloff>:/images/book.svg</iconset>
</property>
<property name="text">
<string>Send specific format to device</string>
</property>
</action>
<action name="action_preferences">
<property name="icon">
<iconset resource="images.qrc">
<action name="action_preferences" >
<property name="icon" >
<iconset resource="images.qrc" >
<normaloff>:/images/config.svg</normaloff>:/images/config.svg</iconset>
</property>
<property name="text">
<property name="text" >
<string>Preferences</string>
</property>
<property name="toolTip">
<property name="toolTip" >
<string>Configure calibre</string>
</property>
<property name="shortcut">
<property name="shortcut" >
<string>Ctrl+P</string>
</property>
</action>
@ -711,7 +710,7 @@
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc"/>
<include location="images.qrc" />
</resources>
<connections>
<connection>
@ -720,11 +719,11 @@
<receiver>search</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel">
<hint type="sourcelabel" >
<x>787</x>
<y>215</y>
</hint>
<hint type="destinationlabel">
<hint type="destinationlabel" >
<x>755</x>
<y>213</y>
</hint>

View File

@ -7,9 +7,9 @@ import re, os, traceback
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, \
QListWidgetItem, QTextCharFormat, QApplication, \
QSyntaxHighlighter, QCursor, QColor, QWidget, QDialog, \
QPixmap
QPixmap, QMovie, QPalette
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, SIGNAL, \
QObject, QRegExp, QString, QSettings, QSize
QRegExp, QSettings, QSize, QModelIndex
from calibre.gui2.jobs2 import DetailView
from calibre.gui2 import human_readable, NONE, TableView, \
@ -21,6 +21,42 @@ from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs
from calibre.gui2.dialogs.warning_ui import Ui_Dialog as Ui_WarningDialog
class ProgressIndicator(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
self.setGeometry(0, 0, 300, 350)
self.movie = QMovie(':/images/jobs-animated.mng')
self.ml = QLabel(self)
self.ml.setMovie(self.movie)
self.movie.start()
self.movie.setPaused(True)
self.status = QLabel(self)
self.status.setWordWrap(True)
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.status.font().setBold(True)
self.status.font().setPointSize(self.font().pointSize()+6)
self.setVisible(False)
def start(self, msg=''):
view = self.parent()
pwidth, pheight = view.size().width(), view.size().height()
self.resize(pwidth, min(pheight, 250))
self.move(0, (pheight-self.size().height())/2.)
self.ml.resize(self.ml.sizeHint())
self.ml.move(int((self.size().width()-self.ml.size().width())/2.), 0)
self.status.resize(self.size().width(), self.size().height()-self.ml.size().height()-10)
self.status.move(0, self.ml.size().height()+10)
self.status.setText(msg)
self.setVisible(True)
self.movie.setPaused(False)
def stop(self):
if self.movie.state() == self.movie.Running:
self.movie.setPaused(True)
self.setVisible(False)
class WarningDialog(QDialog, Ui_WarningDialog):
def __init__(self, title, msg, details, parent=None):
@ -168,6 +204,13 @@ class LocationModel(QAbstractListModel):
font = QFont('monospace')
font.setBold(row == self.highlight_row)
data = QVariant(font)
elif role == Qt.ForegroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.HighlightedText))
elif role == Qt.BackgroundRole and row == self.highlight_row:
return QVariant(QApplication.palette().brush(
QPalette.Highlight))
return data
def headerData(self, section, orientation, role):
@ -182,7 +225,8 @@ class LocationModel(QAbstractListModel):
def location_changed(self, row):
self.highlight_row = row
self.reset()
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
self.index(0), self.index(self.rowCount(QModelIndex())-1))
class LocationView(QListView):
@ -190,17 +234,19 @@ class LocationView(QListView):
QListView.__init__(self, parent)
self.setModel(LocationModel(self))
self.reset()
QObject.connect(self.selectionModel(), SIGNAL('currentChanged(QModelIndex, QModelIndex)'), self.current_changed)
self.setCursor(Qt.PointingHandCursor)
self.currentChanged = self.current_changed
def count_changed(self, new_count):
self.model().count = new_count
self.model().reset()
def current_changed(self, current, previous):
i = current.row()
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
if current.isValid():
i = current.row()
location = 'library' if i == 0 else 'main' if i == 1 else 'card'
self.emit(SIGNAL('location_selected(PyQt_PyObject)'), location)
self.model().location_changed(i)
def location_changed(self, row):
if 0 <= row and row <= 2:

View File

@ -25,7 +25,8 @@ if iswindows:
else:
Structure = _Structure
if hasattr(sys, 'frozen') and iswindows:
_libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable), 'unrar.dll'))
_libunrar = cdll.LoadLibrary(os.path.join(os.path.dirname(sys.executable),
'unrar.dll'))
_libunrar = load_library(_librar_name, cdll)
RAR_OM_LIST = 0

View File

@ -41,6 +41,8 @@ entry_points = {
'calibre-customize = calibre.customize.ui:main',
'pdftrim = calibre.ebooks.pdf.pdftrim:main' ,
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main',
'calibre-smtp = calibre.utils.smtp:main',
],
'gui_scripts' : [
__appname__+' = calibre.gui2.main:main',
@ -160,6 +162,7 @@ def setup_completion(fatal_errors):
from calibre.ebooks.epub.from_comic import option_parser as comic2epub
from calibre.ebooks.metadata.fetch import option_parser as fem_op
from calibre.gui2.main import option_parser as guiop
from calibre.utils.smtp import option_parser as smtp_op
any_formats = ['epub', 'htm', 'html', 'xhtml', 'xhtm', 'rar', 'zip',
'txt', 'lit', 'rtf', 'pdf', 'prc', 'mobi', 'fb2', 'odt']
f = open_file('/etc/bash_completion.d/libprs500')
@ -194,6 +197,7 @@ def setup_completion(fatal_errors):
f.write(opts_and_words('feeds2epub', feeds2epub, feed_titles))
f.write(opts_and_words('feeds2mobi', feeds2mobi, feed_titles))
f.write(opts_and_words('fetch-ebook-metadata', fem_op, []))
f.write(opts_and_words('calibre-smtp', smtp_op, []))
f.write('''
_prs500_ls()
{

View File

@ -5,7 +5,7 @@ import re, textwrap
DEPENDENCIES = [
#(Generic, version, gentoo, ubuntu, fedora)
('python', '2.5', None, None, None),
('python', '2.6', None, None, None),
('setuptools', '0.6c5', 'setuptools', 'python-setuptools', 'python-setuptools-devel'),
('Python Imaging Library', '1.1.6', 'imaging', 'python-imaging', 'python-imaging'),
('libusb', '0.1.12', None, None, None),
@ -18,6 +18,7 @@ DEPENDENCIES = [
('lxml', '2.1.5', 'lxml', 'python-lxml', 'python-lxml'),
('python-dateutil', '1.4.1', 'python-dateutil', 'python-dateutil', 'python-dateutil'),
('BeautifulSoup', '3.0.5', 'beautifulsoup', 'python-beautifulsoup', 'python-BeautifulSoup'),
('dnspython', '1.6.0', 'dnspython', 'dnspython', 'dnspython', 'dnspython'),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

238
src/calibre/utils/smtp.py Normal file
View File

@ -0,0 +1,238 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
'''
This module implements a simple commandline SMTP client that supports:
* Delivery via an SMTP relay with SSL or TLS
* Background delivery with failures being saved in a maildir mailbox
'''
import sys, traceback, os
from email import encoders
def create_mail(from_, to, subject, text=None, attachment_data=None,
attachment_type=None, attachment_name=None):
assert text or attachment_data
from email.mime.multipart import MIMEMultipart
outer = MIMEMultipart()
outer['Subject'] = subject
outer['To'] = to
outer['From'] = from_
outer.preamble = 'You will not see this in a MIME-aware mail reader.\n'
if text is not None:
from email.mime.text import MIMEText
msg = MIMEText(text)
outer.attach(msg)
if attachment_data is not None:
from email.mime.base import MIMEBase
assert attachment_data and attachment_name
try:
maintype, subtype = attachment_type.split('/', 1)
except AttributeError:
maintype, subtype = 'application', 'octet-stream'
msg = MIMEBase(maintype, subtype)
msg.set_payload(attachment_data)
encoders.encode_base64(msg)
msg.add_header('Content-Disposition', 'attachment',
filename=attachment_name)
outer.attach(msg)
return outer.as_string()
def get_mx(host):
import dns.resolver
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())
timeout=None # Non blocking sockets sometimes don't work
s = smtplib.SMTP(timeout=timeout, local_hostname=localhost)
s.set_debuglevel(verbose)
if not hosts:
raise ValueError('No mail server found for address: %s'%to)
last_error = last_traceback = None
for host in hosts:
try:
s.connect(host, 25)
s.sendmail(from_, [to], msg)
return s.quit()
except Exception, e:
last_error, last_traceback = e, traceback.format_exc()
if last_error is not None:
print last_traceback
raise IOError('Failed to send mail: '+repr(last_error))
def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
relay=None, username=None, password=None, encryption='TLS',
port=-1):
if relay is None:
for x in to:
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
import smtplib
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
timeout = None # Non-blocking sockets sometimes don't work
port = int(port)
s = cls(timeout=timeout, local_hostname=localhost)
s.set_debuglevel(verbose)
if port < 0:
port = 25 if encryption == 'TLS' else 465
s.connect(relay, port)
if encryption == 'TLS':
s.starttls()
s.ehlo()
if username is not None and password is not None:
s.login(username, password)
s.sendmail(from_, to, msg)
return s.quit()
def option_parser():
try:
from calibre.utils.config import OptionParser
OptionParser
except ImportError:
from optparse import OptionParser
import textwrap
parser = OptionParser(textwrap.dedent('''\
%prog [options] [from to text]
Send mail using the SMTP protocol. %prog has two modes of operation. In the
compose mode you specify from to and text and these are used to build and
send an email message. In the filter mode, %prog reads a complete email
message from STDIN and sends it.
text is the body of the email message.
If text is not specified, a complete email message is read from STDIN.
from is the email address of the sender and to is the email address
of the recipient. When a complete email is read from STDIN, from and to
are only used in the SMTP negotiation, the message headers are not modified.
'''))
c=parser.add_option_group('COMPOSE MAIL',
'Options to compose an email. Ignored if text is not specified').add_option
c('-a', '--attachment', help='File to attach to the email')
c('-s', '--subject', help='Subject of the email')
parser.add_option('-l', '--localhost',
help=('Host name of localhost. Used when connecting '
'to SMTP server.'))
r=parser.add_option_group('SMTP RELAY',
'Options to use an SMTP relay server to send mail. '
'%prog will try to send the email directly unless --relay is '
'specified.').add_option
r('-r', '--relay', help=('An SMTP relay server to use to send mail.'))
r('-p', '--port', default=-1,
help='Port to connect to on relay server. Default is to use 465 if '
'encryption method is SSL and 25 otherwise.')
r('-u', '--username', help='Username for relay')
r('-p', '--password', help='Password for relay')
r('-e', '--encryption-method', default='TLS',
choices=['TLS', 'SSL'],
help='Encryption method to use when connecting to relay. Choices are '
'TLS and SSL. Default is TLS.')
parser.add_option('-o', '--outbox', help='Path to maildir folder to store '
'failed email messages in.')
parser.add_option('-f', '--fork', default=False, action='store_true',
help='Fork and deliver message in background. '
'If you use this option, you should also use --outbox '
'to handle delivery failures.')
parser.add_option('-t', '--timeout', help='Timeout for connection')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose')
return parser
def extract_email_address(raw):
from email.utils import parseaddr
return parseaddr(raw)[-1]
def compose_mail(from_, to, text, subject=None, attachment=None,
attachment_name=None):
attachment_type = attachment_data = None
if attachment is not None:
try:
from calibre import guess_type
guess_type
except ImportError:
from mimetypes import guess_type
attachment_data = attachment.read() if hasattr(attachment, 'read') \
else open(attachment, 'rb').read()
attachment_type = guess_type(getattr(attachment, 'name', attachment))[0]
if attachment_name is None:
attachment_name = os.path.basename(getattr(attachment,
'name', attachment))
subject = subject if subject else 'no subject'
return create_mail(from_, to, subject, text=text,
attachment_data=attachment_data, attachment_type=attachment_type,
attachment_name=attachment_name)
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
if len(args) > 1:
msg = compose_mail(args[1], args[2], args[3], subject=opts.subject,
attachment=opts.attachment)
from_, to = args[1:3]
efrom, eto = map(extract_email_address, (from_, to))
eto = [eto]
else:
msg = sys.stdin.read()
from email.parser import Parser
from email.utils import getaddresses
eml = Parser.parsestr(msg, headersonly=True)
tos = eml.get_all('to', [])
ccs = eml.get_all('cc', [])
eto = getaddresses(tos + ccs)
if not eto:
raise ValueError('Email from STDIN does not specify any recipients')
efrom = getaddresses(eml.get_all('from', []))
if not efrom:
raise ValueError('Email from STDIN does not specify a sender')
efrom = efrom[0]
outbox = None
if opts.outbox is not None:
outbox = os.path.abspath(os.path.expanduser(opts.outbox))
from mailbox import Maildir
outbox = Maildir(opts.outbox, factory=None)
if opts.fork:
if os.fork() != 0:
return 0
try:
sendmail(msg, efrom, eto, localhost=opts.localhost, verbose=opts.verbose,
timeout=opts.timeout, relay=opts.relay, username=opts.username,
password=opts.password, port=opts.port,
encryption=opts.encryption_method)
except:
if outbox is not None:
outbox.add(msg)
print 'Delivery failed. Message saved to', opts.outbox
raise
return 0
def config(defaults=None):
from calibre.utils.config import Config, StringConfig
desc = _('Control email delivery')
c = Config('smtp',desc) if defaults is None else StringConfig(defaults,desc)
c.add_opt('from_')
c.add_opt('accounts', default={})
c.add_opt('relay_host')
c.add_opt('relay_port', default=25)
c.add_opt('relay_username')
c.add_opt('relay_password')
c.add_opt('encryption', default='TLS', choices=['TLS', 'SSL'])
return c
if __name__ == '__main__':
sys.exit(main())

View File

@ -379,7 +379,7 @@ class BasicNewsRecipe(object):
if raw:
return _raw
if not isinstance(_raw, unicode) and self.encoding:
_raw = _raw.decode(self.encoding)
_raw = _raw.decode(self.encoding, 'replace')
massage = list(BeautifulSoup.MARKUP_MASSAGE)
massage.append((re.compile(r'&(\S+?);'), lambda match: entity_to_unicode(match, encoding=self.encoding)))
return BeautifulSoup(_raw, markupMassage=massage)

View File

@ -36,7 +36,7 @@ recipe_modules = ['recipe_' + r for r in (
'el_universal', 'mediapart', 'wikinews_en', 'ecogeek', 'daily_mail',
'new_york_review_of_books_no_sub', 'politico', 'adventuregamers',
'mondedurable', 'instapaper', 'dnevnik_cro', 'vecernji_list',
'nacional_cro', '24sata',
'nacional_cro', '24sata', 'dnevni_avaz', 'glas_srpske', '24sata_rs',
)]
import re, imp, inspect, time, os

View File

@ -0,0 +1,52 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
24sata.rs
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class Ser24Sata(BasicNewsRecipe):
title = '24 Sata - Sr'
__author__ = 'Darko Miletic'
description = '24 sata portal vesti iz Srbije'
publisher = 'Ringier d.o.o.'
category = 'news, politics, entertainment, Serbia'
oldest_article = 1
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
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
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
feeds = [(u'Vesti Dana', u'http://www.24sata.rs/rss.php')]
def preprocess_html(self, soup):
soup.html['xml:lang'] = 'sr-Latn-RS'
soup.html['lang'] = 'sr-Latn-RS'
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup
def print_version(self, url):
article, sep, rest = url.partition('#')
return article.replace('/show.php','/_print.php')

View File

@ -5,7 +5,6 @@ __copyright__ = '2008-2009, Darko Miletic <darko.miletic at gmail.com>'
'''
b92.net
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
@ -13,57 +12,53 @@ class B92(BasicNewsRecipe):
title = 'B92'
__author__ = 'Darko Miletic'
description = 'Dnevne vesti iz Srbije i sveta'
oldest_article = 2
publisher = 'B92.net'
publisher = 'B92'
category = 'news, politics, Serbia'
oldest_article = 1
max_articles_per_feed = 100
remove_javascript = True
no_stylesheets = True
use_embedded_content = False
cover_url = 'http://static.b92.net/images/fp/logo.gif'
remove_javascript = True
encoding = 'cp1250'
language = _('Serbian')
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: serif1, serif} .article_description{font-family: sans1, sans-serif}'
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
'--comment', description
, '--category', category
, '--publisher', publisher
, '--ignore-tables'
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
keep_only_tags = [ dict(name='div', attrs={'class':'sama_vest'}) ]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True\noverride_css=" p {text-indent: 0em; margin-top: 0em; margin-bottom: 0.5em}"'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='table', attrs={'class':'maindocument'})]
remove_tags = [
dict(name='ul', attrs={'class':'comment-nav'})
,dict(name=['embed','link','base'] )
]
feeds = [
(u'Vesti', u'http://www.b92.net/info/rss/vesti.xml')
,(u'Biz' , u'http://www.b92.net/info/rss/biz.xml' )
,(u'Zivot', u'http://www.b92.net/info/rss/zivot.xml')
,(u'Sport', u'http://www.b92.net/info/rss/sport.xml')
]
def print_version(self, url):
main, sep, article_id = url.partition('nav_id=')
rmain, rsep, rrest = main.partition('.php?')
mrmain , rsepp, nnt = rmain.rpartition('/')
mprmain, rrsep, news_type = mrmain.rpartition('/')
nurl = 'http://www.b92.net/mobilni/' + news_type + '/index.php?nav_id=' + article_id
brbiz, biz, bizrest = rmain.partition('/biz/')
if biz:
nurl = 'http://www.b92.net/mobilni/biz/index.php?nav_id=' + article_id
return nurl
return url + '&version=print'
def preprocess_html(self, soup):
lng = 'sr-Latn-RS'
soup.html['xml:lang'] = lng
soup.html['lang'] = lng
mtag = '<meta http-equiv="Content-Language" content="sr-Latn-RS"/>'
del soup.body['onload']
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>'
soup.head.insert(0,mtag)
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll(name='img',align=True):
for item in soup.findAll(align=True):
del item['align']
item.insert(0,'<br /><br />')
for item in soup.findAll('font'):
item.name='p'
if item.has_key('size'):
del item['size']
return soup

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
dnevniavaz.ba
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class DnevniAvaz(BasicNewsRecipe):
title = 'Dnevni Avaz'
__author__ = 'Darko Miletic'
description = 'Latest news from Bosnia'
publisher = 'Dnevni Avaz'
category = 'news, politics, Bosnia and Herzegovina'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
cover_url = 'http://www.dnevniavaz.ba/img/logo.gif'
lang = 'bs-BA'
language = _('Bosnian')
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} img {margin-top: 0em; margin-bottom: 0.4em}"'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'id':['fullarticle-title','fullarticle-leading','fullarticle-date','fullarticle-text','articleauthor']})]
remove_tags = [dict(name=['object','link','base'])]
feeds = [
(u'Najnovije' , u'http://www.dnevniavaz.ba/rss/novo' )
,(u'Najpopularnije', u'http://www.dnevniavaz.ba/rss/popularno')
]
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Language" content="bs-BA"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup

View File

@ -1,29 +1,50 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
__copyright__ = '2008-2009, Kovid Goyal <kovid at kovidgoyal.net>, Darko Miletic <darko at gmail.com>'
'''
Profile to download FAZ.net
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class FazNet(BasicNewsRecipe):
title = 'FAZ NET'
__author__ = 'Kovid Goyal'
description = 'Frankfurter Allgemeine Zeitung'
use_embedded_content = False
language = _('German')
title = 'FAZ NET'
__author__ = 'Kovid Goyal, Darko Miletic'
description = 'Frankfurter Allgemeine Zeitung'
publisher = 'FAZ Electronic Media GmbH'
category = 'news, politics, Germany'
use_embedded_content = False
language = _('German')
max_articles_per_feed = 30
no_stylesheets = True
encoding = 'utf-8'
remove_javascript = True
preprocess_regexps = [
(re.compile(r'Zum Thema</span>.*?</BODY>', re.IGNORECASE | re.DOTALL),
lambda match : ''),
]
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [dict(name='div', attrs={'class':'Article'})]
remove_tags = [
dict(name=['object','link','embed','base'])
,dict(name='div', attrs={'class':['LinkBoxModulSmall','ModulVerlagsInfo']})
]
feeds = [ ('FAZ.NET', 'http://www.faz.net/s/Rub/Tpl~Epartner~SRss_.xml') ]
def print_version(self, url):
return url.replace('.html?rss_aktuell', '~Afor~Eprint.html')
article, sep, rest = url.partition('?')
return article.replace('.html', '~Afor~Eprint.html')
def preprocess_html(self, soup):
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
soup.head.insert(0,mtag)
del soup.body['onload']
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
'''
glassrpske.com
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class GlasSrpske(BasicNewsRecipe):
title = 'Glas Srpske'
__author__ = 'Darko Miletic'
description = 'Latest news from republika srpska'
publisher = 'GLAS SRPSKE'
category = 'Novine, Dnevne novine, Vijesti, Novosti, Ekonomija, Sport, Crna Hronika, Banja Luka,, Republika Srpska, Bosna i Hercegovina'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
remove_javascript = True
cover_url = 'http://www.glassrpske.com/var/slike/glassrpske-logo.png'
lang = 'sr-BA'
language = _('Serbian')
INDEX = 'http://www.glassrpske.com'
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} img {margin-top: 0em; margin-bottom: 0.4em}"'
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'class':'gl_cv paragraf'})]
remove_tags = [dict(name=['object','link','base'])]
feeds = [
(u'Novosti' , u'http://www.glassrpske.com/vijest/2/novosti/lat/' )
,(u'Drustvo' , u'http://www.glassrpske.com/vijest/3/drustvo/lat/' )
,(u'Biznis' , u'http://www.glassrpske.com/vijest/4/ekonomija/lat/' )
,(u'Kroz RS' , u'http://www.glassrpske.com/vijest/5/krozrs/lat/' )
,(u'Hronika' , u'http://www.glassrpske.com/vijest/6/hronika/lat/' )
,(u'Srbija' , u'http://www.glassrpske.com/vijest/8/srbija/lat/' )
,(u'Region' , u'http://www.glassrpske.com/vijest/18/region/lat/' )
,(u'Svijet' , u'http://www.glassrpske.com/vijest/12/svijet/lat/' )
,(u'Kultura' , u'http://www.glassrpske.com/vijest/9/kultura/lat/' )
,(u'Banja Luka', u'http://www.glassrpske.com/vijest/10/banjaluka/lat/')
,(u'Jet Set' , u'http://www.glassrpske.com/vijest/11/jetset/lat/' )
,(u'Muzika' , u'http://www.glassrpske.com/vijest/19/muzika/lat/' )
,(u'Sport' , u'http://www.glassrpske.com/vijest/13/sport/lat/' )
,(u'Kolumne' , u'http://www.glassrpske.com/vijest/16/kolumne/lat/' )
,(u'Plus' , u'http://www.glassrpske.com/vijest/7/plus/lat/' )
]
def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Language" content="sr-BA"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">'
soup.head.insert(0,mtag)
return soup
def parse_index(self):
totalfeeds = []
lfeeds = self.get_feeds()
for feedobj in lfeeds:
feedtitle, feedurl = feedobj
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
soup = self.index_to_soup(feedurl)
for item in soup.findAll('div', attrs={'class':'gl_rub'}):
atag = item.find('a')
ptag = item.find('p')
datetag = item.find('span')
url = self.INDEX + atag['href']
title = self.tag_to_string(atag)
description = self.tag_to_string(ptag)
date,sep,rest = self.tag_to_string(ptag).partition('|')
articles.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append((feedtitle, articles))
return totalfeeds

View File

@ -15,6 +15,7 @@ class Joelonsoftware(BasicNewsRecipe):
language = _('English')
no_stylesheets = True
use_embedded_content = True
oldest_article = 60
cover_url = 'http://www.joelonsoftware.com/RssJoelOnSoftware.jpg'

View File

@ -8,7 +8,7 @@ Fetch Spiegel Online.
import re
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class SpeigelOnline(BasicNewsRecipe):
@ -36,3 +36,12 @@ class SpeigelOnline(BasicNewsRecipe):
tokens = url.split(',')
tokens[-2:-2] = ['druck|']
return ','.join(tokens).replace('|,','-')
def postprocess_html(self, soup, first_fetch):
if soup.contents[0].name == 'head':
x = BeautifulSoup('<html></html>')
for y in reversed(soup.contents):
x.contents[0].insert(0, y)
soup = x
return soup

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,85 @@
changes:
date: 2008-09-17
change: Enabled the ability to override the default template names.
date: 2008-08-26
change: Upgraded post_detail.html to now use new Django refactored comments. Sidenote: basic.remarks have been removed.
date: 2008-07-14
change: Removed get_query_set from Blog manager to fix a problem where saving a post marked as Draft would not save.
change: Added a get_previous_post and get_next_post method for front end template. These will not return Draft posts.
date: 2008-06-17
change: BlogPostFeed is now BlogPostsFeed and there is a new BlogPostsByCategory.
date: 2008-05-18
change: Converted everything to 4 space tabs and made a few other changes to comply with Python Style Guide.
date: 2008-04-23
change: Added an inline admin interface helper for choosing inlines to go into posts.
change: The inline app is now a dependancy of the blog.
date: 2008-04-22
change: Removed the 'render_inlines' filter from the Blog template tags. The tag is now in an app called inlines which can be used with any django app.
date: 2008-02-27
change: Added 'allow_comments' field to the Post model.
change: Removed 'Closed' choice from status field of Post model
date: 2008-02-18
fix: Fixed feed pointing to hardcoded url.
date: 2008-02-15
change: Internationalized models
date: 2008-02-04
change: Added 'get_links' template filter.
change: Templates: added a {% block content_title %}
date: 2008-02-02
change: Added a sitemap
date: 2008-01-30
change: Renamed 'do_inlines' filter to 'render_inlines'
date: 2008-01-29
change: BeautifulSoup is no longer a dependancy unless you want to use the do_inlines filter.
date: 2008-01-27
fix: removed 'tagging.register(Post)' from model. It was causing too many unnecessary SQL JOINS.
change: Changed the inlines tag to a filter. (Example: {{ object.text|do_inlines }})
date: 2008-01-22
change: Registered the Post model with the tagging app
date: 2008-01-19
change: Renamed the 'list' class to 'link_list'
date: 2008-01-09
change: Changed urls.py so you can have /posts/page/2/ or /posts/?page=2
date: 2008-01-07
change: Removed PublicPostManager in favor of ManagerWithPublished.
change: Made wrappers for generic views.
date: 2008-01-06
fix: In blog.py changed 'beautifulsoup' to 'BeautifulSoup'
date: 2007-12-31
change: Changed some syntax in managers.py to hopefully fix a bug.
change: Removed an inline template that didn't belong.
date: 2007-12-21
change: Added markup tag that formats inlines.
date: 2007-12-12
change: Cleaned up unit tests.
date: 2007-12-11
change: Add documentation to templatetags and views.
change: Smartened up the previous/next blog part of the post_detail.html template.
date: 2007-12-09
change: Added feed templates and wrapped up feeds.py.
change: Changed Post.live manager to Post.public
change: Added a search view along with templates

View File

@ -0,0 +1,18 @@
===========================================
Django Basic Blog
http://code.google.com/p/django-basic-apps/
===========================================
A simple blog application for Django projects.
To install this app, simply create a folder somewhere in
your PYTHONPATH named 'basic' and place the 'blog'
app inside. Then add 'basic.blog' to your projects
INSTALLED_APPS list in your settings.py file.
=== Dependancies ===
* Basic Inlines
* [http://www.djangoproject.com/documentation/add_ons/#comments Django Comments]
* [http://code.google.com/p/django-tagging Django Tagging]
* [http://www.djangoproject.com/documentation/add_ons/#markup Markup]
* [http://www.crummy.com/software/BeautifulSoup/ BeautifulSoup] - only if you want to use the [http://code.google.com/p/django-basic-blog/wiki/BlogInlinesProposal render_inlines] filter, otherwise it's not necessary.

View File

View File

@ -0,0 +1,17 @@
from django.contrib import admin
from calibre.www.apps.blog.models import *
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Category, CategoryAdmin)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'publish', 'status')
list_filter = ('publish', 'categories', 'status')
search_fields = ('title', 'body')
prepopulated_fields = {'slug': ('title',)}
admin.site.register(Post, PostAdmin)

View File

@ -0,0 +1,42 @@
from django.contrib.syndication.feeds import FeedDoesNotExist
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.sites.models import Site
from django.contrib.syndication.feeds import Feed
from django.core.urlresolvers import reverse
from calibre.www.apps.blog.models import Post, Category
class BlogPostsFeed(Feed):
_site = Site.objects.get_current()
title = '%s feed' % _site.name
description = '%s posts feed.' % _site.name
def link(self):
return reverse('blog_index')
def items(self):
return Post.objects.published()[:10]
def item_pubdate(self, obj):
return obj.publish
class BlogPostsByCategory(Feed):
_site = Site.objects.get_current()
title = '%s posts category feed' % _site.name
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
return Category.objects.get(slug__exact=bits[0])
def link(self, obj):
if not obj:
raise FeedDoesNotExist
return obj.get_absolute_url()
def description(self, obj):
return "Posts recently categorized as %s" % obj.title
def items(self, obj):
return obj.post_set.published()[:10]

View File

@ -0,0 +1,9 @@
from django.db.models import Manager
import datetime
class PublicManager(Manager):
"""Returns published posts that are not in the future."""
def published(self):
return self.get_query_set().filter(status__gte=2, publish__lte=datetime.datetime.now())

View File

@ -0,0 +1,80 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.db.models import permalink
from django.contrib.auth.models import User
from calibre.www.apps.tagging.fields import TagField
from calibre.www.apps.blog.managers import PublicManager
import calibre.www.apps.tagging as tagging
class Category(models.Model):
"""Category model."""
title = models.CharField(_('title'), max_length=100)
slug = models.SlugField(_('slug'), unique=True)
class Meta:
verbose_name = _('category')
verbose_name_plural = _('categories')
db_table = 'blog_categories'
ordering = ('title',)
class Admin:
pass
def __unicode__(self):
return u'%s' % self.title
@permalink
def get_absolute_url(self):
return ('blog_category_detail', None, {'slug': self.slug})
class Post(models.Model):
"""Post model."""
STATUS_CHOICES = (
(1, _('Draft')),
(2, _('Public')),
)
title = models.CharField(_('title'), max_length=200)
slug = models.SlugField(_('slug'), unique_for_date='publish')
author = models.ForeignKey(User, blank=True, null=True)
body = models.TextField(_('body'))
tease = models.TextField(_('tease'), blank=True)
status = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=2)
allow_comments = models.BooleanField(_('allow comments'), default=True)
publish = models.DateTimeField(_('publish'))
created = models.DateTimeField(_('created'), auto_now_add=True)
modified = models.DateTimeField(_('modified'), auto_now=True)
categories = models.ManyToManyField(Category, blank=True)
tags = TagField()
objects = PublicManager()
class Meta:
verbose_name = _('post')
verbose_name_plural = _('posts')
db_table = 'blog_posts'
ordering = ('-publish',)
get_latest_by = 'publish'
class Admin:
list_display = ('title', 'publish', 'status')
list_filter = ('publish', 'categories', 'status')
search_fields = ('title', 'body')
def __unicode__(self):
return u'%s' % self.title
@permalink
def get_absolute_url(self):
return ('blog_detail', None, {
'year': self.publish.year,
'month': self.publish.strftime('%b').lower(),
'day': self.publish.day,
'slug': self.slug
})
def get_previous_post(self):
return self.get_previous_by_publish(status__gte=2)
def get_next_post(self):
return self.get_next_by_publish(status__gte=2)

View File

@ -0,0 +1,13 @@
from django.contrib.sitemaps import Sitemap
from calibre.www.apps.blog.models import Post
class BlogSitemap(Sitemap):
changefreq = "never"
priority = 0.5
def items(self):
return Post.objects.published()
def lastmod(self, obj):
return obj.publish

View File

@ -0,0 +1,56 @@
{% extends "admin/change_form.html" %}
{% block extrahead %}
{% load adminmedia inlines %}
{{ block.super }}
<script type="text/javascript">
function InlineInit() {
var body_div = document.getElementById('id_body').parentNode;
var content = ''
content += '{% get_inline_types as inline_list %}'
content += '<label>Body inlines:</label>'
content += '<strong>Inline type:</strong> '
content += '<select id="id_inline_content_type" onchange="document.getElementById(\'lookup_id_inline\').href = \'../../../\'+this.value+\'/\';" style="margin-right:20px;">'
content += ' <option>----------</option>'
content += ' {% for inline in inline_list %}'
content += ' <option value="{{ inline.content_type.app_label }}/{{ inline.content_type.model }}">{{ inline.content_type.app_label|capfirst }}: {{ inline.content_type.model|capfirst }}</option>'
content += ' {% endfor %}'
content += '</select> '
content += '<strong>Object:</strong> '
content += '<input type="text" class="vIntegerField" id="id_inline" size="10" /> '
content += '<a id="lookup_id_inline" href="#" class="related-lookup" onclick="if(document.getElementById(\'id_inline_content_type\').value != \'----------\') { return showRelatedObjectLookupPopup(this); }" style="margin-right:20px;"><img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Loopup" /></a> '
content += '<strong>Class:</strong> '
content += '<select id="id_inline_class">'
content += ' <option value="small_left">Small left</option>'
content += ' <option value="small_right">Small right</option>'
content += ' <option value="medium_left">Medium left</option>'
content += ' <option value="medium_right">Medium right</option>'
content += ' <option value="large_left">Large left</option>'
content += ' <option value="large_right">Large right</option>'
content += ' <option value="full">Full</option>'
content += '</select>'
content += '<input type="button" value="Add" style="margin-left:10px;" onclick="return insertInline(document.getElementById(\'id_inline_content_type\').value, document.getElementById(\'id_inline\').value, document.getElementById(\'id_inline_class\').value)" />'
content += '<p class="help">Insert inlines into your body by choosing an inline type, then an object, then a class.</p>'
var div = document.createElement('div');
div.setAttribute('style', 'margin-top:10px;');
div.innerHTML = content;
body_div.insertBefore(div);
}
function insertInline(type, id, classname) {
if (type != '----------' && id != '') {
inline = '<inline type="'+type.replace('/', '.')+'" id="'+id+'" class="'+classname+'" />';
body = document.getElementById('id_body');
body.value = body.value + inline + '\n';
}
}
addEvent(window, 'load', InlineInit);
</script>
{% endblock %}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body id="{% block body_id %}{% endblock %}">
<div id="body">
{% block body %}
<div>
{% block content_title %}{% endblock %}
</div>
<div class="content">
{% block content %}{% endblock %}
</div>
{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block body_class %}blog{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "blog/base_blog.html" %}
{% block title %}Posts for {{ category.title }}{% endblock %}
{% block body_class %}{{ block.super }} category_detail{% endblock %}
{% block body_id %}category_{{ category.id }}{% endblock %}
{% block content_title %}
<h2>Posts for {{ category.title }}</h2>
{% endblock %}
{% block content %}
{% load markup %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post categories{% endblock %}
{% block body_class %}{{ block.super }} category_list{% endblock %}
{% block content_title %}
<h2>Post categories</h2>
{% endblock %}
{% block content %}
{% load markup %}
<ul class="link_list">
{% for category in object_list %}
<li><a href="{{ category.get_absolute_url }}">{{ category }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive for {{ day|date:"d F Y" }}{% endblock %}
{% block body_class %}{{ block.super }} post_archive_day{% endblock %}
{% block content_title %}
<h2>Post archive for {{ day|date:"d F Y" }}</h2>
{% endblock %}
{% block content %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive for {{ month|date:"F Y" }}{% endblock %}
{% block body_class %}{{ block.super }} post_archive_month{% endblock %}
{% block content_title %}
<h2>Post archive for {{ month|date:"F Y" }}</h2>
{% endblock %}
{% block content %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive for {{ year }}{% endblock %}
{% block body_class %}{{ block.super }} post_archive_year{% endblock %}
{% block content_title %}
<h2>Post archive for {{ year }}</h2>
{% endblock %}
{% block content %}
{% load markup %}
<ul class="link_list">
{% for month in date_list %}
<li><a href="{% url blog_index %}{{ year }}/{{ month|date:"b" }}/">{{ month|date:"F" }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,67 @@
{% extends "blog/base_blog.html" %}
{% block title %}{{ object.title }}{% endblock %}
{% block body_class %}{{ block.super }} post_detail{% endblock %}
{% block body_id %}post_{{ object.id }}{% endblock %}
{% block content_title %}
<h2>{{ object.title }}</h2>
<p class="other_posts">
{% if object.get_previous_by_publish %}
<a class="previous" href="{{ object.get_previous_post.get_absolute_url }}">&laquo; {{ object.get_previous_post }}</a>
{% endif %}
{% if object.get_next_by_publish %}
| <a class="next" href="{{ object.get_next_post.get_absolute_url }}">{{ object.get_next_post }} &raquo;</a>
{% endif %}
</p>
{% endblock %}
{% block content %}
{% load blog markup comments tagging_tags %}
<p class="date">{{ object.publish|date:"j F Y" }}</p>
<div class="body">
{{ object.body|markdown:"safe" }}
</div>
{% tags_for_object object as tag_list %}
{% if tag_list %}
<p class="inline_tag_list"><strong>Related tags:</strong>
{% for tag in tag_list %}
{{ tag }}{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% get_comment_list for object as comment_list %}
{% if comment_list %}
<div id="comments">
<a name="comments"></a>
<h3 class="comments_title">Comments</h3>
{% for comment in comment_list %}
{% if comment.is_public %}
<div class="comment">
<h5 class="name">
<a name="c{{ comment.id }}" href="{{ comment.get_absolute_url }}" title="Permalink to {{ comment.person_name }}'s comment" class="count">{{ forloop.counter }}</a>
{% if comment.user_url %}<a href="{{ comment.user_url }}">{{ comment.user_name }}</a>{% else %}{{ comment.user_name }}{% endif %} says...
</h5>
{{ comment.comment|urlizetrunc:"60"|markdown:"safe" }}
<p class="date">Posted at {{ comment.submit_date|date:"P" }} on {{ comment.submit_date|date:"F j, Y" }}</p>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if object.allow_comments %}
{% render_comment_form for object %}
{% else %}
<div id="comment_form">
<h3>Comments are closed.</h3>
<p>Comments have been close for this post.</p>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post archive{% endblock %}
{% block body_class %}{{ block.super }} post_list{% endblock %}
{% block content_title %}
<h2>Post archive</h2>
{% endblock %}
{% block content %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
</div>
{% endfor %}
</div>
{% if is_paginated %}
<p class="pagination">
{% if has_next %}
<a class="older" href="?page={{ next }}">Older</a>
{% endif %}
{% if has_next and has_previous %} | {% endif %}
{% if has_previous %}
<a class="newer" href="?page={{ previous }}">Newer</a>
{% endif %}
</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "blog/base_blog.html" %}
{% block title %}Post search{% endblock %}
{% block body_class %}{{ block.super }} post_search{% endblock %}
{% block content_title %}
<h2>Search</h2>
{% endblock %}
{% block content %}
<form action="." method="get" id="post_search_form">
<p>
<input type="text" name="q" value="{{ search_term }}" id="search">
<input type="submit" class="button" value="Search">
</p>
</form>
{% if message %}
<p class="message">{{ message }}</p>
{% endif %}
{% if object_list %}
<div class="post_list">
{% for post in object_list %}
<div>
<h3 class="title"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h3>
<p class="date">{{ post.publish|date:"Y F d" }}</p>
<p class="tease">{{ post.tease }}</p>
<p class="comments">{% if comment_count %}{{ comment_count }} comment{{ comment_count|pluralize }}{% endif %}</p>
</div>
{% endfor %}
</div>
{% endif %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More