mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Pull from trunk
This commit is contained in:
commit
83dedd68fc
@ -29,3 +29,4 @@ src/cssutils/scripts/
|
||||
src/cssutils/css/.svn/
|
||||
src/cssutils/stylesheets/.svn/
|
||||
src/odf/.svn
|
||||
tags
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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.''',
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
16
session.vim
Normal 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
|
@ -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.
|
||||
|
@ -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):
|
||||
|
@ -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) :
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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 : ' '),
|
||||
|
||||
]
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,6 +611,10 @@ class Manifest(object):
|
||||
elif not namespace(data.tag):
|
||||
data.attrib['xmlns'] = XHTML_NS
|
||||
data = etree.tostring(data, encoding=unicode)
|
||||
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
|
||||
@ -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
@ -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
|
||||
|
||||
|
@ -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])
|
||||
|
||||
|
||||
|
@ -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())
|
||||
@ -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,7 +623,8 @@ 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'),
|
||||
@ -440,7 +632,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
||||
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_()
|
||||
|
@ -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 &from:</string>
|
||||
</property>
|
||||
<property name="buddy" >
|
||||
<cstring>email_from</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="email_from" >
|
||||
<property name="toolTip" >
|
||||
<string><p>This is what will be present in the From: field of emails sent by calibre.<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>&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 &default</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="email_remove" >
|
||||
<property name="text" >
|
||||
<string>&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><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 &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 <b>optionally</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>&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>&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>&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>&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>&Show</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" >
|
||||
<widget class="QLabel" name="label_21" >
|
||||
<property name="text" >
|
||||
<string>&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>&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>&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>
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -23,7 +23,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="tlabel" >
|
||||
<property name="text" >
|
||||
<string><p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</a> and exter you access key below.</string>
|
||||
<string><p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</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" >
|
||||
|
@ -50,7 +50,10 @@ class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
self.running_time_timer.start(1000)
|
||||
|
||||
def update_running_time(self, *args):
|
||||
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():
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,13 +361,38 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
return
|
||||
self.fetch_cover_button.setEnabled(False)
|
||||
self.setCursor(Qt.WaitCursor)
|
||||
QCoreApplication.instance().processEvents()
|
||||
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_()
|
||||
|
||||
def hangcheck(self):
|
||||
if not (self.cover_fetcher.isFinished() or time.time()-self.cf_start_time > 150):
|
||||
return
|
||||
|
||||
self._hangcheck.stop()
|
||||
try:
|
||||
login(d.username(), d.password(), force=False)
|
||||
cover_data = cover_from_isbn(isbn, timeout=self.timeout)[0]
|
||||
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(cover_data)
|
||||
pix.loadFromData(self.cover_fetcher.cover_data)
|
||||
if pix.isNull():
|
||||
error_dialog(self.window, _('Bad cover'),
|
||||
_('The cover is not a valid picture')).exec_()
|
||||
@ -352,16 +400,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
||||
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()
|
||||
|
||||
else:
|
||||
error_dialog(self, _('Cannot fetch cover'),
|
||||
_('You must specify the ISBN identifier for this book.')).exec_()
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
def fetch_metadata(self):
|
||||
@ -371,6 +413,7 @@ 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)
|
||||
with d:
|
||||
d.exec_()
|
||||
if d.result() == QDialog.Accepted:
|
||||
book = d.selected_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)
|
||||
|
BIN
src/calibre/gui2/images/gmail_logo.png
Normal file
BIN
src/calibre/gui2/images/gmail_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
270
src/calibre/gui2/images/mail.svg
Normal file
270
src/calibre/gui2/images/mail.svg
Normal 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 |
BIN
src/calibre/gui2/images/news/24sata_rs.png
Normal file
BIN
src/calibre/gui2/images/news/24sata_rs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 830 B |
BIN
src/calibre/gui2/images/news/dnevni_avaz.png
Normal file
BIN
src/calibre/gui2/images/news/dnevni_avaz.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 811 B |
BIN
src/calibre/gui2/images/news/glas_srpske.png
Normal file
BIN
src/calibre/gui2/images/news/glas_srpske.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 388 B |
@ -135,7 +135,7 @@ 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
|
||||
|
@ -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
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0" >
|
||||
<author>Kovid Goyal</author>
|
||||
<class>MainWindow</class>
|
||||
@ -12,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -34,7 +33,7 @@
|
||||
<item>
|
||||
<widget class="LocationView" name="location_view" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -51,12 +50,21 @@
|
||||
<property name="horizontalScrollBarPolicy" >
|
||||
<enum>Qt::ScrollBarAsNeeded</enum>
|
||||
</property>
|
||||
<property name="editTriggers" >
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="tabKeyNavigation" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode" >
|
||||
<enum>QAbstractItemView::NoSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior" >
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="iconSize" >
|
||||
<size>
|
||||
<width>40</width>
|
||||
@ -111,7 +119,7 @@
|
||||
<item>
|
||||
<widget class="QLabel" name="vanity" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<sizepolicy vsizetype="Preferred" hsizetype="Preferred" >
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -196,7 +204,7 @@
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<sizepolicy vsizetype="Fixed" hsizetype="Expanding" >
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
@ -205,10 +213,10 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip" >
|
||||
<string>Search the list of books by title or author<br><br>Words separated by spaces are ANDed</string>
|
||||
<string>Search the list of books by title or author<br><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<br><br>Words separated by spaces are ANDed</string>
|
||||
<string>Search the list of books by title, author, publisher, tags and comments<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="autoFillBackground" >
|
||||
<bool>false</bool>
|
||||
@ -274,7 +282,7 @@
|
||||
<item row="2" column="0" >
|
||||
<widget class="QStackedWidget" name="stack" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>100</verstretch>
|
||||
</sizepolicy>
|
||||
@ -336,7 +344,7 @@
|
||||
<item>
|
||||
<widget class="BooksView" name="library_view" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
@ -376,7 +384,7 @@
|
||||
<item row="0" column="0" >
|
||||
<widget class="DeviceBooksView" name="memory_view" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<sizepolicy vsizetype="Expanding" hsizetype="Expanding" >
|
||||
<horstretch>100</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
@ -414,7 +422,7 @@
|
||||
<item row="0" column="0" >
|
||||
<widget class="DeviceBooksView" name="card_view" >
|
||||
<property name="sizePolicy" >
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<sizepolicy vsizetype="Expanding" hsizetype="Preferred" >
|
||||
<horstretch>10</horstretch>
|
||||
<verstretch>10</verstretch>
|
||||
</sizepolicy>
|
||||
@ -658,15 +666,6 @@
|
||||
<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" >
|
||||
|
@ -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):
|
||||
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:
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
6124
src/calibre/translations/da.po
Normal file
6124
src/calibre/translations/da.po
Normal file
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
6076
src/calibre/translations/ja.po
Normal file
6076
src/calibre/translations/ja.po
Normal file
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
238
src/calibre/utils/smtp.py
Normal 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())
|
@ -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)
|
||||
|
@ -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
|
||||
|
52
src/calibre/web/feeds/recipes/recipe_24sata_rs.py
Normal file
52
src/calibre/web/feeds/recipes/recipe_24sata_rs.py
Normal 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')
|
||||
|
@ -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,16 +12,16 @@ 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
|
||||
@ -31,39 +30,35 @@ class B92(BasicNewsRecipe):
|
||||
, '--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
|
||||
|
55
src/calibre/web/feeds/recipes/recipe_dnevni_avaz.py
Normal file
55
src/calibre/web/feeds/recipes/recipe_dnevni_avaz.py
Normal 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
|
@ -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'
|
||||
__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
|
||||
|
96
src/calibre/web/feeds/recipes/recipe_glas_srpske.py
Normal file
96
src/calibre/web/feeds/recipes/recipe_glas_srpske.py
Normal 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
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
10
src/calibre/www/__init__.py
Normal file
10
src/calibre/www/__init__.py
Normal 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'
|
||||
|
||||
|
||||
|
10
src/calibre/www/apps/__init__.py
Normal file
10
src/calibre/www/apps/__init__.py
Normal 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'
|
||||
|
||||
|
||||
|
85
src/calibre/www/apps/blog/CHANGELOG.yml
Normal file
85
src/calibre/www/apps/blog/CHANGELOG.yml
Normal 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
|
18
src/calibre/www/apps/blog/README.txt
Normal file
18
src/calibre/www/apps/blog/README.txt
Normal 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.
|
0
src/calibre/www/apps/blog/__init__.py
Normal file
0
src/calibre/www/apps/blog/__init__.py
Normal file
17
src/calibre/www/apps/blog/admin.py
Normal file
17
src/calibre/www/apps/blog/admin.py
Normal 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)
|
42
src/calibre/www/apps/blog/feeds.py
Normal file
42
src/calibre/www/apps/blog/feeds.py
Normal 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]
|
9
src/calibre/www/apps/blog/managers.py
Normal file
9
src/calibre/www/apps/blog/managers.py
Normal 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())
|
80
src/calibre/www/apps/blog/models.py
Normal file
80
src/calibre/www/apps/blog/models.py
Normal 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)
|
13
src/calibre/www/apps/blog/sitemap.py
Normal file
13
src/calibre/www/apps/blog/sitemap.py
Normal 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
|
@ -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 %}
|
21
src/calibre/www/apps/blog/templates/base.html
Normal file
21
src/calibre/www/apps/blog/templates/base.html
Normal 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>
|
4
src/calibre/www/apps/blog/templates/blog/base_blog.html
Normal file
4
src/calibre/www/apps/blog/templates/blog/base_blog.html
Normal file
@ -0,0 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block body_class %}blog{% endblock %}
|
@ -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 %}
|
20
src/calibre/www/apps/blog/templates/blog/category_list.html
Normal file
20
src/calibre/www/apps/blog/templates/blog/category_list.html
Normal 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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
67
src/calibre/www/apps/blog/templates/blog/post_detail.html
Normal file
67
src/calibre/www/apps/blog/templates/blog/post_detail.html
Normal 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 }}">« {{ 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 }} »</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 %}
|
35
src/calibre/www/apps/blog/templates/blog/post_list.html
Normal file
35
src/calibre/www/apps/blog/templates/blog/post_list.html
Normal 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 %}
|
37
src/calibre/www/apps/blog/templates/blog/post_search.html
Normal file
37
src/calibre/www/apps/blog/templates/blog/post_search.html
Normal 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
Loading…
x
Reference in New Issue
Block a user