merged 07.17 changes

This commit is contained in:
ldolse 2010-09-04 15:20:03 +10:00
commit 572633bac1
85 changed files with 32301 additions and 23268 deletions

View File

@ -4,6 +4,63 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.17
date: 2010-09-03
new features:
- title: "Content server: Show custom column data in the book listing"
- title: "Add preference to automatically set a tag when adding books (Preferences->General)"
- title: "Add a tweak to create compound search terms. Show error message in tooltip when user inputs an invalid search query."
- title: "Managing multiple libraries: Allow renaming/deleting libraries from the Choose library menu"
- title: "Searching on series index is now possible. See the User Manual for details."
bug fixes:
- title: "Fix regression in 0.7.16 that broke conversion of HTML files with preprocess turned on"
- title: "MOBI Output: When converting an input document that specifies an inline TOC in the <guide> but not in the <spine>, add it correctly. Fixes #6661 (Conversion to MOBI fails to create TOC)"
tickets: [6661]
- title: "JetBook driver: Only use JetBook naming scheme for txt, pdf and fb2 files."
tickets: [6638]
- title: "Copy to library action now respects merge preferences"
tickets: [6641]
- title: "Fix bug in email sending when using an SSL connection"
- title: "Kobo driver: Fix bug that prevented metadata caching from working correctly"
tickets: [6015]
- title: "Fix regression in 0.7.16 that caused calibre to forget its preferences on each restart for new installs on linux"
- title: "News downloads: Cut off long downloaded from URLs"
tickets: [6649]
new recipes:
- title: "HOY"
author: Fco Javier Nieto
- title: "Milenio"
author: bmsleight
- title: "Winnipeg Free Press"
author: buyo
- title: "Field and stream blog, West Hawaii Today, Marietta Daily Journal"
author: Tony Stegall
- title: "Europa Sur"
author: "Darko Miletic"
improved recipes:
- La Jornada
- Slate
- version: 0.7.16
date: 2010-08-27

View File

@ -50,7 +50,7 @@ function render_book(book) {
var comments = $.trim(book.text()).replace(/\n\n/, '<br/>');
var formats = new Array();
var size = (parseFloat(book.attr('size'))/(1024*1024)).toFixed(1);
var tags = book.attr('tags').replace(/,/g, ', ');
var tags = book.attr('tags')
formats = book.attr("formats").split(",");
if (formats.length > 0) {
for (i=0; i < formats.length; i++) {
@ -59,7 +59,14 @@ function render_book(book) {
title = title.slice(0, title.length-2);
title += '&nbsp;({0}&nbsp;MB)&nbsp;'.format(size);
}
if (tags) title += '[{0}]'.format(tags);
if (tags) title += 'Tags=[{0}] '.format(tags);
custcols = book.attr("custcols").split(',')
for ( i = 0; i < custcols.length; i++) {
if (custcols[i].length > 0) {
vals = book.attr(custcols[i]).split(':#:', 2);
title += '{0}=[{1}] '.format(vals[0], vals[1]);
}
}
title += '<img style="display:none" alt="" src="get/cover/{0}" /></span>'.format(id);
title += '<div class="comments">{0}</div>'.format(comments)
// Render authors cell

View File

@ -91,3 +91,26 @@ save_template_title_series_sorting = 'library_order'
# auto_connect_to_folder = 'C:\\Users\\someone\\Desktop\\testlib'
# auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = ''
# Create search terms to apply a query across several built-in search terms.
# Syntax: {'new term':['existing term 1', 'term 2', ...], 'new':['old'...] ...}
# Example: create the term 'myseries' that when used as myseries:foo would
# search all of the search categories 'series', '#myseries', and '#myseries2':
# grouped_search_terms={'myseries':['series','#myseries', '#myseries2']}
# Example: two search terms 'a' and 'b' both that search 'tags' and '#mytags':
# grouped_search_terms={'a':['tags','#mytags'], 'b':['tags','#mytags']}
# Note: You cannot create a search term that is a duplicate of an existing term.
# Such duplicates will be silently ignored. Also note that search terms ignore
# case. 'MySearch' and 'mysearch' are the same term.
grouped_search_terms = {}
# Set this to True (not 'True') to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library
add_new_book_tags_when_importing_books = False
# Set the maximum number of tags to show per book in the content server
max_content_server_tags_shown=5

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

View File

@ -18,7 +18,7 @@ class Clarin(BasicNewsRecipe):
use_embedded_content = False
no_stylesheets = True
encoding = 'utf8'
language = 'es_AR'
language = 'es'
publication_type = 'newspaper'
INDEX = 'http://www.clarin.com'
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'

View File

@ -20,7 +20,7 @@ class Europasur(BasicNewsRecipe):
delay = 2
no_stylesheets = True
encoding = 'cp1252'
language = 'es_ES'
language = 'es'
publication_type = 'newspaper'
extra_css = """ body{font-family: Verdana,Arial,Helvetica,sans-serif}
h2{font-family: Georgia,Times New Roman,Times,serif}

View File

@ -0,0 +1,64 @@
from calibre.web.feeds.news import BasicNewsRecipe
class FIELDSTREAM(BasicNewsRecipe):
title = 'Field and Stream'
__author__ = 'Starson17 and Tonythebookworm'
description = 'Hunting and Fishing and Gun Talk'
language = 'en'
no_stylesheets = True
publisher = 'Starson17 and Tonythebookworm'
category = 'food recipes, hunting, fishing, guns'
use_embedded_content= False
no_stylesheets = True
oldest_article = 24
remove_javascript = True
remove_empty_feeds = True
masthead_url = 'http://www.fieldandstream.com/sites/all/themes/fs/logo.png'
cover_url = 'http://www.arrowheadflyangler.com/Portals/1/Articles/FieldStream/Field%20and%20Stream%20March%20Fishing%20Edition%20Article%20Cover.jpg'
# recursions = 0
max_articles_per_feed = 10
INDEX = 'http://www.fieldandstream.com'
keep_only_tags = [dict(name='div', attrs={'class':['interior-main']})
]
remove_tags = [dict(name='div', attrs={'id':['comments']})]
def parse_index(self):
feeds = []
for title, url in [
(u"Wild Chef", u"http://www.fieldandstream.com/blogs/wild-chef"),
(u"The Gun Nut", u"http://www.fieldandstream.com/blogs/gun-nut"),
(u"Whitetail 365", u"http://www.fieldandstream.com/blogs/whitetail-365"),
(u"Fly Talk", u"http://www.fieldandstream.com/blogs/flytalk"),
(u"Generation Wild", u"http://www.fieldandstream.com/blogs/generation-wild"),
(u"Conservationist", u"http://www.fieldandstream.com/blogs/conservationist"),
(u"Honest Angler", u"http://www.fieldandstream.com/blogs/honest-angler"),
(u"Mans Best Friend", u"http://www.fieldandstream.com/blogs/mans-best-friend"),
]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
return feeds
def make_links(self, url):
title = 'Temp'
current_articles = []
soup = self.index_to_soup(url)
print 'The soup is: ', soup
for item in soup.findAll('h2'):
print 'item is: ', item
link = item.find('a')
print 'the link is: ', link
if link:
url = self.INDEX + link['href']
title = self.tag_to_string(link)
print 'the title is: ', title
print 'the url is: ', url
print 'the title is: ', title
current_articles.append({'title': title, 'url': url, 'description':'', 'date':''}) # append all this
return current_articles

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2010, Francisco Javier Nieto <frjanibo at gmail.com>'
'''
www.hoy.es
'''
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class Hoy(BasicNewsRecipe):
title = 'HOY'
__author__ = 'Fco Javier Nieto'
description = u'Noticias desde Extremadura'
publisher = 'HOY'
category = 'news, politics, Spain, Extremadura'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
delay = 1
encoding = 'cp1252'
language = 'es'
feeds = [
(u'Portada' , u'http://www.hoy.es/portada.xml' ),
(u'Regional' , u'http://www.hoy.es/rss/feeds/regional.xml' ),
(u'Prov de Badajoz' , u'http://www.hoy.es/rss/feeds/prov_badajoz.xml' ),
(u'Prov de Caceres' , u'http://www.hoy.es/rss/feeds/prov_caceres.xml' ),
(u'Badajoz' , u'http://www.hoy.es/rss/feeds/badajoz.xml' ),
(u'Caceres' , u'http://www.hoy.es/rss/feeds/caceres.xml' ),
(u'Merida' , u'http://www.hoy.es/rss/feeds/merida.xml' ),
(u'Opinion' , u'http://www.hoy.es/rss/feeds/opinion.xml' ),
(u'Nacional' , u'http://www.hoy.es/rss/feeds/nacional.xml' ),
(u'Internacional' , u'http://www.hoy.es/rss/feeds/internacional.xml' ),
(u'Economia' , u'http://www.hoy.es/rss/feeds/economia.xml' ),
(u'Deportes' , u'http://www.hoy.es/rss/feeds/deportes.xml' ),
(u'Sociedad' , u'http://www.hoy.es/rss/feeds/sociedad.xml' ),
(u'Cultura' , u'http://www.hoy.es/rss/feeds/cultura.xml' ),
(u'Television' , u'http://www.hoy.es/rss/feeds/television.xml' ),
(u'contraportada' , u'http://www.hoy.es/rss/feeds/contraportada.xml' )
]
keep_only_tags = [
dict(name='h1', attrs={'class':['headline']}),
dict(name='h2', attrs={'class':['subhead']}),
dict(name='div', attrs={'class':['text']})
]
remove_tags = [
dict(name=['object','link','script'])
,dict(name='div', attrs={'class':['colC_articulo','peu']})
]
remove_tags_after = [dict(name='div', attrs={'class':'text'})]
extra_css = '.headline {font: sans-serif 2em;}\n.subhead,h2{font: sans-serif 1.5em\n'
def preprocess_html(self, soup):
soup.html['dir' ] = self.direction
mcharset = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset=utf-8")])
soup.head.insert(0,mcharset)
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,120 +1,64 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Rogelio Dominguez <rogelio.dominguez at gmail.com>'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.jornada.unam.mx
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
import re
class LaJornada(BasicNewsRecipe):
title = u'La Jornada'
language = 'es'
oldest_article = 1
__author__ = 'rogeliodh'
max_articles_per_feed = 100
remove_tags = [dict(name='div', attrs={'class':['go gui','go gui top','comment-cont',]})]
remove_tags_before = dict(id='article-cont')
remove_tags_after = dict(id='article-cont')
class LaJornada_mx(BasicNewsRecipe):
title = 'La Jornada (Mexico)'
__author__ = 'Darko Miletic'
description = 'Noticias del diario mexicano La Jornada'
publisher = 'DEMOS, Desarrollo de Medios, S.A. de C.V.'
category = 'news, Mexico'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
extra_css = ' .series{ \
border-bottom: 1px solid #626366; \
font-weight: bold; \
} \
.sumario{ \
font-weight: bold; \
margin-top: 2em; \
text-align: center \
} \
p.sumario{ \
text-align: center \
} \
.sumarios{font-weight: bold} \
.cabeza{ font-size: 1.5em} \
.pie-foto { \
text-align: justify; \
font-size: 0.8em; \
text-align: justify; \
} \
.pie-foto .credito { \
font-weight: bold; \
display: block \
} \
.credito-autor{ \
margin-top: 1.5em; \
padding-left: 0.6em; \
border-bottom: 1px solid #626366; \
font-variant: small-caps; \
font-weight: bold \
} \
.credito-articulo{ \
margin-top: 1.5em; \
padding-left: 0.6em; \
border-bottom: 1px solid #626366; \
font-variant: small-caps; \
font-weight: bold \
} \
.credito-titulo{text-align: right} \
.hemero { \
text-align: right; \
font-size: 0.9em; \
margin-bottom: 8px; \
} \
.loc { \
font-weight: bold; \
} \
.carton { \
text-align: center; \
} \
.credit { \
font-weight: bold; \
} \
'
encoding = 'utf8'
use_embedded_content = False
language = 'es'
remove_empty_feeds = True
cover_url = strftime("http://www.jornada.unam.mx/%Y/%m/%d/planitas/portadita.jpg")
masthead_url = 'http://www.jornada.unam.mx/v7.0/imagenes/la-jornada-trans.png'
extra_css = """
body{font-family: "Times New Roman",serif }
.cabeza{font-size: xx-large; font-weight: bold }
.credito-articulo{font-size: 1.3em}
"""
preprocess_regexps = [
# Remove capitalized initial letter on some articles (editorial)
(re.compile(r'<div class="inicial">(.*)</div><p class="s-s">', re.DOTALL|re.IGNORECASE),
lambda match: match.group(1)),
# Cartons section uses a class instead of a div to identify the main content. Change it.
(re.compile(r'class="carton"', re.DOTALL|re.IGNORECASE),
lambda match: 'id="article-cont" class="carton"'),
# Remove <link rel="alternate"> as calibre has a bug (to report)
(re.compile(r'<link rel="alternate".*?/>', re.DOTALL|re.IGNORECASE),
lambda match: ''),
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [
dict(name='div', attrs={'class':['documentContent','cabeza','sumarios','text']})
,dict(name='div', attrs={'id':'renderComments'})
]
remove_tags = [dict(name='div', attrs={'class':'buttonbar'})]
INDEX = 'http://www.jornada.unam.mx/rss/edicion.xml'
feeds = [
(u'Opinion','http://www.jornada.unam.mx/rss/opinion.xml'),
(u'Cartones','http://www.jornada.unam.mx/rss/cartones.xml'),
(u'Política','http://www.jornada.unam.mx/rss/politica.xml'),
(u'Economía','http://www.jornada.unam.mx/rss/economia.xml'),
(u'Mundo','http://www.jornada.unam.mx/rss/mundo.xml'),
(u'Estados','http://www.jornada.unam.mx/rss/estados.xml'),
(u'Capital','http://www.jornada.unam.mx/rss/capital.xml'),
(u'Sociedad','http://www.jornada.unam.mx/rss/sociedad.xml'),
(u'Ciencias','http://www.jornada.unam.mx/rss/ciencias.xml'),
(u'Cultura','http://www.jornada.unam.mx/rss/cultura.xml'),
(u'Gastronomia','http://www.jornada.unam.mx/rss/gastronomia.xml'),
(u'Espectáculos','http://www.jornada.unam.mx/rss/espectaculos.xml'),
(u'Deportes','http://www.jornada.unam.mx/rss/deportes.xml'),
(u'Ultimas noticias' , u'http://www.jornada.unam.mx/ultimas/news/RSS' )
,(u'Opinion' , u'http://www.jornada.unam.mx/rss/opinion.xml' )
,(u'Politica' , u'http://www.jornada.unam.mx/rss/politica.xml' )
,(u'Economia' , u'http://www.jornada.unam.mx/rss/economia.xml' )
,(u'Mundo' , u'http://www.jornada.unam.mx/rss/mundo.xml' )
,(u'Estados' , u'http://www.jornada.unam.mx/rss/estados.xml' )
,(u'Capital' , u'http://www.jornada.unam.mx/rss/capital.xml' )
,(u'Sociedad y justicia' , u'http://www.jornada.unam.mx/rss/sociedad.xml' )
,(u'Ciencias' , u'http://www.jornada.unam.mx/rss/ciencias.xml' )
,(u'Cultura' , u'http://www.jornada.unam.mx/rss/cultura.xml' )
,(u'Gastronomia' , u'http://www.jornada.unam.mx/rss/gastronomia.xml' )
,(u'Espectaculos' , u'http://www.jornada.unam.mx/rss/espectaculos.xml' )
,(u'Deportes' , u'http://www.jornada.unam.mx/rss/deportes.xml' )
]
def get_cover_url(self):
'''
Cover URL is http://www.jornada.unam.mx/YYYY/MM/DD/portada.pdf
'''
cover_url = None
soup = self.index_to_soup(self.INDEX)
soupstone = BeautifulStoneSoup(str(soup))
urlbase = str(soupstone('link')[0])
r= re.compile(r'.*http://www.jornada.unam.mx/([0-9]{4})/([0-9]{2})/([0-9]{2})', re.DOTALL|re.IGNORECASE)
m = r.match(urlbase)
if m:
cover_url = 'http://www.jornada.unam.mx/' + m.groups()[0] + '/' + m.groups()[1] + '/' + m.groups()[2] + '/portada.pdf'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup
return cover_url

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Brendan Sleight <bms.calibre at barwap.com>'
'''
impreso.milenio.com
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
import datetime
class Milenio(BasicNewsRecipe):
title = u'Milenio-diario'
__author__ = 'Bmsleight'
language = 'es'
description = 'Milenio-diario'
oldest_article = 10
max_articles_per_feed = 100
no_stylesheets = False
index = 'http://impreso.milenio.com'
keep_only_tags = [
dict(name='div', attrs={'class':'content'})
]
def parse_index(self):
# "%m/%d/%Y"
# http://impreso.milenio.com/Nacional/2010/09/01/
totalfeeds = []
soup = self.index_to_soup(self.index + "/Nacional/" + datetime.date.today().strftime("%Y/%m/%d"))
maincontent = soup.find('div',attrs={'class':'content'})
mfeed = []
if maincontent:
for itt in maincontent.findAll('a',href=True):
if "/node/" in str(itt['href']):
url = self.index + itt['href']
title = self.tag_to_string(itt)
description = ''
date = strftime(self.timefmt)
mfeed.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Articles', mfeed))
return totalfeeds

View File

@ -102,7 +102,6 @@ class PeriodicalNameHere(BasicNewsRecipe):
todays_section = soup.find(True, attrs={'class':'todaydateline'})
self.section_dates.append(self.tag_to_string(todays_section,use_alt=False))
self.section_dates.append(self.tag_to_string(todays_section,use_alt=False))
older_section_dates = soup.findAll(True, attrs={'class':'maindateline'})
for older_section in older_section_dates :

View File

@ -0,0 +1,30 @@
from calibre.web.feeds.news import BasicNewsRecipe
class WinnipegFreePress(BasicNewsRecipe):
title = u'Winnipeg Free Press'
__author__ = 'buyo'
description = 'News from Winnipeg, Manitoba, Canada'
oldest_article = 1
max_articles_per_feed = 15
category = 'News, Winnipeg, Canada'
cover_url = 'http://media.winnipegfreepress.com/designimages/winnipegfreepress_WFP.gif'
no_stylesheets = True
encoding = 'UTF-8'
remove_javascript = True
use_embedded_content = False
language = 'en_CA'
feeds = [(u'Breaking News', u'http://www.winnipegfreepress.com/rss?path=/breakingnews'),
(u'Local News',u'http://www.winnipegfreepress.com/rss?path=/local'),
(u'Breaking Business News',u'http://www.winnipegfreepress.com/rss?path=/business/finance'),
(u'Business',u'http://www.winnipegfreepress.com/rss?path=/business'),
(u'Editorials',u'http://www.winnipegfreepress.com/rss?path=/opinion/editorials'),
(u'Views from the West',u'http://www.winnipegfreepress.com/rss?path=/opinion/westview'),
(u'Life & Style',u'http://www.winnipegfreepress.com/rss?path=/life'),
(u'Food & Drink',u'http://www.winnipegfreepress.com/rss?path=/life/food')
]
keep_only_tags = [
dict(name='div', attrs={'id':'article_header'}),
dict(name='div', attrs={'class':'article'}),
]

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.16'
__version__ = '0.7.17'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re
@ -84,6 +84,9 @@ if plugins is None:
# }}}
# config_dir {{{
CONFIG_DIR_MODE = 0700
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
@ -98,7 +101,11 @@ elif isosx:
else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
if not os.access(config_dir, os.W_OK):
try:
os.makedirs(config_dir, mode=CONFIG_DIR_MODE)
except:
pass
if not os.access(config_dir, os.W_OK) or not os.access(config_dir, os.X_OK):
print 'No write acces to', config_dir, 'using a temporary dir instead'
import tempfile, atexit
config_dir = tempfile.mkdtemp(prefix='calibre-config-')

View File

@ -391,6 +391,8 @@ class PreferencesPlugin(Plugin): # {{{
#: The category this plugin should be in
category = None
#: The category name displayed to the user for this plugin
gui_category = None
#: The name displayed to the user for this plugin
gui_name = None

View File

@ -679,12 +679,40 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
class LookAndFeel(PreferencesPlugin):
name = 'Look & Feel'
gui_name = _('Look and Feel')
category = _('Interface')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 1
config_widget = 'calibre.gui2.preferences.look_feel'
plugins += [LookAndFeel]
class Behavior(PreferencesPlugin):
name = 'Behavior'
gui_name = _('Behavior')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 2
config_widget = 'calibre.gui2.preferences.behavior'
class Columns(PreferencesPlugin):
name = 'Custom Columns'
gui_name = _('Add your own columns')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 3
config_widget = 'calibre.gui2.preferences.columns'
class Toolbar(PreferencesPlugin):
name = 'Toolbar'
gui_name = _('Customize the toolbar')
category = 'Interface'
gui_category = _('Interface')
category_order = 1
name_order = 4
config_widget = 'calibre.gui2.preferences.toolbar'
plugins += [LookAndFeel, Behavior, Columns, Toolbar]
#}}}

View File

@ -41,7 +41,7 @@ class ANDROID(USBMS):
0x502 : { 0x3203 : [0x0100]},
# Dell
0x413c : { 0xb007 : [0x0100]},
0x413c : { 0xb007 : [0x0100, 0x0224]},
# Eken?
0x040d : { 0x0851 : [0x0001]},

View File

@ -50,6 +50,8 @@ class JETBOOK(USBMS):
def filename_callback(self, fname, mi):
fileext = os.path.splitext(os.path.basename(fname))[1]
if fileext.lower() not in ('txt', 'pdf', 'fb2'):
return fname
title = mi.title if mi.title else 'Unknown'
title = title.replace(' ', '_')
au = mi.format_authors()

View File

@ -23,7 +23,7 @@ class Book(MetaInformation):
'uuid', 'device_collections',
]
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, other=None):
def __init__(self, prefix, lpath, title, authors, mime, date, ContentType, thumbnail_name, size=None, other=None):
MetaInformation.__init__(self, '')
self.device_collections = []
@ -42,10 +42,8 @@ class Book(MetaInformation):
else:
self.authors = [authors]
self.mime = mime
try:
self.size = os.path.getsize(self.path)
except OSError:
self.size = 0
self.size = size # will be set later if None
try:
if ContentType == '6':
self.datetime = time.strptime(date, "%Y-%m-%dT%H:%M:%S.%f")

View File

@ -94,19 +94,19 @@ class KOBO(USBMS):
idx = bl_cache.get(lpath, None)
if idx is not None:
bl_cache[lpath] = None
if ImageID is not None:
imagename = self.normalize_path(self._main_prefix + '.kobo/images/' + ImageID + ' - NickelBookCover.parsed')
#print "Image name Normalized: " + imagename
if imagename is not None:
bl[idx].thumbnail = ImageWrapper(imagename)
bl_cache[lpath] = None
if ContentType != '6':
if self.update_metadata_item(bl[idx]):
# print 'update_metadata_item returned true'
changed = True
bl[idx].device_collections = playlist_map.get(lpath, [])
else:
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
book = self.book_from_path(prefix, lpath, title, authors, mime, date, ContentType, ImageID)
# print 'Update booklist'
if bl.add_book(book, replace_metadata=False):
changed = True
@ -316,10 +316,10 @@ class KOBO(USBMS):
lpath = lpath[1:]
#print "path: " + lpath
#book = self.book_class(prefix, lpath, other=info)
lpath = self.normalize_path(prefix + lpath)
book = Book(prefix, lpath, '', '', '', '', '', '', other=info)
if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size
book._new_book = True # Must be before add_book
booklists[blist].add_book(book, replace_metadata=True)
self.report_progress(1.0, _('Adding books to device metadata listing...'))
@ -380,3 +380,19 @@ class KOBO(USBMS):
return USBMS.get_file(self, path, *args, **kwargs)
@classmethod
def book_from_path(cls, prefix, lpath, title, authors, mime, date, ContentType, ImageID):
from calibre.ebooks.metadata import MetaInformation
if cls.settings().read_metadata or cls.MUST_READ_METADATA:
mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath)))
else:
from calibre.ebooks.metadata.meta import metadata_from_filename
mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)),
cls.build_template_regexp())
if mi is None:
mi = MetaInformation(os.path.splitext(os.path.basename(lpath))[0],
[_('Unknown')])
size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size
book = Book(prefix, lpath, title, authors, mime, date, ContentType, ImageID, size=size, other=mi)
return book

View File

@ -196,8 +196,7 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
try:
new_cdata = open(mi.cover, 'rb').read()
except:
import traceback
traceback.print_exc()
pass
if new_cdata and raster_cover:
try:
cpath = posixpath.join(posixpath.dirname(reader.opf_path),

View File

@ -62,7 +62,16 @@ class HTMLTOCAdder(object):
def __call__(self, oeb, context):
if 'toc' in oeb.guide:
# Ensure toc pointed to in <guide> is in spine
from calibre.ebooks.oeb.base import urlnormalize
href = urlnormalize(oeb.guide['toc'].href)
if href in oeb.manifest.hrefs:
item = oeb.manifest.hrefs[href]
if oeb.spine.index(item) < 0:
oeb.spine.add(item, linear=False)
return
else:
oeb.guide.remove('toc')
if not getattr(getattr(oeb, 'toc', False), 'nodes', False):
return
oeb.logger.info('Generating in-line TOC...')

View File

@ -5,15 +5,16 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
import os, shutil
from functools import partial
from PyQt4.Qt import QMenu, Qt
from PyQt4.Qt import QMenu, Qt, QInputDialog
from calibre import isbytestring
from calibre.constants import filesystem_encoding
from calibre.utils.config import prefs
from calibre.gui2 import gprefs, warning_dialog, Dispatcher
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
question_dialog
from calibre.gui2.actions import InterfaceAction
class LibraryUsageStats(object):
@ -66,6 +67,13 @@ class LibraryUsageStats(object):
loc = loc[:-1]
return loc.split('/')[-1]
def rename(self, location, newloc):
newloc = self.canonicalize_path(newloc)
stats = self.stats.pop(location, None)
if stats is not None:
self.stats[newloc] = stats
self.write_stats()
class ChooseLibraryAction(InterfaceAction):
@ -80,7 +88,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection)
self.stats = LibraryUsageStats()
self.create_action(spec=(_('Switch to library...'), 'lt.png', None,
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library,
type=Qt.QueuedConnection)
@ -90,7 +98,13 @@ class ChooseLibraryAction(InterfaceAction):
self.quick_menu = QMenu(_('Quick switch'))
self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
self.qs_separator = self.choose_menu.addSeparator()
self.rename_menu = QMenu(_('Rename library'))
self.rename_menu_action = self.choose_menu.addMenu(self.rename_menu)
self.delete_menu = QMenu(_('Delete library'))
self.delete_menu_action = self.choose_menu.addMenu(self.delete_menu)
self.rename_separator = self.choose_menu.addSeparator()
self.switch_actions = []
for i in range(5):
ac = self.create_action(spec=('', None, None, None),
@ -123,9 +137,15 @@ class ChooseLibraryAction(InterfaceAction):
ac.setVisible(False)
self.quick_menu.clear()
self.qs_locations = [i[1] for i in locations]
self.rename_menu.clear()
self.delete_menu.clear()
for name, loc in locations:
self.quick_menu.addAction(name, Dispatcher(partial(self.switch_requested,
loc)))
self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested,
name, loc)))
self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested,
name, loc)))
for i, x in enumerate(locations[:len(self.switch_actions)]):
name, loc = x
@ -134,12 +154,50 @@ class ChooseLibraryAction(InterfaceAction):
ac.setVisible(True)
self.quick_menu_action.setVisible(bool(locations))
self.rename_menu_action.setVisible(bool(locations))
self.delete_menu_action.setVisible(bool(locations))
def location_selected(self, loc):
enabled = loc == 'library'
self.qaction.setEnabled(enabled)
def rename_requested(self, name, location):
loc = location.replace('/', os.sep)
base = os.path.dirname(loc)
newname, ok = QInputDialog.getText(self.gui, _('Rename') + ' ' + name,
'<p>'+_('Choose a new name for the library <b>%s</b>. ')%name +
'<p>'+_('Note that the actual library folder will be renamed.'),
text=name)
newname = unicode(newname)
if not ok or not newname or newname == name:
return
newloc = os.path.join(base, newname)
if os.path.exists(newloc):
return error_dialog(self.gui, _('Already exists'),
_('The folder %s already exists. Delete it first.') %
newloc, show=True)
os.rename(loc, newloc)
self.stats.rename(location, newloc)
self.build_menus()
def delete_requested(self, name, location):
loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), '<p>'+
_('All files from %s will be '
'<b>permanently deleted</b>. Are you sure?') % loc,
show_copy_button=False):
return
exists = self.gui.library_view.model().db.exists_at(loc)
if exists:
try:
shutil.rmtree(loc, ignore_errors=True)
except:
pass
self.stats.remove(location)
self.build_menus()
def switch_requested(self, location):
if not self.change_library_allowed():
return

View File

@ -5,6 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from functools import partial
from threading import Thread
@ -13,6 +14,7 @@ from PyQt4.Qt import QMenu, QToolButton
from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.utils.config import prefs, tweaks
class Worker(Thread):
@ -38,6 +40,13 @@ class Worker(Thread):
self.done()
def add_formats(self, id, paths, newdb, replace=True):
for path in paths:
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
with open(path, 'rb') as f:
newdb.add_format(id, fmt, f, index_is_id=True,
notify=False, replace=replace)
def doit(self):
from calibre.library.database2 import LibraryDatabase2
newdb = LibraryDatabase2(self.loc)
@ -49,14 +58,21 @@ class Worker(Thread):
else: fmts = fmts.split(',')
paths = [self.db.format_abspath(x, fmt, index_is_id=True) for fmt in
fmts]
newdb.import_book(mi, paths, notify=False, import_hooks=False)
added = False
if prefs['add_formats_to_existing']:
identical_book_list = newdb.find_identical_books(mi)
if identical_book_list: # books with same author and nearly same title exist in newdb
added = True
for identical_book in identical_book_list:
self.add_formats(identical_book, paths, newdb, replace=False)
if not added:
newdb.import_book(mi, paths, notify=False, import_hooks=False,
apply_import_tags=tweaks['add_new_book_tags_when_importing_books'])
co = self.db.conversion_options(x, 'PIPE')
if co is not None:
newdb.set_conversion_options(x, 'PIPE', co)
class CopyToLibraryAction(InterfaceAction):
name = 'Copy To Library'

View File

@ -21,7 +21,8 @@ class SimilarBooksAction(InterfaceAction):
m = QMenu(self.gui)
for text, icon, target, shortcut in [
(_('Books by same author'), 'user_profile.svg', 'authors', _('Alt+A')),
(_('Books in this series'), 'books_in_series.svg', 'series', _('Alt+S')),
(_('Books in this series'), 'books_in_series.svg', 'series',
_('Alt+Shift+S')),
(_('Books by this publisher'), 'publisher.png', 'publisher', _('Alt+P')),
(_('Books with the same tags'), 'tags.svg', 'tag', _('Alt+T')),]:
ac = self.create_action(spec=(text, icon, None, shortcut),

View File

@ -1,7 +1,7 @@
'''
UI for adding books to the database and saving books to disk
'''
import os, shutil, time, re
import os, shutil, time
from Queue import Queue, Empty
from threading import Thread
@ -94,14 +94,6 @@ class DBAdder(Thread): # {{{
self.daemon = True
self.input_queue = Queue()
self.output_queue = Queue()
self.fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
[
(r'[\[\](){}<>\'";,:#]', ''),
(r'^(the|a|an) ', ''),
(r'[-._]', ' '),
(r'\s+', ' ')
]
]
self.merged_books = set([])
def run(self):
@ -138,33 +130,6 @@ class DBAdder(Thread): # {{{
fmts[-1] = fmt
return fmts
def fuzzy_title(self, title):
title = title.strip().lower()
for pat, repl in self.fuzzy_title_patterns:
title = pat.sub(repl, title)
return title
def find_identical_books(self, mi):
identical_book_ids = set([])
if mi.authors:
try:
query = u' and '.join([u'author:"=%s"'%(a.replace('"', '')) for a in
mi.authors])
except ValueError:
return identical_book_ids
try:
book_ids = self.db.data.parse(query)
except:
import traceback
traceback.print_exc()
return identical_book_ids
for book_id in book_ids:
fbook_title = self.db.title(book_id, index_is_id=True)
fbook_title = self.fuzzy_title(fbook_title)
mbook_title = self.fuzzy_title(mi.title)
if fbook_title == mbook_title:
identical_book_ids.add(book_id)
return identical_book_ids
def add(self, id, opf, cover, name):
formats = self.ids.pop(id)
@ -191,7 +156,7 @@ class DBAdder(Thread): # {{{
orig_formats = formats
formats = [f for f in formats if not f.lower().endswith('.opf')]
if prefs['add_formats_to_existing']:
identical_book_list = self.find_identical_books(mi)
identical_book_list = self.db.find_identical_books(mi)
if identical_book_list: # books with same author and nearly same title exist in db
self.merged_books.add(mi.title)

View File

@ -457,6 +457,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.priority.setCurrentIndex(p)
self.priority.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
self.new_book_tags.setText(', '.join(prefs['new_book_tags']))
self._plugin_model = PluginModel()
self.plugin_view.setModel(self._plugin_model)
self.plugin_view.setStyleSheet(
@ -906,6 +907,9 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['disable_tray_notification'] = not self.systray_notifications.isChecked()
p = {0:'normal', 1:'high', 2:'low'}[self.priority.currentIndex()]
prefs['worker_process_priority'] = p
nbt = [x.strip() for x in
unicode(self.new_book_tags.text()).strip().split(',')]
prefs['new_book_tags'] = [x for x in nbt if x]
prefs['output_format'] = unicode(self.output_format.currentText()).upper()
config['cover_flow_queue_length'] = self.cover_browse.value()
prefs['language'] = str(self.language.itemData(self.language.currentIndex()).toString())

View File

@ -136,7 +136,7 @@
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Default network &amp;timeout:</string>
@ -146,7 +146,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="2" column="1">
<widget class="QSpinBox" name="timeout">
<property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
@ -165,10 +165,10 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QComboBox" name="language"/>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (requires restart):</string>
@ -178,7 +178,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QComboBox" name="priority">
<item>
<property name="text">
@ -197,7 +197,7 @@
</item>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="priority_label">
<property name="text">
<string>Job &amp;priority:</string>
@ -207,7 +207,7 @@
</property>
</widget>
</item>
<item row="0" column="0">
<item row="1" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Preferred &amp;output format:</string>
@ -217,9 +217,26 @@
</property>
</widget>
</item>
<item row="0" column="1">
<item row="1" column="1">
<widget class="QComboBox" name="output_format"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_230">
<property name="text">
<string>Tags to apply when adding a book:</string>
</property>
<property name="buddy">
<cstring>new_book_tags</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="new_book_tags">
<property name="toolTip">
<string>A comma-separated list of tags that will be applied to books added to the library</string>
</property>
</widget>
</item>
</layout>
</item>
<item>

View File

@ -105,6 +105,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
if col.endswith('_index'):
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':

View File

@ -236,8 +236,8 @@ class BooksModel(QAbstractTableModel): # {{{
def search(self, text, reset=True):
try:
self.db.search(text)
except ParseException:
self.searched.emit(False)
except ParseException as e:
self.searched.emit(e.msg)
return
self.last_search = text
if reset:

View File

@ -229,6 +229,8 @@ class BooksView(QTableView): # {{{
def cleanup_sort_history(self, sort_history):
history = []
for col, order in sort_history:
if col == 'date':
col = 'timestamp'
if col in self.column_map and (not history or history[0][0] != col):
history.append([col, order])
return history

View File

@ -17,6 +17,9 @@ class ConfigWidgetInterface(object):
def genesis(self, gui):
raise NotImplementedError()
def initialize(self):
raise NotImplementedError()
def restore_defaults(self):
pass
@ -26,39 +29,39 @@ class ConfigWidgetInterface(object):
class Setting(object):
def __init__(self, name, config_obj, widget, gui_name=None,
empty_string_is_None=True, choices=None):
empty_string_is_None=True, choices=None, restart_required=False):
self.name, self.gui_name = name, gui_name
self.empty_string_is_None = empty_string_is_None
self.restart_required = restart_required
self.choices = choices
if gui_name is None:
self.gui_name = 'opt_'+name
self.config_obj = config_obj
self.gui_obj = getattr(widget, self.gui_name)
self.widget = widget
if isinstance(self.gui_obj, QCheckBox):
self.datatype = 'bool'
self.gui_obj.stateChanged.connect(lambda x:
widget.changed_signal.emit())
self.gui_obj.stateChanged.connect(self.changed)
elif isinstance(self.gui_obj, QAbstractSpinBox):
self.datatype = 'number'
self.gui_obj.valueChanged.connect(lambda x:
widget.changed_signal.emit())
self.gui_obj.valueChanged.connect(self.changed)
elif isinstance(self.gui_obj, QLineEdit):
self.datatype = 'string'
self.gui_obj.textChanged.connect(lambda x:
widget.changed_signal.emit())
self.gui_obj.textChanged.connect(self.changed)
elif isinstance(self.gui_obj, QComboBox):
self.datatype = 'choice'
self.gui_obj.editTextChanged.connect(lambda x:
widget.changed_signal.emit())
self.gui_obj.currentIndexChanged.connect(lambda x:
widget.changed_signal.emit())
self.gui_obj.editTextChanged.connect(self.changed)
self.gui_obj.currentIndexChanged.connect(self.changed)
else:
raise ValueError('Unknown data type')
def changed(self, *args):
self.widget.changed_signal.emit()
def initialize(self):
self.gui_obj.blockSignals(True)
if self.datatype == 'choices':
if self.datatype == 'choice':
self.gui_obj.clear()
for x in self.choices:
if isinstance(x, basestring):
@ -66,9 +69,15 @@ class Setting(object):
self.gui_obj.addItem(x[0], QVariant(x[1]))
self.set_gui_val(self.get_config_val(default=False))
self.gui_obj.blockSignals(False)
self.initial_value = self.get_gui_val()
def commit(self):
val = self.get_gui_val()
oldval = self.get_config_val()
changed = val != oldval
if changed:
self.set_config_val(self.get_gui_val())
return changed and self.restart_required
def restore_defaults(self):
self.set_gui_val(self.get_config_val(default=True))
@ -90,7 +99,7 @@ class Setting(object):
self.gui_obj.setValue(val)
elif self.datatype == 'string':
self.gui_obj.setText(val if val else '')
elif self.datatype == 'choices':
elif self.datatype == 'choice':
idx = self.gui_obj.findData(QVariant(val))
if idx == -1:
idx = 0
@ -100,17 +109,32 @@ class Setting(object):
if self.datatype == 'bool':
val = bool(self.gui_obj.isChecked())
elif self.datatype == 'number':
val = self.gui_obj.value(val)
val = self.gui_obj.value()
elif self.datatype == 'string':
val = unicode(self.gui_name.text()).strip()
if self.empty_string_is_None and not val:
val = None
elif self.datatype == 'choices':
elif self.datatype == 'choice':
idx = self.gui_obj.currentIndex()
if idx < 0: idx = 0
val = unicode(self.gui_obj.itemData(idx).toString())
return val
class CommaSeparatedList(Setting):
def set_gui_val(self, val):
x = ''
if val:
x = u', '.join(val)
self.gui_obj.setText(x)
def get_gui_val(self):
val = unicode(self.gui_obj.text()).strip()
ans = []
if val:
ans = [x.strip() for x in val.split(',')]
ans = [x for x in ans if x]
return ans
class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
@ -122,10 +146,11 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
self.setupUi(self)
self.settings = {}
def register(self, name, config_obj, gui_name=None, choices=None, setting=Setting):
def register(self, name, config_obj, gui_name=None, choices=None,
restart_required=False, setting=Setting):
setting = setting(name, config_obj, self, gui_name=gui_name,
choices=choices)
self.register_setting(setting)
choices=choices, restart_required=restart_required)
return self.register_setting(setting)
def register_setting(self, setting):
self.settings[setting.name] = setting
@ -135,9 +160,13 @@ class ConfigWidgetBase(QWidget, ConfigWidgetInterface):
for setting in self.settings.values():
setting.initialize()
def commit(self):
def commit(self, *args):
restart_required = False
for setting in self.settings.values():
setting.commit()
rr = setting.commit()
if rr:
restart_required = True
return restart_required
def restore_defaults(self, *args):
for setting in self.settings.values():
@ -158,6 +187,7 @@ def test_widget(category, name, gui=None): # {{{
pl = get_plugin(category, name)
d = QDialog()
d.resize(750, 550)
d.setWindowTitle(category + " - " + name)
bb = QDialogButtonBox(d)
bb.setStandardButtons(bb.Apply|bb.Cancel|bb.RestoreDefaults)
bb.accepted.connect(d.accept)
@ -165,11 +195,13 @@ def test_widget(category, name, gui=None): # {{{
w = pl.create_widget(d)
bb.button(bb.RestoreDefaults).clicked.connect(w.restore_defaults)
bb.button(bb.Apply).setEnabled(False)
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnable(True))
bb.button(bb.Apply).clicked.connect(d.accept)
w.changed_signal.connect(lambda : bb.button(bb.Apply).setEnabled(True))
l = QVBoxLayout()
d.setLayout(l)
l.addWidget(w)
l.addWidget(bb)
mygui = gui is None
if gui is None:
from calibre.gui2.ui import Main
from calibre.gui2.main import option_parser
@ -181,7 +213,14 @@ def test_widget(category, name, gui=None): # {{{
gui = Main(opts)
gui.initialize(db.library_path, db, None, actions, show_gui=False)
w.genesis(gui)
w.initialize()
restart_required = False
if d.exec_() == QDialog.Accepted:
w.commit()
restart_required = w.commit()
if restart_required:
from calibre.gui2 import warning_dialog
warning_dialog(gui, 'Restart required', 'Restart required', show=True)
if mygui:
gui.shutdown()
# }}}

View File

@ -0,0 +1,169 @@
#!/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 re
from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
CommaSeparatedList
from calibre.gui2.preferences.behavior_ui import Ui_Form
from calibre.gui2 import config, info_dialog, dynamic
from calibre.utils.config import prefs
from calibre.customize.ui import available_output_formats, all_input_formats
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = gui.library_view.model().db
r = self.register
r('worker_process_priority', prefs, choices=
[(_('Low'), 'low'), (_('Normal'), 'normal'), (_('High'), 'high')])
r('network_timeout', prefs)
r('overwrite_author_title_metadata', config)
r('get_social_metadata', config)
r('new_version_notification', config)
r('upload_news_to_device', config)
r('delete_news_from_library_on_upload', config)
output_formats = list(sorted(available_output_formats()))
output_formats.remove('oeb')
choices = [(x.upper(), x) for x in output_formats]
r('output_format', prefs, choices=choices)
restrictions = sorted(saved_searches().names(),
cmp=lambda x,y: cmp(x.lower(), y.lower()))
choices = [('', '')] + [(x, x) for x in restrictions]
r('gui_restriction', db.prefs, choices=choices)
r('new_book_tags', prefs, setting=CommaSeparatedList)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation_dialogs)
self.input_up_button.clicked.connect(self.up_input)
self.input_down_button.clicked.connect(self.down_input)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_internally_viewed_formats, 'item'+signal)
signal.connect(self.internally_viewed_formats_changed)
self.settings['worker_process_priority'].gui_obj.setVisible(iswindows)
self.priority_label.setVisible(iswindows)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_input_order()
self.init_internally_viewed_formats()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.init_input_order(defaults=True)
self.init_internally_viewed_formats(defaults=True)
self.changed_signal.emit()
def commit(self):
input_map = prefs['input_format_order']
input_cols = [unicode(self.opt_input_order.item(i).data(Qt.UserRole).toString()) for
i in range(self.opt_input_order.count())]
if input_map != input_cols:
prefs['input_format_order'] = input_cols
fmts = self.current_internally_viewed_formats
old = config['internally_viewed_formats']
if fmts != old:
config['internally_viewed_formats'] = fmts
return ConfigWidgetBase.commit(self)
# Internally viewed formats {{{
def internally_viewed_formats_changed(self, *args):
fmts = self.current_internally_viewed_formats
old = config['internally_viewed_formats']
if fmts != old:
self.changed_signal.emit()
def init_internally_viewed_formats(self, defaults=False):
if defaults:
fmts = config.defaults['internally_viewed_formats']
else:
fmts = config['internally_viewed_formats']
viewer = self.opt_internally_viewed_formats
viewer.blockSignals(True)
exts = set([])
for ext in BOOK_EXTENSIONS:
ext = ext.lower()
ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext)
if ext == 'lrf' or is_supported('book.'+ext):
exts.add(ext)
viewer.clear()
for ext in sorted(exts):
viewer.addItem(ext.upper())
item = viewer.item(viewer.count()-1)
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if
ext.upper() in fmts else Qt.Unchecked)
viewer.blockSignals(False)
@property
def current_internally_viewed_formats(self):
fmts = []
viewer = self.opt_internally_viewed_formats
for i in range(viewer.count()):
if viewer.item(i).checkState() == Qt.Checked:
fmts.append(unicode(viewer.item(i).text()))
return fmts
# }}}
# Input format order {{{
def init_input_order(self, defaults=False):
if defaults:
input_map = prefs.defaults['input_format_order']
else:
input_map = prefs['input_format_order']
all_formats = set()
self.opt_input_order.clear()
for fmt in all_input_formats().union(set(['ZIP', 'RAR'])):
all_formats.add(fmt.upper())
for format in input_map + list(all_formats.difference(input_map)):
item = QListWidgetItem(format, self.opt_input_order)
item.setData(Qt.UserRole, QVariant(format))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable)
def up_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx > 0:
self.opt_input_order.insertItem(idx-1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_input(self, *args):
idx = self.opt_input_order.currentRow()
if idx < self.opt_input_order.count()-1:
self.opt_input_order.insertItem(idx+1, self.opt_input_order.takeItem(idx))
self.opt_input_order.setCurrentRow(idx+1)
self.changed_signal.emit()
# }}}
def reset_confirmation_dialogs(self, *args):
for key in dynamic.keys():
if key.endswith('_again') and dynamic[key] is False:
dynamic[key] = True
info_dialog(self, _('Done'),
_('Confirmation dialogs have all been reset'), show=True)
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Behavior')

View File

@ -29,21 +29,21 @@
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="new_version_notification">
<widget class="QCheckBox" name="opt_new_version_notification">
<property name="text">
<string>Show notification when &amp;new version is available</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<widget class="QCheckBox" name="opt_upload_news_to_device">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<widget class="QCheckBox" name="opt_delete_news_from_library_on_upload">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
@ -57,12 +57,12 @@
<string>Default network &amp;timeout:</string>
</property>
<property name="buddy">
<cstring>timeout</cstring>
<cstring>opt_network_timeout</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="timeout">
<widget class="QSpinBox" name="opt_network_timeout">
<property name="toolTip">
<string>Set the default timeout for network fetches (i.e. anytime we go out to the internet to get information)</string>
</property>
@ -81,7 +81,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="priority">
<widget class="QComboBox" name="opt_worker_process_priority">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
@ -111,7 +111,7 @@
<string>Job &amp;priority:</string>
</property>
<property name="buddy">
<cstring>priority</cstring>
<cstring>opt_worker_process_priority</cstring>
</property>
</widget>
</item>
@ -121,12 +121,12 @@
<string>Preferred &amp;output format:</string>
</property>
<property name="buddy">
<cstring>output_format</cstring>
<cstring>opt_output_format</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="output_format">
<widget class="QComboBox" name="opt_output_format">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
@ -164,6 +164,20 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="opt_new_book_tags">
<property name="toolTip">
<string>A comma-separated list of tags that will be applied to books added to the library</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_230">
<property name="text">
<string>Tags to apply when adding a book:</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="6" column="0" colspan="2">
@ -182,7 +196,7 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="QListWidget" name="input_order">
<widget class="QListWidget" name="opt_input_order">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
@ -194,7 +208,7 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_10">
<item>
<widget class="QToolButton" name="input_up">
<widget class="QToolButton" name="input_up_button">
<property name="text">
<string>...</string>
</property>
@ -218,7 +232,7 @@
</spacer>
</item>
<item>
<widget class="QToolButton" name="input_down">
<widget class="QToolButton" name="input_down_button">
<property name="text">
<string>...</string>
</property>
@ -242,7 +256,7 @@
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QListWidget" name="viewer">
<widget class="QListWidget" name="opt_internally_viewed_formats">
<property name="alternatingRowColors">
<bool>true</bool>
</property>

View File

@ -0,0 +1,172 @@
#!/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 copy, sys
from PyQt4.Qt import Qt, QVariant, QListWidgetItem
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
class ConfigWidget(ConfigWidgetBase, Ui_Form):
def genesis(self, gui):
self.gui = gui
db = self.gui.library_view.model().db
self.custcols = copy.deepcopy(db.field_metadata.get_custom_field_metadata())
self.column_up.clicked.connect(self.up_column)
self.column_down.clicked.connect(self.down_column)
self.del_custcol_button.clicked.connect(self.del_custcol)
self.add_custcol_button.clicked.connect(self.add_custcol)
self.add_col_button.clicked.connect(self.add_custcol)
self.edit_custcol_button.clicked.connect(self.edit_custcol)
for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'):
signal = getattr(self.opt_columns, 'item'+signal)
signal.connect(self.columns_changed)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.init_columns()
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
self.init_columns(defaults=True)
self.changed_signal.emit()
def commit(self):
rr = ConfigWidgetBase.commit(self)
return self.apply_custom_column_changes() or rr
def columns_changed(self, *args):
self.changed_signal.emit()
def columns_state(self, defaults=False):
if defaults:
return self.gui.library_view.get_default_state()
return self.gui.library_view.get_state()
def init_columns(self, defaults=False):
# Set up columns
self.opt_columns.blockSignals(True)
model = self.gui.library_view.model()
colmap = list(model.column_map)
state = self.columns_state(defaults)
hidden_cols = state['hidden_columns']
positions = state['column_positions']
colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y]))
self.opt_columns.clear()
for col in colmap:
item = QListWidgetItem(model.headers[col], self.opt_columns)
item.setData(Qt.UserRole, QVariant(col))
flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable
if col != 'ondevice':
flags |= Qt.ItemIsUserCheckable
item.setFlags(flags)
if col != 'ondevice':
item.setCheckState(Qt.Unchecked if col in hidden_cols else
Qt.Checked)
self.opt_columns.blockSignals(False)
def up_column(self):
idx = self.opt_columns.currentRow()
if idx > 0:
self.opt_columns.insertItem(idx-1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx-1)
self.changed_signal.emit()
def down_column(self):
idx = self.opt_columns.currentRow()
if idx < self.opt_columns.count()-1:
self.opt_columns.insertItem(idx+1, self.opt_columns.takeItem(idx))
self.opt_columns.setCurrentRow(idx+1)
self.changed_signal.emit()
def del_custcol(self):
idx = self.opt_columns.currentRow()
if idx < 0:
return error_dialog(self, '', _('You must select a column to delete it'),
show=True)
col = unicode(self.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in self.custcols:
return error_dialog(self, '',
_('The selected column is not a custom column'), show=True)
if not question_dialog(self, _('Are you sure?'),
_('Do you really want to delete column %s and all its data?') %
self.custcols[col]['name'], show_copy_button=False):
return
self.opt_columns.item(idx).setCheckState(False)
self.opt_columns.takeItem(idx)
self.custcols[col]['*deleteme'] = True
self.changed_signal.emit()
def add_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, False, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def edit_custcol(self):
model = self.gui.library_view.model()
CreateCustomColumn(self, True, model.orig_headers, ALL_COLUMNS)
self.changed_signal.emit()
def apply_custom_column_changes(self):
model = self.gui.library_view.model()
db = model.db
config_cols = [unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count())]
if not config_cols:
config_cols = ['title']
removed_cols = set(model.column_map) - set(config_cols)
hidden_cols = set([unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.opt_columns.count()) \
if self.opt_columns.item(i).checkState()==Qt.Unchecked])
hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols
hidden_cols = list(hidden_cols.intersection(set(model.column_map)))
if 'ondevice' in hidden_cols:
hidden_cols.remove('ondevice')
def col_pos(x, y):
xidx = config_cols.index(x) if x in config_cols else sys.maxint
yidx = config_cols.index(y) if y in config_cols else sys.maxint
return cmp(xidx, yidx)
positions = {}
for i, col in enumerate((sorted(model.column_map, cmp=col_pos))):
positions[col] = i
state = {'hidden_columns': hidden_cols, 'column_positions':positions}
self.gui.library_view.apply_state(state)
self.gui.library_view.save_state()
must_restart = False
for c in self.custcols:
if self.custcols[c]['colnum'] is None:
db.create_custom_column(
label=self.custcols[c]['label'],
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
is_multiple=self.custcols[c]['is_multiple'],
display = self.custcols[c]['display'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
db.delete_custom_column(label=self.custcols[c]['label'])
must_restart = True
elif '*edited' in self.custcols[c]:
cc = self.custcols[c]
db.set_custom_column_metadata(cc['colnum'], name=cc['name'],
label=cc['label'],
display = self.custcols[c]['display'])
if '*must_restart' in self.custcols[c]:
must_restart = True
return must_restart
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Custom Columns')

View File

@ -25,7 +25,7 @@
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="columns">
<widget class="QListWidget" name="opt_columns">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
@ -155,7 +155,7 @@
</layout>
</item>
<item row="2" column="0" colspan="2">
<widget class="QPushButton" name="pushButton">
<widget class="QPushButton" name="add_col_button">
<property name="text">
<string>Add &amp;custom column</string>
</property>

View File

@ -0,0 +1,174 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to create a new custom column'''
import re
from functools import partial
from PyQt4.QtCore import SIGNAL
from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
from calibre.gui2.preferences.create_custom_column_ui import Ui_QCreateCustomColumn
from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
column_types = {
0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'),
'is_multiple':False},
1:{'datatype':'*text',
'text':_('Comma separated text, like tags, shown in the tag browser'),
'is_multiple':True},
2:{'datatype':'comments',
'text':_('Long text, like comments, not shown in the tag browser'),
'is_multiple':False},
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
4:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
5:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
6:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
7:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
QDialog.__init__(self, parent)
Ui_QCreateCustomColumn.__init__(self)
self.setupUi(self)
# Remove help icon on title bar
icon = self.windowIcon()
self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint))
self.setWindowIcon(icon)
self.simple_error = partial(error_dialog, self, show=True,
show_copy_button=False)
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
self.parent = parent
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
if not self.editing_col:
self.datatype_changed()
self.exec_()
return
idx = parent.opt_columns.currentRow()
if idx < 0:
self.simple_error(_('No column selected'),
_('No column has been selected'))
return
col = unicode(parent.opt_columns.item(idx).data(Qt.UserRole).toString())
if col not in parent.custcols:
self.simple_error('', _('Selected column is not a user-defined column'))
return
c = parent.custcols[col]
self.column_name_box.setText(c['label'])
self.column_heading_box.setText(c['name'])
ct = c['datatype'] if not c['is_multiple'] else '*text'
self.orig_column_number = c['colnum']
self.orig_column_name = col
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
self.column_type_box.setCurrentIndex(column_numbers[ct])
self.column_type_box.setEnabled(False)
if ct == 'datetime':
if c['display'].get('date_format', None):
self.date_format_box.setText(c['display'].get('date_format', ''))
self.datatype_changed()
self.exec_()
def datatype_changed(self, *args):
try:
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
except:
col_type = None
df_visible = col_type == 'datetime'
for x in ('box', 'default_label', 'label'):
getattr(self, 'date_format_'+x).setVisible(df_visible)
def accept(self):
col = unicode(self.column_name_box.text())
if not col:
return self.simple_error('', _('No lookup name was provided'))
if re.match('^\w*$', col) is None or not col[0].isalpha() or col.lower() != col:
return self.simple_error('', _('The lookup name must contain only lower case letters, digits and underscores, and start with a letter'))
if col.endswith('_index'):
return self.simple_error('', _('Lookup names cannot end with _index, because these names are reserved for the index of a series column.'))
col_heading = unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':
col_type='text'
is_multiple = True
else:
is_multiple = False
if not col_heading:
return self.simple_error('', _('No column heading was provided'))
bad_col = False
if col in self.parent.custcols:
if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number:
bad_col = True
if bad_col:
return self.simple_error('', _('The lookup name %s is already used')%col)
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number:
bad_head = True
for t in self.standard_colheads:
if self.standard_colheads[t] == col_heading:
bad_head = True
if bad_head:
return self.simple_error('', _('The heading %s is already used')%col_heading)
date_format = {}
if col_type == 'datetime':
if self.date_format_box.text():
date_format = {'date_format':unicode(self.date_format_box.text())}
else:
date_format = {'date_format': None}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
if not self.editing_col:
db.field_metadata
self.parent.custcols[key] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':date_format,
'normalized':None,
'colnum':None,
'is_multiple':is_multiple,
}
item = QListWidgetItem(col_heading, self.parent.opt_columns)
item.setData(Qt.UserRole, QVariant(key))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked)
else:
idx = self.parent.opt_columns.currentRow()
item = self.parent.opt_columns.item(idx)
item.setData(Qt.UserRole, QVariant(key))
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
def reject(self):
QDialog.reject(self)

View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QCreateCustomColumn</class>
<widget class="QDialog" name="QCreateCustomColumn">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>528</width>
<height>199</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Create or edit custom columns</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="margin">
<number>5</number>
</property>
<item row="2" column="0">
<layout class="QGridLayout" name="gridLayout">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Lookup name</string>
</property>
<property name="buddy">
<cstring>column_name_box</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column &amp;heading</string>
</property>
<property name="buddy">
<cstring>column_heading_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="column_name_box">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Used for searching the column. Must contain only digits and lower case letters.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="column_heading_box">
<property name="toolTip">
<string>Column heading in the library view and category name in the tag browser</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Column &amp;type</string>
</property>
<property name="buddy">
<cstring>column_type_box</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="column_type_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>What kind of information will be kept in the column.</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="date_format_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;p&gt;Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.&lt;/p&gt;
&lt;p&gt;For example:
&lt;ul&gt;
&lt;li&gt; ddd, d MMM yyyy gives Mon, 5 Jan 2010&lt;li&gt;
&lt;li&gt;dd MMMM yy gives 05 January 10&lt;/li&gt;
&lt;/ul&gt; </string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="date_format_default_label">
<property name="toolTip">
<string>Use MMM yyyy for month + year, yyyy for year only</string>
</property>
<property name="text">
<string>Default: dd MMM yyyy.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="date_format_label">
<property name="text">
<string>Format for &amp;dates</string>
</property>
<property name="buddy">
<cstring>date_format_box</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Create or edit custom columns</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>column_name_box</tabstop>
<tabstop>column_heading_box</tabstop>
<tabstop>column_type_box</tabstop>
<tabstop>date_format_box</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -20,7 +20,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r = self.register
r('gui_layout', config, choices=
r('gui_layout', config, restart_required=True, choices=
[(_('Wide'), 'wide'), (_('Narrow'), 'narrow')])
r('cover_flow_queue_length', config)
@ -32,19 +32,19 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if l != lang]
if lang != 'en':
items.append(('en', get_language('en')))
items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
items.sort(cmp=lambda x, y: cmp(x[1].lower(), y[1].lower()))
choices = [(y, x) for x, y in items]
# Default language is the autodetected one
choices = [get_language(lang), lang] + choices
r('language', prefs, choices=choices)
choices = [(get_language(lang), lang)] + choices
r('language', prefs, choices=choices, restart_required=True)
r('show_avg_rating', config)
r('disable_animations', config)
r('systray_icon', config)
r('systray_icon', config, restart_required=True)
r('show_splash_screen', gprefs)
r('disable_tray_notification', config)
r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config)
r('separate_cover_flow', config, restart_required=True)
r('search_as_you_type', config)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'),

View File

@ -0,0 +1,293 @@
#!/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'
from functools import partial
from PyQt4.Qt import QAbstractListModel, Qt, QIcon, \
QVariant, QItemSelectionModel
from calibre.gui2.preferences.toolbar_ui import Ui_Form
from calibre.gui2 import gprefs, NONE, warning_dialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
class FakeAction(object):
def __init__(self, name, icon, tooltip=None,
dont_add_to=frozenset([]), dont_remove_from=frozenset([])):
self.name = name
self.action_spec = (name, icon, tooltip, None)
self.dont_remove_from = dont_remove_from
self.dont_add_to = dont_add_to
class BaseModel(QAbstractListModel):
def name_to_action(self, name, gui):
if name == 'Donate':
return FakeAction(name, 'donate.svg',
dont_add_to=frozenset(['context-menu',
'context-menu-device']))
if name == 'Location Manager':
return FakeAction(name, None,
_('Switch between library and device views'),
dont_remove_from=set(['toolbar-device']))
if name is None:
return FakeAction('--- '+_('Separator')+' ---', None)
return gui.iactions[name]
def rowCount(self, parent):
return len(self._data)
def data(self, index, role):
row = index.row()
action = self._data[row].action_spec
if role == Qt.DisplayRole:
text = action[0]
text = text.replace('&', '')
if text == _('%d books'):
text = _('Choose library')
return QVariant(text)
if role == Qt.DecorationRole:
ic = action[1]
if ic is None:
ic = 'blank.svg'
return QVariant(QIcon(I(ic)))
if role == Qt.ToolTipRole and action[2] is not None:
return QVariant(action[2])
return NONE
def names(self, indexes):
rows = [i.row() for i in indexes]
ans = []
for i in rows:
n = self._data[i].name
if n.startswith('---'):
n = None
ans.append(n)
return ans
class AllModel(BaseModel):
def __init__(self, key, gui):
BaseModel.__init__(self)
self.gprefs_name = 'action-layout-'+key
current = gprefs[self.gprefs_name]
self.gui = gui
self.key = key
self._data = self.get_all_actions(current)
def get_all_actions(self, current):
all = list(self.gui.iactions.keys()) + ['Donate']
all = [x for x in all if x not in current] + [None]
all = [self.name_to_action(x, self.gui) for x in all]
all = [x for x in all if self.key not in x.dont_add_to]
all.sort()
return all
def add(self, names):
actions = []
for name in names:
if name is None or name.startswith('---'): continue
actions.append(self.name_to_action(name, self.gui))
self._data.extend(actions)
self._data.sort()
self.reset()
def remove(self, indices, allowed):
rows = [i.row() for i in indices]
remove = set([])
for row in rows:
ac = self._data[row]
if ac.name.startswith('---'): continue
if ac.name in allowed:
remove.add(row)
ndata = []
for i, ac in enumerate(self._data):
if i not in remove:
ndata.append(ac)
self._data = ndata
self.reset()
def restore_defaults(self):
current = gprefs.defaults[self.gprefs_name]
self._data = self.get_all_actions(current)
self.reset()
class CurrentModel(BaseModel):
def __init__(self, key, gui):
BaseModel.__init__(self)
self.gprefs_name = 'action-layout-'+key
current = gprefs[self.gprefs_name]
self._data = [self.name_to_action(x, gui) for x in current]
self.key = key
self.gui = gui
def move(self, idx, delta):
row = idx.row()
if row < 0 or row >= len(self._data):
return
nrow = row + delta
if nrow < 0 or nrow >= len(self._data):
return
t = self._data[row]
self._data[row] = self._data[nrow]
self._data[nrow] = t
ni = self.index(nrow)
self.dataChanged.emit(idx, idx)
self.dataChanged.emit(ni, ni)
return ni
def add(self, names):
actions = []
reject = set([])
for name in names:
ac = self.name_to_action(name, self.gui)
if self.key in ac.dont_add_to:
reject.add(ac)
else:
actions.append(ac)
self._data.extend(actions)
self.reset()
return reject
def remove(self, indices):
rows = [i.row() for i in indices]
remove, rejected = set([]), set([])
for row in rows:
ac = self._data[row]
if self.key in ac.dont_remove_from:
rejected.add(ac)
continue
remove.add(row)
ndata = []
for i, ac in enumerate(self._data):
if i not in remove:
ndata.append(ac)
self._data = ndata
self.reset()
return rejected
def commit(self):
old = gprefs[self.gprefs_name]
new = []
for x in self._data:
n = x.name
if n.startswith('---'):
n = None
new.append(n)
new = tuple(new)
if new != old:
defaults = gprefs.defaults[self.gprefs_name]
if defaults == new:
del gprefs[self.gprefs_name]
else:
gprefs[self.gprefs_name] = new
def restore_defaults(self):
current = gprefs.defaults[self.gprefs_name]
self._data = [self.name_to_action(x, self.gui) for x in current]
self.reset()
class ConfigWidget(ConfigWidgetBase, Ui_Form):
LOCATIONS = [
('toolbar', _('The main toolbar')),
('toolbar-device', _('The main toolbar when a device is connected')),
('context-menu', _('The context menu for the books in the '
'calibre library')),
('context-menu-device', _('The context menu for the books on '
'the device'))
]
def genesis(self, gui):
self.models = {}
for key, text in self.LOCATIONS:
self.what.addItem(text, key)
all_model = AllModel(key, gui)
current_model = CurrentModel(key, gui)
self.models[key] = (all_model, current_model)
self.what.setCurrentIndex(0)
self.what.currentIndexChanged[int].connect(self.what_changed)
self.what_changed(0)
self.add_action_button.clicked.connect(self.add_action)
self.remove_action_button.clicked.connect(self.remove_action)
self.action_up_button.clicked.connect(partial(self.move, -1))
self.action_down_button.clicked.connect(partial(self.move, 1))
def what_changed(self, idx):
key = unicode(self.what.itemData(idx).toString())
self.all_actions.setModel(self.models[key][0])
self.current_actions.setModel(self.models[key][1])
def add_action(self, *args):
x = self.all_actions.selectionModel().selectedIndexes()
names = self.all_actions.model().names(x)
if names:
not_added = self.current_actions.model().add(names)
ns = set([x.name for x in not_added])
added = set(names) - ns
self.all_actions.model().remove(x, added)
if not_added:
warning_dialog(self, _('Cannot add'),
_('Cannot add the actions %s to this location') %
','.join([a.action_spec[0] for a in not_added]),
show=True)
if added:
ca = self.current_actions
idx = ca.model().index(ca.model().rowCount(None)-1)
ca.scrollTo(idx)
self.changed_signal.emit()
def remove_action(self, *args):
x = self.current_actions.selectionModel().selectedIndexes()
names = self.current_actions.model().names(x)
if names:
not_removed = self.current_actions.model().remove(x)
ns = set([x.name for x in not_removed])
removed = set(names) - ns
self.all_actions.model().add(removed)
if not_removed:
warning_dialog(self, _('Cannot remove'),
_('Cannot remove the actions %s from this location') %
','.join([a.action_spec[0] for a in not_removed]),
show=True)
else:
self.changed_signal.emit()
def move(self, delta, *args):
ci = self.current_actions.currentIndex()
m = self.current_actions.model()
if ci.isValid():
ni = m.move(ci, delta)
if ni is not None:
self.current_actions.setCurrentIndex(ni)
self.current_actions.selectionModel().select(ni,
QItemSelectionModel.ClearAndSelect)
self.changed_signal.emit()
def commit(self):
for am, cm in self.models.values():
cm.commit()
return False
def restore_defaults(self):
for am, cm in self.models.values():
cm.restore_defaults()
am.restore_defaults()
self.changed_signal.emit()
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Toolbar')

View File

@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>831</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Customize the actions in:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QComboBox" name="what">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>A&amp;vailable actions</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListView" name="all_actions">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Current actions</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListView" name="current_actions">
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="spacing">
<number>10</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QToolButton" name="action_up_button">
<property name="toolTip">
<string>Move selected action up</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.svg</normaloff>:/images/arrow-up.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="action_down_button">
<property name="toolTip">
<string>Move selected action down</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-down.svg</normaloff>:/images/arrow-down.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="1" column="1" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QToolButton" name="add_action_button">
<property name="toolTip">
<string>Add selected actions to toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.svg</normaloff>:/images/forward.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<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>
<item>
<widget class="QToolButton" name="remove_action_button">
<property name="toolTip">
<string>Remove selected actions from toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/back.svg</normaloff>:/images/back.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -90,6 +90,7 @@ class SearchBox2(QComboBox):
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.setMinimumContentsLength(25)
self._in_a_search = False
self.tool_tip_text = self.toolTip()
def initialize(self, opt_name, colorize=False, help_text=_('Search')):
self.as_you_type = config['search_as_you_type']
@ -100,6 +101,7 @@ class SearchBox2(QComboBox):
self.clear_to_help()
def normalize_state(self):
self.setToolTip(self.tool_tip_text)
if self.help_state:
self.setEditText('')
self.line_edit.setStyleSheet(
@ -112,6 +114,7 @@ class SearchBox2(QComboBox):
self.normal_background)
def clear_to_help(self):
self.setToolTip(self.tool_tip_text)
if self.help_state:
return
self.help_state = True
@ -131,6 +134,9 @@ class SearchBox2(QComboBox):
self.clear_to_help()
def search_done(self, ok):
if isinstance(ok, basestring):
self.setToolTip(ok)
ok = False
if not unicode(self.currentText()).strip():
return self.clear_to_help()
self._in_a_search = ok

View File

@ -233,7 +233,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
######################### Search Restriction ##########################
SearchRestrictionMixin.__init__(self)
self.apply_named_search_restriction(db.prefs.get('gui_restriction', ''))
self.apply_named_search_restriction(db.prefs['gui_restriction'])
########################### Cover Flow ################################
@ -378,7 +378,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.set_window_title()
self.apply_named_search_restriction('') # reset restriction to null
self.saved_searches_changed() # reload the search restrictions combo box
self.apply_named_search_restriction(db.prefs.get('gui_restriction', ''))
self.apply_named_search_restriction(db.prefs['gui_restriction'])
def set_window_title(self):
self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name())

View File

@ -319,12 +319,18 @@ class ResultCache(SearchQueryParser):
matches.add(item[0])
return matches
def get_matches(self, location, query):
def get_matches(self, location, query, allow_recursion=True):
matches = set([])
if query and query.strip():
# get metadata key associated with the search term. Eliminates
# dealing with plurals and other aliases
location = self.field_metadata.search_term_to_key(location.lower().strip())
if isinstance(location, list):
if allow_recursion:
for loc in location:
matches |= self.get_matches(loc, query, allow_recursion=False)
return matches
raise ParseException(query, len(query), 'Recursive query group detected', self)
# take care of dates special case
if location in self.field_metadata and \

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
'''
The database used to store ebook metadata
'''
import os, sys, shutil, cStringIO, glob, time, functools, traceback
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re
from itertools import repeat
from math import floor
@ -145,6 +145,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def initialize_dynamic(self):
self.prefs = DBPrefs(self)
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):
@ -264,7 +266,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# account for the series index column. Field_metadata knows that
# the series index is one larger than the series. If you change
# it here, be sure to change it there as well.
self.FIELD_MAP[str(col)+'_s_index'] = base = base+1
self.FIELD_MAP[str(col)+'_index'] = base = base+1
self.field_metadata.set_field_record_index(
self.custom_column_num_map[col]['label']+'_index',
base,
prefer_custom=True)
self.FIELD_MAP['cover'] = base+1
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)
@ -293,6 +299,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if len(saved_searches().names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
gst = tweaks['grouped_search_terms']
for t in gst:
try:
self.field_metadata._add_search_terms_to_map(gst[t], [t])
except ValueError:
traceback.print_exc()
self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.field_metadata)
self.search = self.data.search
@ -546,6 +559,43 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return bool(self.conn.get('SELECT id FROM books where title=?', (title,), all=False))
return False
def find_identical_books(self, mi):
fuzzy_title_patterns = [(re.compile(pat), repl) for pat, repl in
[
(r'[\[\](){}<>\'";,:#]', ''),
(r'^(the|a|an) ', ''),
(r'[-._]', ' '),
(r'\s+', ' ')
]
]
def fuzzy_title(title):
title = title.strip().lower()
for pat, repl in fuzzy_title_patterns:
title = pat.sub(repl, title)
return title
identical_book_ids = set([])
if mi.authors:
try:
query = u' and '.join([u'author:"=%s"'%(a.replace('"', '')) for a in
mi.authors])
except ValueError:
return identical_book_ids
try:
book_ids = self.data.parse(query)
except:
import traceback
traceback.print_exc()
return identical_book_ids
for book_id in book_ids:
fbook_title = self.title(book_id, index_is_id=True)
fbook_title = fuzzy_title(fbook_title)
mbook_title = fuzzy_title(mi.title)
if fbook_title == mbook_title:
identical_book_ids.add(book_id)
return identical_book_ids
def has_cover(self, index, index_is_id=False):
id = index if index_is_id else self.id(index)
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
@ -1677,7 +1727,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path = path_or_stream
return run_plugins_on_import(path, format)
def _add_newbook_tag(self, mi):
tags = prefs['new_book_tags']
if tags:
for tag in [t.strip() for t in tags]:
if tag:
if mi.tags is None:
mi.tags = [tag]
else:
mi.tags.append(tag)
def create_book_entry(self, mi, cover=None, add_duplicates=True):
self._add_newbook_tag(mi)
if not add_duplicates and self.has_book(mi):
return None
series_index = 1.0 if mi.series_index is None else mi.series_index
@ -1716,6 +1777,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
ids = []
for path in paths:
mi = metadata.next()
self._add_newbook_tag(mi)
format = formats.next()
if not add_duplicates and self.has_book(mi):
duplicates.append((path, format, mi))
@ -1754,8 +1816,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return (paths, formats, metadata), len(ids)
return None, len(ids)
def import_book(self, mi, formats, notify=True, import_hooks=True):
def import_book(self, mi, formats, notify=True, import_hooks=True,
apply_import_tags=True):
series_index = 1.0 if mi.series_index is None else mi.series_index
if apply_import_tags:
self._add_newbook_tag(mi)
if not mi.title:
mi.title = _('Unknown')
if not mi.authors:

View File

@ -36,7 +36,7 @@ class FieldMetadata(dict):
treated as a single term. If not None, it contains a string, and the field
is assumed to contain a list of terms separated by that string
kind == standard: is a db field.
kind == field: is a db field.
kind == category: standard tag category that isn't a field. see news.
kind == user: user-defined tag category.
kind == search: saved-searches category.
@ -239,7 +239,7 @@ class FieldMetadata(dict):
'is_multiple':None,
'kind':'field',
'name':None,
'search_terms':[],
'search_terms':['series_index'],
'is_custom':False,
'is_category':False}),
('sort', {'table':None,
@ -395,6 +395,18 @@ class FieldMetadata(dict):
'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key
if datatype == 'series':
key += '_index'
self._tb_cats[key] = {'table':None, 'column':None,
'datatype':'float', 'is_multiple':False,
'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{},
'is_custom':False, 'is_category':False,
'link_column':None, 'category_sort':None,
'is_editable': False,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label+'_index'] = key
def remove_custom_fields(self):
for key in self.get_custom_fields():
@ -463,9 +475,7 @@ class FieldMetadata(dict):
# ])
def get_search_terms(self):
s_keys = []
for v in self._tb_cats.itervalues():
map((lambda x:s_keys.append(x)), v['search_terms'])
s_keys = sorted(self._search_term_map.keys())
for v in self.search_items:
s_keys.append(v)
# if set(s_keys) != self.DEFAULT_LOCATIONS:
@ -476,6 +486,9 @@ class FieldMetadata(dict):
def _add_search_terms_to_map(self, key, terms):
if terms is not None:
for t in terms:
t = t.lower()
if t in self._search_term_map:
raise ValueError('Attempt to add duplicate search term "%s"'%t)
self._search_term_map[t] = key
def search_term_to_key(self, term):

View File

@ -13,11 +13,11 @@ from lxml import html
from lxml.html.builder import HTML, HEAD, TITLE, LINK, DIV, IMG, BODY, \
OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR
from calibre.library.server.utils import strftime
from calibre.library.server.utils import strftime, format_tag_string
from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import __appname__
from calibre import human_readable
from calibre.utils.date import utcfromtimestamp
from calibre.utils.date import utcfromtimestamp, format_date
def CLASS(*args, **kwargs): # class is a reserved word in Python
kwargs['class'] = ' '.join(args)
@ -85,7 +85,7 @@ def build_navigation(start, num, total, url_base): # {{{
# }}}
def build_index(books, num, search, sort, order, start, total, url_base):
def build_index(books, num, search, sort, order, start, total, url_base, CKEYS):
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
search_box = build_search_box(num, search, sort, order)
@ -123,10 +123,16 @@ def build_index(books, num, search, sort, order, start, total, url_base):
series = u'[%s - %s]'%(book['series'], book['series_index']) \
if book['series'] else ''
tags = u'[%s]'%book['tags'] if book['tags'] else ''
tags = u'Tags=[%s]'%book['tags'] if book['tags'] else ''
text = u'\u202f%s %s by %s - %s - %s %s' % (book['title'], series,
book['authors'], book['size'], book['timestamp'], tags)
ctext = ''
for key in CKEYS:
val = book.get(key, None)
if val:
ctext += '%s=[%s] '%tuple(val.split(':#:'))
text = u'\u202f%s %s by %s - %s - %s %s %s' % (book['title'], series,
book['authors'], book['size'], book['timestamp'], tags, ctext)
if last is None:
data.text = text
@ -150,7 +156,7 @@ def build_index(books, num, search, sort, order, start, total, url_base):
class MobileServer(object):
'A view optimized for browsers in mobile devices'
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2)')
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2|Kindle)')
def add_routes(self, connect):
connect('mobile', '/mobile', self.mobile)
@ -189,6 +195,10 @@ class MobileServer(object):
if sort is not None:
self.sort(items, sort, (order.lower().strip() == 'ascending'))
CFM = self.db.field_metadata
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
books = []
for record in items[(start-1):(start-1)+num]:
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
@ -203,12 +213,37 @@ class MobileServer(object):
book['authors'] = authors
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
book['series'] = record[FM['series']]
book['tags'] = record[FM['tags']]
book['tags'] = format_tag_string(record[FM['tags']], ',')
book['title'] = record[FM['title']]
for x in ('timestamp', 'pubdate'):
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
book['id'] = record[FM['id']]
books.append(book)
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
name = CFM[key]['name']
if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name, format_tag_string(val, '|'))
elif datatype == 'series':
book[key] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
book[key] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
book[key] = concat(name, __builtin__._('Yes'))
else:
book[key] = concat(name, __builtin__._('No'))
else:
book[key] = concat(name, val)
updated = self.db.last_modified()
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
@ -218,7 +253,7 @@ class MobileServer(object):
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
return html.tostring(build_index(books, num, search, sort, order,
start, len(ids), url_base),
start, len(ids), url_base, CKEYS),
encoding='utf-8', include_meta_content_type=True,
pretty_print=True)

View File

@ -19,6 +19,7 @@ from calibre.ebooks.metadata import fmt_sidx
from calibre.library.comments import comments_to_html
from calibre import guess_type
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.date import format_date
BASE_HREFS = {
0 : '/stanza',
@ -130,7 +131,7 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
link
)
def ACQUISITION_ENTRY(item, version, FM, updated):
def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
title = item[FM['title']]
if not title:
title = _('Unknown')
@ -153,6 +154,21 @@ def ACQUISITION_ENTRY(item, version, FM, updated):
extra.append(_('SERIES: %s [%s]<br />')%\
(series,
fmt_sidx(float(item[FM['series_index']]))))
for key in CKEYS:
val = item[CFM[key]['rec_index']]
if val is not None:
name = CFM[key]['name']
datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']:
extra.append('%s: %s<br />'%(name, ', '.join(val.split('|'))))
elif datatype == 'series':
extra.append('%s: %s [%s]<br />'%(name, val,
fmt_sidx(item[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
extra.append('%s: %s<br />'%(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy'))))
else:
extra.append('%s: %s <br />' % (CFM[key]['name'], val))
comments = item[FM['comments']]
if comments:
comments = comments_to_html(comments)
@ -260,10 +276,14 @@ class NavFeed(Feed):
class AcquisitionFeed(NavFeed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
FM):
FM, CFM):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
for item in items:
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated))
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated,
CFM, CKEYS))
class CategoryFeed(NavFeed):
@ -360,7 +380,7 @@ class OPDSServer(object):
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
return str(AcquisitionFeed(updated, id_, items, offsets,
page_url, up_url, version, self.db.FIELD_MAP))
page_url, up_url, version, self.db.FIELD_MAP, self.db.field_metadata))
def opds_search(self, query=None, version=0, offset=0):
try:
@ -568,7 +588,10 @@ class OPDSServer(object):
(_('Newest'), _('Date'), 'Onewest'),
(_('Title'), _('Title'), 'Otitle'),
]
for category in categories:
def getter(x):
return category_meta[x]['name'].lower()
for category in sorted(categories,
cmp=lambda x,y: cmp(getter(x), getter(y))):
if len(categories[category]) == 0:
continue
if category == 'formats':

View File

@ -11,6 +11,7 @@ import cherrypy
from calibre import strftime as _strftime, prints
from calibre.utils.date import now as nowf
from calibre.utils.config import tweaks
def expose(func):
@ -43,4 +44,14 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except:
return _strftime(fmt, nowf().timetuple())
def format_tag_string(tags, sep):
MAX = tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
else:
tlist = []
tlist.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
return u'%s'%(', '.join(tlist)) if tlist else ''

View File

@ -11,10 +11,11 @@ import cherrypy
from lxml.builder import ElementMaker
from lxml import etree
from calibre.library.server.utils import strftime
from calibre.library.server.utils import strftime, format_tag_string
from calibre.ebooks.metadata import fmt_sidx
from calibre.constants import preferred_encoding
from calibre import isbytestring
from calibre.utils.date import format_date
E = ElementMaker()
@ -83,9 +84,44 @@ class XMLServer(object):
for x in ('isbn', 'formats', 'series', 'tags', 'publisher',
'comments'):
y = record[FM[x]]
if x == 'tags':
y = format_tag_string(y, ',')
kwargs[x] = serialize(y) if y else ''
c = kwargs.pop('comments')
CFM = self.db.field_metadata
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
custcols = []
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
k = str('CF_'+key[1:])
name = CFM[key]['name']
custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']:
kwargs[k] = concat(name, format_tag_string(val,'|'))
elif datatype == 'series':
kwargs[k] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
kwargs[k] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
kwargs[k] = concat(name, __builtin__._('Yes'))
else:
kwargs[k] = concat(name, __builtin__._('No'))
else:
kwargs[k] = concat(name, val)
kwargs['custcols'] = ','.join(custcols)
books.append(E.book(c, **kwargs))
updated = self.db.last_modified()

View File

@ -223,7 +223,7 @@ the server has IP address 63.45.128.5, in the browser, you would type::
http://63.45.128.5:8080
Some devices, like the Kindle, do not allow you to access port 8080 (the default port on which the content
Some devices, like the Kindle (1/2/DX), do not allow you to access port 8080 (the default port on which the content
server runs. In that case, change the port in the |app| Preferences to 80. (On some operating systems,
you may not be able to run the server on a port number less than 1024 because of security settings. In
this case the simplest solution is to adjust your router to forward requests on port 80 to port 8080).

View File

@ -166,7 +166,7 @@ Search & Sort
The Search & Sort section allows you to perform several powerful actions on your book collections.
* You can sort them by title, author, date, rating etc. by clicking on the column titles.
* You can sort them by title, author, date, rating etc. by clicking on the column titles. You can also sub-sort (i.e. sort on multiple columns). For example, if you click on the title column and then the author column, the book will be sorted by author and then all the entries for the same author will be sorted by title.
* You can search for a particular book or set of books using the search bar. More on that below.
@ -212,9 +212,10 @@ metadata.
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
clicking the button |sbi|.
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format,
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field
becomes available. To find the search name for a custom column, hover your mouse over the column header.
Available fields for searching are: ``tag, title, author, publisher, series, series_index, rating, cover,
comments, format, isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the
``ondevice`` field becomes available. To find the search name for a custom column, hover your mouse over the
column header.
The syntax for searching for dates is::
@ -223,9 +224,8 @@ The syntax for searching for dates is::
pubdate:=2009 Will find all books published in 2009
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009.
Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. Some
special date strings are available. The string ``today`` translates to today's date, whatever it is. The
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
@ -234,9 +234,15 @@ You can search for books that have a format of a certain size like this::
size:>1.1M Will find books with a format larger than 1.1MB
size:<=1K Will find books with a format smaller than 1KB
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher.
Dates and numeric fields support the relational operators ``=`` (equals), ``>`` (greater than), ``>=``
(greater than or equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to).
Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3
or higher.
Series indices are searchable. For the standard series, the search name is 'series_index'. For
custom series columns, use the column search name followed by _index. For example, to search the indices for a
custom series column named ``#my_series``, you would use the search name ``#my_series_index``.
Series indices are numbers, so you can use the relational operators described above.
The special field ``search`` is used for saved searches. So if you save a search with the name
"My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved
@ -310,6 +316,70 @@ Jobs
The Jobs panel shows you the number of currently running jobs. Jobs are tasks that run in a separate process, they include converting ebooks and talking to your reader device. You can click on the jobs panel to access the list of jobs. Once a job has completed, by double-clicking it in the list, you can see a detailed log from that job. This is useful to debug jobs that may not have completed successfully.
Keyboard Shortcuts
---------------------
Calibre has several keyboard shortcuts to save you time and mouse movement. These shortcuts are active in the book list view (when you're not editing the details of a particular book), and most of them affect the title you have selected. The |app| e-book viewer has its own shortcuts, which can be customised by clicking the Preferences button in the viewer.
.. note::
Note: The Calibre keyboard shortcuts do not require a modifier key (Command, Option, Control etc.), unless specifically noted. You only need to press the letter key, e.g. E to edit.
.. list-table:: Keyboard Shortcuts
:widths: 10 100
:header-rows: 1
* - Keyboard Shortcut
- Action
* - :kbd:`A`
- Add Books
* - :kbd:`C`
- Convert selected Books
* - :kbd:`D`
- Send to device
* - :kbd:`Del`
- Remove selected Books
* - :kbd:`E`
- Edit metadata of selected books
* - :kbd:`I`
- Show book details
* - :kbd:`M`
- Merge selected records
* - :kbd:`O`
- Open containing folder
* - :kbd:`S`
- Save to Disk
* - :kbd:`V`
- View
* - :kbd:`Alt+V/Cmd+V in OS X`
- View specific format
* - :kbd:`Alt+Shift+J`
- Toggle jobs list
* - :kbd:`Alt+Shift+B`
- Toggle Cover Browser
* - :kbd:`Alt+Shift+T`
- Toggle Tag Browser
* - :kbd:`Alt+A`
- Show books by the Same author as the current book
* - :kbd:`Alt+T`
- Show books with the same tags as current book
* - :kbd:`Alt+P`
- Show books by the same publisher as current book
* - :kbd:`Alt+Shift+S`
- Show books in the same series as current book
* - :kbd:`/, Ctrl+F`
- Focus the search bar
* - :kbd:`Ctrl+D`
- Download metadata and shortcuts
* - :kbd:`Ctrl+R`
- Restart calibre
* - :kbd:`Ctrl+Q`
- Quit calibre

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,14 +13,12 @@ from optparse import OptionParser as _OptionParser
from optparse import IndentedHelpFormatter
from collections import defaultdict
from calibre.constants import terminal_controller, config_dir, \
from calibre.constants import terminal_controller, config_dir, CONFIG_DIR_MODE, \
__appname__, __version__, __author__
from calibre.utils.lock import LockError, ExclusiveFile
plugin_dir = os.path.join(config_dir, 'plugins')
CONFIG_DIR_MODE = 0700
def make_config_dir():
if not os.path.exists(plugin_dir):
os.makedirs(plugin_dir, mode=CONFIG_DIR_MODE)
@ -719,6 +717,7 @@ def _prefs():
c.add_opt('add_formats_to_existing', default=False,
help=_('Add new formats to existing book records'))
c.add_opt('installation_uuid', default=None, help='Installation UUID')
c.add_opt('new_book_tags', default=[], help=_('Tags to apply to books added to the library'))
# these are here instead of the gui preferences because calibredb and
# calibre server can execute searches

View File

@ -81,12 +81,7 @@ def sendmail(msg, from_, to, localhost=None, verbose=0, timeout=30,
for x in to:
return sendmail_direct(from_, x, msg, timeout, localhost, verbose)
import smtplib
class SMTP_SSL(smtplib.SMTP_SSL): # Workaround for bug in smtplib.py
def _get_socket(self, host, port, timeout):
smtplib.SMTP_SSL._get_socket(self, host, port, timeout)
return self.sock
cls = smtplib.SMTP if encryption == 'TLS' else SMTP_SSL
cls = smtplib.SMTP if encryption == 'TLS' else smtplib.SMTP_SSL
timeout = None # Non-blocking sockets sometimes don't work
port = int(port)
s = cls(timeout=timeout, local_hostname=localhost)

View File

@ -193,9 +193,11 @@ class NavBarTemplate(Template):
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_70',
style='text-align:'+align))
if bottom:
if not url.startswith('file://'):
navbar.append(HR())
text = 'This article was downloaded by '
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
p = PT(text, STRONG(__appname__), A(url, href=url),
style='text-align:left; max-width: 100%; overflow: hidden;')
p[0].tail = ' from '
navbar.append(p)
navbar.append(BR())