mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
GwR apple driver wip
This commit is contained in:
commit
66e5f106e3
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
22 May 2010
|
22 May 2010
|
||||||
'''
|
'''
|
||||||
import datetime, re, sys
|
import datetime, re, sys, time
|
||||||
|
|
||||||
from calibre.constants import isosx, iswindows
|
from calibre.constants import isosx, iswindows
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
@ -23,7 +23,18 @@ if iswindows:
|
|||||||
|
|
||||||
class UserInteractionRequired(Exception):
|
class UserInteractionRequired(Exception):
|
||||||
print "UserInteractionRequired() exception"
|
print "UserInteractionRequired() exception"
|
||||||
#pass
|
pass
|
||||||
|
|
||||||
|
class UserFeedback(Exception):
|
||||||
|
INFO = 0
|
||||||
|
WARN = 1
|
||||||
|
ERROR = 2
|
||||||
|
|
||||||
|
def __init__(self, msg, details, level):
|
||||||
|
Exception.__init__(self, msg)
|
||||||
|
self.level = level
|
||||||
|
self.details = details
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
class ITUNES(DevicePlugin):
|
class ITUNES(DevicePlugin):
|
||||||
name = 'Apple device interface'
|
name = 'Apple device interface'
|
||||||
@ -46,8 +57,8 @@ class ITUNES(DevicePlugin):
|
|||||||
|
|
||||||
# Properties
|
# Properties
|
||||||
cached_books = {}
|
cached_books = {}
|
||||||
ejected = False
|
|
||||||
iTunes= None
|
iTunes= None
|
||||||
|
needs_update = False
|
||||||
path_template = 'iTunes/%s - %s.epub'
|
path_template = 'iTunes/%s - %s.epub'
|
||||||
sources = None
|
sources = None
|
||||||
verbose = True
|
verbose = True
|
||||||
@ -66,7 +77,20 @@ class ITUNES(DevicePlugin):
|
|||||||
(L{books}(oncard=None), L{books}(oncard='carda'),
|
(L{books}(oncard=None), L{books}(oncard='carda'),
|
||||||
L{books}(oncard='cardb')).
|
L{books}(oncard='cardb')).
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError
|
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
|
||||||
|
|
||||||
|
print "booklists[0]:"
|
||||||
|
for book in booklists[0]:
|
||||||
|
print " book: '%s'" % book.path
|
||||||
|
print
|
||||||
|
|
||||||
def books(self, oncard=None, end_session=True):
|
def books(self, oncard=None, end_session=True):
|
||||||
"""
|
"""
|
||||||
@ -90,31 +114,15 @@ class ITUNES(DevicePlugin):
|
|||||||
if isosx:
|
if isosx:
|
||||||
|
|
||||||
# Fetch Library|Books
|
# Fetch Library|Books
|
||||||
lib = self.iTunes.sources['library']
|
library_books = self._get_library_books()
|
||||||
if 'Books' in lib.playlists.name():
|
|
||||||
lib_books = lib.playlists['Books'].file_tracks()
|
|
||||||
library_books = {}
|
|
||||||
for book in lib_books:
|
|
||||||
path = self.path_template % (book.name(), book.artist())
|
|
||||||
library_books[path] = book
|
|
||||||
|
|
||||||
# Fetch iPod|Books, ignore iPod|Purchased books
|
|
||||||
if 'iPod' in self.sources:
|
if 'iPod' in self.sources:
|
||||||
device = self.sources['iPod']
|
device = self.sources['iPod']
|
||||||
if 'Purchased' in self.iTunes.sources[device].playlists.name():
|
|
||||||
purchased_book_ids = [pb.database_ID() for pb in self.iTunes.sources[device].playlists['Purchased'].file_tracks()]
|
|
||||||
else:
|
|
||||||
purchased_books_ids = []
|
|
||||||
|
|
||||||
if 'Books' in self.iTunes.sources[device].playlists.name():
|
if 'Books' in self.iTunes.sources[device].playlists.name():
|
||||||
booklist = BookList()
|
booklist = BookList()
|
||||||
cached_books = {}
|
cached_books = {}
|
||||||
device_books = self.iTunes.sources[device].playlists['Books'].file_tracks()
|
device_books = self._get_device_books()
|
||||||
for book in device_books:
|
for book in device_books:
|
||||||
if book.database_ID() in purchased_book_ids:
|
|
||||||
if self.verbose:
|
|
||||||
print " skipping purchased book '%s'" % book.name()
|
|
||||||
continue
|
|
||||||
this_book = Book(book.name(), book.artist())
|
this_book = Book(book.name(), book.artist())
|
||||||
this_book.datetime = parse_date(str(book.date_added())).timetuple()
|
this_book.datetime = parse_date(str(book.date_added())).timetuple()
|
||||||
this_book.db_id = None
|
this_book.db_id = None
|
||||||
@ -144,6 +152,9 @@ class ITUNES(DevicePlugin):
|
|||||||
# No books installed on this device
|
# No books installed on this device
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@ -158,15 +169,11 @@ class ITUNES(DevicePlugin):
|
|||||||
- iTunes is running
|
- iTunes is running
|
||||||
- there is an iPod-type device connected
|
- there is an iPod-type device connected
|
||||||
This gets called first when the device fingerprint is read, so it needs to
|
This gets called first when the device fingerprint is read, so it needs to
|
||||||
instantate iTunes if necessary
|
instantiate iTunes if necessary
|
||||||
This gets called ~1x/second while device fingerprint is sensed
|
This gets called ~1x/second while device fingerprint is sensed
|
||||||
'''
|
'''
|
||||||
# print "ITUNES:can_handle()"
|
|
||||||
if isosx:
|
|
||||||
if self.ejected:
|
|
||||||
print "ITUNES:can_handle(): device detected, but ejected from iTunes"
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
if isosx:
|
||||||
if self.iTunes:
|
if self.iTunes:
|
||||||
# Check for connected book-capable device
|
# Check for connected book-capable device
|
||||||
names = [s.name() for s in self.iTunes.sources()]
|
names = [s.name() for s in self.iTunes.sources()]
|
||||||
@ -179,7 +186,7 @@ class ITUNES(DevicePlugin):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print "ITUNES.can_handle(): device not connected"
|
print "ITUNES.can_handle(): device ejected"
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# can_handle() is called once before open(), so need to return True
|
# can_handle() is called once before open(), so need to return True
|
||||||
@ -225,18 +232,26 @@ class ITUNES(DevicePlugin):
|
|||||||
'''
|
'''
|
||||||
Delete books at paths on device.
|
Delete books at paths on device.
|
||||||
iTunes doesn't let us directly delete a book on the device.
|
iTunes doesn't let us directly delete a book on the device.
|
||||||
Delete the path(s) from the library, then update iPad
|
If the requested paths are deletable (i.e., it's in the Library|Books list),
|
||||||
|
delete the paths from the library, then update iPad
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
undeletable_titles = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
title = self.cached_books[path]['title']
|
if self.cached_books[path]['lib_book']:
|
||||||
author = self.cached_books[path]['author']
|
title = self.cached_books[path]['title']
|
||||||
dev_book = self.cached_books[path]['dev_book']
|
author = self.cached_books[path]['author']
|
||||||
lib_book = self.cached_books[path]['lib_book']
|
dev_book = self.cached_books[path]['dev_book']
|
||||||
if self.verbose:
|
lib_book = self.cached_books[path]['lib_book']
|
||||||
print "ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)
|
if self.verbose:
|
||||||
self.iTunes.delete(lib_book)
|
print "ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)
|
||||||
self._update_device()
|
self.iTunes.delete(lib_book)
|
||||||
|
self.needs_update = True
|
||||||
|
else:
|
||||||
|
undeletable_titles.append(self.cached_books[path]['title'])
|
||||||
|
|
||||||
|
if undeletable_titles:
|
||||||
|
raise UserFeedback(_('You cannot delete purchased books. To do so delete them from the device itself. The books that could not be deleted are:'), details='\n'.join(undeletable_titles), level=UserFeedback.WARN)
|
||||||
|
|
||||||
def eject(self):
|
def eject(self):
|
||||||
'''
|
'''
|
||||||
@ -275,6 +290,7 @@ class ITUNES(DevicePlugin):
|
|||||||
@return: (device name, device version, software version on device, mime type)
|
@return: (device name, device version, software version on device, mime type)
|
||||||
"""
|
"""
|
||||||
print "ITUNES:get_device_information()"
|
print "ITUNES:get_device_information()"
|
||||||
|
|
||||||
return ('iPad','hw v1.0','sw v1.0', 'mime type')
|
return ('iPad','hw v1.0','sw v1.0', 'mime type')
|
||||||
|
|
||||||
def get_file(self, path, outfile, end_session=True):
|
def get_file(self, path, outfile, end_session=True):
|
||||||
@ -295,6 +311,7 @@ class ITUNES(DevicePlugin):
|
|||||||
this function that should serve as a good example for USB Mass storage
|
this function that should serve as a good example for USB Mass storage
|
||||||
devices.
|
devices.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if isosx:
|
if isosx:
|
||||||
# Launch iTunes if not already running
|
# Launch iTunes if not already running
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
@ -319,13 +336,18 @@ class ITUNES(DevicePlugin):
|
|||||||
kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()]
|
kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()]
|
||||||
self.sources = sources = dict(zip(kinds,names))
|
self.sources = sources = dict(zip(kinds,names))
|
||||||
|
|
||||||
# If we're running, but 'iPod' is not a listed source, device was
|
# Check to see if Library|Books out of sync with Device|Books
|
||||||
# previously ejected but not physically removed. can_handle() needs to know this
|
if 'iPod' in self.sources:
|
||||||
if not 'iPod' in self.sources:
|
lb_count = len(self._get_library_books())
|
||||||
self.ejected = True
|
db_count = len(self._get_device_books())
|
||||||
|
pb_count = len(self._get_purchased_book_ids())
|
||||||
else:
|
if db_count != lb_count + pb_count:
|
||||||
print "ITUNES:open(): check for presync here ..."
|
if self.verbose:
|
||||||
|
print "ITUNES.open(): pre-syncing iTunes with device"
|
||||||
|
print " Library|Books : %d" % len(self._get_library_books())
|
||||||
|
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")
|
||||||
|
|
||||||
def post_yank_cleanup(self):
|
def post_yank_cleanup(self):
|
||||||
'''
|
'''
|
||||||
@ -344,12 +366,14 @@ class ITUNES(DevicePlugin):
|
|||||||
'''
|
'''
|
||||||
print "ITUNES.remove_books_from_metadata():"
|
print "ITUNES.remove_books_from_metadata():"
|
||||||
for path in paths:
|
for path in paths:
|
||||||
print " Removing '%s' from calibre booklist, index: %d" % (path, self.cached_books[path]['bl_index'])
|
if self.cached_books[path]['lib_book']:
|
||||||
booklists[0].pop(self.cached_books[path]['bl_index'])
|
print " Removing '%s' from calibre booklist, index: %d" % (path, self.cached_books[path]['bl_index'])
|
||||||
|
booklists[0].pop(self.cached_books[path]['bl_index'])
|
||||||
print " Removing '%s' from self.cached_books" % path
|
|
||||||
self.cached_books.pop(path)
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
def reset(self, key='-1', log_packets=False, report_progress=None,
|
def reset(self, key='-1', log_packets=False, report_progress=None,
|
||||||
detected_device=None) :
|
detected_device=None) :
|
||||||
@ -400,6 +424,9 @@ class ITUNES(DevicePlugin):
|
|||||||
L{books}(oncard='cardb')).
|
L{books}(oncard='cardb')).
|
||||||
'''
|
'''
|
||||||
print "ITUNES:sync_booklists():"
|
print "ITUNES:sync_booklists():"
|
||||||
|
if self.needs_update:
|
||||||
|
self._update_device(msg="sync_booklists responding to self.needs_update")
|
||||||
|
self.needs_update = False
|
||||||
|
|
||||||
def total_space(self, end_session=True):
|
def total_space(self, end_session=True):
|
||||||
"""
|
"""
|
||||||
@ -442,17 +469,89 @@ class ITUNES(DevicePlugin):
|
|||||||
be used in preference. The thumbnail attribute is of the form
|
be used in preference. The thumbnail attribute is of the form
|
||||||
(width, height, cover_data as jpeg).
|
(width, height, cover_data as jpeg).
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
if self.verbose:
|
||||||
|
print
|
||||||
|
print "ITUNES.upload_books():"
|
||||||
|
for file in files:
|
||||||
|
print "file: %s" % file
|
||||||
|
print
|
||||||
|
|
||||||
|
print "names:"
|
||||||
|
for name in names:
|
||||||
|
print "name: %s" % name
|
||||||
|
print
|
||||||
|
|
||||||
|
print "metadata:"
|
||||||
|
for md in metadata:
|
||||||
|
print " title: %s" % md.title
|
||||||
|
print " title_sort: %s" % md.title_sort
|
||||||
|
print " author: %s" % md.author[0]
|
||||||
|
print " author_sort: %s" % md.author_sort
|
||||||
|
print " tags: %s" % md.tags
|
||||||
|
print " rating: %s" % md.rating
|
||||||
|
print " cover: %s" % md.cover
|
||||||
|
#print " cover_data: %s" % repr(md.cover_data)
|
||||||
|
#print "thumbnail: %s" % repr(md.thumbnail)
|
||||||
|
|
||||||
|
print
|
||||||
|
print
|
||||||
|
|
||||||
|
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, [], [])
|
||||||
|
|
||||||
# Private methods
|
# Private methods
|
||||||
def _update_device(self):
|
def _get_library_books(self):
|
||||||
|
lib = self.iTunes.sources['library']
|
||||||
|
library_books = {}
|
||||||
|
if 'Books' in lib.playlists.name():
|
||||||
|
lib_books = lib.playlists['Books'].file_tracks()
|
||||||
|
for book in lib_books:
|
||||||
|
path = self.path_template % (book.name(), book.artist())
|
||||||
|
library_books[path] = book
|
||||||
|
return library_books
|
||||||
|
|
||||||
|
def _get_device_books(self):
|
||||||
|
if 'iPod' in self.sources:
|
||||||
|
device = self.sources['iPod']
|
||||||
|
device_books = []
|
||||||
|
if 'Books' in self.iTunes.sources[device].playlists.name():
|
||||||
|
return self.iTunes.sources[device].playlists['Books'].file_tracks()
|
||||||
|
|
||||||
|
def _get_purchased_book_ids(self):
|
||||||
|
if 'iPod' in self.sources:
|
||||||
|
device = self.sources['iPod']
|
||||||
|
purchased_book_ids = []
|
||||||
|
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 _update_device(self, msg='', wait=True):
|
||||||
'''
|
'''
|
||||||
This probably needs a job spinner
|
This probably needs a job spinner
|
||||||
'''
|
'''
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print "ITUNES:_update_device(): Syncing device with iTunes Library"
|
print "ITUNES:_update_device(): %s" % msg
|
||||||
self.iTunes.update()
|
self.iTunes.update()
|
||||||
|
|
||||||
|
if wait:
|
||||||
|
# This works if iTunes has books not yet synced to iPad.
|
||||||
|
print "Waiting for iPad sync to complete ...",
|
||||||
|
while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())):
|
||||||
|
sys.stdout.write('.')
|
||||||
|
sys.stdout.flush()
|
||||||
|
time.sleep(2)
|
||||||
|
print
|
||||||
|
|
||||||
|
|
||||||
class BookList(list):
|
class BookList(list):
|
||||||
'''
|
'''
|
||||||
A list of books. Each Book object must have the fields:
|
A list of books. Each Book object must have the fields:
|
||||||
|
@ -37,6 +37,7 @@ class DeviceJob(BaseJob):
|
|||||||
self.exception = None
|
self.exception = None
|
||||||
self.job_manager = job_manager
|
self.job_manager = job_manager
|
||||||
self._details = _('No details available.')
|
self._details = _('No details available.')
|
||||||
|
self._aborted = False
|
||||||
|
|
||||||
def start_work(self):
|
def start_work(self):
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
@ -55,7 +56,11 @@ class DeviceJob(BaseJob):
|
|||||||
self.start_work()
|
self.start_work()
|
||||||
try:
|
try:
|
||||||
self.result = self.func(*self.args, **self.kwargs)
|
self.result = self.func(*self.args, **self.kwargs)
|
||||||
|
if self._aborted:
|
||||||
|
return
|
||||||
except (Exception, SystemExit), err:
|
except (Exception, SystemExit), err:
|
||||||
|
if self._aborted:
|
||||||
|
return
|
||||||
self.failed = True
|
self.failed = True
|
||||||
self._details = unicode(err) + '\n\n' + \
|
self._details = unicode(err) + '\n\n' + \
|
||||||
traceback.format_exc()
|
traceback.format_exc()
|
||||||
@ -63,6 +68,12 @@ class DeviceJob(BaseJob):
|
|||||||
finally:
|
finally:
|
||||||
self.job_done()
|
self.job_done()
|
||||||
|
|
||||||
|
def abort(self, err):
|
||||||
|
self._aborted = True
|
||||||
|
self.failed = True
|
||||||
|
self._details = unicode(err)
|
||||||
|
self.exception = err
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_file(self):
|
def log_file(self):
|
||||||
return cStringIO.StringIO(self._details.encode('utf-8'))
|
return cStringIO.StringIO(self._details.encode('utf-8'))
|
||||||
|
@ -628,23 +628,24 @@ class ResultCache(SearchQueryParser):
|
|||||||
|
|
||||||
def search(self, query, return_matches=False,
|
def search(self, query, return_matches=False,
|
||||||
ignore_search_restriction=False):
|
ignore_search_restriction=False):
|
||||||
|
q = ''
|
||||||
if not query or not query.strip():
|
if not query or not query.strip():
|
||||||
q = ''
|
|
||||||
if not ignore_search_restriction:
|
if not ignore_search_restriction:
|
||||||
q = self.search_restriction
|
q = self.search_restriction
|
||||||
elif not ignore_search_restriction:
|
else:
|
||||||
q = u'%s (%s)' % (self.search_restriction, query)
|
q = query
|
||||||
|
if not ignore_search_restriction:
|
||||||
|
q = u'%s (%s)' % (self.search_restriction, query)
|
||||||
if not q:
|
if not q:
|
||||||
if return_matches:
|
if return_matches:
|
||||||
return list(self._map) # when return_matches, do not update the maps!
|
return list(self._map) # when return_matches, do not update the maps!
|
||||||
self._map_filtered = list(self._map)
|
self._map_filtered = list(self._map)
|
||||||
return []
|
return
|
||||||
matches = sorted(self.parse(q))
|
matches = sorted(self.parse(q))
|
||||||
ans = [id for id in self._map if id in matches]
|
ans = [id for id in self._map if id in matches]
|
||||||
if return_matches:
|
if return_matches:
|
||||||
return ans
|
return ans
|
||||||
self._map_filtered = ans
|
self._map_filtered = ans
|
||||||
return []
|
|
||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
self.search_restriction = s
|
self.search_restriction = s
|
||||||
|
@ -66,7 +66,6 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
|
|||||||
self.embedded = embedded
|
self.embedded = embedded
|
||||||
self.max_cover_width, self.max_cover_height = \
|
self.max_cover_width, self.max_cover_height = \
|
||||||
map(int, self.opts.max_cover.split('x'))
|
map(int, self.opts.max_cover.split('x'))
|
||||||
self.max_stanza_items = opts.max_opds_items
|
|
||||||
path = P('content_server')
|
path = P('content_server')
|
||||||
self.build_time = fromtimestamp(os.stat(path).st_mtime)
|
self.build_time = fromtimestamp(os.stat(path).st_mtime)
|
||||||
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
self.default_cover = open(P('content_server/default_cover.jpg'), 'rb').read()
|
||||||
|
@ -36,10 +36,10 @@ 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,
|
NAVLINK = partial(E.link, rel='subsection',
|
||||||
type='application/atom+xml;type=feed;profile=opds-catalog')
|
type='application/atom+xml;type=feed;profile=opds-catalog')
|
||||||
|
|
||||||
def SEARCH(base_href, *args, **kwargs):
|
def SEARCH_LINK(base_href, *args, **kwargs):
|
||||||
kwargs['rel'] = 'search'
|
kwargs['rel'] = 'search'
|
||||||
kwargs['title'] = 'Search'
|
kwargs['title'] = 'Search'
|
||||||
kwargs['href'] = base_href+'/search/{searchTerms}'
|
kwargs['href'] = base_href+'/search/{searchTerms}'
|
||||||
@ -64,43 +64,105 @@ def NAVCATALOG_ENTRY(base_href, updated, title, description, query):
|
|||||||
NAVLINK(href=href)
|
NAVLINK(href=href)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
START_LINK = partial(NAVLINK, rel='start')
|
||||||
|
UP_LINK = partial(NAVLINK, rel='up')
|
||||||
|
FIRST_LINK = partial(NAVLINK, rel='first')
|
||||||
|
LAST_LINK = partial(NAVLINK, rel='last')
|
||||||
|
NEXT_LINK = partial(NAVLINK, rel='next')
|
||||||
|
PREVIOUS_LINK = partial(NAVLINK, rel='previous')
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class Feed(object):
|
class Feed(object):
|
||||||
|
|
||||||
|
def __init__(self, id_, updated, version, subtitle=None,
|
||||||
|
title=__appname__ + ' ' + _('Library'),
|
||||||
|
up_link=None, first_link=None, last_link=None,
|
||||||
|
next_link=None, previous_link=None):
|
||||||
|
self.base_href = BASE_HREFS[version]
|
||||||
|
|
||||||
|
self.root = \
|
||||||
|
FEED(
|
||||||
|
TITLE(title),
|
||||||
|
AUTHOR(__appname__, uri='http://calibre-ebook.com'),
|
||||||
|
ID(id_),
|
||||||
|
UPDATED(updated),
|
||||||
|
SEARCH_LINK(self.base_href),
|
||||||
|
START_LINK(self.base_href)
|
||||||
|
)
|
||||||
|
if up_link:
|
||||||
|
self.root.append(UP_LINK(up_link))
|
||||||
|
if first_link:
|
||||||
|
self.root.append(FIRST_LINK(first_link))
|
||||||
|
if last_link:
|
||||||
|
self.root.append(LAST_LINK(last_link))
|
||||||
|
if next_link:
|
||||||
|
self.root.append(NEXT_LINK(next_link))
|
||||||
|
if previous_link:
|
||||||
|
self.root.append(PREVIOUS_LINK(previous_link))
|
||||||
|
if subtitle:
|
||||||
|
self.root.insert(1, SUBTITLE(subtitle))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return etree.tostring(self.root, pretty_print=True, encoding='utf-8',
|
return etree.tostring(self.root, pretty_print=True, encoding='utf-8',
|
||||||
xml_declaration=True)
|
xml_declaration=True)
|
||||||
|
|
||||||
class TopLevel(Feed):
|
class TopLevel(Feed): # {{{
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
updated, # datetime object in UTC
|
updated, # datetime object in UTC
|
||||||
categories,
|
categories,
|
||||||
version,
|
version,
|
||||||
id_ = 'urn:calibre:main',
|
id_ = 'urn:calibre:main',
|
||||||
|
subtitle = _('Books in your library')
|
||||||
):
|
):
|
||||||
base_href = BASE_HREFS[version]
|
Feed.__init__(self, id_, updated, version, subtitle=subtitle)
|
||||||
self.base_href = base_href
|
|
||||||
subc = partial(NAVCATALOG_ENTRY, base_href, updated)
|
|
||||||
|
|
||||||
|
subc = partial(NAVCATALOG_ENTRY, self.base_href, updated)
|
||||||
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]
|
||||||
|
for x in subcatalogs:
|
||||||
|
self.root.append(x)
|
||||||
|
# }}}
|
||||||
|
|
||||||
self.root = \
|
class AcquisitionFeed(Feed):
|
||||||
FEED(
|
|
||||||
TITLE(__appname__ + ' ' + _('Library')),
|
def __init__(self, updated, id_, items, offsets, page_url, up_url, version):
|
||||||
ID(id_),
|
kwargs = {'up_link': up_url}
|
||||||
UPDATED(updated),
|
kwargs['first_link'] = page_url
|
||||||
SEARCH(base_href),
|
kwargs['last_link'] = page_url+'?offset=%d'%offsets.last_offset
|
||||||
AUTHOR(__appname__, uri='http://calibre-ebook.com'),
|
if offsets.offset > 0:
|
||||||
SUBTITLE(_('Books in your library')),
|
kwargs['previous_link'] = \
|
||||||
*subcatalogs
|
page_url+'?offset=%d'%offsets.previous_offset
|
||||||
)
|
if offsets.next_offset > -1:
|
||||||
|
kwargs['next_offset'] = \
|
||||||
|
page_url+'?offset=%d'%offsets.next_offset
|
||||||
|
Feed.__init__(self, id_, updated, version, **kwargs)
|
||||||
|
|
||||||
STANZA_FORMATS = frozenset(['epub', 'pdb'])
|
STANZA_FORMATS = frozenset(['epub', 'pdb'])
|
||||||
|
|
||||||
|
class OPDSOffsets(object):
|
||||||
|
|
||||||
|
def __init__(self, offset, delta, total):
|
||||||
|
if offset < 0:
|
||||||
|
offset = 0
|
||||||
|
if offset >= total:
|
||||||
|
raise cherrypy.HTTPError(404, 'Invalid offset: %r'%offset)
|
||||||
|
self.offset = offset
|
||||||
|
self.next_offset = offset + delta
|
||||||
|
if self.next_offset >= total:
|
||||||
|
self.next_offset = -1
|
||||||
|
if self.next_offset >= total:
|
||||||
|
self.next_offset = -1
|
||||||
|
self.previous_offset = self.offset - delta
|
||||||
|
if self.previous_offset < 0:
|
||||||
|
self.previous_offset = 0
|
||||||
|
self.last_offset = total - delta
|
||||||
|
if self.last_offset < 0:
|
||||||
|
self.last_offset = 0
|
||||||
|
|
||||||
|
|
||||||
class OPDSServer(object):
|
class OPDSServer(object):
|
||||||
|
|
||||||
def add_routes(self, connect):
|
def add_routes(self, connect):
|
||||||
@ -110,18 +172,39 @@ class OPDSServer(object):
|
|||||||
connect(base, base_href, self.opds, version=version)
|
connect(base, base_href, self.opds, version=version)
|
||||||
connect('opdsnavcatalog_'+base, base_href+'/navcatalog/{which}',
|
connect('opdsnavcatalog_'+base, base_href+'/navcatalog/{which}',
|
||||||
self.opds_navcatalog, version=version)
|
self.opds_navcatalog, version=version)
|
||||||
connect('opdssearch_'+base, base_href+'/search/{terms}',
|
connect('opdssearch_'+base, 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):
|
||||||
search = '' if version > 0 else ' '.join(['format:='+x for x in
|
search = '' if version > 0 else ' '.join(['format:='+x for x in
|
||||||
STANZA_FORMATS])
|
STANZA_FORMATS])
|
||||||
self.seach_cache(search)
|
self.search_cache(search)
|
||||||
|
|
||||||
def opds_search(self, terms=None, version=0):
|
def get_opds_acquisition_feed(self, ids, offset, page_url, up_url, id_,
|
||||||
version = int(version)
|
sort_by='title', ascending=True, version=0):
|
||||||
if not terms or version not in BASE_HREFS:
|
idx = self.db.FIELD_MAP['id']
|
||||||
|
ids &= self.get_opds_allowed_ids_for_version(version)
|
||||||
|
items = [x for x in self.db.data.iterall() if x[idx] in ids]
|
||||||
|
self.sort(items, sort_by, ascending)
|
||||||
|
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))
|
||||||
|
|
||||||
|
def opds_search(self, query=None, version=0, offset=0):
|
||||||
|
try:
|
||||||
|
offset = int(offset)
|
||||||
|
version = int(version)
|
||||||
|
except:
|
||||||
raise cherrypy.HTTPError(404, 'Not found')
|
raise cherrypy.HTTPError(404, 'Not found')
|
||||||
|
if query is None or version not in BASE_HREFS:
|
||||||
|
raise cherrypy.HTTPError(404, 'Not found')
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
def opds_navcatalog(self, which=None, version=0):
|
def opds_navcatalog(self, which=None, version=0):
|
||||||
version = int(version)
|
version = int(version)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user