GwR apple driver wip

This commit is contained in:
GRiker 2010-05-26 02:21:08 -06:00
commit 66e5f106e3
5 changed files with 273 additions and 80 deletions

View File

@ -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:

View File

@ -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'))

View File

@ -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

View File

@ -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()

View File

@ -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)