This commit is contained in:
GRiker 2010-05-27 07:29:28 -06:00
commit 8348c7085d
8 changed files with 375 additions and 112 deletions

View File

@ -5,17 +5,22 @@
22 May 2010
'''
import datetime, re, sys, time
import cStringIO, datetime, os, re, shutil, sys, time
from calibre import fit_image
from calibre.constants import isosx, iswindows
from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation
from calibre.library.server.utils import strftime
from calibre.utils.config import Config
from calibre.utils.date import parse_date
from PIL import Image as PILImage
if isosx:
print "running in OSX"
import appscript
import appscript, osax
if iswindows:
print "running in Windows"
@ -58,15 +63,19 @@ class ITUNES(DevicePlugin):
# Properties
cached_books = {}
iTunes= None
needs_update = False
path_template = 'iTunes/%s - %s.epub'
presync = True
purge_list = None
sources = None
update_msg = None
update_needed = False
use_thumbnail_as_cover = False
verbose = True
# Public methods
def add_books_to_metadata(cls, locations, metadata, booklists):
def add_books_to_metadata(self, locations, metadata, booklists):
'''
Add locations to the booklists. This function must not communicate with
the device.
@ -78,19 +87,22 @@ class ITUNES(DevicePlugin):
L{books}(oncard='cardb')).
'''
print "ITUNES.add_books_to_metadata()"
if locations:
for location in locations:
print " location: %s" % location
print "metadata:"
for md in metadata:
print md
print
self._dump_booklist(booklists[0])
# Delete any obsolete copies of the book from the booklist
if self.purge_list:
if self.verbose:
print " purging updated books"
for library_id in self.purge_list:
for i,book in enumerate(booklists[0]):
if book.library_id == library_id:
booklists[0].pop(i)
self.purge_list = []
print "booklists[0]:"
for book in booklists[0]:
print " book: '%s'" % book.path
print
# Add new books to booklists[0]
for new_book in locations[0]:
booklists[0].append(new_book)
self._dump_booklist(booklists[0])
def books(self, oncard=None, end_session=True):
"""
@ -124,20 +136,23 @@ class ITUNES(DevicePlugin):
device_books = self._get_device_books()
for book in device_books:
this_book = Book(book.name(), book.artist())
this_book.path = self.path_template % (book.name(), book.artist())
this_book.datetime = parse_date(str(book.date_added())).timetuple()
this_book.db_id = None
this_book.device_collections = []
this_book.path = self.path_template % (book.name(), book.artist())
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
this_book.size = book.size()
this_book.thumbnail = None
cached_books[this_book.path] = { 'title':book.name(),
'author':book.artist(),
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
'dev_book':book,
'bl_index':len(booklist)
}
this_book.thumbnail = self._generate_thumbnail(book)
booklist.add_book(this_book, False)
cached_books[this_book.path] = {
'title':book.name(),
'author':book.artist(),
'lib_book':library_books[this_book.path] if this_book.path in library_books else None
}
if self.verbose:
print
print "%-40.40s %-12.12s" % ('Device Books','In Library')
@ -151,10 +166,6 @@ class ITUNES(DevicePlugin):
else:
# No books installed on this device
return []
else:
return []
@ -239,14 +250,13 @@ class ITUNES(DevicePlugin):
undeletable_titles = []
for path in paths:
if self.cached_books[path]['lib_book']:
title = self.cached_books[path]['title']
author = self.cached_books[path]['author']
dev_book = self.cached_books[path]['dev_book']
lib_book = self.cached_books[path]['lib_book']
if self.verbose:
print "ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)
self.iTunes.delete(lib_book)
self.needs_update = True
self._remove_iTunes_dir(self.cached_books[path])
self.iTunes.delete(self.cached_books[path]['lib_book'])
self.update_needed = True
self.update_msg = "Deleted books from device"
else:
undeletable_titles.append(self.cached_books[path]['title'])
@ -337,7 +347,7 @@ class ITUNES(DevicePlugin):
self.sources = sources = dict(zip(kinds,names))
# Check to see if Library|Books out of sync with Device|Books
if 'iPod' in self.sources:
if 'iPod' in self.sources and self.presync:
lb_count = len(self._get_library_books())
db_count = len(self._get_device_books())
pb_count = len(self._get_purchased_book_ids())
@ -348,6 +358,9 @@ class ITUNES(DevicePlugin):
print " Devices|iPad|Books : %d" % len(self._get_device_books())
print " Devices|iPad|Purchased: %d" % len(self._get_purchased_book_ids())
self._update_device(msg="Presyncing iTunes with device, mismatched book count")
else:
if self.verbose:
print "Skipping pre-sync check"
def post_yank_cleanup(self):
'''
@ -367,13 +380,19 @@ class ITUNES(DevicePlugin):
print "ITUNES.remove_books_from_metadata():"
for path in paths:
if self.cached_books[path]['lib_book']:
print " Removing '%s' from calibre booklist, index: %d" % (path, self.cached_books[path]['bl_index'])
booklists[0].pop(self.cached_books[path]['bl_index'])
# Remove from the booklist
for i,book in enumerate(booklists[0]):
if book.path == path:
print " removing '%s' from calibre booklist, index: %d" % (path, i)
booklists[0].pop(i)
break
# Remove from cached_books
print " Removing '%s' from self.cached_books" % path
self.cached_books.pop(path)
else:
print " Skipping non-Library book, can't removed via automation interface"
print " skipping purchased book, can't remove via automation interface"
def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) :
@ -424,9 +443,9 @@ class ITUNES(DevicePlugin):
L{books}(oncard='cardb')).
'''
print "ITUNES:sync_booklists():"
if self.needs_update:
self._update_device(msg="sync_booklists responding to self.needs_update")
self.needs_update = False
if self.update_needed:
self._update_device(msg=self.update_msg)
self.update_needed = False
def total_space(self, end_session=True):
"""
@ -469,7 +488,7 @@ class ITUNES(DevicePlugin):
be used in preference. The thumbnail attribute is of the form
(width, height, cover_data as jpeg).
'''
if self.verbose:
if False:
print
print "ITUNES.upload_books():"
for file in files:
@ -482,6 +501,7 @@ class ITUNES(DevicePlugin):
print
print "metadata:"
print dir(metadata[0])
for md in metadata:
print " title: %s" % md.title
print " title_sort: %s" % md.title_sort
@ -496,19 +516,108 @@ class ITUNES(DevicePlugin):
print
print
#print "thumbnail: width: %d height: %d" % (metadata[0].thumbnail[0], metadata[0].thumbnail[1])
#self._hexdump(metadata[0].thumbnail[2])
new_booklist = []
self.purge_list = []
if isosx:
for (i,file) in enumerate(files):
path = self.path_template % (metadata[i].title, metadata[i].author[0])
print " path: %s" % path
if path in self.cached_books:
print " '%s' already exists in Library" % path
#delete_book, do not sync
else:
print " adding '%s' to Library" % path
# return (list of added books, [], [])
# Delete existing from Library|Books, add to self.purge_list
# for deletion from booklist[0] during add_books_to_metadata
if path in self.cached_books:
self.purge_list.append(self.cached_books[path])
if self.verbose:
print " deleting existing '%s' at\n %s" % (path,self.cached_books[path]['lib_book'])
self._remove_iTunes_dir(self.cached_books[path])
self.iTunes.delete(self.cached_books[path]['lib_book'])
# Add to iTunes Library|Books
added = self.iTunes.add(appscript.mactypes.File(files[i]))
thumb = None
if self.use_thumbnail_as_cover:
# Use thumbnail data as artwork
added.artworks[1].data_.set(metadata[i].thumbnail[2])
thumb = metadata[i].thumbnail[2]
else:
# Use cover data as artwork
cover_data = open(metadata[i].cover,'rb')
added.artworks[1].data_.set(cover_data.read())
# Resize for thumb
width = metadata[i].thumbnail[0]
height = metadata[i].thumbnail[1]
im = PILImage.open(metadata[i].cover)
im = im.resize((width, height), PILImage.ANTIALIAS)
of = cStringIO.StringIO()
im.convert('RGB').save(of, 'JPEG')
thumb = of.getvalue()
# Create a new Book
this_book = Book(metadata[i].title, metadata[i].author[0])
this_book.datetime = parse_date(str(added.date_added())).timetuple()
this_book.db_id = None
this_book.device_collections = []
this_book.library_id = added
this_book.path = path
this_book.size = added.size() # GwR this is wrong, needs to come from device or fake it
this_book.thumbnail = thumb
this_book.iTunes_id = added
new_booklist.append(this_book)
# Flesh out the iTunes metadata
added.comment.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S'))
added.rating.set(metadata[i].rating*10)
added.sort_artist.set(metadata[i].author_sort)
added.sort_name.set(this_book.title_sorter)
# Set genre from metadata
# iTunes grabs the first dc:subject from the opf metadata,
# But we can manually override
# added.genre.set(metadata[i].tags[0])
# Add new_book to self.cached_paths
self.cached_books[this_book.path] = {
'title': this_book.title,
'author': this_book.author,
'lib_book': this_book.library_id
}
# Tell sync_booklists we need a re-sync
self.update_needed = True
self.update_msg = "Added books to device"
return (new_booklist, [], [])
# Private methods
def _dump_booklist(self,booklist, header="booklists[0]"):
print
print header
print "%s" % ('-' * len(header))
for i,book in enumerate(booklist):
print "%2d %-25.25s %s" % (i,book.title, book.library_id)
print
def _hexdump(self, src, length=16):
# Diagnostic
FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
N=0; result=''
while src:
s,src = src[:length],src[length:]
hexa = ' '.join(["%02X"%ord(x) for x in s])
s = s.translate(FILTER)
result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
N+=length
print result
def _get_library_books(self):
lib = self.iTunes.sources['library']
library_books = {}
@ -526,6 +635,41 @@ class ITUNES(DevicePlugin):
if 'Books' in self.iTunes.sources[device].playlists.name():
return self.iTunes.sources[device].playlists['Books'].file_tracks()
def _generate_thumbnail(self, book):
'''
Convert iTunes artwork to thumbnail
Cache generated thumbnails
'''
print "ITUNES._generate_thumbnail()"
try:
n = len(book.artworks())
print "Library '%s' has %d artwork items" % (book.name(),n)
# for art in book.artworks():
# print "description: %s" % art.description()
# if str(art.description()) == 'calibre_thumb':
# print "using cached thumb"
# return art.raw_data().data
# Resize the cover
data = book.artworks[1].raw_data().data
#self._hexdump(data[:256])
im = PILImage.open(cStringIO.StringIO(data))
scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80)
im = im.resize((int(width),int(height)), PILImage.ANTIALIAS)
thumb = cStringIO.StringIO()
im.convert('RGB').save(thumb,'JPEG')
# Cache the tagged thumb
# print "caching thumb"
# book.artworks[n+1].data_.set(thumb.getvalue())
# book.artworks[n+1].description.set(u'calibre_thumb')
return thumb.getvalue()
except:
print "Can't generate thumb for '%s'" % book.name()
return None
def _get_purchased_book_ids(self):
if 'iPod' in self.sources:
device = self.sources['iPod']
@ -533,6 +677,16 @@ class ITUNES(DevicePlugin):
if 'Purchased' in self.iTunes.sources[device].playlists.name():
return [pb.database_ID() for pb in self.iTunes.sources[device].playlists['Purchased'].file_tracks()]
def _remove_iTunes_dir(self, cached_book):
'''
iTunes does not delete books from storage when removing from database
'''
storage_path = os.path.split(cached_book['lib_book'].location().path)
if self.verbose:
print "ITUNES._remove_iTunes_dir():"
print " removing storage_path: %s" % storage_path[0]
shutil.rmtree(storage_path[0])
def _update_device(self, msg='', wait=True):
'''

View File

@ -89,11 +89,17 @@ CALIBRE_METADATA_FIELDS = frozenset([
)
CALIBRE_RESERVED_LABELS = frozenset([
'search', # reserved for saved searches
'date',
'all',
'ondevice',
'inlibrary',
'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 ...
]
)

View File

@ -631,7 +631,10 @@ class BooksModel(QAbstractTableModel): # {{{
if section >= len(self.column_map): # same problem as in data, the column_map can be wrong
return None
if role == Qt.ToolTipRole:
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
ht = self.column_map[section]
if ht == 'timestamp': # change help text because users know this field as 'date'
ht = 'date'
return QVariant(_('The lookup/search name is "{0}"').format(ht))
if role == Qt.DisplayRole:
return QVariant(self.headers[self.column_map[section]])
return NONE
@ -730,11 +733,13 @@ class BooksModel(QAbstractTableModel): # {{{
class OnDeviceSearch(SearchQueryParser): # {{{
USABLE_LOCATIONS = [
'collections',
'title',
'author',
'format',
'all',
'author',
'authors',
'collections',
'format',
'formats',
'title',
]

View File

@ -14,8 +14,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.library.database2 import Tag
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
class TagsView(QTreeView): # {{{
@ -204,17 +203,22 @@ class TagsModel(QAbstractItemModel): # {{{
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
# before a QPaintDevice'
self.category_icon_map = {'authors': QIcon(I('user_profile.svg')),
# before a QPaintDevice'. The ':' in front avoids polluting either the
# user-defined categories (':' at end) or columns namespaces (no ':').
self.category_icon_map = {
'authors' : QIcon(I('user_profile.svg')),
'series' : QIcon(I('series.svg')),
'formats' : QIcon(I('book.svg')),
'publishers': QIcon(I('publisher.png')),
'ratings':QIcon(I('star.png')),
'publisher' : QIcon(I('publisher.png')),
'rating' : QIcon(I('star.png')),
'news' : QIcon(I('news.svg')),
'tags' : QIcon(I('tags.svg')),
'*custom':QIcon(I('column.svg')),
'*user':QIcon(I('drawer.svg')),
':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.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.db = db
self.search_restriction = ''
@ -381,9 +385,9 @@ class TagsModel(QAbstractItemModel): # {{{
def tokens(self):
ans = []
tags_seen = []
tags_seen = set()
for i, key in enumerate(self.row_map):
if key.endswith('*'): # User category, so skip it. The tag will be marked in its real category
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[i]
for tag_item in category_item.children:
@ -394,10 +398,10 @@ class TagsModel(QAbstractItemModel): # {{{
if tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else:
if category == 'tag':
if category == 'tags':
if tag.name in tags_seen:
continue
tags_seen.append(tag.name)
tags_seen.add(tag.name)
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans

View File

@ -37,6 +37,7 @@ from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
if iswindows:
import calibre.utils.winshell as winshell
@ -137,10 +138,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
('formats', {'table':None, 'column':None,
'type':None, 'is_multiple':False,
'kind':'standard', 'name':_('Formats')}),
('publishers',{'table':'publishers', 'column':'name',
('publisher', {'table':'publishers', 'column':'name',
'type':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Publishers')}),
('ratings', {'table':'ratings', 'column':'rating',
('rating', {'table':'ratings', 'column':'rating',
'type':'rating', 'is_multiple':False,
'kind':'standard', 'name':_('Ratings')}),
('news', {'table':'news', 'column':'name',
@ -152,6 +153,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
]
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()
@ -694,25 +697,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if category in icon_map:
icon = icon_map[category]
elif self.tag_browser_categories[category]['kind'] == 'custom':
icon = icon_map['*custom']
icon_map[category] = icon_map['*custom']
icon = icon_map[':custom']
icon_map[category] = icon
tooltip = self.custom_column_label_map[category]['name']
datatype = self.tag_browser_categories[category]['type']
if datatype == 'rating':
item_zero_func = (lambda x: len(formatter(r[1])) > 0)
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors':
item_zero_func = (lambda x: x[2] > 0)
item_not_zero_func = (lambda x: x[2] > 0)
# Clean up the authors strings to human-readable form
formatter = (lambda x: x.replace('|', ','))
else:
item_zero_func = (lambda x: x[2] > 0)
item_not_zero_func = (lambda x: x[2] > 0)
formatter = (lambda x:x)
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip)
for r in data if item_zero_func(r)]
for r in data if item_not_zero_func(r)]
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@ -767,14 +770,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
items.append(taglist[label][name])
# else: do nothing, to not include nodes w zero counts
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] = {
'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
if icon_map is not None:
icon_map[cat_name] = icon_map['*user']
icon_map[cat_name] = icon_map[':user']
if sort_on_count:
categories[cat_name] = \
sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))

View File

@ -17,10 +17,14 @@ class Cache(object):
def search_cache(self, search):
old = self._search_cache.get(search, None)
if old is None or old[0] <= self.db.last_modified():
matches = self.db.data.search(search)
self._search_cache[search] = frozenset(matches)
matches = self.db.data.search(search, return_matches=True,
ignore_search_restriction=True)
if not matches:
matches = []
self._search_cache[search] = (utcnow(), frozenset(matches))
if len(self._search_cache) > 10:
self._search_cache.popitem(last=False)
return self._search_cache[search][1]
def categories_cache(self, restrict_to=frozenset([])):

View File

@ -7,18 +7,24 @@ __docformat__ = 'restructuredtext en'
import hashlib, binascii
from functools import partial
from itertools import repeat
from lxml import etree
from lxml import etree, html
from lxml.builder import ElementMaker
import cherrypy
from calibre.constants import __appname__
from calibre.ebooks.metadata import fmt_sidx
from calibre.library.comments import comments_to_html
from calibre import guess_type
BASE_HREFS = {
0 : '/stanza',
1 : '/opds',
}
STANZA_FORMATS = frozenset(['epub', 'pdb'])
# Vocabulary for building OPDS feeds {{{
E = ElementMaker(namespace='http://www.w3.org/2005/Atom',
nsmap={
@ -71,9 +77,72 @@ LAST_LINK = partial(NAVLINK, rel='last')
NEXT_LINK = partial(NAVLINK, rel='next')
PREVIOUS_LINK = partial(NAVLINK, rel='previous')
def html_to_lxml(raw):
raw = u'<div>%s</div>'%raw
root = html.fragment_fromstring(raw)
root.set('xmlns', "http://www.w3.org/1999/xhtml")
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
def ACQUISITION_ENTRY(item, version, FM, updated):
title = item[FM['title']]
if not title:
title = _('Unknown')
authors = item[FM['authors']]
if not authors:
authors = _('Unknown')
authors = ' & '.join([i.replace('|', ',') for i in
authors.split(',')])
extra = []
rating = item[FM['rating']]
if rating > 0:
rating = u''.join(repeat(u'\u2605', int(rating/2.)))
extra.append(_('RATING: %s<br />')%rating)
tags = item[FM['tags']]
if tags:
extra.append(_('TAGS: %s<br />')%\
', '.join(tags.split(',')))
series = item[FM['series']]
if series:
extra.append(_('SERIES: %s [%s]<br />')%\
(series,
fmt_sidx(float(item[FM['series_index']]))))
comments = item[FM['comments']]
if comments:
comments = comments_to_html(comments)
extra.append(comments)
if extra:
extra = html_to_lxml('\n'.join(extra))
idm = 'calibre' if version == 0 else 'uuid'
id_ = 'urn:%s:%s'%(idm, item[FM['uuid']])
ans = E.entry(TITLE(title), E.author(E.name(authors)), ID(id_),
UPDATED(updated))
if extra:
ans.append(E.content(extra, type='xhtml'))
formats = item[FM['formats']]
if formats:
for fmt in formats.split(','):
fmt = fmt.lower()
mt = guess_type('a.'+fmt)[0]
href = '/get/%s/%s'%(fmt, item[FM['id']])
if mt:
link = E.link(type=mt, href=href)
if version > 0:
link.set('rel', "http://opds-spec.org/acquisition")
ans.append(link)
ans.append(E.link(type='image/jpeg', href='/get/cover/%s'%item[FM['id']],
rel="x-stanza-cover-image" if version == 0 else
"http://opds-spec.org/cover"))
ans.append(E.link(type='image/jpeg', href='/get/thumb/%s'%item[FM['id']],
rel="x-stanza-cover-image-thumbnail" if version == 0 else
"http://opds-spec.org/thumbnail"))
return ans
# }}}
class Feed(object):
class Feed(object): # {{{
def __init__(self, id_, updated, version, subtitle=None,
title=__appname__ + ' ' + _('Library'),
@ -106,6 +175,7 @@ class Feed(object):
def __str__(self):
return etree.tostring(self.root, pretty_print=True, encoding='utf-8',
xml_declaration=True)
# }}}
class TopLevel(Feed): # {{{
@ -126,9 +196,9 @@ class TopLevel(Feed): # {{{
self.root.append(x)
# }}}
class AcquisitionFeed(Feed):
class NavFeed(Feed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version):
def __init__(self, id_, updated, version, offsets, page_url, up_url):
kwargs = {'up_link': up_url}
kwargs['first_link'] = page_url
kwargs['last_link'] = page_url+'?offset=%d'%offsets.last_offset
@ -140,7 +210,14 @@ class AcquisitionFeed(Feed):
page_url+'?offset=%d'%offsets.next_offset
Feed.__init__(self, id_, updated, version, **kwargs)
STANZA_FORMATS = frozenset(['epub', 'pdb'])
class AcquisitionFeed(NavFeed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
FM):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
for item in items:
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated))
class OPDSOffsets(object):
@ -176,9 +253,10 @@ class OPDSServer(object):
self.opds_search, version=version)
def get_opds_allowed_ids_for_version(self, version):
search = '' if version > 0 else ' '.join(['format:='+x for x in
search = '' if version > 0 else ' or '.join(['format:='+x for x in
STANZA_FORMATS])
self.search_cache(search)
ids = self.search_cache(search)
return ids
def get_opds_acquisition_feed(self, ids, offset, page_url, up_url, id_,
sort_by='title', ascending=True, version=0):
@ -189,7 +267,8 @@ class OPDSServer(object):
max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items))
items = items[offsets.offset:offsets.next_offset]
return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets, page_url, up_url, version))
return str(AcquisitionFeed(self.db.last_modified(), id_, items, offsets,
page_url, up_url, version, self.db.FIELD_MAP))
def opds_search(self, query=None, version=0, offset=0):
try:
@ -203,8 +282,9 @@ class OPDSServer(object):
ids = self.search_cache(query)
except:
raise cherrypy.HTTPError(404, 'Search: %r not understood'%query)
return self.get_opds_acquisition_feed(ids,
sort_by='title', version=version)
return self.get_opds_acquisition_feed(ids, offset, '/search/'+query,
BASE_HREFS[version], 'calibre-search:'+query,
version=version)
def opds_navcatalog(self, which=None, version=0):
version = int(version)

View File

@ -22,7 +22,7 @@ from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppres
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
from calibre.constants import preferred_encoding
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.
@ -87,21 +87,25 @@ class SearchQueryParser(object):
'''
DEFAULT_LOCATIONS = [
'tag',
'title',
'author',
'all',
'author', # compatibility
'authors',
'comment', # compatibility
'comments',
'cover',
'date',
'format', # compatibility
'formats',
'isbn',
'ondevice',
'pubdate',
'publisher',
'search',
'series',
'rating',
'cover',
'comments',
'format',
'isbn',
'search',
'date',
'pubdate',
'ondevice',
'all',
'tag', # compatibility
'tags',
'title',
]
@staticmethod
@ -118,6 +122,9 @@ class SearchQueryParser(object):
return failed
def __init__(self, locations=None, 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