mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
f72286d44a
@ -1,64 +1,102 @@
|
|||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008 Kovid Goyal kovid@kovidgoyal.net, 2010 Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
http://www.businessweek.com/magazine/news/articles/business_news.htm
|
www.businessweek.com
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from calibre import strftime
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class BWmagazine(BasicNewsRecipe):
|
class BusinessWeek(BasicNewsRecipe):
|
||||||
title = 'BusinessWeek Magazine'
|
title = 'Business Week'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Kovid Goyal and Darko Miletic'
|
||||||
description = 'Stay up to date with BusinessWeek magazine articles. Read news on international business, personal finances & the economy in the BusinessWeek online magazine.'
|
description = 'Read the latest international business news & stock market news. Get updated company profiles, financial advice, global economy and technology news.'
|
||||||
publisher = 'Bloomberg L.P.'
|
publisher = 'Bloomberg L.P.'
|
||||||
category = 'news, International Business News, current news in international business,international business articles, personal business, business week magazine, business week magazine articles, business week magazine online, business week online magazine'
|
category = 'Business, business news, stock market, stock market news, financial advice, company profiles, financial advice, global economy, technology news'
|
||||||
oldest_article = 10
|
oldest_article = 7
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 200
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf-8'
|
encoding = 'utf8'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'en'
|
language = 'en'
|
||||||
INDEX = 'http://www.businessweek.com/magazine/news/articles/business_news.htm'
|
remove_empty_feeds = True
|
||||||
|
publication_type = 'magazine'
|
||||||
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
|
cover_url = 'http://images.businessweek.com/mz/covers/current_120x160.jpg'
|
||||||
|
masthead_url = 'http://assets.businessweek.com/images/bw-logo.png'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Helvetica,Arial,sans-serif }
|
||||||
|
img{margin-bottom: 0.4em; display:block}
|
||||||
|
.tagline{color: gray; font-style: italic}
|
||||||
|
.photoCredit{font-size: small; color: gray}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
, 'tags' : category
|
, 'tags' : category
|
||||||
, 'publisher' : publisher
|
, 'publisher' : publisher
|
||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(attrs={'class':'inStory'})
|
||||||
|
,dict(name=['meta','link','iframe','base','embed','object','table','th','tr','td'])
|
||||||
|
,dict(attrs={'id':['inset','videoDisplay']})
|
||||||
|
]
|
||||||
|
keep_only_tags = [dict(name='div', attrs={'id':['story-body','storyBody']})]
|
||||||
|
remove_attributes = ['lang']
|
||||||
|
match_regexps = [r'http://www.businessweek.com/.*_page_[1-9].*']
|
||||||
|
|
||||||
def parse_index(self):
|
|
||||||
articles = []
|
|
||||||
soup = self.index_to_soup(self.INDEX)
|
|
||||||
ditem = soup.find('div',attrs={'id':'column2'})
|
|
||||||
if ditem:
|
|
||||||
for item in ditem.findAll('h3'):
|
|
||||||
title_prefix = ''
|
|
||||||
description = ''
|
|
||||||
feed_link = item.find('a')
|
|
||||||
if feed_link and feed_link.has_key('href'):
|
|
||||||
url = 'http://www.businessweek.com/magazine/' + feed_link['href'].partition('../../')[2]
|
|
||||||
title = title_prefix + self.tag_to_string(feed_link)
|
|
||||||
date = strftime(self.timefmt)
|
|
||||||
articles.append({
|
|
||||||
'title' :title
|
|
||||||
,'date' :date
|
|
||||||
,'url' :url
|
|
||||||
,'description':description
|
|
||||||
})
|
|
||||||
return [(soup.head.title.string, articles)]
|
|
||||||
|
|
||||||
keep_only_tags = dict(name='div', attrs={'id':'storyBody'})
|
feeds = [
|
||||||
|
(u'Top Stories', u'http://www.businessweek.com/topStories/rss/topStories.rss'),
|
||||||
|
(u'Top News' , u'http://www.businessweek.com/rss/bwdaily.rss' ),
|
||||||
|
(u'Asia', u'http://www.businessweek.com/rss/asia.rss'),
|
||||||
|
(u'Autos', u'http://www.businessweek.com/rss/autos/index.rss'),
|
||||||
|
(u'Classic Cars', u'http://rss.businessweek.com/bw_rss/classiccars'),
|
||||||
|
(u'Hybrids', u'http://rss.businessweek.com/bw_rss/hybrids'),
|
||||||
|
(u'Europe', u'http://www.businessweek.com/rss/europe.rss'),
|
||||||
|
(u'Auto Reviews', u'http://rss.businessweek.com/bw_rss/autoreviews'),
|
||||||
|
(u'Innovation & Design', u'http://www.businessweek.com/rss/innovate.rss'),
|
||||||
|
(u'Architecture', u'http://www.businessweek.com/rss/architecture.rss'),
|
||||||
|
(u'Brand Equity', u'http://www.businessweek.com/rss/brandequity.rss'),
|
||||||
|
(u'Auto Design', u'http://www.businessweek.com/rss/carbuff.rss'),
|
||||||
|
(u'Game Room', u'http://rss.businessweek.com/bw_rss/gameroom'),
|
||||||
|
(u'Technology', u'http://www.businessweek.com/rss/technology.rss'),
|
||||||
|
(u'Investing', u'http://rss.businessweek.com/bw_rss/investor'),
|
||||||
|
(u'Small Business', u'http://www.businessweek.com/rss/smallbiz.rss'),
|
||||||
|
(u'Careers', u'http://rss.businessweek.com/bw_rss/careers'),
|
||||||
|
(u'B-Schools', u'http://www.businessweek.com/rss/bschools.rss'),
|
||||||
|
(u'Magazine Selections', u'http://www.businessweek.com/rss/magazine.rss'),
|
||||||
|
(u'CEO Guide to Tech', u'http://www.businessweek.com/rss/ceo_guide_tech.rss'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
url = article.get('guid', None)
|
||||||
|
if 'podcasts' in url:
|
||||||
|
return None
|
||||||
|
if 'surveys' in url:
|
||||||
|
return None
|
||||||
|
if 'images' in url:
|
||||||
|
return None
|
||||||
|
if 'feedroom' in url:
|
||||||
|
return None
|
||||||
|
if '/magazine/toc/' in url:
|
||||||
|
return None
|
||||||
|
rurl, sep, rest = url.rpartition('?')
|
||||||
|
if rurl:
|
||||||
|
return rurl
|
||||||
|
return rest
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
rurl = url.rpartition('?')[0]
|
if '/news/' in url or '/blog/ in url':
|
||||||
if rurl == '':
|
return url
|
||||||
rurl = url
|
rurl = url.replace('http://www.businessweek.com/','http://www.businessweek.com/print/')
|
||||||
return rurl.replace('.com/magazine/','.com/print/magazine/')
|
return rurl.replace('/investing/','/investor/')
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for alink in soup.findAll('a'):
|
||||||
|
if alink.string is not None:
|
||||||
|
tstr = alink.string
|
||||||
|
alink.replaceWith(tstr)
|
||||||
|
return soup
|
||||||
|
@ -4,7 +4,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
|
|||||||
class LeMonde(BasicNewsRecipe):
|
class LeMonde(BasicNewsRecipe):
|
||||||
title = 'Le Monde'
|
title = 'Le Monde'
|
||||||
__author__ = 'veezh'
|
__author__ = 'veezh'
|
||||||
description = 'Actualités'
|
description = u'Actualit\xe9s'
|
||||||
oldest_article = 1
|
oldest_article = 1
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
58
resources/recipes/nrc-nl-epub.recipe
Normal file
58
resources/recipes/nrc-nl-epub.recipe
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#Based on Lars Jacob's Taz Digiabo recipe
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, veezh'
|
||||||
|
|
||||||
|
'''
|
||||||
|
www.nrc.nl
|
||||||
|
'''
|
||||||
|
import os, urllib2, zipfile
|
||||||
|
import time
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
|
||||||
|
|
||||||
|
class NRCHandelsblad(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'NRC Handelsblad'
|
||||||
|
description = u'De EPUB-versie van NRC'
|
||||||
|
language = 'nl'
|
||||||
|
lang = 'nl-NL'
|
||||||
|
|
||||||
|
__author__ = 'veezh'
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'no_default_epub_cover' : True
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_index(self):
|
||||||
|
today = time.strftime("%Y%m%d")
|
||||||
|
domain = "http://digitaleeditie.nrc.nl"
|
||||||
|
|
||||||
|
url = domain + "/digitaleeditie/helekrant/epub/nrc_" + today + ".epub"
|
||||||
|
# print url
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = urllib2.urlopen(url)
|
||||||
|
except urllib2.HTTPError:
|
||||||
|
self.report_progress(0,_('Kan niet inloggen om editie te downloaden'))
|
||||||
|
raise ValueError('Krant van vandaag nog niet beschikbaar')
|
||||||
|
|
||||||
|
tmp = PersistentTemporaryFile(suffix='.epub')
|
||||||
|
self.report_progress(0,_('downloading epub'))
|
||||||
|
tmp.write(f.read())
|
||||||
|
tmp.close()
|
||||||
|
|
||||||
|
zfile = zipfile.ZipFile(tmp.name, 'r')
|
||||||
|
self.report_progress(0,_('extracting epub'))
|
||||||
|
|
||||||
|
zfile.extractall(self.output_dir)
|
||||||
|
|
||||||
|
tmp.close()
|
||||||
|
index = os.path.join(self.output_dir, 'content.opf')
|
||||||
|
|
||||||
|
self.report_progress(1,_('epub downloaded and extracted'))
|
||||||
|
|
||||||
|
return index
|
@ -46,7 +46,7 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
br = BasicNewsRecipe.get_browser()
|
br = BasicNewsRecipe.get_browser()
|
||||||
if self.username is not None and self.password is not None:
|
if self.username is not None and self.password is not None:
|
||||||
br.open('http://commerce.wsj.com/auth/login')
|
br.open('http://commerce.wsj.com/auth/login')
|
||||||
br.select_form(nr=0)
|
br.select_form(nr=1)
|
||||||
br['user'] = self.username
|
br['user'] = self.username
|
||||||
br['password'] = self.password
|
br['password'] = self.password
|
||||||
res = br.submit()
|
res = br.submit()
|
||||||
|
@ -11,12 +11,11 @@ import os, re, uuid, logging
|
|||||||
from mimetypes import types_map
|
from mimetypes import types_map
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import count
|
from itertools import count
|
||||||
from urlparse import urldefrag, urlparse, urlunparse
|
from urlparse import urldefrag, urlparse, urlunparse, urljoin
|
||||||
from urllib import unquote as urlunquote
|
from urllib import unquote as urlunquote
|
||||||
from urlparse import urljoin
|
|
||||||
|
|
||||||
from lxml import etree, html
|
from lxml import etree, html
|
||||||
from cssutils import CSSParser
|
from cssutils import CSSParser, parseString, parseStyle, replaceUrls
|
||||||
from cssutils.css import CSSRule
|
from cssutils.css import CSSRule
|
||||||
|
|
||||||
import calibre
|
import calibre
|
||||||
@ -88,11 +87,11 @@ def XLINK(name):
|
|||||||
def CALIBRE(name):
|
def CALIBRE(name):
|
||||||
return '{%s}%s' % (CALIBRE_NS, name)
|
return '{%s}%s' % (CALIBRE_NS, name)
|
||||||
|
|
||||||
_css_url_re = re.compile(r'url\((.*?)\)', re.I)
|
_css_url_re = re.compile(r'url\s*\((.*?)\)', re.I)
|
||||||
_css_import_re = re.compile(r'@import "(.*?)"')
|
_css_import_re = re.compile(r'@import "(.*?)"')
|
||||||
_archive_re = re.compile(r'[^ ]+')
|
_archive_re = re.compile(r'[^ ]+')
|
||||||
|
|
||||||
def iterlinks(root):
|
def iterlinks(root, find_links_in_css=True):
|
||||||
'''
|
'''
|
||||||
Iterate over all links in a OEB Document.
|
Iterate over all links in a OEB Document.
|
||||||
|
|
||||||
@ -134,6 +133,8 @@ def iterlinks(root):
|
|||||||
yield (el, attr, attribs[attr], 0)
|
yield (el, attr, attribs[attr], 0)
|
||||||
|
|
||||||
|
|
||||||
|
if not find_links_in_css:
|
||||||
|
continue
|
||||||
if tag == XHTML('style') and el.text:
|
if tag == XHTML('style') and el.text:
|
||||||
for match in _css_url_re.finditer(el.text):
|
for match in _css_url_re.finditer(el.text):
|
||||||
yield (el, None, match.group(1), match.start(1))
|
yield (el, None, match.group(1), match.start(1))
|
||||||
@ -180,7 +181,7 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
|||||||
'''
|
'''
|
||||||
if resolve_base_href:
|
if resolve_base_href:
|
||||||
resolve_base_href(root)
|
resolve_base_href(root)
|
||||||
for el, attrib, link, pos in iterlinks(root):
|
for el, attrib, link, pos in iterlinks(root, find_links_in_css=False):
|
||||||
new_link = link_repl_func(link.strip())
|
new_link = link_repl_func(link.strip())
|
||||||
if new_link == link:
|
if new_link == link:
|
||||||
continue
|
continue
|
||||||
@ -203,6 +204,40 @@ def rewrite_links(root, link_repl_func, resolve_base_href=False):
|
|||||||
new = cur[:pos] + new_link + cur[pos+len(link):]
|
new = cur[:pos] + new_link + cur[pos+len(link):]
|
||||||
el.attrib[attrib] = new
|
el.attrib[attrib] = new
|
||||||
|
|
||||||
|
def set_property(v):
|
||||||
|
if v.CSS_PRIMITIVE_VALUE == v.cssValueType and \
|
||||||
|
v.CSS_URI == v.primitiveType:
|
||||||
|
v.setStringValue(v.CSS_URI,
|
||||||
|
link_repl_func(v.getStringValue()))
|
||||||
|
|
||||||
|
for el in root.iter():
|
||||||
|
try:
|
||||||
|
tag = el.tag
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tag == XHTML('style') and el.text and \
|
||||||
|
(_css_url_re.search(el.text) is not None or '@import' in
|
||||||
|
el.text):
|
||||||
|
stylesheet = parseString(el.text)
|
||||||
|
replaceUrls(stylesheet, link_repl_func)
|
||||||
|
el.text = '\n'+stylesheet.cssText + '\n'
|
||||||
|
|
||||||
|
if 'style' in el.attrib:
|
||||||
|
text = el.attrib['style']
|
||||||
|
if _css_url_re.search(text) is not None:
|
||||||
|
stext = parseStyle(text)
|
||||||
|
for p in stext.getProperties(all=True):
|
||||||
|
v = p.cssValue
|
||||||
|
if v.CSS_VALUE_LIST == v.cssValueType:
|
||||||
|
for item in v:
|
||||||
|
set_property(item)
|
||||||
|
elif v.CSS_PRIMITIVE_VALUE == v.cssValueType:
|
||||||
|
set_property(v)
|
||||||
|
el.attrib['style'] = stext.cssText.replace('\n', ' ').replace('\r',
|
||||||
|
' ')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EPUB_MIME = types_map['.epub']
|
EPUB_MIME = types_map['.epub']
|
||||||
XHTML_MIME = types_map['.xhtml']
|
XHTML_MIME = types_map['.xhtml']
|
||||||
@ -622,7 +657,10 @@ class Metadata(object):
|
|||||||
attrib[key] = prefixname(value, nsrmap)
|
attrib[key] = prefixname(value, nsrmap)
|
||||||
if namespace(self.term) == DC11_NS:
|
if namespace(self.term) == DC11_NS:
|
||||||
elem = element(parent, self.term, attrib=attrib)
|
elem = element(parent, self.term, attrib=attrib)
|
||||||
elem.text = self.value
|
try:
|
||||||
|
elem.text = self.value
|
||||||
|
except:
|
||||||
|
elem.text = repr(self.value)
|
||||||
else:
|
else:
|
||||||
elem = element(parent, OPF('meta'), attrib=attrib)
|
elem = element(parent, OPF('meta'), attrib=attrib)
|
||||||
elem.attrib['name'] = prefixname(self.term, nsrmap)
|
elem.attrib['name'] = prefixname(self.term, nsrmap)
|
||||||
|
@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import posixpath
|
import posixpath
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag, urlparse
|
||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
import cssutils
|
import cssutils
|
||||||
@ -67,6 +67,10 @@ class RenameFiles(object): # {{{
|
|||||||
|
|
||||||
def url_replacer(self, orig_url):
|
def url_replacer(self, orig_url):
|
||||||
url = urlnormalize(orig_url)
|
url = urlnormalize(orig_url)
|
||||||
|
parts = urlparse(url)
|
||||||
|
if parts.scheme:
|
||||||
|
# Only rewrite local URLs
|
||||||
|
return orig_url
|
||||||
path, frag = urldefrag(url)
|
path, frag = urldefrag(url)
|
||||||
if self.renamed_items_map:
|
if self.renamed_items_map:
|
||||||
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
|
orig_item = self.renamed_items_map.get(self.current_item.href, self.current_item)
|
||||||
|
@ -166,6 +166,7 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.choose_menu = QMenu(self.gui)
|
self.choose_menu = QMenu(self.gui)
|
||||||
self.qaction.setMenu(self.choose_menu)
|
self.qaction.setMenu(self.choose_menu)
|
||||||
|
|
||||||
|
|
||||||
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
|
||||||
self.choose_menu.addAction(self.action_choose)
|
self.choose_menu.addAction(self.action_choose)
|
||||||
|
|
||||||
@ -176,6 +177,11 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.delete_menu = QMenu(_('Delete library'))
|
self.delete_menu = QMenu(_('Delete library'))
|
||||||
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
|
||||||
|
|
||||||
|
ac = self.create_action(spec=(_('Pick a random book'), 'catalog.png',
|
||||||
|
None, None), attr='action_pick_random')
|
||||||
|
ac.triggered.connect(self.pick_random)
|
||||||
|
self.choose_menu.addAction(ac)
|
||||||
|
|
||||||
self.rename_separator = self.choose_menu.addSeparator()
|
self.rename_separator = self.choose_menu.addSeparator()
|
||||||
|
|
||||||
self.switch_actions = []
|
self.switch_actions = []
|
||||||
@ -213,6 +219,12 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
self.maintenance_menu.addAction(ac)
|
self.maintenance_menu.addAction(ac)
|
||||||
self.choose_menu.addMenu(self.maintenance_menu)
|
self.choose_menu.addMenu(self.maintenance_menu)
|
||||||
|
|
||||||
|
def pick_random(self, *args):
|
||||||
|
import random
|
||||||
|
pick = random.randint(0, self.gui.library_view.model().rowCount(None))
|
||||||
|
self.gui.library_view.set_current_row(pick)
|
||||||
|
self.gui.library_view.scroll_to_row(pick)
|
||||||
|
|
||||||
def library_name(self):
|
def library_name(self):
|
||||||
db = self.gui.library_view.model().db
|
db = self.gui.library_view.model().db
|
||||||
path = db.library_path
|
path = db.library_path
|
||||||
|
@ -11,9 +11,9 @@ from lxml import html
|
|||||||
from lxml.html import soupparser
|
from lxml.html import soupparser
|
||||||
|
|
||||||
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
from PyQt4.Qt import QApplication, QFontInfo, QSize, QWidget, QPlainTextEdit, \
|
||||||
QToolBar, QVBoxLayout, QAction, QIcon, QWebPage, Qt, QTabWidget, QUrl, \
|
QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
|
||||||
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
QSyntaxHighlighter, QColor, QChar, QColorDialog, QMenu, QInputDialog
|
||||||
from PyQt4.QtWebKit import QWebView
|
from PyQt4.QtWebKit import QWebView, QWebPage
|
||||||
|
|
||||||
from calibre.ebooks.chardet import xml_to_unicode
|
from calibre.ebooks.chardet import xml_to_unicode
|
||||||
from calibre import xml_replace_entities
|
from calibre import xml_replace_entities
|
||||||
|
@ -16,6 +16,7 @@ from calibre.gui2 import error_dialog, NONE, info_dialog, config
|
|||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre import strftime, force_unicode
|
from calibre import strftime, force_unicode
|
||||||
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
|
from calibre.customize.ui import get_isbndb_key, set_isbndb_key
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
_hung_fetchers = set([])
|
_hung_fetchers = set([])
|
||||||
|
|
||||||
@ -72,27 +73,33 @@ class Matches(QAbstractTableModel):
|
|||||||
def summary(self, row):
|
def summary(self, row):
|
||||||
return self.matches[row].comments
|
return self.matches[row].comments
|
||||||
|
|
||||||
|
def data_as_text(self, book, col):
|
||||||
|
if col == 0 and book.title is not None:
|
||||||
|
return book.title
|
||||||
|
elif col == 1:
|
||||||
|
return ', '.join(book.authors)
|
||||||
|
elif col == 2 and book.author_sort is not None:
|
||||||
|
return book.author_sort
|
||||||
|
elif col == 3 and book.publisher is not None:
|
||||||
|
return book.publisher
|
||||||
|
elif col == 4 and book.isbn is not None:
|
||||||
|
return book.isbn
|
||||||
|
elif col == 5 and hasattr(book.pubdate, 'timetuple'):
|
||||||
|
return strftime('%b %Y', book.pubdate.timetuple())
|
||||||
|
elif col == 6 and book.has_cover:
|
||||||
|
return 'y'
|
||||||
|
elif col == 7 and book.comments:
|
||||||
|
return 'y'
|
||||||
|
return ''
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
book = self.matches[row]
|
book = self.matches[row]
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
res = None
|
res = self.data_as_text(book, col)
|
||||||
if col == 0:
|
if col <= 5 and res:
|
||||||
res = book.title
|
return QVariant(res)
|
||||||
elif col == 1:
|
return NONE
|
||||||
res = ', '.join(book.authors)
|
|
||||||
elif col == 2:
|
|
||||||
res = book.author_sort
|
|
||||||
elif col == 3:
|
|
||||||
res = book.publisher
|
|
||||||
elif col == 4:
|
|
||||||
res = book.isbn
|
|
||||||
elif col == 5:
|
|
||||||
if hasattr(book.pubdate, 'timetuple'):
|
|
||||||
res = strftime('%b %Y', book.pubdate.timetuple())
|
|
||||||
if not res:
|
|
||||||
return NONE
|
|
||||||
return QVariant(res)
|
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
if col == 6 and book.has_cover:
|
if col == 6 and book.has_cover:
|
||||||
return self.yes_icon
|
return self.yes_icon
|
||||||
@ -100,6 +107,16 @@ class Matches(QAbstractTableModel):
|
|||||||
return self.yes_icon
|
return self.yes_icon
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
|
def sort(self, col, order, reset=True):
|
||||||
|
if not self.matches:
|
||||||
|
return
|
||||||
|
descending = order == Qt.DescendingOrder
|
||||||
|
self.matches.sort(None,
|
||||||
|
lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))),
|
||||||
|
descending)
|
||||||
|
if reset:
|
||||||
|
self.reset()
|
||||||
|
|
||||||
class FetchMetadata(QDialog, Ui_FetchMetadata):
|
class FetchMetadata(QDialog, Ui_FetchMetadata):
|
||||||
|
|
||||||
HANG_TIME = 75 #seconds
|
HANG_TIME = 75 #seconds
|
||||||
@ -136,6 +153,11 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
|
||||||
self.show_summary)
|
self.show_summary)
|
||||||
self.matches.setMouseTracking(True)
|
self.matches.setMouseTracking(True)
|
||||||
|
# Enabling sorting and setting a sort column will not change the initial
|
||||||
|
# order of the results, as they are filled in later
|
||||||
|
self.matches.setSortingEnabled(True)
|
||||||
|
self.matches.horizontalHeader().sectionClicked.connect(self.show_sort_indicator)
|
||||||
|
self.matches.horizontalHeader().setSortIndicatorShown(False)
|
||||||
self.fetch_metadata()
|
self.fetch_metadata()
|
||||||
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
|
self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
|
||||||
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
|
self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
|
||||||
@ -243,3 +265,7 @@ class FetchMetadata(QDialog, Ui_FetchMetadata):
|
|||||||
def chosen(self, index):
|
def chosen(self, index):
|
||||||
self.matches.setCurrentIndex(index)
|
self.matches.setCurrentIndex(index)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def show_sort_indicator(self, *args):
|
||||||
|
self.matches.horizontalHeader().setSortIndicatorShown(True)
|
||||||
|
|
||||||
|
@ -809,7 +809,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
|
self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
|
||||||
summ = book.comments
|
summ = book.comments
|
||||||
if summ:
|
if summ:
|
||||||
prefix = self.comment.html
|
prefix = self.comments.html
|
||||||
if prefix:
|
if prefix:
|
||||||
prefix += '\n'
|
prefix += '\n'
|
||||||
self.comments.html = prefix + comments_to_html(summ)
|
self.comments.html = prefix + comments_to_html(summ)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>994</width>
|
<width>994</width>
|
||||||
<height>726</height>
|
<height>716</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>986</width>
|
<width>986</width>
|
||||||
<height>687</height>
|
<height>677</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
@ -495,6 +495,22 @@ Using this button to create author sort will change author sort from red to gree
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeType">
|
||||||
|
<enum>QSizePolicy::Fixed</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="layoutWidget_2">
|
<widget class="QWidget" name="layoutWidget_2">
|
||||||
|
@ -3,8 +3,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
'''
|
# Imports {{{
|
||||||
'''
|
|
||||||
import os, math, re, glob, sys
|
import os, math, re, glob, sys
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@ -19,11 +18,14 @@ from calibre.utils.config import Config, StringConfig
|
|||||||
from calibre.utils.localization import get_language
|
from calibre.utils.localization import get_language
|
||||||
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
from calibre.gui2.viewer.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.viewer.flip import SlideFlip
|
from calibre.gui2.viewer.flip import SlideFlip
|
||||||
|
from calibre.gui2.viewer.gestures import Gestures
|
||||||
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
|
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
|
||||||
from calibre.constants import iswindows
|
from calibre.constants import iswindows
|
||||||
from calibre import prints, guess_type
|
from calibre import prints, guess_type
|
||||||
from calibre.gui2.viewer.keys import SHORTCUTS
|
from calibre.gui2.viewer.keys import SHORTCUTS
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
|
bookmarks = referencing = hyphenation = jquery = jquery_scrollTo = \
|
||||||
hyphenator = images = hyphen_pats = None
|
hyphenator = images = hyphen_pats = None
|
||||||
|
|
||||||
@ -33,6 +35,7 @@ def load_builtin_fonts():
|
|||||||
QFontDatabase.addApplicationFont(f)
|
QFontDatabase.addApplicationFont(f)
|
||||||
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
|
return 'Liberation Serif', 'Liberation Sans', 'Liberation Mono'
|
||||||
|
|
||||||
|
# Config {{{
|
||||||
def config(defaults=None):
|
def config(defaults=None):
|
||||||
desc = _('Options to customize the ebook viewer')
|
desc = _('Options to customize the ebook viewer')
|
||||||
if defaults is None:
|
if defaults is None:
|
||||||
@ -137,8 +140,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
|
|||||||
str(self.hyphenate_default_lang.itemData(idx).toString()))
|
str(self.hyphenate_default_lang.itemData(idx).toString()))
|
||||||
return QDialog.accept(self, *args)
|
return QDialog.accept(self, *args)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class Document(QWebPage):
|
class Document(QWebPage): # {{{
|
||||||
|
|
||||||
def set_font_settings(self):
|
def set_font_settings(self):
|
||||||
opts = config().parse()
|
opts = config().parse()
|
||||||
@ -449,7 +453,9 @@ class Document(QWebPage):
|
|||||||
self.height+amount)
|
self.height+amount)
|
||||||
self.setPreferredContentsSize(s)
|
self.setPreferredContentsSize(s)
|
||||||
|
|
||||||
class EntityDeclarationProcessor(object):
|
# }}}
|
||||||
|
|
||||||
|
class EntityDeclarationProcessor(object): # {{{
|
||||||
|
|
||||||
def __init__(self, html):
|
def __init__(self, html):
|
||||||
self.declared_entities = {}
|
self.declared_entities = {}
|
||||||
@ -460,14 +466,16 @@ class EntityDeclarationProcessor(object):
|
|||||||
self.processed_html = html
|
self.processed_html = html
|
||||||
for key, val in self.declared_entities.iteritems():
|
for key, val in self.declared_entities.iteritems():
|
||||||
self.processed_html = self.processed_html.replace('&%s;'%key, val)
|
self.processed_html = self.processed_html.replace('&%s;'%key, val)
|
||||||
|
# }}}
|
||||||
|
|
||||||
class DocumentView(QWebView):
|
class DocumentView(QWebView): # {{{
|
||||||
|
|
||||||
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
DISABLED_BRUSH = QBrush(Qt.lightGray, Qt.Dense5Pattern)
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
QWebView.__init__(self, *args)
|
QWebView.__init__(self, *args)
|
||||||
self.flipper = SlideFlip(self)
|
self.flipper = SlideFlip(self)
|
||||||
|
self.gestures = Gestures()
|
||||||
self.is_auto_repeat_event = False
|
self.is_auto_repeat_event = False
|
||||||
self.debug_javascript = False
|
self.debug_javascript = False
|
||||||
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
|
||||||
@ -953,6 +961,29 @@ class DocumentView(QWebView):
|
|||||||
self.manager.viewport_resized(self.scroll_fraction)
|
self.manager.viewport_resized(self.scroll_fraction)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def event(self, ev):
|
||||||
|
typ = ev.type()
|
||||||
|
if typ == ev.TouchBegin:
|
||||||
|
try:
|
||||||
|
self.gestures.start_gesture('touch', ev)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
elif typ == ev.TouchEnd:
|
||||||
|
try:
|
||||||
|
gesture = self.gestures.end_gesture('touch', ev, self.rect())
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
if gesture is not None:
|
||||||
|
ev.accept()
|
||||||
|
if gesture == 'lineleft':
|
||||||
|
self.next_page()
|
||||||
|
elif gesture == 'lineright':
|
||||||
|
self.previous_page()
|
||||||
|
return True
|
||||||
|
return QWebView.event(self, ev)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
opos = self.document.ypos
|
opos = self.document.ypos
|
||||||
ret = QWebView.mouseReleaseEvent(self, ev)
|
ret = QWebView.mouseReleaseEvent(self, ev)
|
||||||
@ -961,4 +992,5 @@ class DocumentView(QWebView):
|
|||||||
self.manager.scrolled(self.scroll_fraction)
|
self.manager.scrolled(self.scroll_fraction)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
61
src/calibre/gui2/viewer/gestures.py
Normal file
61
src/calibre/gui2/viewer/gestures.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Gestures(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.in_progress = {}
|
||||||
|
|
||||||
|
def get_boundary_point(self, event):
|
||||||
|
t = time.time()
|
||||||
|
id_ = None
|
||||||
|
if hasattr(event, 'touchPoints'):
|
||||||
|
tps = list(event.touchPoints())
|
||||||
|
tp = None
|
||||||
|
for t in tps:
|
||||||
|
if t.isPrimary():
|
||||||
|
tp = t
|
||||||
|
break
|
||||||
|
if tp is None:
|
||||||
|
tp = tps[0]
|
||||||
|
gp, p = tp.screenPos(), tp.pos()
|
||||||
|
id_ = tp.id()
|
||||||
|
else:
|
||||||
|
gp, p = event.globalPos(), event.pos()
|
||||||
|
return (t, gp, p, id_)
|
||||||
|
|
||||||
|
def start_gesture(self, typ, event):
|
||||||
|
self.in_progress[typ] = self.get_boundary_point(event)
|
||||||
|
|
||||||
|
def is_in_progress(self, typ):
|
||||||
|
return typ in self.in_progress
|
||||||
|
|
||||||
|
def end_gesture(self, typ, event, widget_rect):
|
||||||
|
if not self.is_in_progress(typ):
|
||||||
|
return
|
||||||
|
start = self.in_progress[typ]
|
||||||
|
end = self.get_boundary_point(event)
|
||||||
|
if start[3] != end[3]:
|
||||||
|
return
|
||||||
|
timespan = end[0] - start[0]
|
||||||
|
start_pos, end_pos = start[1], end[1]
|
||||||
|
xspan = end_pos.x() - start_pos.x()
|
||||||
|
yspan = end_pos.y() - start_pos.y()
|
||||||
|
|
||||||
|
width = widget_rect.width()
|
||||||
|
|
||||||
|
if timespan < 1.1 and abs(xspan) >= width/5. and \
|
||||||
|
abs(yspan) < abs(xspan)/5.:
|
||||||
|
# Quick horizontal gesture
|
||||||
|
return 'line'+('left' if xspan < 0 else 'right')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -181,16 +181,17 @@ How do I use |app| with my iPad/iPhone/iTouch?
|
|||||||
Over the air
|
Over the air
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the calibre sontent server, which makes your collection available over the net. First perform the following steps in |app|
|
||||||
|
|
||||||
First perform the following steps in |app|
|
|
||||||
|
|
||||||
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`)
|
* Set the Preferred Output Format in |app| to EPUB (The output format can be set under :guilabel:`Preferences->Interface->Behavior`)
|
||||||
* Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->Page Setup`
|
* Set the output profile to iPad (this will work for iPhone/iPods as well), under :guilabel:`Preferences->Conversion->Common Options->Page Setup`
|
||||||
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
* Convert the books you want to read on your iPhone to EPUB format by selecting them and clicking the Convert button.
|
||||||
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
* Turn on the Content Server in |app|'s preferences and leave |app| running.
|
||||||
|
|
||||||
Install the free Stanza reader app on your iPad/iPhone/iTouch using iTunes.
|
Now on your iPad/iPhone you have two choices, use either iBooks (version 1.2 and later) or Stanza (version 3.0 and later). Both are available free from the app store.
|
||||||
|
|
||||||
|
Using Stanza
|
||||||
|
***************
|
||||||
|
|
||||||
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
Now you should be able to access your books on your iPhone by opening Stanza. Go to "Get Books" and then click the "Shared" tab. Under Shared you will see an entry "Books in calibre". If you don't, make sure your iPad/iPhone is connected using the WiFi network in your house, not 3G. If the |app| catalog is still not detected in Stanza, you can add it manually in Stanza. To do this, click the "Shared" tab, then click the "Edit" button and then click "Add book source" to add a new book source. In the Add Book Source screen enter whatever name you like and in the URL field, enter the following::
|
||||||
|
|
||||||
@ -200,6 +201,18 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
|
|||||||
|
|
||||||
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
||||||
|
|
||||||
|
Using iBooks
|
||||||
|
**************
|
||||||
|
|
||||||
|
Start the Safari browser and type in the IP address and port of the computer running the calibre server, like this::
|
||||||
|
|
||||||
|
http://192.168.1.2:8080/
|
||||||
|
|
||||||
|
Replace ``192.168.1.2`` with the local IP address of the computer running |app|. If you have changed the port the |app| content server is running on, you will have to change ``8080`` as well to the new port. The local IP address is the IP address you computer is assigned on your home network. A quick Google search will tell you how to find out your local IP address.
|
||||||
|
|
||||||
|
You wills ee a list of books in Safari, just click on the epub link for whichever book you want to read, Safari will then prompt you to open it with iBooks.
|
||||||
|
|
||||||
|
|
||||||
With the USB cable
|
With the USB cable
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user