Windows support for the iPad

This commit is contained in:
Kovid Goyal 2010-05-31 10:04:27 -06:00
commit acba22eb09
3 changed files with 755 additions and 270 deletions

View File

@ -237,6 +237,9 @@ class OutputProfile(Plugin):
# If True the MOBI renderer on the device supports MOBI indexing # If True the MOBI renderer on the device supports MOBI indexing
supports_mobi_indexing = False supports_mobi_indexing = False
# Device supports displaying a nested TOC
supports_nested_toc = True
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
return escape(', '.join(tags)) return escape(', '.join(tags))
@ -250,6 +253,7 @@ class iPadOutput(OutputProfile):
screen_size = (768, 1024) screen_size = (768, 1024)
comic_screen_size = (768, 1024) comic_screen_size = (768, 1024)
dpi = 132.0 dpi = 132.0
supports_nested_toc = False
class SonyReaderOutput(OutputProfile): class SonyReaderOutput(OutputProfile):

View File

@ -5,7 +5,7 @@ __copyright__ = '2010, Gregory Riker'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import cStringIO, os, re, shutil, sys, time, zipfile import cStringIO, os, re, shutil, sys, tempfile, time, zipfile
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre import fit_image from calibre import fit_image
@ -24,8 +24,24 @@ from PIL import Image as PILImage
if isosx: if isosx:
import appscript import appscript
#if iswindows: if iswindows:
# import win32com.client import pythoncom, win32com.client
Sources = [
'Unknown',
'Library',
'iPod',
'AudioCD',
'MP3CD',
'Device',
'RadioTuner',
'SharedLibrary']
ArtworkFormat = [
'Unknown',
'JPEG',
'PNG',
'BMP'
]
class ITUNES(DevicePlugin): class ITUNES(DevicePlugin):
@ -33,20 +49,19 @@ class ITUNES(DevicePlugin):
gui_name = 'Apple device' gui_name = 'Apple device'
icon = I('devices/ipad.png') icon = I('devices/ipad.png')
description = _('Communicate with iBooks through iTunes.') description = _('Communicate with iBooks through iTunes.')
supported_platforms = ['osx'] supported_platforms = ['osx','windows']
author = 'GRiker' author = 'GRiker'
driver_version = '0.1' driver_version = '0.2'
OPEN_FEEDBACK_MESSAGE = _( OPEN_FEEDBACK_MESSAGE = _(
'Apple device detected, launching iTunes, please wait ...') 'Apple device detected, launching iTunes, please wait ...')
FORMATS = ['epub'] FORMATS = ['epub']
VENDOR_ID = [0x05ac]
# Product IDs: # Product IDs:
# 0x129a:iPad
# 0x1292:iPhone 3G # 0x1292:iPhone 3G
#PRODUCT_ID = [0x129a,0x1292] # 0x129a:iPad
VENDOR_ID = [0x05ac]
PRODUCT_ID = [0x129a] PRODUCT_ID = [0x129a]
BCD = [0x01] BCD = [0x01]
@ -56,7 +71,6 @@ class ITUNES(DevicePlugin):
iTunes= None iTunes= None
log = Log() log = Log()
path_template = 'iTunes/%s - %s.epub' path_template = 'iTunes/%s - %s.epub'
presync = False
problem_titles = [] problem_titles = []
problem_msg = None problem_msg = None
report_progress = None report_progress = None
@ -64,7 +78,6 @@ class ITUNES(DevicePlugin):
sources = None sources = None
update_msg = None update_msg = None
update_needed = False update_needed = False
use_thumbnail_as_cover = False
# Public methods # Public methods
def add_books_to_metadata(self, locations, metadata, booklists): def add_books_to_metadata(self, locations, metadata, booklists):
@ -78,13 +91,14 @@ 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')).
''' '''
if DEBUG:
self.log.info( "ITUNES.add_books_to_metadata()")
task_count = float(len(self.update_list)) task_count = float(len(self.update_list))
# Delete any obsolete copies of the book from the booklist # Delete any obsolete copies of the book from the booklist
if self.update_list: if self.update_list:
if isosx:
if DEBUG:
self.log.info( "ITUNES.add_books_to_metadata()")
for (j,p_book) in enumerate(self.update_list): for (j,p_book) in enumerate(self.update_list):
#self.log.info("ITUNES.add_books_to_metadata(): looking for %s" % p_book['lib_book']) #self.log.info("ITUNES.add_books_to_metadata(): looking for %s" % p_book['lib_book'])
for i,bl_book in enumerate(booklists[0]): for i,bl_book in enumerate(booklists[0]):
@ -94,7 +108,25 @@ class ITUNES(DevicePlugin):
#self.log.info("ITUNES.add_books_to_metadata(): removing %s" % p_book['title']) #self.log.info("ITUNES.add_books_to_metadata(): removing %s" % p_book['title'])
break break
else: else:
self.log.error("ITUNES.add_books_to_metadata(): update_list item '%s' not found in booklists[0]" % p_book['title']) self.log.error(" update_list item '%s' not found in booklists[0]" % p_book['title'])
if self.report_progress is not None:
self.report_progress(j+1/task_count, _('Updating device metadata listing...'))
elif iswindows:
if DEBUG:
self.log.info("ITUNES.add_books_to_metadata()")
for (j,p_book) in enumerate(self.update_list):
#self.log.info(" looking for '%s' by %s" % (p_book['title'],p_book['author']))
for i,bl_book in enumerate(booklists[0]):
#self.log.info(" evaluating '%s' by %s" % (bl_book.title,bl_book.author[0]))
if bl_book.title == p_book['title'] and \
bl_book.author[0] == p_book['author']:
booklists[0].pop(i)
self.log.info(" removing outdated version of '%s'" % p_book['title'])
break
else:
self.log.error(" update_list item '%s' not found in booklists[0]" % p_book['title'])
if self.report_progress is not None: if self.report_progress is not None:
self.report_progress(j+1/task_count, _('Updating device metadata listing...')) self.report_progress(j+1/task_count, _('Updating device metadata listing...'))
@ -126,16 +158,16 @@ class ITUNES(DevicePlugin):
if not oncard: if not oncard:
# Fetch a list of books from iPod device connected to iTunes # Fetch a list of books from iPod device connected to iTunes
if isosx:
# Fetch Library|Books # Fetch Library|Books
library_books = self._get_library_books() library_books = self._get_library_books()
if 'iPod' in self.sources: if 'iPod' in self.sources:
device = self.sources['iPod'] #device = self.sources['iPod']
if 'Books' in self.iTunes.sources[device].playlists.name():
booklist = BookList(self.log) booklist = BookList(self.log)
cached_books = {} cached_books = {}
if isosx:
device_books = self._get_device_books() device_books = self._get_device_books()
book_count = float(len(device_books)) book_count = float(len(device_books))
for (i,book) in enumerate(device_books): for (i,book) in enumerate(device_books):
@ -159,19 +191,49 @@ class ITUNES(DevicePlugin):
'lib_book':library_books[this_book.path] if this_book.path in library_books else None 'lib_book':library_books[this_book.path] if this_book.path in library_books else None
} }
if self.report_progress is not None:
self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
device_books = self._get_device_books()
book_count = float(len(device_books))
for (i,book) in enumerate(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.DateAdded)).timetuple()
this_book.db_id = None
this_book.device_collections = []
this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
this_book.size = book.Size
# Hack to discover if we're running in GUI environment
if self.report_progress is not None:
this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
else:
this_book.thumbnail = None
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.report_progress is not None: if self.report_progress is not None:
self.report_progress(i+1/book_count, self.report_progress(i+1/book_count,
_('%d of %d') % (i+1, book_count)) _('%d of %d') % (i+1, book_count))
finally:
pythoncom.CoUninitialize()
if self.report_progress is not None: if self.report_progress is not None:
self.report_progress(1.0, _('finished')) self.report_progress(1.0, _('finished'))
self.cached_books = cached_books self.cached_books = cached_books
if DEBUG: if DEBUG:
self._dump_cached_books() self._dump_cached_books()
return booklist return booklist
else:
# No books installed on this device
return []
else: else:
return [] return []
@ -190,14 +252,16 @@ class ITUNES(DevicePlugin):
This gets called ~1x/second while device fingerprint is sensed This gets called ~1x/second while device fingerprint is sensed
''' '''
if isosx:
if self.iTunes: if self.iTunes:
# Check for connected book-capable device # Check for connected book-capable device
try: try:
'''
names = [s.name() for s in self.iTunes.sources()] names = [s.name() for s in self.iTunes.sources()]
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 'iPod' in sources: '''
self.sources = self._get_sources()
if 'iPod' in self.sources:
if DEBUG: if DEBUG:
sys.stdout.write('.') sys.stdout.write('.')
sys.stdout.flush() sys.stdout.flush()
@ -229,8 +293,35 @@ class ITUNES(DevicePlugin):
:param device_info: On windows a device ID string. On Unix a tuple of :param device_info: On windows a device ID string. On Unix a tuple of
``(vendor_id, product_id, bcd)``. ``(vendor_id, product_id, bcd)``.
''' '''
if self.iTunes:
# Check for connected book-capable device
try:
'''
names = [s.name() 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 = self._get_sources()
if 'iPod' in self.sources:
if DEBUG:
sys.stdout.write('.')
sys.stdout.flush()
return True
else:
if DEBUG:
self.log.info("ITUNES.can_handle(): device ejected")
return False return False
except:
# iTunes connection failed, probably not running anymore
self.log.error("ITUNES.can_handle(): lost connection to iTunes")
return False
else:
# can_handle_windows() is called once before open(), so need to return True
# to keep things going
if DEBUG:
self.log.info("ITUNES:can_handle(): iTunes not yet instantiated")
return True
def card_prefix(self, end_session=True): def card_prefix(self, end_session=True):
''' '''
@ -251,18 +342,28 @@ 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.
If the requested paths are deletable (i.e., it's in the Library|Books list), If the requested paths are deletable (i.e., it's in the Library|Books list),
delete the paths from the library, then update iPad delete the paths from the library, then resync iPad
''' '''
self.problem_titles = [] self.problem_titles = []
self.problem_msg = _("Certain books may only be deleted from within the iBooks app.\n" self.problem_msg = _("Some books not found in iTunes database.\n"
"Delete using the iBooks app.\n"
"Click 'Show Details' for a list.") "Click 'Show Details' for a list.")
for path in paths: for path in paths:
if self.cached_books[path]['lib_book']: if self.cached_books[path]['lib_book']:
if DEBUG: if DEBUG:
self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path)) self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path))
self._remove_iTunes_dir(self.cached_books[path])
self.iTunes.delete(self.cached_books[path]['lib_book']) if isosx:
self._remove_from_iTunes(self.cached_books[path])
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
self._remove_from_iTunes(self.cached_books[path])
finally:
pythoncom.CoUninitialize()
self.update_needed = True self.update_needed = True
self.update_msg = "Deleted books from device" self.update_msg = "Deleted books from device"
else: else:
@ -276,7 +377,16 @@ class ITUNES(DevicePlugin):
''' '''
if DEBUG: if DEBUG:
self.log.info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod']) self.log.info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod'])
if isosx:
self.iTunes.eject(self.sources['iPod']) self.iTunes.eject(self.sources['iPod'])
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
self.iTunes.sources.ItemByName(self.sources['iPod']).EjectIPod()
finally:
pythoncom.CoUninitialize()
self.iTunes = None self.iTunes = None
self.sources = None self.sources = None
@ -299,6 +409,16 @@ class ITUNES(DevicePlugin):
connected_device = self.sources['iPod'] connected_device = self.sources['iPod']
free_space = self.iTunes.sources[connected_device].free_space() free_space = self.iTunes.sources[connected_device].free_space()
elif iswindows:
if 'iPod' in self.sources:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
connected_device = self.sources['iPod']
free_space = self.iTunes.sources.ItemByName(connected_device).FreeSpace
finally:
pythoncom.CoUninitialize()
return (free_space,-1,-1) return (free_space,-1,-1)
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
@ -309,7 +429,7 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
self.log.info("ITUNES:get_device_information()") self.log.info("ITUNES:get_device_information()")
return ('iPad','hw v1.0','sw v1.0', 'mime type') return ('iDevice','hw v1.0','sw v1.0', 'mime type normally goes here')
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
''' '''
@ -351,25 +471,39 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
self.log.info( " %s - %s (%s), driver version %s" % self.log.info( " %s - %s (%s), driver version %s" %
(self.iTunes.name(), self.iTunes.version(), self.driver_version, initial_status)) (self.iTunes.name(), self.iTunes.version(), initial_status, self.driver_version))
# Init the iTunes source list # Init the iTunes source list
'''
names = [s.name() for s in self.iTunes.sources()] names = [s.name() for s in self.iTunes.sources()]
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 = dict(zip(kinds,names)) self.sources = dict(zip(kinds,names))
'''
self.sources = self._get_sources()
# Check to see if Library|Books out of sync with Device|Books elif iswindows:
if self.presync and 'iPod' in self.sources : # Launch iTunes if not already running
lb_count = len(self._get_library_books())
db_count = len(self._get_device_books())
pb_count = len(self._get_purchased_book_ids())
if db_count != lb_count + pb_count:
if DEBUG: if DEBUG:
self.log.info( "ITUNES.open(): pre-syncing iTunes with device") self.log.info("ITUNES:open(): Instantiating iTunes")
self.log.info( " Library|Books : %d" % lb_count)
self.log.info( " Devices|iPad|Books : %d" % db_count) # Instantiate iTunes
self.log.info( " Devices|iPad|Purchased: %d" % pb_count) try:
self._update_device(msg="Presyncing iTunes with device, mismatched book count") pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
if not DEBUG:
self.iTunes.Windows[0].Minimized = True
initial_status = 'launched'
if DEBUG:
self.log.info( " %s - %s (%s), driver version %s" %
(self.iTunes.Windows[0].name, self.iTunes.Version, initial_status, self.driver_version))
# Init the iTunes source list
self.sources = self._get_sources()
finally:
pythoncom.CoUninitialize()
# Confirm/create thumbs archive # Confirm/create thumbs archive
archive_path = os.path.join(self.cache_dir, "thumbs.zip") archive_path = os.path.join(self.cache_dir, "thumbs.zip")
@ -388,20 +522,6 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
self.log.info(" existing thumb cache at '%s'" % archive_path) self.log.info(" existing thumb cache at '%s'" % archive_path)
if iswindows:
# Launch iTunes if not already running
if DEBUG:
self.log.info("ITUNES:open(): Instantiating iTunes")
# Instantiate iTunes
# Init the iTunes source list
# Check to see if Library|Books out of sync with Device|Books
# Confirm/create thumbs archive
def remove_books_from_metadata(self, paths, booklists): def remove_books_from_metadata(self, paths, booklists):
''' '''
Remove books from the metadata list. This function must not communicate Remove books from the metadata list. This function must not communicate
@ -486,9 +606,29 @@ class ITUNES(DevicePlugin):
# Get actual size of updated books on device # Get actual size of updated books on device
if self.update_list: if self.update_list:
if DEBUG: if DEBUG:
self.log.info("ITUNES:sync_booklists(): update_list:") self.log.info("ITUNES:sync_booklists()\n update_list:")
for ub in self.update_list: for ub in self.update_list:
self.log.info(" '%s'" % ub['title']) self.log.info(" '%s' by %s" % (ub['title'], ub['author']))
if isosx:
for updated_book in self.update_list:
size_on_device = self._get_device_book_size(updated_book['title'], updated_book['author'])
if size_on_device:
for book in booklists[0]:
if book.title == updated_book['title'] and \
book.author[0] == updated_book['author']:
book.size = size_on_device
break
else:
self.log.error("ITUNES:sync_booklists(): could not update book size for '%s'" % updated_book['title'])
else:
self.log.error("ITUNES:sync_booklists(): could not find '%s' on device" % updated_book['title'])
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
for updated_book in self.update_list: for updated_book in self.update_list:
size_on_device = self._get_device_book_size(updated_book['title'], updated_book['author']) size_on_device = self._get_device_book_size(updated_book['title'], updated_book['author'])
@ -503,6 +643,9 @@ class ITUNES(DevicePlugin):
else: else:
self.log.error("ITUNES:sync_booklists(): could not find '%s' on device" % updated_book['title']) self.log.error("ITUNES:sync_booklists(): could not find '%s' on device" % updated_book['title'])
finally:
pythoncom.CoUninitialize()
self.update_list = [] self.update_list = []
# Inform user of any problem books # Inform user of any problem books
@ -556,17 +699,14 @@ class ITUNES(DevicePlugin):
new_booklist = [] new_booklist = []
self.update_list = [] self.update_list = []
strip_tags = re.compile(r'<[^<]*?/?>') strip_tags = re.compile(r'<[^<]*?/?>')
if isosx:
file_count = float(len(files)) file_count = float(len(files))
self.problem_titles = [] self.problem_titles = []
self.problem_msg = _("Some cover art could not be converted.\n" self.problem_msg = _("Some cover art could not be converted.\n"
"Click 'Show Details' for a list.") "Click 'Show Details' for a list.")
if isosx:
for (i,file) in enumerate(files): for (i,file) in enumerate(files):
path = self.path_template % (metadata[i].title, metadata[i].author[0]) path = self.path_template % (metadata[i].title, metadata[i].author[0])
# Delete existing from Library|Books, add to self.update_list # Delete existing from Library|Books, add to self.update_list
# for deletion from booklist[0] during add_books_to_metadata # for deletion from booklist[0] during add_books_to_metadata
if path in self.cached_books: if path in self.cached_books:
@ -575,8 +715,7 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
self.log.info("ITUNES.upload_books():") self.log.info("ITUNES.upload_books():")
self.log.info( " deleting existing '%s'" % (path)) self.log.info( " deleting existing '%s'" % (path))
self._remove_iTunes_dir(self.cached_books[path]) self._remove_from_iTunes(self.cached_books[path])
self.iTunes.delete(self.cached_books[path]['lib_book'])
# Add to iTunes Library|Books # Add to iTunes Library|Books
if isinstance(file,PersistentTemporaryFile): if isinstance(file,PersistentTemporaryFile):
@ -585,12 +724,8 @@ class ITUNES(DevicePlugin):
added = self.iTunes.add(appscript.mactypes.File(file)) added = self.iTunes.add(appscript.mactypes.File(file))
thumb = None thumb = None
if metadata[i].cover:
try: try:
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 # Use cover data as artwork
cover_data = open(metadata[i].cover,'rb') cover_data = open(metadata[i].cover,'rb')
added.artworks[1].data_.set(cover_data.read()) added.artworks[1].data_.set(cover_data.read())
@ -631,6 +766,7 @@ class ITUNES(DevicePlugin):
# Flesh out the iTunes metadata # Flesh out the iTunes metadata
added.description.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S')) added.description.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S'))
if metadata[i].comments:
added.comment.set(strip_tags.sub('',metadata[i].comments)) added.comment.set(strip_tags.sub('',metadata[i].comments))
if metadata[i].rating: if metadata[i].rating:
added.rating.set(metadata[i].rating*10) added.rating.set(metadata[i].rating*10)
@ -654,8 +790,146 @@ class ITUNES(DevicePlugin):
# Report progress # Report progress
if self.report_progress is not None: if self.report_progress is not None:
self.report_progress(i+1/file_count, self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
_('%d of %d') % (i+1, file_count)) elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
lib = self.iTunes.sources.ItemByName('Library')
lib_playlists = [pl.Name for pl in lib.Playlists]
if not 'Books' in lib_playlists:
self.log.error(" no 'Books' playlist in Library")
library_books = lib.Playlists.ItemByName('Books')
for (i,file) in enumerate(files):
path = self.path_template % (metadata[i].title, metadata[i].author[0])
# Delete existing from Library|Books, add to self.update_list
# for deletion from booklist[0] during add_books_to_metadata
if path in self.cached_books:
self.update_list.append(self.cached_books[path])
if DEBUG:
self.log.info("ITUNES.upload_books():")
self.log.info( " deleting existing '%s'" % (path))
self._remove_from_iTunes(self.cached_books[path])
else:
if DEBUG:
self.log.info(" '%s' not in cached_books" % metadata[i].title)
# Add to iTunes Library|Books
if isinstance(file,PersistentTemporaryFile):
op_status = library_books.AddFile(file._name)
self.log.info("ITUNES.upload_books():\n iTunes adding '%s'" % file._name)
else:
op_status = library_books.AddFile(file)
self.log.info(" iTunes adding '%s'" % file)
if DEBUG:
sys.stdout.write(" iTunes copying '%s' ..." % metadata[i].title)
sys.stdout.flush()
while op_status.InProgress:
time.sleep(0.5)
if DEBUG:
sys.stdout.write('.')
sys.stdout.flush()
if DEBUG:
sys.stdout.write("\n")
sys.stdout.flush()
if False:
# According to the Apple API, .Tracks should be populated once the xfer
# is complete, but I can't seem to make that work.
if DEBUG:
sys.stdout.write(" waiting for handle to '%s' ..." % metadata[i].title)
sys.stdout.flush()
while not op_status.Tracks:
time.sleep(0.5)
if DEBUG:
sys.stdout.write('.')
sys.stdout.flush()
if DEBUG:
print
added = op_status.Tracks.Item[1]
else:
# This approach simply scans Library|Books for the book we just added
added = self._find_library_book(
{'title': metadata[i].title,'author': metadata[i].author[0]})
if not added:
self.log.error("ITUNES.upload_books():\n could not find added book in iTunes")
thumb = None
# Use cover data as artwork
if metadata[i].cover:
if added.Artwork.Count:
added.Artwork.Item(1).SetArtworkFromFile(metadata[i].cover)
else:
added.AddArtworkFromFile(metadata[i].cover)
try:
# 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()
# Refresh the thumbnail cache
if DEBUG:
self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title)
archive_path = os.path.join(self.cache_dir, "thumbs.zip")
zfw = zipfile.ZipFile(archive_path, mode='a')
thumb_path = path.rpartition('.')[0] + '.jpg'
zfw.writestr(thumb_path, thumb)
zfw.close()
except:
self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0]))
self.log.error("ITUNES.upload_books():\n error converting '%s' to thumb for '%s'" % (metadata[i].cover,metadata[i].title))
# Create a new Book
this_book = Book(metadata[i].title, metadata[i].author[0])
this_book.datetime = parse_date(str(added.DateAdded)).timetuple()
this_book.db_id = None
this_book.device_collections = []
this_book.library_id = added
this_book.path = path
this_book.size = added.Size # Updated later from actual storage size
this_book.thumbnail = thumb
this_book.iTunes_id = added
new_booklist.append(this_book)
# Flesh out the iTunes metadata
added.Description = ("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S'))
if metadata[i].comments:
added.Comment = (strip_tags.sub('',metadata[i].comments))
if metadata[i].rating:
added.AlbumRating = (metadata[i].rating*10)
added.SortArtist = (metadata[i].author_sort.title())
added.SortName = (this_book.title_sorter)
# Set genre from metadata
# iTunes grabs the first dc:subject from the opf metadata,
# But we can manually override with first tag starting with alpha
for tag in metadata[i].tags:
if self._is_alpha(tag[0]):
added.Category = (tag)
break
# Add new_book to self.cached_paths
self.cached_books[this_book.path] = {
'title': metadata[i].title,
'author': metadata[i].author[0],
'lib_book': added
}
# Report progress
if self.report_progress is not None:
self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
finally:
pythoncom.CoUninitialize()
if self.report_progress is not None: if self.report_progress is not None:
self.report_progress(1.0, _('finished')) self.report_progress(1.0, _('finished'))
@ -699,42 +973,49 @@ class ITUNES(DevicePlugin):
N+=length N+=length
print result print result
def _get_library_books(self): def _find_device_book(self, cached_book):
''' '''
Windows-only method to get a handle to a device book in the current pythoncom session
''' '''
lib = self.iTunes.sources['library'] SearchField = ['All','Visible','Artists','Titles','Composers','SongNames']
library_books = {} if iswindows:
if 'Books' in lib.playlists.name(): dev_books = self.iTunes.sources.ItemByName(self.sources['iPod']).Playlists.ItemByName('Books')
lib_books = lib.playlists['Books'].file_tracks() hits = dev_books.Search(cached_book['title'],SearchField.index('Titles'))
for book in lib_books: if hits:
path = self.path_template % (book.name(), book.artist()) for hit in hits:
library_books[path] = book if hit.Artist == cached_book['author']:
return library_books return hit
def _get_device_book_size(self, title, author):
'''
Fetch the size of a book stored on the device
'''
if DEBUG:
self.log.info("ITUNES._get_device_book_size(): looking for title: '%s' author: %s" % (title,author))
device_books = self._get_device_books()
for d_book in device_books:
if DEBUG:
self.log.info(" evaluating title: '%s' author: '%s'" % (d_book.name(), d_book.artist()))
if d_book.name() == title and d_book.artist() == author:
return d_book.size()
else:
self.log.error("ITUNES._get_device_book_size(): could not find '%s' by '%s' in device_books" % (title,author))
return None return None
def _get_device_books(self): def _find_library_book(self, cached_book):
''' '''
Windows-only method to get a handle to a library book in the current pythoncom session
''' '''
if 'iPod' in self.sources: SearchField = ['All','Visible','Artists','Titles','Composers','SongNames']
device = self.sources['iPod'] if iswindows:
if 'Books' in self.iTunes.sources[device].playlists.name(): if DEBUG:
return self.iTunes.sources[device].playlists['Books'].file_tracks() self.log.info("ITUNES._find_library_book()")
self.log.info(" looking for '%s' by %s" % (cached_book['title'], cached_book['author']))
lib_books = self.iTunes.sources.ItemByName('Library').Playlists.ItemByName('Books')
attempts = 9
while attempts:
# Find all books by this author, then match title
hits = lib_books.Search(cached_book['author'],SearchField.index('Artists'))
if hits:
for hit in hits:
self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
if hit.Name == cached_book['title']:
self.log.info(" matched '%s' by %s" % (hit.Name, hit.Artist))
return hit
attempts -= 1
time.sleep(0.5)
if DEBUG:
self.log.warning(" attempt #%d" % (10 - attempts))
if DEBUG:
self.log.error(" search yielded no hits")
return None
def _generate_thumbnail(self, book_path, book): def _generate_thumbnail(self, book_path, book):
''' '''
@ -754,9 +1035,14 @@ class ITUNES(DevicePlugin):
zfw = zipfile.ZipFile(archive_path, mode='a') zfw = zipfile.ZipFile(archive_path, mode='a')
else: else:
if DEBUG: if DEBUG:
if isosx:
self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.name()) self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.name())
elif iswindows:
self.log.info("ITUNES._generate_thumbnail(): cached thumb found for '%s'" % book.Name)
return thumb_data return thumb_data
if isosx:
try: try:
# Resize the cover # Resize the cover
data = book.artworks[1].raw_data().data data = book.artworks[1].raw_data().data
@ -777,15 +1063,150 @@ class ITUNES(DevicePlugin):
self.log.error("ITUNES._generate_thumbnail(): error generating thumb for '%s'" % book.name()) self.log.error("ITUNES._generate_thumbnail(): error generating thumb for '%s'" % book.name())
return None return None
elif iswindows:
if DEBUG:
self.log.info("ITUNES._generate_thumbnail()")
if not book.Artwork.Count:
if DEBUG:
self.log.info(" no artwork available")
return None
# Save the cover from iTunes
tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % ArtworkFormat[book.Artwork.Item(1).Format])
book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb)
try:
# Resize the cover
im = PILImage.open(tmp_thumb)
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')
os.remove(tmp_thumb)
# Cache the tagged thumb
if DEBUG:
self.log.info(" generated thumb for '%s', caching" % book.Name)
zfw.writestr(thumb_path, thumb.getvalue())
zfw.close()
return thumb.getvalue()
except:
self.log.error(" error generating thumb for '%s'" % book.Name)
return None
def _get_device_book_size(self, title, author):
'''
Fetch the size of a book stored on the device
'''
if DEBUG:
self.log.info("ITUNES._get_device_book_size():\n looking for title: '%s' author: %s" % (title,author))
device_books = self._get_device_books()
if isosx:
for d_book in device_books:
if DEBUG:
self.log.info(" evaluating title: '%s' author: '%s'" % (d_book.name(), d_book.artist()))
if d_book.name() == title and d_book.artist() == author:
return d_book.size()
else:
self.log.error("ITUNES._get_device_book_size(): could not find '%s' by '%s' in device_books" % (title,author))
return None
elif iswindows:
for d_book in device_books:
'''
if DEBUG:
self.log.info(" evaluating title: '%s' author: '%s'" % (d_book.Name, d_book.Artist))
'''
if d_book.Name == title and d_book.Artist == author:
self.log.info(" found it")
return d_book.Size
else:
self.log.error(" could not find '%s' by '%s' in device_books" % (title,author))
return None
def _get_device_books(self):
'''
Assumes pythoncom wrapper
'''
if isosx:
if 'iPod' in self.sources:
connected_device = self.sources['iPod']
if 'Books' in self.iTunes.sources[connected_device].playlists.name():
return self.iTunes.sources[connected_device].playlists['Books'].file_tracks()
return []
elif iswindows:
if 'iPod' in self.sources:
connected_device = self.sources['iPod']
dev = self.iTunes.sources.ItemByName(connected_device)
dev_playlists = [pl.Name for pl in dev.Playlists]
if 'Books' in dev_playlists:
return self.iTunes.sources.ItemByName(connected_device).Playlists.ItemByName('Books').Tracks
return []
def _get_library_books(self):
'''
Populate a dict of paths from iTunes Library|Books
'''
library_books = {}
if isosx:
lib = self.iTunes.sources['library']
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
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
lib = self.iTunes.sources.ItemByName('Library')
lib_playlists = [pl.Name for pl in lib.Playlists]
if 'Books' in lib_playlists:
lib_books = lib.Playlists.ItemByName('Books').Tracks
for book in lib_books:
path = self.path_template % (book.Name, book.Artist)
library_books[path] = book
finally:
pythoncom.CoUninitialize()
return library_books
def _get_purchased_book_ids(self): def _get_purchased_book_ids(self):
''' '''
Return Device|Purchased
''' '''
if 'iPod' in self.sources: if 'iPod' in self.sources:
device = self.sources['iPod'] connected_device = self.sources['iPod']
if 'Purchased' in self.iTunes.sources[device].playlists.name(): if isosx:
return [pb.database_ID() for pb in self.iTunes.sources[device].playlists['Purchased'].file_tracks()] if 'Purchased' in self.iTunes.sources[connected_device].playlists.name():
return [pb.database_ID() for pb in self.iTunes.sources[connected_device].playlists['Purchased'].file_tracks()]
else: else:
return [] return []
elif iswindows:
dev = self.iTunes.sources.ItemByName(connected_device)
dev_playlists = [pl.Name for pl in dev.Playlists]
if 'Purchased' in dev_playlists:
return self.iTunes.sources.ItemByName(connected_device).Playlists.ItemByName('Purchased').Tracks
else:
return []
def _get_sources(self):
'''
Return a dict of sources
'''
if isosx:
names = [s.name() for s in self.iTunes.sources()]
kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()]
return dict(zip(kinds,names))
elif iswindows:
it_sources = ['Unknown','Library','iPod','AudioCD','MP3CD','Device','RadioTuner','SharedLibrary']
names = [s.name for s in self.iTunes.sources]
kinds = [it_sources[s.kind] for s in self.iTunes.sources]
return dict(zip(kinds,names))
def _is_alpha(self,char): def _is_alpha(self,char):
''' '''
@ -795,34 +1216,90 @@ class ITUNES(DevicePlugin):
else: else:
return True return True
def _remove_iTunes_dir(self, cached_book): def _remove_from_iTunes(self, cached_book):
''' '''
iTunes does not delete books from storage when removing from database iTunes does not delete books from storage when removing from database
''' '''
if isosx:
storage_path = os.path.split(cached_book['lib_book'].location().path) storage_path = os.path.split(cached_book['lib_book'].location().path)
if DEBUG: if DEBUG:
self.log.info( "ITUNES._remove_iTunes_dir():") self.log.info("ITUNES._remove_from_iTunes():")
self.log.info(" removing storage_path: %s" % storage_path[0]) self.log.info(" removing storage_path: %s" % storage_path[0])
shutil.rmtree(storage_path[0]) shutil.rmtree(storage_path[0])
self.iTunes.delete(cached_book['lib_book'])
elif iswindows:
# Assume we're wrapped in a pythoncom
# Windows stores the book under a common author directory, so we just delete the .epub
if DEBUG:
self.log.info("ITUNES._remove_from_iTunes(): '%s'" % cached_book['title'])
book = self._find_library_book(cached_book)
if book:
if DEBUG:
self.log.info("ITUNES._remove_from_iTunes():\n deleting '%s' at %s" %
(cached_book['title'], book.Location))
folder = os.path.split(book.Location)[0]
path = book.Location
book.Delete()
try:
os.remove(path)
except:
self.log.warning(" could not find '%s' in iTunes storage" % path)
try:
os.rmdir(folder)
self.log.info(" removed folder '%s'" % folder)
except:
self.log.info(" folder '%s' not found or not empty" % folder)
else:
self.log.warning(" could not find '%s' in iTunes storage" % cached_book['title'])
def _update_device(self, msg='', wait=True): def _update_device(self, msg='', wait=True):
''' '''
Trigger a sync, wait for completion
''' '''
if DEBUG: if DEBUG:
self.log.info("ITUNES:_update_device(): %s" % msg) self.log.info("ITUNES:_update_device():\n %s" % msg)
if isosx:
self.iTunes.update() self.iTunes.update()
if wait: if wait:
# This works if iTunes has books not yet synced to iPad. # This works if iTunes has books not yet synced to iPad.
if DEBUG: if DEBUG:
self.log.info("Waiting for iPad sync to complete ...",) sys.stdout.write(" waiting for iPad sync to complete ...")
sys.stdout.flush()
while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())): while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())):
if DEBUG: if DEBUG:
sys.stdout.write('.') sys.stdout.write('.')
sys.stdout.flush() sys.stdout.flush()
time.sleep(2) time.sleep(2)
print print
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
#result = self.iTunes.UpdateIPod()
if wait:
if DEBUG:
sys.stdout.write(" waiting for iPad sync to complete ...")
sys.stdout.flush()
while True:
db_count = len(self._get_device_books())
lb_count = len(self._get_library_books())
pb_count = len(self._get_purchased_book_ids())
if db_count != lb_count + pb_count:
if DEBUG:
sys.stdout.write('.')
sys.stdout.flush()
time.sleep(2)
else:
sys.stdout.write('\n')
sys.stdout.flush()
break
finally:
pythoncom.CoUninitialize()
class BookList(list): class BookList(list):
''' '''
@ -855,7 +1332,7 @@ class BookList(list):
metadata. Return True if booklists must be sync'ed metadata. Return True if booklists must be sync'ed
''' '''
if DEBUG: if DEBUG:
self.log.info("BookList.add_book(): adding %s" % book) self.log.info("BookList.add_book():\n%s" % book)
self.append(book) self.append(book)
def remove_book(self, book): def remove_book(self, book):

View File

@ -4,9 +4,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: calibre 0.6.95\n" "Project-Id-Version: calibre 0.6.55\n"
"POT-Creation-Date: 2010-05-30 22:56+MDT\n" "POT-Creation-Date: 2010-05-31 10:04+MDT\n"
"PO-Revision-Date: 2010-05-30 22:56+MDT\n" "PO-Revision-Date: 2010-05-31 10:04+MDT\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n" "Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -247,7 +247,7 @@ msgid "This profile tries to provide sane defaults and is useful if you know not
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:57 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:57
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:258 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:262
msgid "This profile is intended for the SONY PRS line. The 500/505/600/700 etc." msgid "This profile is intended for the SONY PRS line. The 500/505/600/700 etc."
msgstr "" msgstr ""
@ -256,62 +256,62 @@ msgid "This profile is intended for the SONY PRS 300."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:78 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:78
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:292 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:296
msgid "This profile is intended for the SONY PRS-900." msgid "This profile is intended for the SONY PRS-900."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:86 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:86
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:322 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:326
msgid "This profile is intended for the Microsoft Reader." msgid "This profile is intended for the Microsoft Reader."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:97 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:97
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:333 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:337
msgid "This profile is intended for the Mobipocket books." msgid "This profile is intended for the Mobipocket books."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:110 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:110
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:346 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:350
msgid "This profile is intended for the Hanlin V3 and its clones." msgid "This profile is intended for the Hanlin V3 and its clones."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:122 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:122
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:358 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:362
msgid "This profile is intended for the Hanlin V5 and its clones." msgid "This profile is intended for the Hanlin V5 and its clones."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:132 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:132
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:366 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:370
msgid "This profile is intended for the Cybook G3." msgid "This profile is intended for the Cybook G3."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:145 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:145
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:379 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:383
msgid "This profile is intended for the Cybook Opus." msgid "This profile is intended for the Cybook Opus."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:157 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:157
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:390 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:394
msgid "This profile is intended for the Amazon Kindle." msgid "This profile is intended for the Amazon Kindle."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:169 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:169
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:425 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:429
msgid "This profile is intended for the Irex Illiad." msgid "This profile is intended for the Irex Illiad."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:181 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:181
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:438 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:442
msgid "This profile is intended for the IRex Digital Reader 1000." msgid "This profile is intended for the IRex Digital Reader 1000."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:194 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:194
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:452 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:456
msgid "This profile is intended for the IRex Digital Reader 800." msgid "This profile is intended for the IRex Digital Reader 800."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:206 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:206
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:466 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:470
msgid "This profile is intended for the B&N Nook." msgid "This profile is intended for the B&N Nook."
msgstr "" msgstr ""
@ -323,27 +323,27 @@ msgstr ""
msgid "This profile tries to provide sane defaults and is useful if you want to produce a document intended to be read at a computer or on a range of devices." msgid "This profile tries to provide sane defaults and is useful if you want to produce a document intended to be read at a computer or on a range of devices."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:248 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:251
msgid "Intended for the iPad and similar devices with a resolution of 768x1024" msgid "Intended for the iPad and similar devices with a resolution of 768x1024"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:271 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:275
msgid "This profile is intended for the Kobo Reader." msgid "This profile is intended for the Kobo Reader."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:283 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:287
msgid "This profile is intended for the SONY PRS-300." msgid "This profile is intended for the SONY PRS-300."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:301 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:305
msgid "This profile is intended for the 5-inch JetBook." msgid "This profile is intended for the 5-inch JetBook."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:310 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:314
msgid "This profile is intended for the SONY PRS line. The 500/505/700 etc, in landscape mode. Mainly useful for comics." msgid "This profile is intended for the SONY PRS line. The 500/505/700 etc, in landscape mode. Mainly useful for comics."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:408 #: /home/kovid/work/calibre/src/calibre/customize/profiles.py:412
msgid "This profile is intended for the Amazon Kindle DX." msgid "This profile is intended for the Amazon Kindle DX."
msgstr "" msgstr ""
@ -415,46 +415,50 @@ msgstr ""
msgid "Communicate with S60 phones." msgid "Communicate with S60 phones."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:35 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:51
msgid "Communicate with iBooks through iTunes." msgid "Communicate with iBooks through iTunes."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:40 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:56
msgid "Apple device detected, launching iTunes, please wait ..." msgid "Apple device detected, launching iTunes, please wait ..."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:100 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:114
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:103 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:132
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:135
msgid "Updating device metadata listing..." msgid "Updating device metadata listing..."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:164 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:195
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:658 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:226
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:793
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:930
msgid "%d of %d" msgid "%d of %d"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:167 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:232
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:661 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:935
msgid "finished" msgid "finished"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:258 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:349
msgid "" msgid ""
"Certain books may only be deleted from within the iBooks app.\n" "Some books not found in iTunes database.\n"
"Delete using the iBooks app.\n"
"Click 'Show Details' for a list." "Click 'Show Details' for a list."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:468 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:588
#: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:28 #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:28
msgid "settings for device drivers" msgid "settings for device drivers"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:470 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:590
#: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:30 #: /home/kovid/work/calibre/src/calibre/devices/usbms/deviceconfig.py:30
msgid "Ordered list of formats the device will accept" msgid "Ordered list of formats the device will accept"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:564 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:704
msgid "" msgid ""
"Some cover art could not be converted.\n" "Some cover art could not be converted.\n"
"Click 'Show Details' for a list." "Click 'Show Details' for a list."