KG revisions

This commit is contained in:
GRiker 2010-05-27 07:36:26 -06:00
commit 84888a43a7
22 changed files with 466 additions and 191 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@ -11,7 +11,7 @@
</head> </head>
<body> <body>
<div id="banner"> <div id="banner">
<a style="border: 0pt" href="http://calibre-ebook.com" alt="calibre" title="calibre"><img style="border:0pt" src="/static/calibre.png" alt="calibre" /></a> <a style="border: 0pt" href="http://calibre-ebook.com" alt="calibre" title="calibre"><img style="border:0pt" src="/static/calibre_banner.png" alt="calibre" /></a>
</div> </div>
<div id="search_box"> <div id="search_box">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gabriele Marini, based on Darko Miletic'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
description = 'On Line Motor News - 01-05-2010'
'''
http://www.infomotori.it/
'''
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.web.feeds.news import BasicNewsRecipe
class infomotori(BasicNewsRecipe):
author = 'Gabriele Marini'
title = u'Infomotori'
cover = 'http://www.infomotori.com/content/files/anniversario_01.gif'
oldest_article = 31
max_articles_per_feed = 100
recursion = 100
use_embedded_content = False
language = 'it'
use_embedded_content = False
remove_javascript = True
no_stylesheets = True
language = 'it'
timefmt = '[%a, %d %b, %Y]'
def print_version(self, url):
raw = self.browser.open(url).read()
soup = BeautifulSoup(raw.decode('utf8', 'replace'))
print_link = soup.find('a', {'class':'printarticle'})
'''if print_link is None:
keep_only_tags = [ dict(name='div', attrs={'class':['article main-column-article photogallery-column','category-header','article-body']})
]
remove_tags = [ dict(name='div', attrs={'class':['thumbnails-article','infoflash-footer','imushortarticle']}),
dict(name='div', attrs={'id':['linkinviastampa','linkspazioblu','altriarticoli','articoliconcorrenti','articolicorrelati','boxbrand']}),
dict(name='table', attrs={'class':'article-page'})
]
remove_tags_after = [ dict(name='div', attrs={'id':'articlebody'})
]
return url
'''
return print_link['href']
feeds = [(u'Ultime Novit\xe0', u'http://feeds.infomotori.com/ultimenovita'),
(u'Auto: Ultime Novit\xe0 ', u'http://feeds.infomotori.com/autonovita'),
(u'Moto: Ultime Novit\xe0 Moto', u'http://feeds.infomotori.com/motonovita'),
(u'Notizie Flash', u'http://feeds.infomotori.com/infoflashmotori'),
(u'Veicoli Ecologici e Mobilit\xe0 Sostenibile', u'http://feeds.infomotori.com/ecomotori'),
(u'4x4 Fuoristrada, Crossover e Suv', u'http://feeds.infomotori.com/fuoristrada'),
(u'Shopping Motori', u'http://feeds.infomotori.com/shoppingmotori')
]

View File

@ -88,35 +88,6 @@ CALIBRE_METADATA_FIELDS = frozenset([
] ]
) )
CALIBRE_RESERVED_LABELS = frozenset([
'all', # search term
'date', # search term
'formats', # search term
'inlibrary', # search term
'news', # search term
'ondevice', # search term
'search', # search term
'format', # The next four are here for backwards compatibility
'tag', # with searching. The terms can be used without the
'author', # trailing 's'.
'comment', # Sigh ...
]
)
RESERVED_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union(
USER_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS).union(
CALIBRE_RESERVED_LABELS)
assert len(RESERVED_METADATA_FIELDS) == sum(map(len, (
SOCIAL_METADATA_FIELDS, PUBLICATION_METADATA_FIELDS,
BOOK_STRUCTURE_FIELDS, USER_METADATA_FIELDS,
DEVICE_METADATA_FIELDS, CALIBRE_METADATA_FIELDS,
CALIBRE_RESERVED_LABELS
)))
SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union( SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
USER_METADATA_FIELDS).union( USER_METADATA_FIELDS).union(

View File

@ -68,13 +68,6 @@ class Metadata(object):
# Don't abuse this privilege # Don't abuse this privilege
self.__dict__[field] = val self.__dict__[field] = val
@property
def reserved_names(self):
'The set of names you cannot use for your own purposes on this object'
_data = object.__getattribute__(self, '_data')
return frozenset(RESERVED_FIELD_NAMES).union(frozenset(
_data['user_metadata'].iterkeys()))
@property @property
def user_metadata_names(self): def user_metadata_names(self):
'The set of user metadata names this object knows about' 'The set of user metadata names this object knows about'
@ -120,10 +113,8 @@ class Metadata(object):
# }}} # }}}
_m = Metadata() # We don't need reserved field names for this object any more. Lets just use a
RESERVED_FIELD_NAMES = \ # protocol like the last char of a user field label should be _ when using this
frozenset(_m.__dict__.iterkeys()).union( # _data # object
RESERVED_METADATA_FIELDS).union( # So mi.tags returns the builtin tags and mi.tags_ returns the user tags
frozenset(Metadata.__dict__.iterkeys())) # methods defined in Metadata
del _m

View File

@ -10,7 +10,6 @@ from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant
from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.ebooks.metadata.book.base import RESERVED_FIELD_NAMES
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
@ -103,14 +102,10 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('No lookup name was provided')) return self.simple_error('', _('No lookup name was provided'))
if not col_heading: if not col_heading:
return self.simple_error('', _('No column heading was provided')) return self.simple_error('', _('No column heading was provided'))
if col in RESERVED_FIELD_NAMES:
return self.simple_error('', _('The lookup name %s is reserved and cannot be used')%col)
bad_col = False bad_col = False
if col in self.parent.custcols: if col in self.parent.custcols:
if not self.editing_col or self.parent.custcols[col]['num'] != self.orig_column_number: if not self.editing_col or self.parent.custcols[col]['num'] != self.orig_column_number:
bad_col = True bad_col = True
if col in self.standard_colnames:
bad_col = True
if bad_col: if bad_col:
return self.simple_error('', _('The lookup name %s is already used')%col) return self.simple_error('', _('The lookup name %s is already used')%col)
bad_head = False bad_head = False

View File

@ -49,7 +49,7 @@ class TagCategories(QDialog, Ui_TagCategories):
cc_map = self.db.custom_column_label_map cc_map = self.db.custom_column_label_map
for cc in cc_map: for cc in cc_map:
if cc_map[cc]['datatype'] == 'text': if cc_map[cc]['datatype'] == 'text':
self.category_labels.append(cc) self.category_labels.append(db.tag_browser_categories.get_search_label(cc))
category_icons.append(cc_icon) category_icons.append(cc_icon)
category_values.append(lambda col=cc: self.db.all_custom(label=col)) category_values.append(lambda col=cc: self.db.all_custom(label=col))
category_names.append(cc_map[cc]['name']) category_names.append(cc_map[cc]['name'])

View File

@ -632,6 +632,8 @@ class BooksModel(QAbstractTableModel): # {{{
return None return None
if role == Qt.ToolTipRole: if role == Qt.ToolTipRole:
ht = self.column_map[section] ht = self.column_map[section]
if self.is_custom_column(self.column_map[section]):
ht = self.db.tag_browser_categories.custom_field_prefix + ht
if ht == 'timestamp': # change help text because users know this field as 'date' if ht == 'timestamp': # change help text because users know this field as 'date'
ht = 'date' ht = 'date'
return QVariant(_('The lookup/search name is "{0}"').format(ht)) return QVariant(_('The lookup/search name is "{0}"').format(ht))
@ -777,8 +779,10 @@ class OnDeviceSearch(SearchQueryParser): # {{{
'title' : lambda x : getattr(x, 'title').lower(), 'title' : lambda x : getattr(x, 'title').lower(),
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(), 'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(), 'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
'format':lambda x: os.path.splitext(x.path)[1].lower() 'format':lambda x: os.path.splitext(x.path)[1].lower(),
} }
for x in ('author', 'format'):
q[x+'s'] = q[x]
for index, row in enumerate(self.model.db): for index, row in enumerate(self.model.db):
for locvalue in locations: for locvalue in locations:
accessor = q[locvalue] accessor = q[locvalue]

View File

@ -14,7 +14,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QAbstractItemModel, QVariant, QModelIndex QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS from calibre.library.field_metadata import TagsIcons
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
@ -205,7 +205,7 @@ class TagsModel(QAbstractItemModel): # {{{
# must do this here because 'QPixmap: Must construct a QApplication # must do this here because 'QPixmap: Must construct a QApplication
# before a QPaintDevice'. The ':' in front avoids polluting either the # before a QPaintDevice'. The ':' in front avoids polluting either the
# user-defined categories (':' at end) or columns namespaces (no ':'). # user-defined categories (':' at end) or columns namespaces (no ':').
self.category_icon_map = { self.category_icon_map = TagsIcons({
'authors' : QIcon(I('user_profile.svg')), 'authors' : QIcon(I('user_profile.svg')),
'series' : QIcon(I('series.svg')), 'series' : QIcon(I('series.svg')),
'formats' : QIcon(I('book.svg')), 'formats' : QIcon(I('book.svg')),
@ -215,10 +215,8 @@ class TagsModel(QAbstractItemModel): # {{{
'tags' : QIcon(I('tags.svg')), 'tags' : QIcon(I('tags.svg')),
':custom' : QIcon(I('column.svg')), ':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.svg')), ':user' : QIcon(I('drawer.svg')),
'search' : QIcon(I('search.svg'))} 'search' : QIcon(I('search.svg'))})
for k in self.category_icon_map.keys():
if not k.startswith(':') and k not in RESERVED_METADATA_FIELDS:
raise ValueError('Tag category [%s] is not a reserved word.' %(k))
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db self.db = db
self.search_restriction = '' self.search_restriction = ''
@ -226,10 +224,14 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_by_popularity']) data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem() self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
if self.db.get_tag_browser_categories()[r]['kind'] != 'user':
tt = _('The lookup/search name is "{0}"').format(r)
else:
tt = ''
c = TagTreeItem(parent=self.root_item, c = TagTreeItem(parent=self.root_item,
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
tooltip=_('The lookup/search name is "{0}"').format(r)) tooltip=tt)
for tag in data[r]: for tag in data[r]:
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
@ -247,7 +249,7 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
tb_categories = self.db.get_tag_browser_categories() tb_categories = self.db.get_tag_browser_categories()
for category in tb_categories.iterkeys(): for category in tb_categories:
if category in data: # They should always be there, but ... if category in data: # They should always be there, but ...
self.row_map.append(category) self.row_map.append(category)
self.categories.append(tb_categories[category]['name']) self.categories.append(tb_categories[category]['name'])

View File

@ -81,7 +81,7 @@ class KindleDX(Kindle):
class Sony505(Device): class Sony505(Device):
output_profile = 'sony' output_profile = 'sony'
name = 'SONY Reader 6" and Touch Editions' name = 'All other SONY devices'
output_format = 'EPUB' output_format = 'EPUB'
manufacturer = 'SONY' manufacturer = 'SONY'
id = 'prs505' id = 'prs505'

View File

@ -17,6 +17,7 @@ from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.date import parse_date, now, UNDEFINED_DATE
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.pyparsing import ParseException from calibre.utils.pyparsing import ParseException
# from calibre.library.field_metadata import FieldMetadata
class CoverCache(QThread): class CoverCache(QThread):
@ -149,15 +150,15 @@ class ResultCache(SearchQueryParser):
''' '''
Stores sorted and filtered metadata in memory. Stores sorted and filtered metadata in memory.
''' '''
def __init__(self, FIELD_MAP, cc_label_map): def __init__(self, FIELD_MAP, cc_label_map, tag_browser_categories):
self.FIELD_MAP = FIELD_MAP self.FIELD_MAP = FIELD_MAP
self.custom_column_label_map = cc_label_map self.custom_column_label_map = cc_label_map
self._map = self._map_filtered = self._data = [] self._map = self._map_filtered = self._data = []
self.first_sort = True self.first_sort = True
self.search_restriction = '' self.search_restriction = ''
SearchQueryParser.__init__(self, self.tag_browser_categories = tag_browser_categories
locations=SearchQueryParser.DEFAULT_LOCATIONS + self.all_search_locations = tag_browser_categories.get_search_labels()
[c for c in cc_label_map]) SearchQueryParser.__init__(self, self.all_search_locations)
self.build_date_relop_dict() self.build_date_relop_dict()
self.build_numeric_relop_dict() self.build_numeric_relop_dict()
@ -379,25 +380,33 @@ class ResultCache(SearchQueryParser):
if location in ('tag', 'author', 'format', 'comment'): if location in ('tag', 'author', 'format', 'comment'):
location += 's' location += 's'
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series',
'formats', 'isbn', 'rating', 'cover', 'ondevice')
MAP = {} MAP = {}
# Fields not used when matching against text contents. These are
# the non-text fields
EXCLUDE_FIELDS = []
for x in all: # get the db columns for the standard searchables # get the db columns for the standard searchables
MAP[x] = self.FIELD_MAP[x] for x in self.tag_browser_categories:
if len(self.tag_browser_categories[x]['search_labels']) and \
not self.tag_browser_categories.is_custom_field(x):
MAP[x] = self.tag_browser_categories[x]['rec_index']
if self.tag_browser_categories[x]['datatype'] != 'text':
EXCLUDE_FIELDS.append(MAP[x])
# add custom columns to MAP. Put the column's type into IS_CUSTOM
IS_CUSTOM = [] IS_CUSTOM = []
for x in range(len(self.FIELD_MAP)): # build a list containing '' the size of FIELD_MAP for x in range(len(self.FIELD_MAP)):
IS_CUSTOM.append('') IS_CUSTOM.append('')
IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating' # normal and custom ratings columns use the same code # normal and custom ratings columns use the same code
for x in self.custom_column_label_map: # add custom columns to MAP. Put the column's type into IS_CUSTOM IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating'
if self.custom_column_label_map[x]['datatype'] != "datetime": for x in self.tag_browser_categories.get_custom_fields():
MAP[x] = self.FIELD_MAP[self.custom_column_label_map[x]['num']] if self.tag_browser_categories[x]['datatype'] != "datetime":
IS_CUSTOM[MAP[x]] = self.custom_column_label_map[x]['datatype'] MAP[x] = self.FIELD_MAP[self.tag_browser_categories[x]['colnum']]
IS_CUSTOM[MAP[x]] = self.tag_browser_categories[x]['datatype']
EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']] SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']]
for x in self.custom_column_label_map: for x in self.tag_browser_categories.get_custom_fields():
if self.custom_column_label_map[x]['is_multiple']: if self.tag_browser_categories[x]['is_multiple']:
SPLITABLE_FIELDS.append(MAP[x]) SPLITABLE_FIELDS.append(MAP[x])
try: try:

View File

@ -144,12 +144,14 @@ class CustomColumns(object):
for k in sorted(self.custom_column_label_map.keys()): for k in sorted(self.custom_column_label_map.keys()):
v = self.custom_column_label_map[k] v = self.custom_column_label_map[k]
if v['normalized']: if v['normalized']:
searchable = True
else:
searchable = False
tn = 'custom_column_{0}'.format(v['num']) tn = 'custom_column_{0}'.format(v['num'])
self.tag_browser_categories[v['label']] = { self.tag_browser_categories.add_custom_field(label=v['label'],
'table':tn, 'column':'value', table=tn, column='value', datatype=v['datatype'],
'type':v['datatype'], 'is_multiple':v['is_multiple'], is_multiple=v['is_multiple'], colnum=v['num'],
'kind':'custom', 'name':v['name'] name=v['name'], searchable=searchable)
}
def get_custom(self, idx, label=None, num=None, index_is_id=False): def get_custom(self, idx, label=None, num=None, index_is_id=False):
if label is not None: if label is not None:

View File

@ -20,6 +20,7 @@ from PyQt4.QtGui import QImage
from calibre.ebooks.metadata import title_sort from calibre.ebooks.metadata import title_sort
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade from calibre.library.schema_upgrades import SchemaUpgrade
from calibre.library.caches import ResultCache from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns from calibre.library.custom_columns import CustomColumns
@ -33,11 +34,10 @@ from calibre.customize.ui import run_plugins_on_import
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp from calibre.utils.date import utcnow, now as nowf, utcfromtimestamp
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
if iswindows: if iswindows:
import calibre.utils.winshell as winshell import calibre.utils.winshell as winshell
@ -116,6 +116,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter') self.books_list_filter = self.conn.create_dynamic_filter('books_list_filter')
def __init__(self, library_path, row_factory=False): def __init__(self, library_path, row_factory=False):
self.tag_browser_categories = FieldMetadata() #.get_tag_browser_categories()
if not os.path.exists(library_path): if not os.path.exists(library_path):
os.makedirs(library_path) os.makedirs(library_path)
self.listeners = set([]) self.listeners = set([])
@ -127,36 +128,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if isinstance(self.dbpath, unicode): if isinstance(self.dbpath, unicode):
self.dbpath = self.dbpath.encode(filesystem_encoding) self.dbpath = self.dbpath.encode(filesystem_encoding)
# Order as has been customary in the tags pane.
tag_browser_categories_items = [
('authors', {'table':'authors', 'column':'name',
'type':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Authors')}),
('series', {'table':'series', 'column':'name',
'type':None, 'is_multiple':False,
'kind':'standard', 'name':_('Series')}),
('formats', {'table':None, 'column':None,
'type':None, 'is_multiple':False,
'kind':'standard', 'name':_('Formats')}),
('publisher', {'table':'publishers', 'column':'name',
'type':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Publishers')}),
('rating', {'table':'ratings', 'column':'rating',
'type':'rating', 'is_multiple':False,
'kind':'standard', 'name':_('Ratings')}),
('news', {'table':'news', 'column':'name',
'type':None, 'is_multiple':False,
'kind':'standard', 'name':_('News')}),
('tags', {'table':'tags', 'column':'name',
'type':'text', 'is_multiple':True,
'kind':'standard', 'name':_('Tags')}),
]
self.tag_browser_categories = OrderedDict()
for k,v in tag_browser_categories_items:
if k not in RESERVED_METADATA_FIELDS:
raise ValueError('Tag category [%s] is not a reserved word.' %(k))
self.tag_browser_categories[k] = v
self.connect() self.connect()
self.is_case_sensitive = not iswindows and not isosx and \ self.is_case_sensitive = not iswindows and not isosx and \
not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB'))
@ -224,6 +195,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
lines.append(line) lines.append(line)
custom_map = self.custom_columns_in_meta() custom_map = self.custom_columns_in_meta()
# custom col labels are numbers (the id in the custom_columns table)
custom_cols = list(sorted(custom_map.keys())) custom_cols = list(sorted(custom_map.keys()))
lines.extend([custom_map[x] for x in custom_cols]) lines.extend([custom_map[x] for x in custom_cols])
@ -233,12 +205,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15, 'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15,
'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19} 'lccn':16, 'pubdate':17, 'flags':18, 'uuid':19}
for k,v in self.FIELD_MAP.iteritems():
self.tag_browser_categories.set_field_record_index(k, v, prefer_custom=False)
base = max(self.FIELD_MAP.values()) base = max(self.FIELD_MAP.values())
for col in custom_cols: for col in custom_cols:
self.FIELD_MAP[col] = base = base+1 self.FIELD_MAP[col] = base = base+1
self.tag_browser_categories.set_field_record_index(
self.custom_column_num_map[col]['label'],
base,
prefer_custom=True)
self.FIELD_MAP['cover'] = base+1 self.FIELD_MAP['cover'] = base+1
self.tag_browser_categories.set_field_record_index('cover', base+1, prefer_custom=False)
self.FIELD_MAP['ondevice'] = base+2 self.FIELD_MAP['ondevice'] = base+2
self.tag_browser_categories.set_field_record_index('ondevice', base+2, prefer_custom=False)
script = ''' script = '''
DROP VIEW IF EXISTS meta2; DROP VIEW IF EXISTS meta2;
@ -251,7 +232,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.commit() self.conn.commit()
self.book_on_device_func = None self.book_on_device_func = None
self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map) self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map,
self.tag_browser_categories)
self.search = self.data.search self.search = self.data.search
self.refresh = functools.partial(self.data.refresh, self) self.refresh = functools.partial(self.data.refresh, self)
self.sort = self.data.sort self.sort = self.data.sort
@ -671,14 +653,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.books_list_filter.change([] if not ids else ids) self.books_list_filter.change([] if not ids else ids)
categories = {} categories = {}
if icon_map is not None and type(icon_map) != TagsIcons:
raise TypeError('icon_map passed to get_categories must be of type TagIcons')
#### First, build the standard and custom-column categories #### #### First, build the standard and custom-column categories ####
for category in self.tag_browser_categories.keys(): tb_cats = self.tag_browser_categories
tn = self.tag_browser_categories[category]['table'] for category in tb_cats.keys():
cat = tb_cats[category]
if cat['kind'] == 'not_cat':
continue
tn = cat['table']
categories[category] = [] #reserve the position in the ordered list categories[category] = [] #reserve the position in the ordered list
if tn is None: # Nothing to do for the moment if tn is None: # Nothing to do for the moment
continue continue
cn = self.tag_browser_categories[category]['column'] cn = cat['column']
if ids is None: if ids is None:
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn) query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
else: else:
@ -692,16 +680,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# icon_map is not None if get_categories is to store an icon and # icon_map is not None if get_categories is to store an icon and
# possibly a tooltip in the tag structure. # possibly a tooltip in the tag structure.
icon, tooltip = None, '' icon, tooltip = None, ''
label = tb_cats.get_field_label(category)
if icon_map: if icon_map:
if self.tag_browser_categories[category]['kind'] == 'standard': if not tb_cats.is_custom_field(category):
if category in icon_map: if category in icon_map:
icon = icon_map[category] icon = icon_map[label]
elif self.tag_browser_categories[category]['kind'] == 'custom': else:
icon = icon_map[':custom'] icon = icon_map[':custom']
icon_map[category] = icon icon_map[category] = icon
tooltip = self.custom_column_label_map[category]['name'] tooltip = self.custom_column_label_map[label]['name']
datatype = self.tag_browser_categories[category]['type'] datatype = cat['datatype']
if datatype == 'rating': if datatype == 'rating':
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0) item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.))) formatter = (lambda x:u'\u2605'*int(round(x/2.)))
@ -711,7 +700,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formatter = (lambda x: x.replace('|', ',')) formatter = (lambda x: x.replace('|', ','))
else: else:
item_not_zero_func = (lambda x: x[2] > 0) item_not_zero_func = (lambda x: x[2] > 0)
formatter = (lambda x:x) formatter = (lambda x:unicode(x))
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip) icon=icon, tooltip = tooltip)
@ -750,9 +739,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# remove all user categories from tag_browser_categories. They can # remove all user categories from tag_browser_categories. They can
# easily come and go. We will add all the existing ones in below. # easily come and go. We will add all the existing ones in below.
for k in self.tag_browser_categories.keys(): for k in tb_cats.keys():
if self.tag_browser_categories[k]['kind'] in ['user', 'search']: if tb_cats[k]['kind'] in ['user', 'search']:
del self.tag_browser_categories[k] del tb_cats[k]
# We want to use same node in the user category as in the source # We want to use same node in the user category as in the source
# category. To do that, we need to find the original Tag node. There is # category. To do that, we need to find the original Tag node. There is
@ -771,10 +760,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# else: do nothing, to not include nodes w zero counts # else: do nothing, to not include nodes w zero counts
if len(items): if len(items):
cat_name = user_cat+':' # add the ':' to avoid name collision cat_name = user_cat+':' # add the ':' to avoid name collision
self.tag_browser_categories[cat_name] = { tb_cats.add_user_category(label=cat_name, name=user_cat)
'table':None, 'column':None,
'type':None, 'is_multiple':False,
'kind':'user', 'name':user_cat}
# Not a problem if we accumulate entries in the icon map # Not a problem if we accumulate entries in the icon map
if icon_map is not None: if icon_map is not None:
icon_map[cat_name] = icon_map[':user'] icon_map[cat_name] = icon_map[':user']
@ -793,10 +779,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for srch in saved_searches.names(): for srch in saved_searches.names():
items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon)) items.append(Tag(srch, tooltip=saved_searches.lookup(srch), icon=icon))
if len(items): if len(items):
self.tag_browser_categories['search'] = { tb_cats.add_search_category(label='search', name=_('Searches'))
'table':None, 'column':None,
'type':None, 'is_multiple':False,
'kind':'search', 'name':_('Searches')}
if icon_map is not None: if icon_map is not None:
icon_map['search'] = icon_map['search'] icon_map['search'] = icon_map['search']
categories['search'] = items categories['search'] = items

View File

@ -0,0 +1,259 @@
'''
Created on 25 May 2010
@author: charles
'''
from UserDict import DictMixin
from calibre.utils.ordered_dict import OrderedDict
class TagsIcons(dict):
'''
If the client wants icons to be in the tag structure, this class must be
instantiated and filled in with real icons. If this class is instantiated
and passed to get_categories, All items must be given a value not None
'''
category_icons = ['authors', 'series', 'formats', 'publisher', 'rating',
'news', 'tags', ':custom', ':user', 'search',]
def __init__(self, icon_dict):
for a in self.category_icons:
if a not in icon_dict:
raise ValueError('Missing category icon [%s]'%a)
self[a] = icon_dict[a]
class FieldMetadata(dict, DictMixin):
# kind == standard: is tag category. May be a search label. Is db col
# or is specially handled (e.g., news)
# kind == not_cat: Is not a tag category. May be a search label. Is db col
# kind == user: user-defined tag category
# kind == search: saved-searches category
# For 'standard', the order below is the order that the categories will
# appear in the tags pane.
#
# label is the column label. key is either the label or in the case of
# custom fields, the label prefixed with 'x'. Because of the prefixing,
# there cannot be a name clash between standard and custom fields, so key
# can be used as the metadata dictionary key.
category_items_ = [
('authors', {'table':'authors', 'column':'name',
'datatype':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Authors'),
'search_labels':['authors', 'author'],
'is_custom':False}),
('series', {'table':'series', 'column':'name',
'datatype':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Series'),
'search_labels':['series'],
'is_custom':False}),
('formats', {'table':None, 'column':None,
'datatype':'text', 'is_multiple':False, # must think what type this is!
'kind':'standard', 'name':_('Formats'),
'search_labels':['formats', 'format'],
'is_custom':False}),
('publisher', {'table':'publishers', 'column':'name',
'datatype':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Publishers'),
'search_labels':['publisher'],
'is_custom':False}),
('rating', {'table':'ratings', 'column':'rating',
'datatype':'rating', 'is_multiple':False,
'kind':'standard', 'name':_('Ratings'),
'search_labels':['rating'],
'is_custom':False}),
('news', {'table':'news', 'column':'name',
'datatype':None, 'is_multiple':False,
'kind':'standard', 'name':_('News'),
'search_labels':[],
'is_custom':False}),
('tags', {'table':'tags', 'column':'name',
'datatype':'text', 'is_multiple':True,
'kind':'standard', 'name':_('Tags'),
'search_labels':['tags', 'tag'],
'is_custom':False}),
('author_sort',{'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('comments', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['comments', 'comment'], 'is_custom':False}),
('cover', {'table':None, 'column':None, 'datatype':None,
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['cover'], 'is_custom':False}),
('flags', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('id', {'table':None, 'column':None, 'datatype':'int',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('isbn', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['isbn'], 'is_custom':False}),
('lccn', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('ondevice', {'table':None, 'column':None, 'datatype':'bool',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('path', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('pubdate', {'table':None, 'column':None, 'datatype':'datetime',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['pubdate'], 'is_custom':False}),
('series_index',{'table':None, 'column':None, 'datatype':'float',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('sort', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('size', {'table':None, 'column':None, 'datatype':'float',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
('timestamp', {'table':None, 'column':None, 'datatype':'datetime',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['date'], 'is_custom':False}),
('title', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':['title'], 'is_custom':False}),
('uuid', {'table':None, 'column':None, 'datatype':'text',
'is_multiple':False, 'kind':'not_cat', 'name':None,
'search_labels':[], 'is_custom':False}),
]
# search labels that are not db columns
search_items = [ 'all',
# 'date',
'search',
]
def __init__(self):
self._tb_cats = OrderedDict()
for k,v in self.category_items_:
self._tb_cats[k] = v
self.custom_field_prefix = '#'
self.get = self._tb_cats.get
def __getitem__(self, key):
return self._tb_cats[key]
def __setitem__(self, key, val):
raise AttributeError('Assigning to this object is forbidden')
def __delitem__(self, key):
del self._tb_cats[key]
def __iter__(self):
for key in self._tb_cats:
yield key
def keys(self):
return self._tb_cats.keys()
def iterkeys(self):
for key in self._tb_cats:
yield key
def iteritems(self):
for key in self._tb_cats:
yield (key, self._tb_cats[key])
def is_custom_field(self, key):
return key.startswith(self.custom_field_prefix)
def get_field_label(self, key):
if 'label' not in self._tb_cats[key]:
return key
return self._tb_cats[key]['label']
def get_search_label(self, label):
if 'label' in self._tb_cats:
return label
if self.is_custom_field(label):
return self.custom_field_prefix+label
raise ValueError('Unknown key [%s]'%(label))
def get_custom_fields(self):
return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']]
def add_custom_field(self, label, table, column, datatype,
is_multiple, colnum, name, searchable):
fn = self.custom_field_prefix + label
if fn in self._tb_cats:
raise ValueError('Duplicate custom field [%s]'%(label))
if searchable:
sl = [fn]
kind = 'standard'
else:
sl = []
kind = 'not_cat'
self._tb_cats[fn] = {'table':table, 'column':column,
'datatype':datatype, 'is_multiple':is_multiple,
'kind':kind, 'name':name,
'search_labels':sl, 'label':label,
'colnum':colnum, 'is_custom':True}
def set_field_record_index(self, label, index, prefer_custom=False):
if prefer_custom:
key = self.custom_field_prefix+label
if key not in self._tb_cats:
key = label
else:
if label in self._tb_cats:
key = label
else:
key = self.custom_field_prefix+label
self._tb_cats[key]['rec_index'] = index # let the exception fly ...
def add_user_category(self, label, name):
if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label))
self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':False,
'kind':'user', 'name':name,
'search_labels':[], 'is_custom':False}
def add_search_category(self, label, name):
if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label))
self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':False,
'kind':'search', 'name':name,
'search_labels':[], 'is_custom':False}
# DEFAULT_LOCATIONS = frozenset([
# 'all',
# 'author', # compatibility
# 'authors',
# 'comment', # compatibility
# 'comments',
# 'cover',
# 'date',
# 'format', # compatibility
# 'formats',
# 'isbn',
# 'ondevice',
# 'pubdate',
# 'publisher',
# 'search',
# 'series',
# 'rating',
# 'tag', # compatibility
# 'tags',
# 'title',
# ])
def get_search_labels(self):
s_labels = []
for v in self._tb_cats.itervalues():
map((lambda x:s_labels.append(x)), v['search_labels'])
for v in self.search_items:
s_labels.append(v)
# if set(s_labels) != self.DEFAULT_LOCATIONS:
# print 'search labels and default_locations do not match:'
# print set(s_labels) ^ self.DEFAULT_LOCATIONS
return s_labels

View File

@ -40,3 +40,6 @@ def server_config(defaults=None):
'This affects Stanza, WordPlayer, etc. integration.')) 'This affects Stanza, WordPlayer, etc. integration.'))
return c return c
def main():
from calibre.library.server.main import main
return main()

View File

@ -12,6 +12,7 @@ from itertools import repeat
from lxml import etree, html from lxml import etree, html
from lxml.builder import ElementMaker from lxml.builder import ElementMaker
import cherrypy import cherrypy
import routes
from calibre.constants import __appname__ from calibre.constants import __appname__
from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata import fmt_sidx
@ -25,6 +26,11 @@ BASE_HREFS = {
STANZA_FORMATS = frozenset(['epub', 'pdb']) STANZA_FORMATS = frozenset(['epub', 'pdb'])
def url_for(name, version, **kwargs):
if not name.endswith('_'):
name += '_'
return routes.url_for(name+str(version), **kwargs)
# Vocabulary for building OPDS feeds {{{ # Vocabulary for building OPDS feeds {{{
E = ElementMaker(namespace='http://www.w3.org/2005/Atom', E = ElementMaker(namespace='http://www.w3.org/2005/Atom',
nsmap={ nsmap={
@ -42,7 +48,7 @@ def UPDATED(dt, *args, **kwargs):
return E.updated(dt.strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs) return E.updated(dt.strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs)
LINK = partial(E.link, type='application/atom+xml') LINK = partial(E.link, type='application/atom+xml')
NAVLINK = partial(E.link, rel='subsection', NAVLINK = partial(E.link,
type='application/atom+xml;type=feed;profile=opds-catalog') type='application/atom+xml;type=feed;profile=opds-catalog')
def SEARCH_LINK(base_href, *args, **kwargs): def SEARCH_LINK(base_href, *args, **kwargs):
@ -59,7 +65,7 @@ def AUTHOR(name, uri=None):
SUBTITLE = E.subtitle SUBTITLE = E.subtitle
def NAVCATALOG_ENTRY(base_href, updated, title, description, query): def NAVCATALOG_ENTRY(base_href, updated, title, description, query, version=0):
href = base_href+'/navcatalog/'+binascii.hexlify(query) href = base_href+'/navcatalog/'+binascii.hexlify(query)
id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest()) id_ = 'calibre-navcatalog:'+str(hashlib.sha1(href).hexdigest())
return E.entry( return E.entry(
@ -74,7 +80,7 @@ START_LINK = partial(NAVLINK, rel='start')
UP_LINK = partial(NAVLINK, rel='up') UP_LINK = partial(NAVLINK, rel='up')
FIRST_LINK = partial(NAVLINK, rel='first') FIRST_LINK = partial(NAVLINK, rel='first')
LAST_LINK = partial(NAVLINK, rel='last') LAST_LINK = partial(NAVLINK, rel='last')
NEXT_LINK = partial(NAVLINK, rel='next') NEXT_LINK = partial(NAVLINK, rel='next', title='Next')
PREVIOUS_LINK = partial(NAVLINK, rel='previous') PREVIOUS_LINK = partial(NAVLINK, rel='previous')
def html_to_lxml(raw): def html_to_lxml(raw):
@ -117,7 +123,7 @@ def ACQUISITION_ENTRY(item, version, FM, updated):
id_ = 'urn:%s:%s'%(idm, item[FM['uuid']]) id_ = 'urn:%s:%s'%(idm, item[FM['uuid']])
ans = E.entry(TITLE(title), E.author(E.name(authors)), ID(id_), ans = E.entry(TITLE(title), E.author(E.name(authors)), ID(id_),
UPDATED(updated)) UPDATED(updated))
if extra: if len(extra):
ans.append(E.content(extra, type='xhtml')) ans.append(E.content(extra, type='xhtml'))
formats = item[FM['formats']] formats = item[FM['formats']]
if formats: if formats:
@ -148,7 +154,7 @@ class Feed(object): # {{{
title=__appname__ + ' ' + _('Library'), title=__appname__ + ' ' + _('Library'),
up_link=None, first_link=None, last_link=None, up_link=None, first_link=None, last_link=None,
next_link=None, previous_link=None): next_link=None, previous_link=None):
self.base_href = BASE_HREFS[version] self.base_href = url_for('opds', version)
self.root = \ self.root = \
FEED( FEED(
@ -157,18 +163,18 @@ class Feed(object): # {{{
ID(id_), ID(id_),
UPDATED(updated), UPDATED(updated),
SEARCH_LINK(self.base_href), SEARCH_LINK(self.base_href),
START_LINK(self.base_href) START_LINK(href=self.base_href)
) )
if up_link: if up_link:
self.root.append(UP_LINK(up_link)) self.root.append(UP_LINK(href=up_link))
if first_link: if first_link:
self.root.append(FIRST_LINK(first_link)) self.root.append(FIRST_LINK(href=first_link))
if last_link: if last_link:
self.root.append(LAST_LINK(last_link)) self.root.append(LAST_LINK(href=last_link))
if next_link: if next_link:
self.root.append(NEXT_LINK(next_link)) self.root.append(NEXT_LINK(href=next_link))
if previous_link: if previous_link:
self.root.append(PREVIOUS_LINK(previous_link)) self.root.append(PREVIOUS_LINK(href=previous_link))
if subtitle: if subtitle:
self.root.insert(1, SUBTITLE(subtitle)) self.root.insert(1, SUBTITLE(subtitle))
@ -188,7 +194,8 @@ class TopLevel(Feed): # {{{
): ):
Feed.__init__(self, id_, updated, version, subtitle=subtitle) Feed.__init__(self, id_, updated, version, subtitle=subtitle)
subc = partial(NAVCATALOG_ENTRY, self.base_href, updated) subc = partial(NAVCATALOG_ENTRY, self.base_href, updated,
version=version)
subcatalogs = [subc(_('By ')+title, subcatalogs = [subc(_('By ')+title,
_('Books sorted by ') + desc, q) for title, desc, q in _('Books sorted by ') + desc, q) for title, desc, q in
categories] categories]
@ -206,7 +213,7 @@ class NavFeed(Feed):
kwargs['previous_link'] = \ kwargs['previous_link'] = \
page_url+'?offset=%d'%offsets.previous_offset page_url+'?offset=%d'%offsets.previous_offset
if offsets.next_offset > -1: if offsets.next_offset > -1:
kwargs['next_offset'] = \ kwargs['next_link'] = \
page_url+'?offset=%d'%offsets.next_offset page_url+'?offset=%d'%offsets.next_offset
Feed.__init__(self, id_, updated, version, **kwargs) Feed.__init__(self, id_, updated, version, **kwargs)
@ -226,16 +233,16 @@ class OPDSOffsets(object):
offset = 0 offset = 0
if offset >= total: if offset >= total:
raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset) raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
last_allowed_index = total - 1
last_current_index = offset + delta - 1
self.offset = offset self.offset = offset
self.next_offset = offset + delta self.next_offset = last_current_index + 1
if self.next_offset >= total: if self.next_offset > last_allowed_index:
self.next_offset = -1
if self.next_offset >= total:
self.next_offset = -1 self.next_offset = -1
self.previous_offset = self.offset - delta self.previous_offset = self.offset - delta
if self.previous_offset < 0: if self.previous_offset < 0:
self.previous_offset = 0 self.previous_offset = 0
self.last_offset = total - delta self.last_offset = last_allowed_index - delta
if self.last_offset < 0: if self.last_offset < 0:
self.last_offset = 0 self.last_offset = 0
@ -243,13 +250,13 @@ class OPDSOffsets(object):
class OPDSServer(object): class OPDSServer(object):
def add_routes(self, connect): def add_routes(self, connect):
for base in ('stanza', 'opds'): for version in (0, 1):
version = 0 if base == 'stanza' else 1
base_href = BASE_HREFS[version] base_href = BASE_HREFS[version]
connect(base, base_href, self.opds, version=version) ver = str(version)
connect('opdsnavcatalog_'+base, base_href+'/navcatalog/{which}', connect('opds_'+ver, base_href, self.opds, version=version)
connect('opdsnavcatalog_'+ver, base_href+'/navcatalog/{which}',
self.opds_navcatalog, version=version) self.opds_navcatalog, version=version)
connect('opdssearch_'+base, base_href+'/search/{query}', connect('opdssearch_'+ver, base_href+'/search/{query}',
self.opds_search, version=version) self.opds_search, version=version)
def get_opds_allowed_ids_for_version(self, version): def get_opds_allowed_ids_for_version(self, version):
@ -266,7 +273,7 @@ class OPDSServer(object):
self.sort(items, sort_by, ascending) self.sort(items, sort_by, ascending)
max_items = self.opts.max_opds_items max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items)) offsets = OPDSOffsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.next_offset] items = items[offsets.offset:offsets.offset+max_items]
return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets, return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets,
page_url, up_url, version, self.db.FIELD_MAP)) page_url, up_url, version, self.db.FIELD_MAP))
@ -282,19 +289,38 @@ class OPDSServer(object):
ids = self.search_cache(query) ids = self.search_cache(query)
except: except:
raise cherrypy.HTTPError(404, 'Search: %r not understood'%query) raise cherrypy.HTTPError(404, 'Search: %r not understood'%query)
return self.get_opds_acquisition_feed(ids, offset, '/search/'+query, page_url = url_for('opdssearch', version, query=query)
BASE_HREFS[version], 'calibre-search:'+query, return self.get_opds_acquisition_feed(ids, offset, page_url,
url_for('opds', version), 'calibre-search:'+query,
version=version) version=version)
def opds_navcatalog(self, which=None, version=0): def get_opds_all_books(self, which, page_url, up_url, version=0, offset=0):
try:
offset = int(offset)
version = int(version)
except:
raise cherrypy.HTTPError(404, 'Not found')
if which not in ('title', 'newest') or version not in BASE_HREFS:
raise cherrypy.HTTPError(404, 'Not found')
sort = 'timestamp' if which == 'newest' else 'title'
ascending = which == 'title'
ids = self.get_opds_allowed_ids_for_version(version)
return self.get_opds_acquisition_feed(ids, offset, page_url, up_url,
id_='calibre-all:'+sort, sort_by=sort, ascending=ascending,
version=version)
def opds_navcatalog(self, which=None, version=0, offset=0):
version = int(version) version = int(version)
if not which or version not in BASE_HREFS: if not which or version not in BASE_HREFS:
raise cherrypy.HTTPError(404, 'Not found') raise cherrypy.HTTPError(404, 'Not found')
page_url = url_for('opdsnavcatalog', version, which=which)
up_url = url_for('opds', version)
which = binascii.unhexlify(which) which = binascii.unhexlify(which)
type_ = which[0] type_ = which[0]
which = which[1:] which = which[1:]
if type_ == 'O': if type_ == 'O':
return self.get_opds_all_books(which) return self.get_opds_all_books(which, page_url, up_url,
version=version, offset=offset)
elif type_ == 'N': elif type_ == 'N':
return self.get_opds_navcatalog(which) return self.get_opds_navcatalog(which)
raise cherrypy.HTTPError(404, 'Not found') raise cherrypy.HTTPError(404, 'Not found')

View File

@ -22,7 +22,6 @@ from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppres
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
''' '''
This class manages access to the preference holding the saved search queries. This class manages access to the preference holding the saved search queries.
@ -86,27 +85,6 @@ class SearchQueryParser(object):
* `(author:Asimov or author:Hardy) and not tag:read` [search for unread books by Asimov or Hardy] * `(author:Asimov or author:Hardy) and not tag:read` [search for unread books by Asimov or Hardy]
''' '''
DEFAULT_LOCATIONS = [
'all',
'author', # compatibility
'authors',
'comment', # compatibility
'comments',
'cover',
'date',
'format', # compatibility
'formats',
'isbn',
'ondevice',
'pubdate',
'publisher',
'search',
'series',
'rating',
'tag', # compatibility
'tags',
'title',
]
@staticmethod @staticmethod
def run_tests(parser, result, tests): def run_tests(parser, result, tests):
@ -121,12 +99,7 @@ class SearchQueryParser(object):
failed.append(test[0]) failed.append(test[0])
return failed return failed
def __init__(self, locations=None, test=False): def __init__(self, locations, test=False):
for k in self.DEFAULT_LOCATIONS:
if k not in RESERVED_METADATA_FIELDS:
raise ValueError('Search location [%s] is not a reserved word.' %(k))
if locations is None:
locations = self.DEFAULT_LOCATIONS
self._tests_failed = False self._tests_failed = False
# Define a token # Define a token
standard_locations = map(lambda x : CaselessLiteral(x)+Suppress(':'), standard_locations = map(lambda x : CaselessLiteral(x)+Suppress(':'),