mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
GwR wip
This commit is contained in:
commit
8348c7085d
@ -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,19 +488,20 @@ 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:
|
||||
print "file: %s" % file
|
||||
print " file: %s" % file
|
||||
print
|
||||
|
||||
print "names:"
|
||||
for name in names:
|
||||
print "name: %s" % name
|
||||
print " name: %s" % name
|
||||
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):
|
||||
'''
|
||||
|
@ -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 ...
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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')),
|
||||
'series': QIcon(I('series.svg')),
|
||||
'formats':QIcon(I('book.svg')),
|
||||
'publishers': QIcon(I('publisher.png')),
|
||||
'ratings':QIcon(I('star.png')),
|
||||
'news':QIcon(I('news.svg')),
|
||||
'tags':QIcon(I('tags.svg')),
|
||||
'*custom':QIcon(I('column.svg')),
|
||||
'*user':QIcon(I('drawer.svg')),
|
||||
'search':QIcon(I('search.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')),
|
||||
'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')),
|
||||
'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
|
||||
|
||||
|
@ -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)))
|
||||
|
@ -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([])):
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user