Sync to trunk.

This commit is contained in:
John Schember 2010-12-22 18:04:34 -05:00
commit c1afa4d36e
39 changed files with 999 additions and 295 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -1,28 +1,33 @@
__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
@ -31,34 +36,67 @@ class BWmagazine(BasicNewsRecipe):
, '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

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
'''
cnd.org
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class TheCND(BasicNewsRecipe):
title = 'CND'
__author__ = 'Derek Liang'
description = ''
INDEX = 'http://cnd.org'
language = 'zh'
conversion_options = {'linearize_tables':True}
remove_tags_before = dict(name='div', id='articleHead')
remove_tags_after = dict(id='copyright')
remove_tags = [dict(name='table', attrs={'align':'right'}), dict(name='img', attrs={'src':'http://my.cnd.org/images/logo.gif'}), dict(name='hr', attrs={}), dict(name='small', attrs={})]
no_stylesheets = True
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
def print_version(self, url):
if url.find('news/article.php') >= 0:
return re.sub("^[^=]*", "http://my.cnd.org/modules/news/print.php?storyid", url)
else:
return re.sub("^[^=]*", "http://my.cnd.org/modules/wfsection/print.php?articleid", url)
def parse_index(self):
soup = self.index_to_soup(self.INDEX)
feeds = []
articles = {}
for a in soup.findAll('a', attrs={'target':'_cnd'}):
url = a['href']
if url.find('article.php') < 0 :
continue
if url.startswith('/'):
url = 'http://cnd.org'+url
title = self.tag_to_string(a)
self.log('\tFound article: ', title, 'at', url)
date = a.nextSibling
if (date is not None) and len(date)>2:
if not articles.has_key(date):
articles[date] = []
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
self.log('\t\tAppend to : ', date)
self.log('log articles', articles)
mostCurrent = sorted(articles).pop()
self.title = 'CND ' + mostCurrent
feeds.append((self.title, articles[mostCurrent]))
return feeds
def populate_article_metadata(self, article, soup, first):
header = soup.find('h3')
self.log('header: ' + self.tag_to_string(header))
pass

View File

@ -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

View 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

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Derek Liang <Derek.liang.ca @@@at@@@ gmail.com>'
'''
wenxuecity.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class TheCND(BasicNewsRecipe):
title = 'wenxuecity - znjy'
__author__ = 'Derek Liang'
description = ''
INDEX = 'http://bbs.wenxuecity.com/znjy/?elite=1'
language = 'zh'
conversion_options = {'linearize_tables':True}
remove_tags_before = dict(name='div', id='message')
remove_tags_after = dict(name='div', id='message')
remove_tags = [dict(name='div', id='postmeta'), dict(name='div', id='footer')]
no_stylesheets = True
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
def print_version(self, url):
return url + '?print'
def parse_index(self):
soup = self.index_to_soup(self.INDEX)
feeds = []
articles = {}
for a in soup.findAll('a', attrs={'class':'post'}):
url = a['href']
if url.startswith('/'):
url = 'http://bbs.wenxuecity.com'+url
title = self.tag_to_string(a)
self.log('\tFound article: ', title, ' at:', url)
dateReg = re.search( '(\d\d?)/(\d\d?)/(\d\d)', self.tag_to_string(a.parent) )
date = '%(y)s/%(m)02d/%(d)02d' % {'y' : dateReg.group(3), 'm' : int(dateReg.group(1)), 'd' : int(dateReg.group(2)) }
if not articles.has_key(date):
articles[date] = []
articles[date].append({'title':title, 'url':url, 'description': '', 'date':''})
self.log('\t\tAppend to : ', date)
self.log('log articles', articles)
mostCurrent = sorted(articles).pop()
self.title = '文学城 - 子女教育 - ' + mostCurrent
feeds.append((self.title, articles[mostCurrent]))
return feeds
def populate_article_metadata(self, article, soup, first):
header = soup.find('h3')
self.log('header: ' + self.tag_to_string(header))
pass

View File

@ -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()

View File

@ -474,10 +474,10 @@ from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \ from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, \
SOVOS, PICO SOVOS, PICO, SUNSTECH_EB700
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD GEMEI, VELOCITYMICRO, PDNOVEL_KOBO, Q600, LUMIREAD, ALURATEK_COLOR
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
@ -579,7 +579,7 @@ plugins += [
ELONEX, ELONEX,
TECLAST_K3, TECLAST_K3,
NEWSMY, NEWSMY,
PICO, PICO, SUNSTECH_EB700,
IPAPYRUS, IPAPYRUS,
SOVOS, SOVOS,
EDGE, EDGE,
@ -600,6 +600,7 @@ plugins += [
VELOCITYMICRO, VELOCITYMICRO,
PDNOVEL_KOBO, PDNOVEL_KOBO,
LUMIREAD, LUMIREAD,
ALURATEK_COLOR,
ITUNES, ITUNES,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \

View File

@ -204,3 +204,23 @@ class LUMIREAD(USBMS):
with open(cfilepath+'.jpg', 'wb') as f: with open(cfilepath+'.jpg', 'wb') as f:
f.write(metadata.thumbnail[-1]) f.write(metadata.thumbnail[-1])
class ALURATEK_COLOR(USBMS):
name = 'Aluratek Color Device Interface'
gui_name = 'Aluratek Color'
description = _('Communicate with the Aluratek Color')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'txt', 'pdf']
VENDOR_ID = [0x1f3a]
PRODUCT_ID = [0x1000]
BCD = [0x0002]
EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'books'
VENDOR_NAME = 'USB_2.0'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB_FLASH_DRIVER'

View File

@ -58,9 +58,16 @@ class PRS505(USBMS):
SUPPORTS_USE_AUTHOR_SORT = True SUPPORTS_USE_AUTHOR_SORT = True
EBOOK_DIR_MAIN = 'database/media/books' EBOOK_DIR_MAIN = 'database/media/books'
ALL_BY_TITLE = _('All by title')
ALL_BY_AUTHOR = _('All by author')
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields ' EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
'to turn into collections on the device. Possibilities include: ')+\ 'to turn into collections on the device. Possibilities include: ')+\
'series, tags, authors' 'series, tags, authors' +\
_('. Two special collections are available: %s:%s and %s:%s. Add '
'these values to the list to enable them. The collections will be '
'given the name provided after the ":" character.')%(
'abt', ALL_BY_TITLE, 'aba', ALL_BY_AUTHOR)
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags']) EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
plugboard = None plugboard = None
@ -151,7 +158,7 @@ class PRS505(USBMS):
blists[i] = booklists[i] blists[i] = booklists[i]
opts = self.settings() opts = self.settings()
if opts.extra_customization: if opts.extra_customization:
collections = [x.lower().strip() for x in collections = [x.strip() for x in
opts.extra_customization.split(',')] opts.extra_customization.split(',')]
else: else:
collections = [] collections = []
@ -179,6 +186,8 @@ class PRS505(USBMS):
self.plugboard_func = pb_func self.plugboard_func = pb_func
def upload_cover(self, path, filename, metadata, filepath): def upload_cover(self, path, filename, metadata, filepath):
return # Disabled as the SONY's don't need this thumbnail anyway and
# older models don't auto delete it
if metadata.thumbnail and metadata.thumbnail[-1]: if metadata.thumbnail and metadata.thumbnail[-1]:
path = path.replace('/', os.sep) path = path.replace('/', os.sep)
is_main = path.startswith(self._main_prefix) is_main = path.startswith(self._main_prefix)

View File

@ -410,6 +410,9 @@ class XMLCache(object):
newmi = book.deepcopy_metadata() newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, plugboard) newmi.template_to_attribute(book, plugboard)
newmi.set('_new_book', getattr(book, '_new_book', False)) newmi.set('_new_book', getattr(book, '_new_book', False))
book.set('_pb_title_sort',
newmi.get('title_sort', newmi.get('title', None)))
book.set('_pb_author_sort', newmi.get('author_sort', ''))
else: else:
newmi = book newmi = book
(gtz_count, ltz_count, use_tz_var) = \ (gtz_count, ltz_count, use_tz_var) = \

View File

@ -72,3 +72,13 @@ class SOVOS(TECLAST_K3):
VENDOR_NAME = 'RK28XX' VENDOR_NAME = 'RK28XX'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'USB-MSC'
class SUNSTECH_EB700(TECLAST_K3):
name = 'Sunstech EB700 device interface'
gui_name = 'EB700'
description = _('Communicate with the Sunstech EB700 reader.')
FORMATS = ['epub', 'fb2', 'pdf', 'pdb', 'txt']
VENDOR_NAME = 'SUNEB700'
WINDOWS_MAIN_MEM = 'USB-MSC'

View File

@ -132,9 +132,24 @@ class CollectionsBookList(BookList):
use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect' use_renaming_rules = prefs['manage_device_metadata'] == 'on_connect'
collections = {} collections = {}
# This map of sets is used to avoid linear searches when testing for
# book equality # get the special collection names
all_by_author = ''
all_by_title = ''
ca = []
for c in collection_attributes:
if c.startswith('aba:') and c[4:]:
all_by_author = c[4:]
elif c.startswith('abt:') and c[4:]:
all_by_title = c[4:]
else:
ca.append(c.lower())
collection_attributes = ca
for book in self: for book in self:
tsval = book.get('_pb_title_sort',
book.get('title_sort', book.get('title', 'zzzz')))
asval = book.get('_pb_author_sort', book.get('author_sort', ''))
# Make sure we can identify this book via the lpath # Make sure we can identify this book via the lpath
lpath = getattr(book, 'lpath', None) lpath = getattr(book, 'lpath', None)
if lpath is None: if lpath is None:
@ -211,22 +226,29 @@ class CollectionsBookList(BookList):
collections[cat_name] = {} collections[cat_name] = {}
if use_renaming_rules and sort_attr: if use_renaming_rules and sort_attr:
sort_val = book.get(sort_attr, None) sort_val = book.get(sort_attr, None)
collections[cat_name][lpath] = \ collections[cat_name][lpath] = (book, sort_val, tsval)
(book, sort_val, book.get('title_sort', 'zzzz'))
elif is_series: elif is_series:
if doing_dc: if doing_dc:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = \
(book, book.get('series_index', sys.maxint), (book, book.get('series_index', sys.maxint), tsval)
book.get('title_sort', 'zzzz'))
else: else:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = \
(book, book.get(attr+'_index', sys.maxint), (book, book.get(attr+'_index', sys.maxint), tsval)
book.get('title_sort', 'zzzz'))
else: else:
if lpath not in collections[cat_name]: if lpath not in collections[cat_name]:
collections[cat_name][lpath] = \ collections[cat_name][lpath] = (book, tsval, tsval)
(book, book.get('title_sort', 'zzzz'),
book.get('title_sort', 'zzzz')) # All books by author
if all_by_author:
if all_by_author not in collections:
collections[all_by_author] = {}
collections[all_by_author][lpath] = (book, asval, tsval)
# All books by title
if all_by_title:
if all_by_title not in collections:
collections[all_by_title] = {}
collections[all_by_title][lpath] = (book, tsval, asval)
# Sort collections # Sort collections
result = {} result = {}

View File

@ -55,9 +55,11 @@ except:
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033]) _ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
def title_sort(title): def title_sort(title, order=None):
if order is None:
order = tweaks['title_series_sorting']
title = title.strip() title = title.strip()
if tweaks['title_series_sorting'] == 'strictly_alphabetic': if order == 'strictly_alphabetic':
return title return title
if title and title[0] in _ignore_starts: if title and title[0] in _ignore_starts:
title = title[1:] title = title[1:]

View File

@ -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)
try:
elem.text = self.value 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)

View File

@ -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)

View File

@ -138,6 +138,10 @@ class CheckIntegrity(QProgressDialog):
'You should check them manually. This can ' 'You should check them manually. This can '
'happen if you manipulate the files in the ' 'happen if you manipulate the files in the '
'library folder directly.'), det_msg=det_msg, show=True) 'library folder directly.'), det_msg=det_msg, show=True)
else:
info_dialog(self, _('No errors found'),
_('The integrity check completed with no uncorrectable errors found.'),
show=True)
self.reset() self.reset()
# }}} # }}}
@ -162,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)
@ -172,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 = []
@ -209,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

View File

@ -5,18 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import re, os
from lxml import html 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, \ QToolBar, QVBoxLayout, QAction, QIcon, Qt, QTabWidget, QUrl, \
QSyntaxHighlighter, QColor, QChar 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
from calibre.gui2 import open_url
class PageAction(QAction): # {{{ class PageAction(QAction): # {{{
@ -44,6 +45,18 @@ class PageAction(QAction): # {{{
# }}} # }}}
class BlockStyleAction(QAction): # {{{
def __init__(self, text, name, view):
QAction.__init__(self, text, view)
self._name = name
self.triggered.connect(self.apply_style)
def apply_style(self, *args):
self.parent().exec_command('formatBlock', self._name)
# }}}
class EditorWidget(QWebView): # {{{ class EditorWidget(QWebView): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
@ -90,14 +103,109 @@ class EditorWidget(QWebView): # {{{
ac = PageAction(wac, icon, text, checkable, self) ac = PageAction(wac, icon, text, checkable, self)
setattr(self, 'action_'+name, ac) setattr(self, 'action_'+name, ac)
self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'),
self)
self.action_color.triggered.connect(self.foreground_color)
self.action_background = QAction(QIcon(I('format-fill-color')),
_('Background color'), self)
self.action_background.triggered.connect(self.background_color)
self.action_block_style = QAction(QIcon(I('format-text-heading')),
_('Style text block'), self)
self.action_block_style.setToolTip(
_('Style the selected text block'))
self.block_style_menu = QMenu(self)
self.action_block_style.setMenu(self.block_style_menu)
self.block_style_actions = []
for text, name in [
(_('Normal'), 'p'),
(_('Heading') +' 1', 'h1'),
(_('Heading') +' 2', 'h2'),
(_('Heading') +' 3', 'h3'),
(_('Heading') +' 4', 'h4'),
(_('Heading') +' 5', 'h5'),
(_('Heading') +' 6', 'h6'),
(_('Pre-formatted'), 'pre'),
(_('Blockquote'), 'blockquote'),
(_('Address'), 'address'),
]:
ac = BlockStyleAction(text, name, self)
self.block_style_menu.addAction(ac)
self.block_style_actions.append(ac)
self.action_insert_link = QAction(QIcon(I('insert-link.png')),
_('Insert link'), self)
self.action_insert_link.triggered.connect(self.insert_link)
self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
self.page().linkClicked.connect(self.link_clicked)
def link_clicked(self, url):
open_url(url)
def foreground_color(self):
col = QColorDialog.getColor(Qt.black, self,
_('Choose foreground color'), QColorDialog.ShowAlphaChannel)
if col.isValid():
self.exec_command('foreColor', unicode(col.name()))
def background_color(self):
col = QColorDialog.getColor(Qt.white, self,
_('Choose background color'), QColorDialog.ShowAlphaChannel)
if col.isValid():
self.exec_command('hiliteColor', unicode(col.name()))
def insert_link(self, *args):
link, ok = QInputDialog.getText(self, _('Create link'),
_('Enter URL'))
if not ok:
return
url = self.parse_link(unicode(link))
if url.isValid():
url = unicode(url.toString())
self.exec_command('createLink', url)
def parse_link(self, link):
link = link.strip()
has_schema = re.match(r'^[a-zA-Z]+:', link)
if has_schema is not None:
url = QUrl(link, QUrl.TolerantMode)
if url.isValid():
return url
if os.path.exists(link):
return QUrl.fromLocalFile(link)
if has_schema is None:
first, _, rest = link.partition('.')
prefix = 'http'
if first == 'ftp':
prefix = 'ftp'
url = QUrl(prefix +'://'+link, QUrl.TolerantMode)
if url.isValid():
return url
return QUrl(link, QUrl.TolerantMode)
def sizeHint(self): def sizeHint(self):
return QSize(150, 150) return QSize(150, 150)
def exec_command(self, cmd, arg=None):
frame = self.page().mainFrame()
if arg is not None:
js = 'document.execCommand("%s", false, "%s");' % (cmd, arg)
else:
js = 'document.execCommand("%s", false, null);' % cmd
frame.evaluateJavaScript(js)
@dynamic_property @dynamic_property
def html(self): def html(self):
def fget(self): def fget(self):
ans = u'' ans = u''
check = unicode(self.page().mainFrame().toPlainText()).strip()
if not check:
return ans
try: try:
raw = unicode(self.page().mainFrame().toHtml()) raw = unicode(self.page().mainFrame().toHtml())
raw = xml_to_unicode(raw, strip_encoding_pats=True, raw = xml_to_unicode(raw, strip_encoding_pats=True,
@ -348,7 +456,7 @@ class Highlighter(QSyntaxHighlighter):
# }}} # }}}
class Editor(QWidget): class Editor(QWidget): # {{{
def __init__(self, parent=None): def __init__(self, parent=None):
QWidget.__init__(self, parent) QWidget.__init__(self, parent)
@ -404,6 +512,15 @@ class Editor(QWidget):
self.toolbar1.addAction(ac) self.toolbar1.addAction(ac)
self.toolbar1.addSeparator() self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_color)
self.toolbar1.addAction(self.editor.action_background)
self.toolbar1.addSeparator()
self.toolbar1.addAction(self.editor.action_block_style)
w = self.toolbar1.widgetForAction(self.editor.action_block_style)
w.setPopupMode(w.InstantPopup)
self.toolbar1.addAction(self.editor.action_insert_link)
self.code_edit.textChanged.connect(self.code_dirtied) self.code_edit.textChanged.connect(self.code_dirtied)
self.editor.page().contentsChanged.connect(self.wyswyg_dirtied) self.editor.page().contentsChanged.connect(self.wyswyg_dirtied)
@ -411,18 +528,21 @@ class Editor(QWidget):
def html(self): def html(self):
def fset(self, v): def fset(self, v):
self.editor.html = v self.editor.html = v
return property(fget=lambda self:self.editor.html, fset=fset) def fget(self):
self.tabs.setCurrentIndex(0)
return self.editor.html
return property(fget=fget, fset=fset)
def change_tab(self, index): def change_tab(self, index):
#print 'reloading:', (index and self.wyswyg_dirty) or (not index and #print 'reloading:', (index and self.wyswyg_dirty) or (not index and
# self.source_dirty) # self.source_dirty)
if index == 1: # changing to code view if index == 1: # changing to code view
if self.wyswyg_dirty: if self.wyswyg_dirty:
self.code_edit.setPlainText(self.html) self.code_edit.setPlainText(self.editor.html)
self.wyswyg_dirty = False self.wyswyg_dirty = False
elif index == 0: #changing to wyswyg elif index == 0: #changing to wyswyg
if self.source_dirty: if self.source_dirty:
self.html = unicode(self.code_edit.toPlainText()) self.editor.html = unicode(self.code_edit.toPlainText())
self.source_dirty = False self.source_dirty = False
def wyswyg_dirtied(self, *args): def wyswyg_dirtied(self, *args):
@ -431,6 +551,8 @@ class Editor(QWidget):
def code_dirtied(self, *args): def code_dirtied(self, *args):
self.source_dirty = True self.source_dirty = True
# }}}
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication([]) app = QApplication([])
w = Editor() w = Editor()

View File

@ -587,8 +587,6 @@ class BulkSeries(BulkBase):
else: else:
s_index = self.db.get_custom_extra(book_id, num=self.col_id, s_index = self.db.get_custom_extra(book_id, num=self.col_id,
index_is_id=True) index_is_id=True)
if s_index is None:
s_index = 1.0
extras.append(s_index) extras.append(s_index)
self.db.set_custom_bulk(book_ids, val, extras=extras, self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify) num=self.col_id, notify=notify)

View File

@ -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
elif col == 1:
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) return QVariant(res)
return NONE
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)

View File

@ -12,6 +12,7 @@ from PyQt4 import QtGui
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.ebooks.metadata import string_to_authors, authors_to_string
from calibre.ebooks.metadata.book.base import composite_formatter
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
@ -311,6 +312,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
def prepare_search_and_replace(self): def prepare_search_and_replace(self):
self.search_for.initialize('bulk_edit_search_for') self.search_for.initialize('bulk_edit_search_for')
self.replace_with.initialize('bulk_edit_replace_with') self.replace_with.initialize('bulk_edit_replace_with')
self.s_r_template.initialize('bulk_edit_template')
self.test_text.initialize('bulk_edit_test_test') self.test_text.initialize('bulk_edit_test_test')
self.all_fields = [''] self.all_fields = ['']
self.writable_fields = [''] self.writable_fields = ['']
@ -325,9 +327,10 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if f in ['sort'] or fm[f]['datatype'] == 'composite': if f in ['sort'] or fm[f]['datatype'] == 'composite':
self.all_fields.append(f) self.all_fields.append(f)
self.all_fields.sort() self.all_fields.sort()
self.all_fields.insert(1, '{template}')
self.writable_fields.sort() self.writable_fields.sort()
self.search_field.setMaxVisibleItems(20) self.search_field.setMaxVisibleItems(25)
self.destination_field.setMaxVisibleItems(20) self.destination_field.setMaxVisibleItems(25)
offset = 10 offset = 10
self.s_r_number_of_books = min(10, len(self.ids)) self.s_r_number_of_books = min(10, len(self.ids))
for i in range(1,self.s_r_number_of_books+1): for i in range(1,self.s_r_number_of_books+1):
@ -403,22 +406,28 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
self.test_text.editTextChanged[str].connect(self.s_r_paint_results) self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
self.comma_separated.stateChanged.connect(self.s_r_paint_results) self.comma_separated.stateChanged.connect(self.s_r_paint_results)
self.case_sensitive.stateChanged.connect(self.s_r_paint_results) self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
self.s_r_template.lost_focus.connect(self.s_r_template_changed)
self.central_widget.setCurrentIndex(0) self.central_widget.setCurrentIndex(0)
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive) self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive) self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
self.s_r_template.completer().setCaseSensitivity(Qt.CaseSensitive)
self.s_r_search_mode_changed(self.search_mode.currentIndex()) self.s_r_search_mode_changed(self.search_mode.currentIndex())
def s_r_get_field(self, mi, field): def s_r_get_field(self, mi, field):
if field: if field:
if field == '{template}':
v = composite_formatter.safe_format\
(unicode(self.s_r_template.text()), mi, _('S/R TEMPLATE ERROR'), mi)
return [v]
fm = self.db.metadata_for_field(field) fm = self.db.metadata_for_field(field)
if field == 'sort': if field == 'sort':
val = mi.get('title_sort', None) val = mi.get('title_sort', None)
else: else:
val = mi.get(field, None) val = mi.get(field, None)
if val is None: if val is None:
val = [] val = [] if fm['is_multiple'] else ['']
elif not fm['is_multiple']: elif not fm['is_multiple']:
val = [val] val = [val]
elif field == 'authors': elif field == 'authors':
@ -427,7 +436,16 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
val = [] val = []
return val return val
def s_r_template_changed(self):
self.s_r_search_field_changed(self.search_field.currentIndex())
def s_r_search_field_changed(self, idx): def s_r_search_field_changed(self, idx):
if self.search_mode.currentIndex() != 0 and idx == 1: # Template
self.s_r_template.setVisible(True)
self.template_label.setVisible(True)
else:
self.s_r_template.setVisible(False)
self.template_label.setVisible(False)
for i in range(0, self.s_r_number_of_books): for i in range(0, self.s_r_number_of_books):
w = getattr(self, 'book_%d_text'%(i+1)) w = getattr(self, 'book_%d_text'%(i+1))
mi = self.db.get_metadata(self.ids[i], index_is_id=True) mi = self.db.get_metadata(self.ids[i], index_is_id=True)
@ -590,11 +608,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
if not dest: if not dest:
dest = source dest = source
dfm = self.db.field_metadata[dest] dfm = self.db.field_metadata[dest]
mi = self.db.get_metadata(id, index_is_id=True,) mi = self.db.get_metadata(id, index_is_id=True,)
val = mi.get(source)
if val is None:
return
val = self.s_r_do_regexp(mi) val = self.s_r_do_regexp(mi)
val = self.s_r_do_destination(mi, val) val = self.s_r_do_destination(mi, val)
if dfm['is_multiple']: if dfm['is_multiple']:

View File

@ -508,6 +508,29 @@ Future conversion of these books will use the default settings.</string>
</layout> </layout>
</item> </item>
<item row="4" column="0"> <item row="4" column="0">
<widget class="QLabel" name="template_label">
<property name="text">
<string>Te&amp;mplate:</string>
</property>
<property name="buddy">
<cstring>s_r_template</cstring>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="HistoryLineEdit" name="s_r_template">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>100</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a template to be used as the source for the search/replace</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="xlabel_2"> <widget class="QLabel" name="xlabel_2">
<property name="text"> <property name="text">
<string>&amp;Search for:</string> <string>&amp;Search for:</string>
@ -517,7 +540,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<widget class="HistoryLineEdit" name="search_for"> <widget class="HistoryLineEdit" name="search_for">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -530,7 +553,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="2"> <item row="5" column="2">
<widget class="QCheckBox" name="case_sensitive"> <widget class="QCheckBox" name="case_sensitive">
<property name="toolTip"> <property name="toolTip">
<string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string> <string>Check this box if the search string must match exactly upper and lower case. Uncheck it if case is to be ignored</string>
@ -543,7 +566,7 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="xlabel_4"> <widget class="QLabel" name="xlabel_4">
<property name="text"> <property name="text">
<string>&amp;Replace with:</string> <string>&amp;Replace with:</string>
@ -553,14 +576,14 @@ Future conversion of these books will use the default settings.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1"> <item row="6" column="1">
<widget class="HistoryLineEdit" name="replace_with"> <widget class="HistoryLineEdit" name="replace_with">
<property name="toolTip"> <property name="toolTip">
<string>The replacement text. The matched search text will be replaced with this string</string> <string>The replacement text. The matched search text will be replaced with this string</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2"> <item row="6" column="2">
<layout class="QHBoxLayout" name="verticalLayout"> <layout class="QHBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="label_41"> <widget class="QLabel" name="label_41">
@ -595,7 +618,7 @@ field is processed. In regular expression mode, only the matched text is process
</item> </item>
</layout> </layout>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="destination_field_label"> <widget class="QLabel" name="destination_field_label">
<property name="text"> <property name="text">
<string>&amp;Destination field:</string> <string>&amp;Destination field:</string>
@ -605,14 +628,15 @@ field is processed. In regular expression mode, only the matched text is process
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="7" column="1">
<widget class="QComboBox" name="destination_field"> <widget class="QComboBox" name="destination_field">
<property name="toolTip"> <property name="toolTip">
<string>The field that the text will be put into after all replacements. If blank, the source field is used.</string> <string>The field that the text will be put into after all replacements.
If blank, the source field is used if the field is modifiable</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="2"> <item row="7" column="2">
<layout class="QHBoxLayout" name="verticalLayout"> <layout class="QHBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="replace_mode_label"> <widget class="QLabel" name="replace_mode_label">
@ -660,7 +684,7 @@ nothing should be put between the original text and the inserted text</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="7" column="1"> <item row="8" column="1">
<widget class="QLabel" name="xlabel_3"> <widget class="QLabel" name="xlabel_3">
<property name="text"> <property name="text">
<string>Test &amp;text</string> <string>Test &amp;text</string>
@ -670,7 +694,7 @@ nothing should be put between the original text and the inserted text</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="2"> <item row="8" column="2">
<widget class="QLabel" name="label_51"> <widget class="QLabel" name="label_51">
<property name="text"> <property name="text">
<string>Test re&amp;sult</string> <string>Test re&amp;sult</string>
@ -791,6 +815,7 @@ nothing should be put between the original text and the inserted text</string>
<tabstop>central_widget</tabstop> <tabstop>central_widget</tabstop>
<tabstop>search_field</tabstop> <tabstop>search_field</tabstop>
<tabstop>search_mode</tabstop> <tabstop>search_mode</tabstop>
<tabstop>s_r_template</tabstop>
<tabstop>search_for</tabstop> <tabstop>search_for</tabstop>
<tabstop>case_sensitive</tabstop> <tabstop>case_sensitive</tabstop>
<tabstop>replace_with</tabstop> <tabstop>replace_with</tabstop>

View File

@ -34,6 +34,7 @@ from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.preferences.social import SocialMetadata from calibre.gui2.preferences.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime from calibre import strftime
from calibre.library.comments import comments_to_html
class CoverFetcher(Thread): # {{{ class CoverFetcher(Thread): # {{{
@ -195,7 +196,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
_file + _(" is not a valid picture")) _file + _(" is not a valid picture"))
d.exec_() d.exec_()
else: else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix) self.cover.setPixmap(pix)
self.update_cover_tooltip() self.update_cover_tooltip()
self.cover_changed = True self.cover_changed = True
@ -409,7 +409,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if mi.series_index is not None: if mi.series_index is not None:
self.series_index.setValue(float(mi.series_index)) self.series_index.setValue(float(mi.series_index))
if mi.comments and mi.comments.strip(): if mi.comments and mi.comments.strip():
self.comments.setPlainText(mi.comments) comments = comments_to_html(mi.comments)
self.comments.html = comments
def sync_formats(self): def sync_formats(self):
@ -556,7 +557,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if rating > 0: if rating > 0:
self.rating.setValue(int(rating/2.)) self.rating.setValue(int(rating/2.))
comments = self.db.comments(row) comments = self.db.comments(row)
self.comments.setPlainText(comments if comments else '') if comments and comments.strip():
comments = comments_to_html(comments)
self.comments.html = comments
cover = self.db.cover(row) cover = self.db.cover(row)
pubdate = db.pubdate(self.id, index_is_id=True) pubdate = db.pubdate(self.id, index_is_id=True)
self.pubdate.setDate(QDate(pubdate.year, pubdate.month, self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
@ -806,10 +809,10 @@ 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 = unicode(self.comments.toPlainText()) prefix = self.comments.html
if prefix: if prefix:
prefix += '\n' prefix += '\n'
self.comments.setPlainText(prefix + summ) self.comments.html = prefix + comments_to_html(summ)
if book.rating is not None: if book.rating is not None:
self.rating.setValue(int(book.rating)) self.rating.setValue(int(book.rating))
if book.tags: if book.tags:
@ -899,7 +902,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.db.set_series_index(self.id, self.series_index.value(), self.db.set_series_index(self.id, self.series_index.value(),
notify=False, commit=False) notify=False, commit=False)
self.db.set_comment(self.id, self.db.set_comment(self.id,
unicode(self.comments.toPlainText()).strip(), self.comments.html,
notify=False, commit=False) notify=False, commit=False)
d = self.pubdate.date() d = self.pubdate.date()
d = qt_to_dt(d) d = qt_to_dt(d)
@ -936,16 +939,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
QDialog.reject(self, *args) QDialog.reject(self, *args)
def read_state(self): def read_state(self):
wg = dynamic.get('metasingle_window_geometry', None) wg = dynamic.get('metasingle_window_geometry2', None)
ss = dynamic.get('metasingle_splitter_state', None) ss = dynamic.get('metasingle_splitter_state2', None)
if wg is not None: if wg is not None:
self.restoreGeometry(wg) self.restoreGeometry(wg)
if ss is not None: if ss is not None:
self.splitter.restoreState(ss) self.splitter.restoreState(ss)
def save_state(self): def save_state(self):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry())) dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state', dynamic.set('metasingle_splitter_state2',
bytes(self.splitter.saveState())) bytes(self.splitter.saveState()))
def break_cycles(self): def break_cycles(self):

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>887</width> <width>994</width>
<height>750</height> <height>716</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -43,8 +43,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>879</width> <width>986</width>
<height>711</height> <height>677</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout_5">
@ -66,8 +66,8 @@
<attribute name="title"> <attribute name="title">
<string>&amp;Basic metadata</string> <string>&amp;Basic metadata</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QGridLayout" name="gridLayout_5">
<item> <item row="0" column="0">
<widget class="QSplitter" name="splitter"> <widget class="QSplitter" name="splitter">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -496,28 +496,147 @@ Using this button to create author sort will change author sort from red to gree
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="groupBox_2"> <spacer name="verticalSpacer_5">
<property name="title"> <property name="orientation">
<string>&amp;Comments</string> <enum>Qt::Vertical</enum>
</property> </property>
<layout class="QGridLayout" name="gridLayout_4"> <property name="sizeType">
<item row="0" column="0"> <enum>QSizePolicy::Fixed</enum>
<widget class="QTextEdit" name="comments">
<property name="tabChangesFocus">
<bool>true</bool>
</property> </property>
<property name="acceptRichText"> <property name="sizeHint" stdset="0">
<bool>false</bool> <size>
<width>20</width>
<height>40</height>
</size>
</property> </property>
</widget> </spacer>
</item>
</layout>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="layoutWidget_2"> <widget class="QWidget" name="layoutWidget_2">
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="bc_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Book Cover</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Change &amp;cover image:</string>
</property>
<property name="buddy">
<cstring>cover_button</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="cover_button">
<property name="text">
<string>&amp;Browse</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="trim_cover_button">
<property name="toolTip">
<string>Remove border (if any) from cover</string>
</property>
<property name="text">
<string>T&amp;rim</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="reset_cover">
<property name="toolTip">
<string>Reset cover to default</string>
</property>
<property name="text">
<string>&amp;Remove</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QPushButton" name="fetch_cover_button">
<property name="text">
<string>Download co&amp;ver</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item> <item>
<widget class="QGroupBox" name="af_group_box"> <widget class="QGroupBox" name="af_group_box">
<property name="sizePolicy"> <property name="sizePolicy">
@ -546,6 +665,12 @@ Using this button to create author sort will change author sort from red to gree
<height>140</height> <height>140</height>
</size> </size>
</property> </property>
<property name="baseSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="dragDropMode"> <property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum> <enum>QAbstractItemView::DropOnly</enum>
</property> </property>
@ -644,129 +769,22 @@ Using this button to create author sort will change author sort from red to gree
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="bc_box"> <widget class="QGroupBox" name="groupBox">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>10</verstretch> <verstretch>10</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="title"> <property name="title">
<string>Book Cover</string> <string>&amp;Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="ImageView" name="cover" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="_4">
<property name="spacing">
<number>6</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<property name="margin"> <property name="margin">
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="label_5"> <widget class="Editor" name="comments" native="true"/>
<property name="text">
<string>Change &amp;cover image:</string>
</property>
<property name="buddy">
<cstring>cover_path</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="_5">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="cover_path">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cover_button">
<property name="text">
<string>&amp;Browse</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/document_open.png</normaloff>:/images/document_open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="trim_cover_button">
<property name="toolTip">
<string>Remove border (if any) from cover</string>
</property>
<property name="text">
<string>T&amp;rim</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trim.png</normaloff>:/images/trim.png</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="reset_cover">
<property name="toolTip">
<string>Reset cover to default</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="_6">
<item>
<widget class="QPushButton" name="fetch_cover_button">
<property name="text">
<string>Download co&amp;ver</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>
@ -828,6 +846,12 @@ Using this button to create author sort will change author sort from red to gree
<header>calibre/gui2/widgets.h</header> <header>calibre/gui2/widgets.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>Editor</class>
<extends>QWidget</extends>
<header location="global">calibre/gui2/comments_editor.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>title</tabstop> <tabstop>title</tabstop>
@ -848,13 +872,11 @@ Using this button to create author sort will change author sort from red to gree
<tabstop>date</tabstop> <tabstop>date</tabstop>
<tabstop>pubdate</tabstop> <tabstop>pubdate</tabstop>
<tabstop>fetch_metadata_button</tabstop> <tabstop>fetch_metadata_button</tabstop>
<tabstop>comments</tabstop>
<tabstop>button_set_cover</tabstop> <tabstop>button_set_cover</tabstop>
<tabstop>button_set_metadata</tabstop> <tabstop>button_set_metadata</tabstop>
<tabstop>formats</tabstop> <tabstop>formats</tabstop>
<tabstop>add_format_button</tabstop> <tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop> <tabstop>remove_format_button</tabstop>
<tabstop>cover_path</tabstop>
<tabstop>cover_button</tabstop> <tabstop>cover_button</tabstop>
<tabstop>trim_cover_button</tabstop> <tabstop>trim_cover_button</tabstop>
<tabstop>reset_cover</tabstop> <tabstop>reset_cover</tabstop>

View File

@ -23,7 +23,6 @@ from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH, MetadataBackup REGEXP_MATCH, MetadataBackup
from calibre.library.cli import parse_series_string
from calibre import strftime, isbytestring, prepare_string_for_xml from calibre import strftime, isbytestring, prepare_string_for_xml
from calibre.constants import filesystem_encoding, DEBUG from calibre.constants import filesystem_encoding, DEBUG
from calibre.gui2.library import DEFAULT_SORT from calibre.gui2.library import DEFAULT_SORT
@ -725,9 +724,7 @@ class BooksModel(QAbstractTableModel): # {{{
return False return False
val = qt_to_dt(val, as_utc=False) val = qt_to_dt(val, as_utc=False)
elif typ == 'series': elif typ == 'series':
val, s_index = parse_series_string(self.db, label, value.toString()) val = unicode(value.toString()).strip()
if not val:
val = s_index = None
elif typ == 'composite': elif typ == 'composite':
tmpl = unicode(value.toString()).strip() tmpl = unicode(value.toString()).strip()
disp = cc['display'] disp = cc['display']

View File

@ -695,8 +695,10 @@ class TagsModel(QAbstractItemModel): # {{{
def setData(self, index, value, role=Qt.EditRole): def setData(self, index, value, role=Qt.EditRole):
if not index.isValid(): if not index.isValid():
return NONE return NONE
# set up to position at the category label # set up to reposition at the same item. We can do this except if
path = self.path_for_index(self.parent(index)) # working with the last item and that item is deleted, in which case
# we position at the parent label
path = index.model().path_for_index(index)
val = unicode(value.toString()) val = unicode(value.toString())
if not val: if not val:
error_dialog(self.tags_view, _('Item is blank'), error_dialog(self.tags_view, _('Item is blank'),
@ -947,18 +949,22 @@ class TagBrowserMixin(object): # {{{
for old_id in to_rename[text]: for old_id in to_rename[text]:
rename_func(old_id, new_name=unicode(text)) rename_func(old_id, new_name=unicode(text))
# Clean up everything, as information could have changed for many books. # Clean up the library view
self.library_view.model().refresh() self.do_tag_item_renamed()
self.tags_view.set_new_model() self.tags_view.set_new_model() # does a refresh for free
self.tags_view.recount()
self.saved_search.clear()
self.search.clear()
def do_tag_item_renamed(self): def do_tag_item_renamed(self):
# Clean up library view and search # Clean up library view and search
self.library_view.model().refresh() # get information to redo the selection
self.saved_search.clear() rows = [r.row() for r in \
self.search.clear() self.library_view.selectionModel().selectedRows()]
m = self.library_view.model()
ids = [m.id(r) for r in rows]
m.refresh(reset=False)
m.research()
self.library_view.select_rows(ids)
# refreshing the tags view happens at the emit()/call() site
def do_author_sort_edit(self, parent, id): def do_author_sort_edit(self, parent, id):
db = self.library_view.model().db db = self.library_view.model().db

View File

@ -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
# }}}

View 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

View File

@ -524,6 +524,8 @@ class EnComboBox(QComboBox):
class HistoryLineEdit(QComboBox): class HistoryLineEdit(QComboBox):
lost_focus = pyqtSignal()
def __init__(self, *args): def __init__(self, *args):
QComboBox.__init__(self, *args) QComboBox.__init__(self, *args)
self.setEditable(True) self.setEditable(True)
@ -559,6 +561,10 @@ class HistoryLineEdit(QComboBox):
def text(self): def text(self):
return self.currentText() return self.currentText()
def focusOutEvent(self, e):
QComboBox.focusOutEvent(self, e)
self.lost_focus.emit()
class ComboBoxWithHelp(QComboBox): class ComboBoxWithHelp(QComboBox):
''' '''
A combobox where item 0 is help text. CurrentText will return '' for item 0. A combobox where item 0 is help text. CurrentText will return '' for item 0.

View File

@ -445,6 +445,9 @@ class CustomColumns(object):
index_is_id=True) index_is_id=True)
val = self.custom_data_adapters[data['datatype']](val, data) val = self.custom_data_adapters[data['datatype']](val, data)
if data['datatype'] == 'series' and extra is None:
(val, extra) = self._get_series_values(val)
if data['normalized']: if data['normalized']:
if data['datatype'] == 'enumeration' and ( if data['datatype'] == 'enumeration' and (
val and val not in data['display']['enum_values']): val and val not in data['display']['enum_values']):

View File

@ -2133,9 +2133,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
self.conn.commit() self.conn.commit()
series_index_pat = re.compile(r'(.*)\s+\[([.0-9]+)\]$')
def _get_series_values(self, val):
if not val:
return (val, None)
match = self.series_index_pat.match(val.strip())
if match is not None:
idx = match.group(2)
try:
idx = float(idx)
return (match.group(1).strip(), idx)
except:
pass
return (val, None)
def set_series(self, id, series, notify=True, commit=True): def set_series(self, id, series, notify=True, commit=True):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1') self.conn.execute('''DELETE FROM series
WHERE (SELECT COUNT(id) FROM books_series_link
WHERE series=series.id) < 1''')
(series, idx) = self._get_series_values(series)
if series: if series:
if not isinstance(series, unicode): if not isinstance(series, unicode):
series = series.decode(preferred_encoding, 'replace') series = series.decode(preferred_encoding, 'replace')
@ -2147,6 +2165,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else: else:
aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid
self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid))
if idx:
self.set_series_index(id, idx, notify=notify, commit=commit)
self.dirtied([id], commit=False) self.dirtied([id], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()

View File

@ -7,6 +7,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, traceback, cStringIO, re, shutil import os, traceback, cStringIO, re, shutil
from functools import partial
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre.utils.config import Config, StringConfig, tweaks from calibre.utils.config import Config, StringConfig, tweaks
@ -139,8 +140,7 @@ class SafeFormat(TemplateFormatter):
def get_components(template, mi, id, timefmt='%b %Y', length=250, def get_components(template, mi, id, timefmt='%b %Y', length=250,
sanitize_func=ascii_filename, replace_whitespace=False, sanitize_func=ascii_filename, replace_whitespace=False,
to_lowercase=False): to_lowercase=False):
library_order = tweaks['save_template_title_series_sorting'] == 'library_order' tsfmt = partial(title_sort, order=tweaks['save_template_title_series_sorting'])
tsfmt = title_sort if library_order else lambda x: x
format_args = FORMAT_ARGS.copy() format_args = FORMAT_ARGS.copy()
format_args.update(mi.all_non_none_fields()) format_args.update(mi.all_non_none_fields())
if mi.title: if mi.title:

View File

@ -16,7 +16,6 @@ from datetime import datetime
from functools import partial from functools import partial
from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat from calibre.utils.date import parse_date, isoformat
from calibre import isbytestring, force_unicode from calibre import isbytestring, force_unicode
from calibre.constants import iswindows, DEBUG from calibre.constants import iswindows, DEBUG

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -208,11 +208,12 @@ The following functions are available in addition to those described in single-f
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
* ``field(name)`` -- returns the metadata field named by ``name``. * ``field(name)`` -- returns the metadata field named by ``name``.
* ``multiply`` -- returns x * y. Throws an exception if either x or y are not numbers. * ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments. * ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
* ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``strcmp(x, y, lt, eq, gt)`` -- does a case-insensitive comparison x and y as strings. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``. * ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
* ``subtract`` -- returns x - y. Throws an exception if either x or y are not numbers. * ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
* ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value.
Special notes for save/send templates Special notes for save/send templates
------------------------------------- -------------------------------------

View File

@ -57,6 +57,11 @@ class _Parser(object):
y = float(y if y else 0) y = float(y if y else 0)
return ops[op](x, y) return ops[op](x, y)
def _template(self, template):
template = template.replace('[[', '{').replace(']]', '}')
return self.parent.safe_format(template, self.parent.kwargs, 'TEMPLATE',
self.parent.book)
local_functions = { local_functions = {
'add' : (2, partial(_math, op='+')), 'add' : (2, partial(_math, op='+')),
'assign' : (2, _assign), 'assign' : (2, _assign),
@ -68,6 +73,7 @@ class _Parser(object):
'strcmp' : (5, _strcmp), 'strcmp' : (5, _strcmp),
'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]), 'substr' : (3, lambda s, x, y, z: x[int(y): len(x) if int(z) == 0 else int(z)]),
'subtract' : (2, partial(_math, op='-')), 'subtract' : (2, partial(_math, op='-')),
'template' : (1, _template)
} }
def __init__(self, val, prog, parent): def __init__(self, val, prog, parent):