KG revisions
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 57 KiB |
@ -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">
|
||||||
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
57
resources/recipes/infomotori.recipe
Normal 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')
|
||||||
|
]
|
@ -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(
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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'])
|
||||||
|
@ -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]
|
||||||
|
@ -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'])
|
||||||
|
@ -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'
|
||||||
|
@ -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:
|
||||||
|
@ -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']:
|
||||||
tn = 'custom_column_{0}'.format(v['num'])
|
searchable = True
|
||||||
self.tag_browser_categories[v['label']] = {
|
else:
|
||||||
'table':tn, 'column':'value',
|
searchable = False
|
||||||
'type':v['datatype'], 'is_multiple':v['is_multiple'],
|
tn = 'custom_column_{0}'.format(v['num'])
|
||||||
'kind':'custom', 'name':v['name']
|
self.tag_browser_categories.add_custom_field(label=v['label'],
|
||||||
}
|
table=tn, column='value', datatype=v['datatype'],
|
||||||
|
is_multiple=v['is_multiple'], colnum=v['num'],
|
||||||
|
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:
|
||||||
|
@ -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
|
||||||
|
259
src/calibre/library/field_metadata.py
Normal 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
|
@ -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()
|
||||||
|
@ -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')
|
||||||
|
@ -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(':'),
|
||||||
|