'
'''
@@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe):
language = 'sr'
publication_type = 'newspaper'
remove_empty_feeds = True
- extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} '
+ extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
+ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
+ .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif}
+ .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif}
+ .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
+ margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small}
+ .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
+ font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} """
conversion_options = {
'comment' : description
@@ -42,19 +48,32 @@ class Danas(BasicNewsRecipe):
]
feeds = [
- (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
- ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
- ,(u'Dru\xc5\xa1tvo', u'http://www.danas.rs/rss/rss.asp?column_id=24')
- ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
- ,(u'Ekonomija', u'http://www.danas.rs/rss/rss.asp?column_id=6' )
- ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
- ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
- ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
- ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
- ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
- ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
- ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
- ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
+ (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
+ ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
+ ,(u'Drustvo' , u'http://www.danas.rs/rss/rss.asp?column_id=24')
+ ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
+ ,(u'Ekonomija' , u'http://www.danas.rs/rss/rss.asp?column_id=6' )
+ ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
+ ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
+ ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
+ ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
+ ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
+ ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
+ ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
+ ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
+ ,(u'Sluzbena beleska' , u'http://www.danas.rs/rss/rss.asp?column_id=48')
+ ,(u'Suocavanja' , u'http://www.danas.rs/rss/rss.asp?column_id=49')
+ ,(u'Moj Izbor' , u'http://www.danas.rs/rss/rss.asp?column_id=50')
+ ,(u'Direktno' , u'http://www.danas.rs/rss/rss.asp?column_id=51')
+ ,(u'I tome slicno' , u'http://www.danas.rs/rss/rss.asp?column_id=52')
+ ,(u'No longer and not yet', u'http://www.danas.rs/rss/rss.asp?column_id=53')
+ ,(u'Resetovanje' , u'http://www.danas.rs/rss/rss.asp?column_id=54')
+ ,(u'Iza scene' , u'http://www.danas.rs/rss/rss.asp?column_id=60')
+ ,(u'Drustvoslovlje' , u'http://www.danas.rs/rss/rss.asp?column_id=55')
+ ,(u'Zvaka u pepeljari' , u'http://www.danas.rs/rss/rss.asp?column_id=56')
+ ,(u'Vostani Serbie' , u'http://www.danas.rs/rss/rss.asp?column_id=57')
+ ,(u'Med&Jad-a' , u'http://www.danas.rs/rss/rss.asp?column_id=58')
+ ,(u'Svetlosti pozornice' , u'http://www.danas.rs/rss/rss.asp?column_id=59')
]
def preprocess_html(self, soup):
@@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe):
def print_version(self, url):
return url + '&action=print'
+ def get_cover_url(self):
+ cover_url = None
+ soup = self.index_to_soup('http://www.danas.rs/')
+ for citem in soup.findAll('img'):
+ if citem['src'].endswith('naslovna.jpg'):
+ return 'http://www.danas.rs' + citem['src']
+ return cover_url
diff --git a/resources/recipes/thairath.recipe b/resources/recipes/thairath.recipe
new file mode 100644
index 0000000000..6ebb84f3a5
--- /dev/null
+++ b/resources/recipes/thairath.recipe
@@ -0,0 +1,58 @@
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1271637235(BasicNewsRecipe):
+
+ title = u'Thairath'
+ __author__ = 'Anat R.'
+ language = 'th'
+
+ oldest_article = 7
+
+ max_articles_per_feed = 100
+ no_stylesheets = True
+
+ remove_javascript = True
+
+ use_embedded_content = False
+ feeds = [(u'News',
+u'http://www.thairath.co.th/rss/news.xml'), (u'Politics',
+u'http://www.thairath.co.th/rss/pol.xml'), (u'Economy',
+u'http://www.thairath.co.th/rss/eco.xml'), (u'International',
+u'http://www.thairath.co.th/rss/oversea.xml'), (u'Sports',
+u'http://www.thairath.co.th/rss/sport.xml'), (u'Life',
+u'http://www.thairath.co.th/rss/life.xml'), (u'Education',
+u'http://www.thairath.co.th/rss/edu.xml'), (u'Tech',
+u'http://www.thairath.co..th/rss/tech.xml'), (u'Entertainment',
+u'http://www.thairath.co.th/rss/ent.xml')]
+ keep_only_tags = []
+
+ keep_only_tags.append(dict(name = 'h1', attrs = {'id' : 'title'}))
+
+ keep_only_tags.append(dict(name = 'ul', attrs = {'class' :
+'detail-info'}))
+
+ keep_only_tags.append(dict(name = 'img', attrs = {'class' :
+'detail-image'}))
+
+ keep_only_tags.append(dict(name = 'div', attrs = {'class' :
+'entry'}))
+ remove_tags = []
+ remove_tags.append(dict(name = 'div', attrs = {'id':
+'menu-holder'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'addthis_toolbox addthis_default_style'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class': 'box top-item'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class': 'column-200 column-margin-430'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'id':
+'detail-related'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'id': 'related'}))
+
+ remove_tags.append(dict(name = 'id', attrs = {'class': 'footer'}))
+
+ remove_tags.append(dict(name = "ul",attrs =
+{'id':'banner-highlights-images'}))
diff --git a/resources/recipes/the_nation_thai.recipe b/resources/recipes/the_nation_thai.recipe
new file mode 100644
index 0000000000..a33a16e0a5
--- /dev/null
+++ b/resources/recipes/the_nation_thai.recipe
@@ -0,0 +1,44 @@
+
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class AdvancedUserRecipe1271596863(BasicNewsRecipe):
+
+ title = u'The Nation'
+ __author__ = 'Anat R.'
+ language = 'en_TH'
+
+ oldest_article = 7
+
+ max_articles_per_feed = 100
+ no_stylesheets = True
+
+ remove_javascript = True
+
+ use_embedded_content = False
+ feeds = [(u'Topstory',
+u'http://www.nationmultimedia.com/home/rss/topstories.rss'),
+(u'National', u'http://www.nationmultimedia.com/home/rss/national.rss'),
+ (u'Politics',
+u'http://www.nationmultimedia.com/home/rss/politics.rss'), (u'Business',
+ u'http://www.nationmultimedia.com/home/rss/business.rss'),
+(u'Regional', u'http://www.nationmultimedia.com/home/rss/regional.rss'),
+ (u'Sports', u'http://www.nationmultimedia.com/home/rss/sport.rss'),
+(u'Travel', u'http://www.nationmultimedia.com/home/rss/travel.rss'),
+(u'Life', u'http://www.nationmultimedia.com/home/rss/life.rss')]
+ keep_only_tags = []
+
+ keep_only_tags.append(dict(name = 'div', attrs = {'class' :
+'pd10'}))
+ remove_tags = []
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'WrapperHeaderCol2-2'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'LayoutMenu2'}))
+
+ remove_tags.append(dict(name = 'div', attrs = {'class':
+'TextHeaderRight'}))
+
+ remove_tags.append(dict(name = "ul",attrs = {'id':'toolZoom'}))
+
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 6b573a0420..8caca1f261 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -30,7 +30,7 @@ def authors_to_string(authors):
def author_to_author_sort(author):
method = tweaks['author_sort_copy_method']
- if method == 'copy' or (method == 'comma' and author.count(',') > 0):
+ if method == 'copy' or (method == 'comma' and ',' in author):
return author
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]
@@ -223,6 +223,7 @@ class MetaInformation(object):
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
+ 'author_sort_map',
'pubdate', 'rights', 'publication_type', 'uuid'):
if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr))
@@ -244,6 +245,7 @@ class MetaInformation(object):
self.tags = getattr(mi, 'tags', [])
#: mi.cover_data = (ext, data)
self.cover_data = getattr(mi, 'cover_data', (None, None))
+ self.author_sort_map = getattr(mi, 'author_sort_map', {})
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
@@ -258,7 +260,7 @@ class MetaInformation(object):
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
- 'rights', 'publication_type', 'uuid'
+ 'rights', 'publication_type', 'uuid', 'author_sort_map'
):
prints(x, getattr(self, x, 'None'))
@@ -288,6 +290,9 @@ class MetaInformation(object):
self.tags += mi.tags
self.tags = list(set(self.tags))
+ if mi.author_sort_map:
+ self.author_sort_map.update(mi.author_sort_map)
+
if getattr(mi, 'cover_data', False):
other_cover = mi.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''
diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py
index 8483c2bddb..c3b95f1188 100644
--- a/src/calibre/ebooks/metadata/book/__init__.py
+++ b/src/calibre/ebooks/metadata/book/__init__.py
@@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([
'title_sort',
# Ordered list of authors. Must never be None, can be [_('Unknown')]
'authors',
+ # Map of sort strings for each author
+ 'author_sort_map',
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index ad5dd17ace..ba34f04f95 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -16,6 +16,7 @@ NULL_VALUES = {
'classifiers' : {},
'languages' : [],
'device_collections': [],
+ 'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
}
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 1f321568f5..3367ab14f6 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -741,7 +741,7 @@ class OPF(object):
def fset(self, val):
for tag in list(self.tags_path(self.metadata)):
- self.metadata.remove(tag)
+ tag.getparent().remove(tag)
for tag in val:
elem = self.create_metadata_element('subject')
self.set_text(elem, unicode(tag))
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 418e39c41b..1056f6ced6 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -43,8 +43,8 @@ def _config():
help=_('Notify when a new version is available'))
c.add_opt('use_roman_numerals_for_series_number', default=True,
help=_('Use Roman numerals for series number'))
- c.add_opt('sort_by_popularity', default=False,
- help=_('Sort tags list by popularity'))
+ c.add_opt('sort_tags_by', default='name',
+ help=_('Sort tags list by name, popularity, or rating'))
c.add_opt('cover_flow_queue_length', default=6,
help=_('Number of covers to show in the cover browsing mode'))
c.add_opt('LRF_conversion_defaults', default=[],
@@ -101,6 +101,8 @@ def _config():
help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='wide')
+ c.add_opt('show_avg_rating', default=True,
+ help=_('Show the average rating per item indication in the tag browser'))
return ConfigProxy(c)
config = _config()
diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py
index 75c045d011..a397ab903d 100644
--- a/src/calibre/gui2/book_details.py
+++ b/src/calibre/gui2/book_details.py
@@ -109,7 +109,7 @@ class CoverView(QWidget): # {{{
def show_data(self, data):
self.animation.stop()
- if data.get('id', None) == self.data.get('id', None):
+ if data.get('id', True) == self.data.get('id', False):
return
self.data = {'id':data.get('id', None)}
if data.has_key('cover'):
@@ -258,8 +258,7 @@ class BookDetails(QWidget):
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
- path = os.path.dirname(val)
- QDesktopServices.openUrl(QUrl.fromLocalFile(path))
+ QDesktopServices.openUrl(QUrl.fromLocalFile(val))
def mouseReleaseEvent(self, ev):
@@ -275,8 +274,6 @@ class BookDetails(QWidget):
self.setToolTip(''+_('Click to open Book Details window') +
'
' + _('Path') + ': ' + data.get(_('Path'), ''))
-
-
def reset_info(self):
self.show_data({})
diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py
index 2026f1cee5..3ddd5674bb 100644
--- a/src/calibre/gui2/convert/metadata.py
+++ b/src/calibre/gui2/convert/metadata.py
@@ -13,7 +13,7 @@ from PyQt4.Qt import QPixmap, SIGNAL
from calibre.gui2 import choose_images, error_dialog
from calibre.gui2.convert.metadata_ui import Ui_Form
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \
- MetaInformation, authors_to_sort_string
+ MetaInformation
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget
@@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form):
au = unicode(self.author.currentText())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
- self.author_sort.setText(authors_to_sort_string(authors))
+ self.author_sort.setText(self.db.author_sort_from_authors(authors))
def initialize_metadata_options(self):
diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py
index cf54e6c1f3..b3a7196b20 100644
--- a/src/calibre/gui2/device.py
+++ b/src/calibre/gui2/device.py
@@ -23,7 +23,7 @@ from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir
-from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
+from calibre.ebooks.metadata import authors_to_string
from calibre import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
@@ -1409,7 +1409,7 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
- book.author_sort = authors_to_sort_string(book.authors)
+ book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
resend_metadata = True
if resend_metadata:
diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index aa68c030b5..ad49848b7b 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -481,6 +481,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value)
+ self.search_as_you_type.setChecked(config['search_as_you_type'])
+ self.show_avg_rating.setChecked(config['show_avg_rating'])
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
li = None
@@ -862,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()
+ config['show_avg_rating'] = self.show_avg_rating.isChecked()
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui
index 84a2b7bbcb..ba92c0d301 100644
--- a/src/calibre/gui2/dialogs/config/config.ui
+++ b/src/calibre/gui2/dialogs/config/config.ui
@@ -371,6 +371,16 @@
-
+
+
+ Show &average ratings in the tags browser
+
+
+ true
+
+
+
+ -
Search as you type
@@ -380,21 +390,21 @@
- -
+
-
Automatically send downloaded &news to ebook reader
- -
+
-
&Delete news from library when it is automatically sent to reader
- -
+
-
-
@@ -411,7 +421,7 @@
- -
+
-
Toolbar
@@ -459,7 +469,7 @@
- -
+
-
-
diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py
new file mode 100644
index 0000000000..842fd7c943
--- /dev/null
+++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
+__docformat__ = 'restructuredtext en'
+__license__ = 'GPL v3'
+
+from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
+
+from calibre.ebooks.metadata import author_to_author_sort
+from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
+
+class tableItem(QTableWidgetItem):
+ def __ge__(self, other):
+ return unicode(self.text()).lower() >= unicode(other.text()).lower()
+
+ def __lt__(self, other):
+ return unicode(self.text()).lower() < unicode(other.text()).lower()
+
+class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
+
+ def __init__(self, parent, db, id_to_select):
+ QDialog.__init__(self, parent)
+ Ui_EditAuthorsDialog.__init__(self)
+ self.setupUi(self)
+
+ self.buttonBox.accepted.connect(self.accepted)
+
+ self.table.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.table.setColumnCount(2)
+ self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
+
+ self.authors = {}
+ auts = db.get_authors_with_ids()
+ self.table.setRowCount(len(auts))
+ select_item = None
+ for row, (id, author, sort) in enumerate(auts):
+ author = author.replace('|', ',')
+ self.authors[id] = (author, sort)
+ aut = tableItem(author)
+ aut.setData(Qt.UserRole, id)
+ sort = tableItem(sort)
+ self.table.setItem(row, 0, aut)
+ self.table.setItem(row, 1, sort)
+ if id == id_to_select:
+ select_item = sort
+ self.table.resizeColumnsToContents()
+
+ # set up the signal after the table is filled
+ self.table.cellChanged.connect(self.cell_changed)
+
+ self.table.setSortingEnabled(True)
+ self.table.sortByColumn(1, Qt.AscendingOrder)
+ if select_item is not None:
+ self.table.setCurrentItem(select_item)
+ self.table.editItem(select_item)
+ else:
+ self.table.setCurrentCell(0, 0)
+
+ def accepted(self):
+ self.result = []
+ for row in range(0,self.table.rowCount()):
+ id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0]
+ aut = unicode(self.table.item(row, 0).text()).strip()
+ sort = unicode(self.table.item(row, 1).text()).strip()
+ orig_aut,orig_sort = self.authors[id]
+ if orig_aut != aut or orig_sort != sort:
+ self.result.append((id, orig_aut, aut, sort))
+
+ def cell_changed(self, row, col):
+ if col == 0:
+ item = self.table.item(row, 0)
+ aut = unicode(item.text()).strip()
+ c = self.table.item(row, 1)
+ c.setText(author_to_author_sort(aut))
+ item = c
+ else:
+ item = self.table.item(row, 1)
+ self.table.setCurrentItem(item)
+ # disable and reenable sorting to force the sort now, so we can scroll
+ # to the item after it moves
+ self.table.setSortingEnabled(False)
+ self.table.setSortingEnabled(True)
+ self.table.scrollToItem(item)
diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.ui b/src/calibre/gui2/dialogs/edit_authors_dialog.ui
new file mode 100644
index 0000000000..d124f1498d
--- /dev/null
+++ b/src/calibre/gui2/dialogs/edit_authors_dialog.ui
@@ -0,0 +1,86 @@
+
+
+ EditAuthorsDialog
+
+
+
+ 0
+ 0
+ 730
+ 342
+
+
+
+
+ 0
+ 0
+
+
+
+ Manage authors
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+ true
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ EditAuthorsDialog
+ accept()
+
+
+ 229
+ 211
+
+
+ 157
+ 234
+
+
+
+
+ buttonBox
+ rejected()
+ EditAuthorsDialog
+ reject()
+
+
+ 297
+ 217
+
+
+ 286
+ 234
+
+
+
+
+
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index eca7fe9c15..8b27ff1999 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -8,7 +8,7 @@ from PyQt4.QtGui import QDialog, QGridLayout
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
-from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
+from calibre.ebooks.metadata import string_to_authors, \
authors_to_string
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
@@ -110,10 +110,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
au = string_to_authors(au)
self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked():
- aut = self.db.authors(id, index_is_id=True)
- aut = aut if aut else ''
- aut = [a.strip().replace('|', ',') for a in aut.strip().split(',')]
- x = authors_to_sort_string(aut)
+ x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text())
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 0e35f938dd..96323ac596 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -23,7 +23,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS
-from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, \
+from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn
from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre import islinux, isfreebsd
@@ -460,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
- self.author_sort.setText(authors_to_sort_string(authors))
+ self.author_sort.setText(self.db.author_sort_from_authors(authors))
def swap_title_author(self):
title = self.title.text()
diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui
index 4f57af745b..39076aa1f6 100644
--- a/src/calibre/gui2/dialogs/tag_list_editor.ui
+++ b/src/calibre/gui2/dialogs/tag_list_editor.ui
@@ -121,6 +121,9 @@
QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+ true
+
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index ff4b2b6ee9..8080769377 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -420,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{
pt.orig_file_path = os.path.abspath(src.name)
pt.seek(0)
if set_metadata:
- _set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
+ try:
+ _set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
format)
+ except:
+ traceback.print_exc()
pt.close()
def to_uni(x):
if isbytestring(x):
diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py
index 90426f8021..9aa9b8262c 100644
--- a/src/calibre/gui2/status.py
+++ b/src/calibre/gui2/status.py
@@ -239,8 +239,7 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
- path = os.path.dirname(val)
- QDesktopServices.openUrl(QUrl.fromLocalFile(path))
+ QDesktopServices.openUrl(QUrl.fromLocalFile(val))
def resizeEvent(self, ev):
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index bc698a3502..daea4e86ea 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -10,10 +10,10 @@ Browsing book collection by tags.
from itertools import izip
from functools import partial
-from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
+from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
- QPushButton, QWidget
+ QPushButton, QWidget, QItemDelegate
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
@@ -22,6 +22,39 @@ from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
+from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
+
+class TagDelegate(QItemDelegate): # {{{
+
+ def paint(self, painter, option, index):
+ item = index.internalPointer()
+ if item.type != TagTreeItem.TAG:
+ QItemDelegate.paint(self, painter, option, index)
+ return
+ r = option.rect
+ model = self.parent().model()
+ icon = model.data(index, Qt.DecorationRole).toPyObject()
+ painter.save()
+ if item.tag.state != 0 or not config['show_avg_rating'] or \
+ item.tag.avg_rating is None:
+ icon.paint(painter, r, Qt.AlignLeft)
+ else:
+ painter.setOpacity(0.3)
+ icon.paint(painter, r, Qt.AlignLeft)
+ painter.setOpacity(1)
+ rating = item.tag.avg_rating
+ painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)),
+ r.width(), r.height())
+ icon.paint(painter, r, Qt.AlignLeft)
+ painter.setClipRect(r)
+
+ # Paint the text
+ r.setLeft(r.left()+r.height()+3)
+ painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
+ model.data(index, Qt.DisplayRole).toString())
+ painter.restore()
+
+ # }}}
class TagsView(QTreeView): # {{{
@@ -30,6 +63,7 @@ class TagsView(QTreeView): # {{{
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
+ author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
@@ -43,13 +77,14 @@ class TagsView(QTreeView): # {{{
self.setAlternatingRowColors(True)
self.setAnimated(True)
self.setHeaderHidden(True)
+ self.setItemDelegate(TagDelegate(self))
- def set_database(self, db, tag_match, popularity):
+ def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=None)
- self.popularity = popularity
+ self.sort_by = sort_by
self.tag_match = tag_match
self.db = db
self.search_restriction = None
@@ -57,8 +92,9 @@ class TagsView(QTreeView): # {{{
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
- self.popularity.setChecked(config['sort_by_popularity'])
- self.popularity.stateChanged.connect(self.sort_changed)
+ pop = config['sort_tags_by']
+ self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
+ self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed)
@@ -69,8 +105,8 @@ class TagsView(QTreeView): # {{{
def match_all(self):
return self.tag_match and self.tag_match.currentIndex() > 0
- def sort_changed(self, state):
- config.set('sort_by_popularity', state == Qt.Checked)
+ def sort_changed(self, pop):
+ config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
self.recount()
def set_search_restriction(self, s):
@@ -112,6 +148,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
+ if action == 'edit_author_sort':
+ self.author_sort_edit.emit(self, index)
+ return
if action == 'hide':
self.hidden_categories.add(category)
elif action == 'show':
@@ -132,6 +171,7 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG:
tag_item = item
tag_name = item.tag.name
+ tag_id = item.tag.id
item = item.parent
if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString())
@@ -147,9 +187,13 @@ class TagsView(QTreeView): # {{{
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'):
- self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
+ self.context_menu.addAction(_('Rename \'%s\'')%tag_name,
partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index))
+ if key == 'authors':
+ self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name,
+ partial(self.context_menu_handler,
+ action='edit_author_sort', index=tag_id))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category,
@@ -166,9 +210,12 @@ class TagsView(QTreeView): # {{{
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \
self.db.field_metadata[key]['is_custom']:
- self.context_menu.addAction(_('Manage ') + category,
+ self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key))
+ elif key == 'authors':
+ self.context_menu.addAction(_('Manage %s')%category,
+ partial(self.context_menu_handler, action='edit_author_sort'))
elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
@@ -332,6 +379,7 @@ class TagsModel(QAbstractItemModel): # {{{
':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.svg')),
'search' : QIcon(I('search.svg'))})
+ self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
@@ -341,7 +389,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map = []
# get_node_tree cannot return None here, because row_map is empty
- data = self.get_node_tree(config['sort_by_popularity'])
+ data = self.get_node_tree(config['sort_tags_by'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
@@ -354,7 +402,14 @@ class TagsModel(QAbstractItemModel): # {{{
data=self.categories[i],
category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r)
+ # This duplicates code in refresh(). Having it here as well
+ # can save seconds during startup, because we avoid a second
+ # call to get_node_tree.
for tag in data[r]:
+ if r not in self.categories_with_ratings and \
+ not self.db.field_metadata[r]['is_custom'] and \
+ not self.db.field_metadata[r]['kind'] == 'user':
+ tag.avg_rating = None
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s):
@@ -378,11 +433,11 @@ class TagsModel(QAbstractItemModel): # {{{
# Now get the categories
if self.search_restriction:
- data = self.db.get_categories(sort_on_count=sort,
+ data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
- data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
+ data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
tb_categories = self.db.field_metadata
for category in tb_categories:
@@ -396,7 +451,7 @@ class TagsModel(QAbstractItemModel): # {{{
return data
def refresh(self):
- data = self.get_node_tree(config['sort_by_popularity']) # get category data
+ data = self.get_node_tree(config['sort_tags_by']) # get category data
if data is None:
return False
row_index = -1
@@ -417,6 +472,10 @@ class TagsModel(QAbstractItemModel): # {{{
if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]:
+ if r not in self.categories_with_ratings and \
+ not self.db.field_metadata[r]['is_custom'] and \
+ not self.db.field_metadata[r]['kind'] == 'user':
+ tag.avg_rating = None
tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows()
@@ -601,12 +660,13 @@ class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db,
- self.tag_match, self.popularity)
+ self.tag_match, self.sort_by)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
+ self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.edit_categories.clicked.connect(lambda x:
@@ -636,6 +696,19 @@ class TagBrowserMixin(object): # {{{
self.saved_search.clear_to_help()
self.search.clear_to_help()
+ def do_author_sort_edit(self, parent, id):
+ db = self.library_view.model().db
+ editor = EditAuthorsDialog(parent, db, id)
+ d = editor.exec_()
+ if d:
+ for (id, old_author, new_author, new_sort) in editor.result:
+ if old_author != new_author:
+ # The id might change if the new author already exists
+ id = db.rename_author(id, new_author)
+ db.set_sort_field_for_author(id, unicode(new_sort))
+ self.library_view.model().refresh()
+ self.tags_view.recount()
+
# }}}
class TagBrowserWidget(QWidget): # {{{
@@ -648,9 +721,13 @@ class TagBrowserWidget(QWidget): # {{{
parent.tags_view = TagsView(parent)
self._layout.addWidget(parent.tags_view)
- parent.popularity = QCheckBox(parent)
- parent.popularity.setText(_('Sort by &popularity'))
- self._layout.addWidget(parent.popularity)
+ parent.sort_by = QComboBox(parent)
+ # Must be in the same order as db2.CATEGORY_SORTS
+ for x in (_('Sort by name'), _('Sort by popularity'),
+ _('Sort by average rating')):
+ parent.sort_by.addItem(x)
+ parent.sort_by.setCurrentIndex(0)
+ self._layout.addWidget(parent.sort_by)
parent.tag_match = QComboBox(parent)
for x in (_('Match any'), _('Match all')):
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 383c67a773..2226520cf2 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -430,7 +430,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
- self.tags_view.set_database(db, self.tag_match, self.popularity)
+ self.tags_view.set_database(db, self.tag_match, self.sort_by)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear_to_help()
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index 23b78f38ae..c0ba91e252 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -461,14 +461,27 @@ class CustomColumns(object):
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
- (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count
+ (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
+ (SELECT AVG(r.rating)
+ FROM {lt},
+ books_ratings_link as bl,
+ ratings as r
+ WHERE {lt}.value={table}.id and bl.book={lt}.book and
+ r.id = bl.rating and r.rating <> 0) avg_rating
FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
- books_list_filter(book)) count
+ books_list_filter(book)) count,
+ (SELECT AVG(r.rating)
+ FROM {lt},
+ books_ratings_link as bl,
+ ratings as r
+ WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
+ r.id = bl.rating AND r.rating <> 0 AND
+ books_list_filter(bl.book)) avg_rating
FROM {table};
'''.format(lt=lt, table=table),
@@ -505,7 +518,6 @@ class CustomColumns(object):
END;
'''.format(table=table),
]
-
script = ' \n'.join(lines)
self.conn.executescript(script)
self.conn.commit()
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 31e9b43f86..c7830187df 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -12,7 +12,7 @@ from math import floor
from PyQt4.QtGui import QImage
-from calibre.ebooks.metadata import title_sort
+from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.library.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade
@@ -20,7 +20,7 @@ from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
- MetaInformation, authors_to_sort_string
+ MetaInformation
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
@@ -56,11 +56,18 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object):
- def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
+ def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
+ tooltip=None, icon=None):
self.name = name
self.id = id
self.count = count
self.state = state
+ self.avg_rating = avg/2.0 if avg is not None else 0
+ self.sort = sort
+ if self.avg_rating > 0:
+ if tooltip:
+ tooltip = tooltip + ': '
+ tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating)
self.tooltip = tooltip
self.icon = icon
@@ -133,7 +140,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
name,
- (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count
+ (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count,
+ (0) as avg_rating,
+ name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@@ -144,7 +153,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
id,
name,
- (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count
+ (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count,
+ (0) as avg_rating,
+ name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@@ -422,6 +433,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
+ if mi.authors:
+ mi.author_sort_map = {}
+ for name, sort in zip(mi.authors, self.authors_sort_strings(idx,
+ index_is_id)):
+ mi.author_sort_map[name] = sort
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@@ -679,7 +695,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tn=field['table'], col=field['link_column']), (id_,))
return set(x[0] for x in ans)
- def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
+ CATEGORY_SORTS = ('name', 'popularity', 'rating')
+
+ def get_categories(self, sort='name', ids=None, icon_map=None):
self.books_list_filter.change([] if not ids else ids)
categories = {}
@@ -698,13 +716,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue
cn = cat['column']
if ids is None:
- query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
+ query = '''SELECT id, {0}, count, avg_rating, sort
+ FROM tag_browser_{1}'''.format(cn, tn)
else:
- query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn)
- if sort_on_count:
- query += ' ORDER BY count DESC'
+ query = '''SELECT id, {0}, count, avg_rating, sort
+ FROM tag_browser_filtered_{1}'''.format(cn, tn)
+ if sort == 'popularity':
+ query += ' ORDER BY count DESC, sort ASC'
+ elif sort == 'name':
+ query += ' ORDER BY sort ASC'
else:
- query += ' ORDER BY {0} ASC'.format(cn)
+ query += ' ORDER BY avg_rating DESC, sort ASC'
data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and
@@ -722,6 +744,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
datatype = cat['datatype']
if datatype == 'rating':
+ # eliminate the zero ratings line as well as count == 0
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors':
@@ -733,15 +756,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formatter = (lambda x:unicode(x))
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
- icon=icon, tooltip = tooltip)
+ avg=r[3], sort=r[4],
+ icon=icon, tooltip=tooltip)
for r in data if item_not_zero_func(r)]
- if category == 'series' and not sort_on_count:
- if tweaks['title_series_sorting'] == 'library_order':
- ts = lambda x: title_sort(x)
- else:
- ts = lambda x:x
- categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
- ts(y.name).lower()))
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@@ -765,11 +782,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if count > 0:
categories['formats'].append(Tag(fmt, count=count, icon=icon))
- if sort_on_count:
- categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count),
- reverse=True)
- else:
- categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name))
+ if sort == 'popularity':
+ categories['formats'].sort(key=lambda x: x.count, reverse=True)
+ else: # no ratings exist to sort on
+ categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. ####
user_categories = prefs['user_categories']
@@ -794,12 +810,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Not a problem if we accumulate entries in the icon map
if icon_map is not None:
icon_map[cat_name] = icon_map[':user']
- if sort_on_count:
+ if sort == 'popularity':
categories[cat_name] = \
- sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))
+ sorted(items, key=lambda x: x.count, reverse=True)
+ elif sort == 'name':
+ categories[cat_name] = \
+ sorted(items, key=lambda x: x.sort.lower())
else:
categories[cat_name] = \
- sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
+ sorted(items, key=lambda x:x.avg_rating, reverse=True)
#### Finally, the saved searches category ####
items = []
@@ -909,6 +928,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True)
self.notify('metadata', [id])
+ # Given a book, return the list of author sort strings for the book's authors
+ def authors_sort_strings(self, id, index_is_id=False):
+ id = id if index_is_id else self.id(id)
+ aut_strings = self.conn.get('''
+ SELECT sort
+ FROM authors, books_authors_link as bl
+ WHERE bl.book=? and authors.id=bl.author
+ ORDER BY bl.id''', (id,))
+ result = []
+ for (sort,) in aut_strings:
+ result.append(sort)
+ return result
+
+ # Given a book, return the author_sort string for authors of the book
+ def author_sort_from_book(self, id, index_is_id=False):
+ auts = self.authors_sort_strings(id, index_is_id)
+ return ' & '.join(auts).replace('|', ',')
+
+ # Given a list of authors, return the author_sort string for the authors,
+ # preferring the author sort associated with the author over the computed
+ # string
+ def author_sort_from_authors(self, authors):
+ result = []
+ for aut in authors:
+ r = self.conn.get('SELECT sort FROM authors WHERE name=?',
+ (aut.replace(',', '|'),), all=False)
+ if r is None:
+ result.append(author_to_author_sort(aut))
+ else:
+ result.append(r)
+ return ' & '.join(result).replace('|', ',')
+
def set_authors(self, id, authors, notify=True):
'''
`authors`: A list of authors.
@@ -935,7 +986,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
(id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata
pass
- ss = authors_to_sort_string(authors)
+ self.conn.commit()
+ ss = self.author_sort_from_book(id, index_is_id=True)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id))
self.conn.commit()
@@ -1007,6 +1059,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_tag(self, old_id, new_name):
+ new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from tags
WHERE name=?''', (new_name,), all=False)
@@ -1046,6 +1099,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_series(self, old_id, new_name):
+ new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from series
WHERE name=?''', (new_name,), all=False)
@@ -1075,7 +1129,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
index = index + 1
self.conn.commit()
-
def delete_series_using_id(self, id):
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
@@ -1091,6 +1144,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_publisher(self, old_id, new_name):
+ new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from publishers
WHERE name=?''', (new_name,), all=False)
@@ -1113,12 +1167,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
self.conn.commit()
- # There is no editor for author, so we do not need get_authors_with_ids or
- # delete_author_using_id.
+ def get_authors_with_ids(self):
+ result = self.conn.get('SELECT id,name,sort FROM authors')
+ if not result:
+ return []
+ return result
+
+ def set_sort_field_for_author(self, old_id, new_sort):
+ self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
+ (new_sort.strip(), old_id))
+ self.conn.commit()
+ # Now change all the author_sort fields in books by this author
+ bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
+ for (book_id,) in bks:
+ ss = self.author_sort_from_book(book_id, index_is_id=True)
+ self.set_author_sort(book_id, ss)
def rename_author(self, old_id, new_name):
# Make sure that any commas in new_name are changed to '|'!
- new_name = new_name.replace(',', '|')
+ new_name = new_name.replace(',', '|').strip()
# Get the list of books we must fix up, one way or the other
# Save the list so we can use it twice
@@ -1141,7 +1208,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id))
self.conn.commit()
- return
+ return new_id
# Author exists. To fix this, we must replace all the authors
# instead of replacing the one. Reason: db integrity checks can stop
# the rename process, which would leave everything half-done. We
@@ -1184,24 +1251,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# now fix the filesystem paths
self.set_path(book_id, index_is_id=True)
# Next fix the author sort. Reset it to the default
- authors = self.conn.get('''
- SELECT authors.name
- FROM authors, books_authors_link as bl
- WHERE bl.book = ? and bl.author = authors.id
- ORDER BY bl.id
- ''' , (book_id,))
- # unpack the double-list structure
- for i,aut in enumerate(authors):
- authors[i] = aut[0]
- ss = authors_to_sort_string(authors)
- # Change the '|'s to ','
- ss = ss.replace('|', ',')
- self.conn.execute('''UPDATE books
- SET author_sort=?
- WHERE id=?''', (ss, book_id))
- self.conn.commit()
+ ss = self.author_sort_from_book(book_id, index_is_id=True)
+ self.set_author_sort(book_id, ss)
# the caller will do a general refresh, so we don't need to
# do one here
+ return new_id
# end convenience methods
@@ -1436,7 +1490,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
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
- aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
+ aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@@ -1476,7 +1530,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
duplicates.append((path, format, mi))
continue
series_index = 1.0 if mi.series_index is None else mi.series_index
- aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
+ aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@@ -1515,7 +1569,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.title = _('Unknown')
if not mi.authors:
mi.authors = [_('Unknown')]
- aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors)
+ aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index 82e4edfdf2..8cb5c9bdad 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -44,9 +44,12 @@ class FieldMetadata(dict):
is_category: is a tag browser category. If true, then:
table: name of the db table used to construct item list
column: name of the column in the normalized table to join on
- link_column: name of the column in the connection table to join on
+ link_column: name of the column in the connection table to join on. This
+ key should not be present if there is no link table
+ category_sort: the field in the normalized table to sort on. This
+ key must be present if is_category is True
If these are None, then the category constructor must know how
- to build the item list (e.g., formats).
+ to build the item list (e.g., formats, news).
The order below is the order that the categories will
appear in the tags pane.
@@ -66,6 +69,7 @@ class FieldMetadata(dict):
('authors', {'table':'authors',
'column':'name',
'link_column':'author',
+ 'category_sort':'sort',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@@ -76,6 +80,7 @@ class FieldMetadata(dict):
('series', {'table':'series',
'column':'name',
'link_column':'series',
+ 'category_sort':'(title_sort(name))',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@@ -95,6 +100,7 @@ class FieldMetadata(dict):
('publisher', {'table':'publishers',
'column':'name',
'link_column':'publisher',
+ 'category_sort':'name',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@@ -105,6 +111,7 @@ class FieldMetadata(dict):
('rating', {'table':'ratings',
'column':'rating',
'link_column':'rating',
+ 'category_sort':'rating',
'datatype':'rating',
'is_multiple':None,
'kind':'field',
@@ -114,6 +121,7 @@ class FieldMetadata(dict):
'is_category':True}),
('news', {'table':'news',
'column':'name',
+ 'category_sort':'name',
'datatype':None,
'is_multiple':None,
'kind':'category',
@@ -124,6 +132,7 @@ class FieldMetadata(dict):
('tags', {'table':'tags',
'column':'name',
'link_column': 'tag',
+ 'category_sort':'name',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@@ -374,7 +383,7 @@ class FieldMetadata(dict):
'search_terms':[key], 'label':label,
'colnum':colnum, 'display':display,
'is_custom':True, 'is_category':is_category,
- 'link_column':'value',
+ 'link_column':'value','category_sort':'value',
'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key
diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py
index 66cf091016..a8ffd9cde4 100644
--- a/src/calibre/library/schema_upgrades.py
+++ b/src/calibre/library/schema_upgrades.py
@@ -296,3 +296,117 @@ class SchemaUpgrade(object):
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_tag_browser_view(field['table'], field['link_column'], field['column'])
+
+ def upgrade_version_11(self):
+ 'Add average rating to tag browser views'
+ def create_std_tag_browser_view(table_name, column_name,
+ view_column_name, sort_column_name):
+ script = ('''
+ DROP VIEW IF EXISTS tag_browser_{tn};
+ CREATE VIEW tag_browser_{tn} AS SELECT
+ id,
+ {vcn},
+ (SELECT COUNT(id) FROM books_{tn}_link WHERE {cn}={tn}.id) count,
+ (SELECT AVG(ratings.rating)
+ FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
+ WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
+ ratings.id = bl.rating AND ratings.rating <> 0) avg_rating,
+ {scn} AS sort
+ FROM {tn};
+ DROP VIEW IF EXISTS tag_browser_filtered_{tn};
+ CREATE VIEW tag_browser_filtered_{tn} AS SELECT
+ id,
+ {vcn},
+ (SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE
+ {cn}={tn}.id AND books_list_filter(book)) count,
+ (SELECT AVG(ratings.rating)
+ FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
+ WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
+ ratings.id = bl.rating AND ratings.rating <> 0 AND
+ books_list_filter(bl.book)) avg_rating,
+ {scn} AS sort
+ FROM {tn};
+
+ '''.format(tn=table_name, cn=column_name,
+ vcn=view_column_name, scn= sort_column_name))
+ self.conn.executescript(script)
+
+ def create_cust_tag_browser_view(table_name, link_table_name):
+ script = '''
+ DROP VIEW IF EXISTS tag_browser_{table};
+ CREATE VIEW tag_browser_{table} AS SELECT
+ id,
+ value,
+ (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
+ (SELECT AVG(r.rating)
+ FROM {lt},
+ books_ratings_link AS bl,
+ ratings AS r
+ WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
+ r.id = bl.rating AND r.rating <> 0) avg_rating,
+ value AS sort
+ FROM {table};
+
+ DROP VIEW IF EXISTS tag_browser_filtered_{table};
+ CREATE VIEW tag_browser_filtered_{table} AS SELECT
+ id,
+ value,
+ (SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
+ books_list_filter(book)) count,
+ (SELECT AVG(r.rating)
+ FROM {lt},
+ books_ratings_link AS bl,
+ ratings AS r
+ WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
+ r.id = bl.rating AND r.rating <> 0 AND
+ books_list_filter(bl.book)) avg_rating,
+ value AS sort
+ FROM {table};
+ '''.format(lt=link_table_name, table=table_name)
+ self.conn.executescript(script)
+
+ for field in self.field_metadata.itervalues():
+ if field['is_category'] and not field['is_custom'] and 'link_column' in field:
+ table = self.conn.get(
+ 'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
+ ('books_%s_link'%field['table'],), all=False)
+ if table is not None:
+ create_std_tag_browser_view(field['table'], field['link_column'],
+ field['column'], field['category_sort'])
+
+ db_tables = self.conn.get('''SELECT name FROM sqlite_master
+ WHERE type='table'
+ ORDER BY name''');
+ tables = []
+ for (table,) in db_tables:
+ tables.append(table)
+ for table in tables:
+ link_table = 'books_%s_link'%table
+ if table.startswith('custom_column_') and link_table in tables:
+ create_cust_tag_browser_view(table, link_table)
+
+ from calibre.ebooks.metadata import author_to_author_sort
+
+ aut = self.conn.get('SELECT id, name FROM authors');
+ records = []
+ for (id, author) in aut:
+ records.append((id, author.replace('|', ',')))
+ for id,author in records:
+ self.conn.execute('UPDATE authors SET sort=? WHERE id=?',
+ (author_to_author_sort(author.replace('|', ',')).strip(), id))
+ self.conn.commit()
+ self.conn.executescript('''
+ DROP TRIGGER IF EXISTS author_insert_trg;
+ CREATE TRIGGER author_insert_trg
+ AFTER INSERT ON authors
+ BEGIN
+ UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
+ END;
+ DROP TRIGGER IF EXISTS author_update_trg;
+ CREATE TRIGGER author_update_trg
+ BEFORE UPDATE ON authors
+ BEGIN
+ UPDATE authors SET sort=author_to_author_sort(NEW.name)
+ WHERE id=NEW.id AND name <> NEW.name;
+ END;
+ ''')
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index d396d73af2..7b8d609dda 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -99,17 +99,20 @@ def html_to_lxml(raw):
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
-def CATALOG_ENTRY(item, base_href, version, updated):
+def CATALOG_ENTRY(item, base_href, version, updated, ignore_count=False):
id_ = 'calibre:category:'+item.name
iid = 'N' + item.name
if item.id is not None:
iid = 'I' + str(item.id)
link = NAVLINK(href = base_href + '/' + hexlify(iid))
+ count = _('%d books')%item.count
+ if ignore_count:
+ count = ''
return E.entry(
TITLE(item.name),
ID(id_),
UPDATED(updated),
- E.content(_('%d books')%item.count, type='text'),
+ E.content(count, type='text'),
link
)
@@ -265,8 +268,12 @@ class CategoryFeed(NavFeed):
def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
base_href = self.base_href + '/category/' + hexlify(which)
+ ignore_count = False
+ if which == 'search':
+ ignore_count = True
for item in items:
- self.root.append(CATALOG_ENTRY(item, base_href, version, updated))
+ self.root.append(CATALOG_ENTRY(item, base_href, version, updated,
+ ignore_count=ignore_count))
class CategoryGroupFeed(NavFeed):
@@ -393,7 +400,7 @@ class OPDSServer(object):
owhich = hexlify('N'+which)
up_url = url_for('opdsnavcatalog', version, which=owhich)
items = categories[category]
- items = [x for x in items if x.name.startswith(which)]
+ items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)]
if not items:
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
which))
@@ -458,11 +465,11 @@ class OPDSServer(object):
def __init__(self, text, count):
self.text, self.count = text, count
- starts = set([x.name[0] for x in items])
+ starts = set([getattr(x, 'sort', x.name)[0] for x in items])
category_groups = OrderedDict()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if
- y.name.startswith(x)])
+ getattr(y, 'sort', y.name).startswith(x)])
items = [Group(x, y) for x, y in category_groups.items()]
max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items))
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index adf6691671..7e0458fba4 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -14,7 +14,7 @@ from Queue import Queue
from threading import RLock
from datetime import datetime
-from calibre.ebooks.metadata import title_sort
+from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
@@ -116,10 +116,12 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
- if tweaks['title_series_sorting'] == 'library_order':
- self.conn.create_function('title_sort', 1, title_sort)
- else:
+ if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, lambda x:x)
+ else:
+ self.conn.create_function('title_sort', 1, title_sort)
+ self.conn.create_function('author_to_author_sort', 1,
+ lambda x: author_to_author_sort(x.replace('|', ',')))
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)
diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py
index 026547ee2e..e60a3233c6 100644
--- a/src/calibre/utils/localization.py
+++ b/src/calibre/utils/localization.py
@@ -103,6 +103,7 @@ _extra_lang_codes = {
'en_TH' : _('English (Thailand)'),
'en_CY' : _('English (Cyprus)'),
'en_PK' : _('English (Pakistan)'),
+ 'en_IL' : _('English (Israel)'),
'en_SG' : _('English (Singapore)'),
'en_YE' : _('English (Yemen)'),
'en_IE' : _('English (Ireland)'),