GwR wip 0.5.0

This commit is contained in:
GRiker 2010-06-09 12:44:17 -06:00
parent eb97c9d963
commit 5c316d4d61

View File

@ -5,7 +5,7 @@ __copyright__ = '2010, Gregory Riker'
__docformat__ = 'restructuredtext en'
import cStringIO, os, re, shutil, subprocess, sys, tempfile, time, zipfile
import cStringIO, ctypes, os, re, shutil, subprocess, sys, tempfile, time, zipfile
from calibre.constants import DEBUG
from calibre import fit_image
@ -14,6 +14,7 @@ from calibre.devices.interface import DevicePlugin
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.ebooks.metadata import MetaInformation
from calibre.library.server.utils import strftime
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import Config, config_dir
from calibre.utils.date import parse_date
from calibre.utils.logging import Log
@ -47,7 +48,7 @@ class ITUNES(DevicePlugin):
supported_platforms = ['osx','windows']
author = 'GRiker'
#: The version of this plugin as a 3-tuple (major, minor, revision)
version = (0, 4, 11)
version = (0, 5, 0)
OPEN_FEEDBACK_MESSAGE = _(
'Apple device detected, launching iTunes, please wait ...')
@ -101,6 +102,15 @@ class ITUNES(DevicePlugin):
'Books',
]
SearchField = [
'All',
'Visible',
'Artists',
'Albums',
'Composers',
'SongNames',
]
# Properties
cached_books = {}
cache_dir = os.path.join(config_dir, 'caches', 'itunes')
@ -108,6 +118,7 @@ class ITUNES(DevicePlugin):
iTunes= None
iTunes_media = None
log = Log()
manual_sync_mode = False
path_template = 'iTunes/%s - %s.epub'
problem_titles = []
problem_msg = None
@ -182,9 +193,6 @@ class ITUNES(DevicePlugin):
(new_book.title, new_book.author))
booklists[0].append(new_book)
# if DEBUG:
# self._dump_booklist(booklists[0],'after add_books_to_metadata()')
def books(self, oncard=None, end_session=True):
"""
Return a list of ebooks on the device.
@ -210,7 +218,6 @@ class ITUNES(DevicePlugin):
library_books = self._get_library_books()
if 'iPod' in self.sources:
#device = self.sources['iPod']
booklist = BookList(self.log)
cached_books = {}
@ -238,7 +245,8 @@ class ITUNES(DevicePlugin):
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
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
'dev_book':book
}
if self.report_progress is not None:
@ -342,7 +350,7 @@ class ITUNES(DevicePlugin):
self.log.warning(" waiting for identified iPad, attempt #%d" % (10 - attempts))
else:
if DEBUG:
self.log.info(' found connected iPad in iTunes')
self.log.info(' found connected iPad')
break
else:
# iTunes running, but not connected iPad
@ -350,9 +358,8 @@ class ITUNES(DevicePlugin):
self.log.info(' self.ejected = True')
self.ejected = True
return False
else:
self.log.info(' found connected iPad in sources')
self._discover_manual_sync_mode()
return True
def can_handle_windows(self, device_id, debug=False):
@ -384,6 +391,7 @@ class ITUNES(DevicePlugin):
if DEBUG:
self.log.info('ITUNES.can_handle_windows:\n confirming connected iPad')
self.ejected = False
self._discover_manual_sync_mode()
return True
else:
if DEBUG:
@ -399,9 +407,6 @@ class ITUNES(DevicePlugin):
pythoncom.CoUninitialize()
else:
# This is called at entry
# We need to know if iTunes sees the iPad
# It may have been ejected
if DEBUG:
self.log.info("ITUNES:can_handle_windows():\n Launching iTunes")
@ -429,8 +434,10 @@ class ITUNES(DevicePlugin):
self.log.info(' self.ejected = True')
self.ejected = True
return False
else:
self.log.info(' found connected iPad in sources')
self.log.info(' found connected iPad in sources')
self._discover_manual_sync_mode(wait=1.0)
finally:
pythoncom.CoUninitialize()
@ -460,23 +467,31 @@ class ITUNES(DevicePlugin):
self.problem_msg = _("Some books not found in iTunes database.\n"
"Delete using the iBooks app.\n"
"Click 'Show Details' for a list.")
self.log.info("ITUNES:delete_books()")
for path in paths:
if self.cached_books[path]['lib_book']:
if DEBUG:
self.log.info("ITUNES:delete_books(): Deleting '%s' from iTunes library" % (path))
self.log.info(" Deleting '%s' from iTunes library" % (path))
if isosx:
self._remove_from_iTunes(self.cached_books[path])
if self.manual_sync_mode:
self._remove_device_book(self.cached_books[path])
elif iswindows:
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
self._remove_from_iTunes(self.cached_books[path])
if self.manual_sync_mode:
self._remove_device_book(self.cached_books[path])
finally:
pythoncom.CoUninitialize()
self.update_needed = True
self.update_msg = "Deleted books from device"
if not self.manual_sync_mode:
self.update_needed = True
self.update_msg = "Deleted books from device"
else:
self.log.info(" skipping sync phase, manual_sync_mode: True")
else:
self.problem_titles.append("'%s' by %s" %
(self.cached_books[path]['title'],self.cached_books[path]['author']))
@ -618,7 +633,7 @@ class ITUNES(DevicePlugin):
# Remove from cached_books
self.cached_books.pop(path)
if DEBUG:
self.log.info(" Removing '%s' from self.cached_books" % path)
self.log.info(" removing '%s' from self.cached_books" % path)
# self._dump_cached_books('remove_books_from_metadata()')
else:
self.log.warning(" skipping purchased book, can't remove via automation interface")
@ -740,34 +755,6 @@ class ITUNES(DevicePlugin):
# Delete existing from Library|Books
# Add to self.update_list for deletion from booklist[0] during add_books_to_metadata
'''
# ---------------------------
# PROVISIONAL
# Use the cover to find the database storage point of the epub
# Pass database copy to iTunes instead of the temporary file
if False:
if DEBUG:
self.log.info(" processing '%s'" % metadata[i].title)
self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file))
self.log.info(" cover: %s" % metadata[i].cover)
calibre_database_item = False
if metadata[i].cover:
passed_file = file
storage_path = os.path.split(metadata[i].cover)[0]
try:
database_epub = filter(lambda x: x.endswith('.epub'), os.listdir(storage_path))[0]
file = os.path.join(storage_path,database_epub)
calibre_database_item = True
self.log.info(" using database file: %s" % file)
except:
self.log.info(" could not find epub in %s" % storage_path)
else:
self.log.info(" no cover available, using temp file")
# ---------------------------
'''
path = self.path_template % (metadata[i].title, metadata[i].author[0])
if path in self.cached_books:
if DEBUG:
@ -778,6 +765,21 @@ class ITUNES(DevicePlugin):
if DEBUG:
self.log.info( " deleting existing '%s'" % (path))
self._remove_from_iTunes(self.cached_books[path])
if self.manual_sync_mode:
dev_book_added = self._remove_device_book(self.cached_books[path])
'''
Old code testing for PTO
Use this with manuals_sync_mode to decide whether to add to Library|Books
if DEBUG:
self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file))
# Add to iTunes Library|Books
if isinstance(file,PersistentTemporaryFile):
added = self.iTunes.add(appscript.mactypes.File(file._name))
else:
added = self.iTunes.add(appscript.mactypes.File(file))
'''
# Add to iTunes Library|Books
fpath = file
@ -785,7 +787,18 @@ class ITUNES(DevicePlugin):
fpath = file.orig_file_path
elif getattr(file, 'name', None) is not None:
fpath = file.name
added = self.iTunes.add(appscript.mactypes.File(fpath))
if isinstance(file,PersistentTemporaryFile) and self.manual_sync_mode:
if DEBUG:
self.log.info(" PTF not added to Library|Books")
else:
added = self.iTunes.add(appscript.mactypes.File(fpath))
if DEBUG:
self.log.info(" file added to Library|Books")
dev_book_added = None
if self.manual_sync_mode:
dev_book_added = self._add_device_book(fpath)
thumb = None
if metadata[i].cover:
@ -853,7 +866,8 @@ class ITUNES(DevicePlugin):
self.cached_books[this_book.path] = {
'title': this_book.title,
'author': this_book.author,
'lib_book': added
'lib_book': added,
'dev_book': dev_book_added
}
# Report progress
@ -864,62 +878,12 @@ class ITUNES(DevicePlugin):
try:
pythoncom.CoInitialize()
self.iTunes = win32com.client.Dispatch("iTunes.Application")
for source in self.iTunes.sources:
if source.Kind == self.Sources.index('Library'):
lib = source
if DEBUG:
self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
break
else:
if DEBUG:
self.log.info(" Library source not found")
if lib is not None:
lib_books = None
for pl in lib.Playlists:
if pl.Kind == self.PlaylistKind.index('User') and \
pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
if DEBUG:
self.log.info(" Books playlist: '%s'" % (pl.Name))
lib_books = pl
break
else:
if DEBUG:
self.log.error(" no Books playlist found")
lib = self.iTunes.LibraryPlaylist
for (i,file) in enumerate(files):
# Delete existing from Library|Books, add to self.update_list
# for deletion from booklist[0] during add_books_to_metadata
'''
# ---------------------------
# PROVISIONAL
# Use the cover to find the database storage point of the epub
# Pass database copy to iTunes instead of the temporary file
if False:
if DEBUG:
self.log.info(" processing '%s'" % metadata[i].title)
self.log.info(" file: %s" % (file._name if isinstance(file,PersistentTemporaryFile) else file))
self.log.info(" cover: %s" % metadata[i].cover)
calibre_database_item = False
if metadata[i].cover:
passed_file = file
storage_path = os.path.split(metadata[i].cover)[0]
try:
database_epub = filter(lambda x: x.endswith('.epub'), os.listdir(storage_path))[0]
file = os.path.join(storage_path,database_epub)
calibre_database_item = True
self.log.info(" using database file: %s" % file)
except:
self.log.info(" could not find epub in %s" % storage_path)
else:
self.log.info(" no cover available, using temp file")
# ---------------------------
'''
path = self.path_template % (metadata[i].title, metadata[i].author[0])
if path in self.cached_books:
self.update_list.append(self.cached_books[path])
@ -928,6 +892,9 @@ class ITUNES(DevicePlugin):
self.log.info("ITUNES.upload_books():")
self.log.info( " deleting existing '%s'" % (path))
self._remove_from_iTunes(self.cached_books[path])
if self.manual_sync_mode:
dev_book_added = self._remove_device_book(self.cached_books[path])
else:
if DEBUG:
self.log.info(" '%s' not in cached_books" % metadata[i].title)
@ -939,41 +906,58 @@ class ITUNES(DevicePlugin):
elif getattr(file, 'name', None) is not None:
fpath = file.name
op_status = lib_books.AddFile(fpath)
self.log.info("ITUNES.upload_books():\n iTunes adding '%s'"
% fpath)
if DEBUG:
sys.stdout.write(" iTunes copying '%s' ..." % metadata[i].title)
sys.stdout.flush()
while op_status.InProgress:
time.sleep(0.5)
# If this file is to be deleted after xfer to device, don't add it to the
# iTunes database, as the file path will be invalid when calibre exits.
# Only possible in manual_sync_mode
if getattr(file, 'deleted_after_upload', False) and self.manual_sync_mode:
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.
self.log.info(" PTF not added to Library|Books")
else:
# Add fpath to Library|Books
file_s = ctypes.c_char_p(fpath)
FileArray = ctypes.c_char_p * 1
fa = FileArray(file_s)
op_status = lib.AddFiles(fa)
if DEBUG:
sys.stdout.write(" waiting for handle to '%s' ..." % metadata[i].title)
self.log.info(" file added to Library|Books")
self.log.info("ITUNES.upload_books():\n iTunes adding '%s'"
% fpath)
if DEBUG:
sys.stdout.write(" iTunes copying '%s' ..." % metadata[i].title)
sys.stdout.flush()
while op_status.Tracks is None:
while op_status.InProgress:
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]})
sys.stdout.write("\n")
sys.stdout.flush()
if True:
if DEBUG:
sys.stdout.write(" waiting for handle to added '%s' ..." % metadata[i].title)
sys.stdout.flush()
while op_status.Tracks is None:
time.sleep(0.5)
if DEBUG:
sys.stdout.write('.')
sys.stdout.flush()
if DEBUG:
print
added = op_status.Tracks[0]
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]})
dev_book_added = None
if self.manual_sync_mode:
dev_book_added = self._add_device_book(fpath)
if added:
thumb = None
@ -1044,7 +1028,8 @@ class ITUNES(DevicePlugin):
self.cached_books[this_book.path] = {
'title': metadata[i].title,
'author': metadata[i].author[0],
'lib_book': added
'lib_book': added,
'dev_book': dev_book_added
}
# Report progress
@ -1059,12 +1044,115 @@ class ITUNES(DevicePlugin):
self.report_progress(1.0, _('finished'))
# Tell sync_booklists we need a re-sync
self.update_needed = True
self.update_msg = "Added books to device"
if not self.manual_sync_mode:
self.update_needed = True
self.update_msg = "Added books to device"
return (new_booklist, [], [])
# Private methods
def _add_device_book(self,fpath):
'''
'''
self.log.info("ITUNES._add_device_book()")
if isosx:
if 'iPod' in self.sources:
connected_device = self.sources['iPod']
device = self.iTunes.sources[connected_device]
for pl in device.playlists():
if pl.special_kind() == appscript.k.Books:
break
else:
if DEBUG:
self.log.error(" Device|Books playlist not found")
# Add the passed book to the Device|Books playlist
added = pl.add(appscript.mactypes.File(fpath),to=pl)
if DEBUG:
self.log.info(" adding '%s' to device" % fpath)
return added
elif iswindows:
if 'iPod' in self.sources:
try:
pythoncom.CoInitialize()
connected_device = self.sources['iPod']
device = self.iTunes.sources.ItemByName(connected_device)
dev_books = None
added = None
for pl in device.Playlists:
if pl.Kind == self.PlaylistKind.index('User') and \
pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
break
else:
if DEBUG:
self.log.info(" no Books playlist found")
# Add the passed book to the Device|Books playlist
if pl:
added = pl.AddFile(fpath)
if DEBUG:
self.log.info(" adding '%s' to device" % fpath)
finally:
pythoncom.CoUninitialize()
return added
def _discover_manual_sync_mode(self, wait=0):
'''
Assumes pythoncom for windows
wait is passed when launching iTunes, as it seems to need a moment to come to its senses
'''
if DEBUG:
self.log.info("ITUNES._discover_manual_sync_mode()")
if isosx:
connected_device = self.sources['iPod']
dev_books = None
device = self.iTunes.sources[connected_device]
for pl in device.playlists():
if pl.special_kind() == appscript.k.Books:
dev_books = pl.file_tracks()
break
else:
self.log.error(" book_playlist not found")
if len(dev_books):
first_book = dev_books[0]
#if DEBUG:
#self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist()))
try:
first_book.bpm.set(0)
self.manual_sync_mode = True
except:
self.manual_sync_mode = False
self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode)
elif iswindows:
if wait:
time.sleep(wait)
connected_device = self.sources['iPod']
device = self.iTunes.sources.ItemByName(connected_device)
dev_books = None
for pl in device.Playlists:
if pl.Kind == self.PlaylistKind.index('User') and \
pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
dev_books = pl.Tracks
break
if dev_books.Count:
first_book = dev_books.Item(1)
#if DEBUG:
#self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.Name, first_book.Artist))
try:
first_book.BPM = 0
self.manual_sync_mode = True
except:
self.manual_sync_mode = False
self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode)
def _dump_booklist(self, booklist, header=None):
'''
'''
@ -1090,10 +1178,11 @@ class ITUNES(DevicePlugin):
self.log.info( "%s" % ('-' * len(msg)))
if isosx:
for cb in self.cached_books.keys():
self.log.info("%-40.40s %-30.30s %-10.10s" %
self.log.info("%-40.40s %-30.30s %-10.10s %-10.10s" %
(self.cached_books[cb]['title'],
self.cached_books[cb]['author'],
str(self.cached_books[cb]['lib_book'])[-9:]))
str(self.cached_books[cb]['lib_book'])[-9:],
str(self.cached_books[cb]['dev_book'])[-9:]))
elif iswindows:
for cb in self.cached_books.keys():
self.log.info("%-40.40s %-30.30s" %
@ -1121,7 +1210,10 @@ class ITUNES(DevicePlugin):
self.log.info(msg)
self.log.info( "%s" % ('-' * len(msg)))
for file in files:
self.log.info(file)
if getattr(file, 'orig_file_path', None) is not None:
self.log.info(" %s" % file.orig_file_path)
elif getattr(file, 'name', None) is not None:
self.log.info(" %s" % file.name)
self.log.info()
def _dump_update_list(self,header=None):
@ -1147,7 +1239,6 @@ class ITUNES(DevicePlugin):
'''
Windows-only method to get a handle to a library book in the current pythoncom session
'''
SearchField = ['All','Visible','Artists','Titles','Composers','SongNames']
if iswindows:
if DEBUG:
self.log.info("ITUNES._find_library_book()")
@ -1178,8 +1269,8 @@ class ITUNES(DevicePlugin):
attempts = 9
while attempts:
# Find all books by this author, then match title
hits = lib_books.Search(cached_book['author'],SearchField.index('Artists'))
# Find book whose Artist field = cached_book['author']
hits = lib_books.Search(cached_book['author'],self.SearchField.index('Artists'))
if hits:
for hit in hits:
self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
@ -1346,6 +1437,30 @@ class ITUNES(DevicePlugin):
return device_books
def _get_device_playlist(self):
'''
'''
if iswindows:
if 'iPod' in self.sources:
pl = None
try:
pythoncom.CoInitialize()
connected_device = self.sources['iPod']
device = self.iTunes.sources.ItemByName(connected_device)
dev_books = None
for pl in device.Playlists:
if pl.Kind == self.PlaylistKind.index('User') and \
pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
break
else:
if DEBUG:
self.log.error(" no iPad|Books playlist found")
finally:
pythoncom.CoUninitialize()
return pl
def _get_library_books(self):
'''
Populate a dict of paths from iTunes Library|Books
@ -1427,17 +1542,20 @@ class ITUNES(DevicePlugin):
if DEBUG:
self.log.error(" no Library playlists found")
for book in lib_books:
# This may need additional entries for international iTunes users
if book.KindAsString in ['MPEG audio file']:
if DEBUG:
self.log.info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString))
else:
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString))
path = self.path_template % (book.Name, book.Artist)
library_books[path] = book
try:
for book in lib_books:
# This may need additional entries for international iTunes users
if book.KindAsString in ['MPEG audio file']:
if DEBUG:
self.log.info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString))
else:
if DEBUG:
self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString))
path = self.path_template % (book.Name, book.Artist)
library_books[path] = book
except:
if DEBUG:
self.log.info(" no books in library")
finally:
pythoncom.CoUninitialize()
@ -1553,6 +1671,30 @@ class ITUNES(DevicePlugin):
self.version[0],self.version[1],self.version[2]))
self.log.info(" iTunes_media: %s" % self.iTunes_media)
def _remove_device_book(self, cached_book):
'''
Windows assumes pythoncom wrapper
'''
self.log.info("ITUNES._remove_device_book()")
if isosx:
if DEBUG:
self.log.info(" deleting %s" % cached_book['dev_book'])
result = cached_book['dev_book'].delete()
print "result: %s" % result
elif iswindows:
dev_pl = self._get_device_playlist()
hits = dev_pl.Search(cached_book['author'],self.SearchField.index('Artists'))
if hits:
for hit in hits:
if DEBUG:
self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
if hit.Name == cached_book['title']:
if DEBUG:
self.log.info(" deleting '%s' by %s" % (hit.Name, hit.Artist))
results = hit.Delete()
break
def _remove_from_iTunes(self, cached_book):
'''
iTunes does not delete books from storage when removing from database