This commit is contained in:
GRiker 2010-06-15 04:13:35 -06:00
parent fa3d694113
commit a726347e2b

View File

@ -5,19 +5,21 @@ __copyright__ = '2010, Gregory Riker'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import cStringIO, ctypes, os, re, shutil, subprocess, sys, tempfile, time, zipfile import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time
from calibre.constants import DEBUG from calibre.constants import DEBUG
from calibre import fit_image from calibre import fit_image
from calibre.constants import isosx, iswindows from calibre.constants import isosx, iswindows
from calibre.devices.errors import UserFeedback
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.library.server.utils import strftime from calibre.library.server.utils import strftime
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import Config, config_dir from calibre.utils.config import Config, config_dir
from calibre.utils.date import parse_date from calibre.utils.date import isoformat, now, parse_date, strptime
from calibre.utils.logging import Log from calibre.utils.logging import Log
from calibre.devices.errors import UserFeedback from calibre.utils.zipfile import safe_replace, ZipFile
from PIL import Image as PILImage from PIL import Image as PILImage
@ -31,6 +33,8 @@ if isosx:
if iswindows: if iswindows:
import pythoncom, win32com.client import pythoncom, win32com.client
from calibre.ebooks.BeautifulSoup import BeautifulSoup
class ITUNES(DevicePlugin): class ITUNES(DevicePlugin):
''' '''
@ -633,7 +637,7 @@ class ITUNES(DevicePlugin):
if not os.path.exists(archive_path): if not os.path.exists(archive_path):
self.log.info(" creating zip archive") self.log.info(" creating zip archive")
zfw = zipfile.ZipFile(archive_path, mode='w') zfw = ZipFile(archive_path, mode='w')
zfw.writestr("iTunes Thumbs Archive",'') zfw.writestr("iTunes Thumbs Archive",'')
zfw.close() zfw.close()
else: else:
@ -786,7 +790,7 @@ class ITUNES(DevicePlugin):
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])
self._remove_existing_copies(path,file,metadata[i]) self._remove_existing_copies(path,file,metadata[i])
fpath = self._get_fpath(file) fpath = self._get_fpath(file, touch=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i]) db_added, lb_added = self._add_new_copy(fpath, metadata[i])
thumb = self._cover_to_thumb(path, metadata[i], lb_added, db_added) thumb = self._cover_to_thumb(path, metadata[i], lb_added, db_added)
this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb) this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb)
@ -813,8 +817,16 @@ class ITUNES(DevicePlugin):
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])
self._remove_existing_copies(path,file,metadata[i]) self._remove_existing_copies(path,file,metadata[i])
fpath = self._get_fpath(file) fpath = self._get_fpath(file, touch=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i]) db_added, lb_added = self._add_new_copy(fpath, metadata[i])
if self.manual_sync_mode and not db_added:
# Problem finding added book, probably title/author change needing to be written to metadata
self.problem_msg = ("Title and/or author metadata mismatch with uploaded books.\n"
"Convert epub - epub to update edited metadata before uploading.\n"
"Click 'Show Details...' for affected books.")
self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0]))
thumb = self._cover_to_thumb(path, metadata[i], lb_added, db_added) thumb = self._cover_to_thumb(path, metadata[i], lb_added, db_added)
this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb) this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb)
new_booklist.append(this_book) new_booklist.append(this_book)
@ -843,9 +855,11 @@ class ITUNES(DevicePlugin):
return (new_booklist, [], []) return (new_booklist, [], [])
# Private methods # Private methods
def _add_device_book(self,fpath, metadata): def _add_device_book(self,fpath, metadata):
''' '''
assumes pythoncom wrapper for windows
''' '''
self.log.info(" ITUNES._add_device_book()") self.log.info(" ITUNES._add_device_book()")
if isosx: if isosx:
@ -867,8 +881,6 @@ class ITUNES(DevicePlugin):
elif iswindows: elif iswindows:
if 'iPod' in self.sources: if 'iPod' in self.sources:
try:
pythoncom.CoInitialize()
connected_device = self.sources['iPod'] connected_device = self.sources['iPod']
device = self.iTunes.sources.ItemByName(connected_device) device = self.iTunes.sources.ItemByName(connected_device)
@ -883,11 +895,6 @@ class ITUNES(DevicePlugin):
# Add the passed book to the Device|Books playlist # Add the passed book to the Device|Books playlist
if pl: if pl:
'''
added = pl.AddFile(fpath)
if DEBUG:
self.log.info(" adding '%s' to device" % fpath)
'''
file_s = ctypes.c_char_p(fpath) file_s = ctypes.c_char_p(fpath)
FileArray = ctypes.c_char_p * 1 FileArray = ctypes.c_char_p * 1
fa = FileArray(file_s) fa = FileArray(file_s)
@ -911,25 +918,32 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title) sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title)
sys.stdout.flush() sys.stdout.flush()
while op_status.Tracks is None: while not op_status.Tracks:
time.sleep(0.5) time.sleep(0.5)
if DEBUG: if DEBUG:
sys.stdout.write('.') sys.stdout.write('.')
sys.stdout.flush() sys.stdout.flush()
if DEBUG: if DEBUG:
print print
added = op_status.Tracks[0] added = op_status.Tracks[0]
else: else:
# This approach simply scans Library|Books for the book we just added # This approach simply scans Library|Books for the book we just added
added = self._find_device_book(
# Try the calibre metadata first
db_added = self._find_device_book(
{'title': metadata.title, {'title': metadata.title,
'author': metadata.author[0]}) 'author': metadata.authors[0],
return added 'source': 'calibre'})
finally: # If that fails, try the epub metadata
pythoncom.CoUninitialize() if not db_added:
title, author = self._get_epub_metadata(fpath)
return added db_added = self._find_device_book(
{'title': title,
'author': author,
'source': 'epub'})
return db_added
def _add_library_book(self,file, metadata): def _add_library_book(self,file, metadata):
''' '''
@ -1061,7 +1075,7 @@ class ITUNES(DevicePlugin):
if DEBUG: if DEBUG:
self.log.info( " refreshing cached thumb for '%s'" % metadata.title) self.log.info( " refreshing cached thumb for '%s'" % metadata.title)
archive_path = os.path.join(self.cache_dir, "thumbs.zip") archive_path = os.path.join(self.cache_dir, "thumbs.zip")
zfw = zipfile.ZipFile(archive_path, mode='a') zfw = ZipFile(archive_path, mode='a')
thumb_path = path.rpartition('.')[0] + '.jpg' thumb_path = path.rpartition('.')[0] + '.jpg'
zfw.writestr(thumb_path, thumb) zfw.writestr(thumb_path, thumb)
zfw.close() zfw.close()
@ -1168,7 +1182,11 @@ class ITUNES(DevicePlugin):
self.manual_sync_mode = True self.manual_sync_mode = True
except: except:
self.manual_sync_mode = False self.manual_sync_mode = False
if DEBUG:
self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode) self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode)
else:
if DEBUG:
self.log.error(" no books on iDevice, can't determine manual sync mode")
def _dump_booklist(self, booklist, header=None): def _dump_booklist(self, booklist, header=None):
''' '''
@ -1291,24 +1309,24 @@ class ITUNES(DevicePlugin):
ub['author'])) ub['author']))
self.log.info() self.log.info()
def _find_device_book(self, cached_book): def _find_device_book(self, search):
''' '''
Windows-only method to get a handle to device book in the current pythoncom session Windows-only method to get a handle to device book in the current pythoncom session
''' '''
if iswindows: if iswindows:
if DEBUG: if DEBUG:
self.log.info(" ITUNES._find_device_book()") self.log.info(" ITUNES._find_device_book()")
self.log.info(" looking for '%s' by %s" % (cached_book['title'], cached_book['author'])) self.log.info(" searching for '%s' by %s (%s metadata)" %
(search['title'], search['author'], search['source']))
dev_books = self._get_device_books_playlist() dev_books = self._get_device_books_playlist()
attempts = 9 attempts = 9
while attempts: while attempts:
# Find book whose Artist field = cached_book['author'] hits = dev_books.Search(search['author'],self.SearchField.index('Artists'))
hits = dev_books.Search(cached_book['author'],self.SearchField.index('Artists'))
if hits: if hits:
for hit in hits: for hit in hits:
self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist)) self.log.info(" evaluating '%s' by %s" % (hit.Name, hit.Artist))
if hit.Name == cached_book['title']: if hit.Name == search['title']:
self.log.info(" matched '%s' by %s" % (hit.Name, hit.Artist)) self.log.info(" matched '%s' by %s" % (hit.Name, hit.Artist))
return hit return hit
attempts -= 1 attempts -= 1
@ -1317,7 +1335,8 @@ class ITUNES(DevicePlugin):
self.log.warning(" attempt #%d" % (10 - attempts)) self.log.warning(" attempt #%d" % (10 - attempts))
if DEBUG: if DEBUG:
self.log.error(" search for '%s' yielded no hits" % cached_book['title']) self.log.error(" search for '%s' using %s metadata yielded no hits" %
(search['title'], search['source']))
return None return None
def _find_library_book(self, cached_book): def _find_library_book(self, cached_book):
@ -1382,11 +1401,11 @@ class ITUNES(DevicePlugin):
thumb_path = book_path.rpartition('.')[0] + '.jpg' thumb_path = book_path.rpartition('.')[0] + '.jpg'
try: try:
zfr = zipfile.ZipFile(archive_path) zfr = ZipFile(archive_path)
thumb_data = zfr.read(thumb_path) thumb_data = zfr.read(thumb_path)
zfr.close() zfr.close()
except: except:
zfw = zipfile.ZipFile(archive_path, mode='a') zfw = ZipFile(archive_path, mode='a')
else: else:
return thumb_data return thumb_data
@ -1445,7 +1464,7 @@ class ITUNES(DevicePlugin):
''' '''
Calculate the exploded size of file Calculate the exploded size of file
''' '''
myZip = zipfile.ZipFile(file,'r') myZip = ZipFile(file,'r')
myZipList = myZip.infolist() myZipList = myZip.infolist()
exploded_file_size = 0 exploded_file_size = 0
for file in myZipList: for file in myZipList:
@ -1542,7 +1561,31 @@ class ITUNES(DevicePlugin):
self.log.error(" no iPad|Books playlist found") self.log.error(" no iPad|Books playlist found")
return pl return pl
def _get_fpath(self,file): def _get_epub_metadata(self, fpath):
'''
Return the original title and author from the .OPF file in the epub bundle
'''
self.log.info(" ITUNES.__get_epub_metadata()")
title = None
author = None
zf = ZipFile(fpath,'r')
fnames = zf.namelist()
opf = [x for x in fnames if '.opf' in x][0]
if opf:
opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue()
soup = BeautifulSoup(opf_raw)
title = soup.find('dc:title').renderContents()
author = soup.find('dc:creator').renderContents()
if not title or not author:
if DEBUG:
self.log.error(" couldn't extract title/author from %s in %s" % (opf,fpath))
self.log.error(" title: %s author: %s" % (title, author))
else:
if DEBUG:
self.log.error(" can't find .opf in %s" % fpath)
return title, author
def _get_fpath(self,file, touch=False):
''' '''
If the database copy will be deleted after upload, we have to If the database copy will be deleted after upload, we have to
use file (the PersistentTemporaryFile), which will be around until use file (the PersistentTemporaryFile), which will be around until
@ -1555,6 +1598,8 @@ class ITUNES(DevicePlugin):
if not getattr(fpath, 'deleted_after_upload', False): if not getattr(fpath, 'deleted_after_upload', False):
if getattr(file, 'orig_file_path', None) is not None: if getattr(file, 'orig_file_path', None) is not None:
fpath = file.orig_file_path fpath = file.orig_file_path
if touch:
self._touch_epub(fpath)
elif getattr(file, 'name', None) is not None: elif getattr(file, 'name', None) is not None:
fpath = file.name fpath = file.name
else: else:
@ -1958,6 +2003,58 @@ class ITUNES(DevicePlugin):
book.Delete() book.Delete()
def _touch_epub(self, fpath):
'''
Touch calibre:timestamp in OPF to force iBooks to recache
'''
self.log.info(" ITUNES._touch_epub()")
title = None
author = None
zf = ZipFile(fpath,'r')
fnames = zf.namelist()
opf = [x for x in fnames if '.opf' in x][0]
if opf:
opf_raw = cStringIO.StringIO(zf.read(opf)).getvalue()
soup = BeautifulSoup(opf_raw)
md = soup.find('metadata')
ots = ts = md.find('meta',attrs={'name':'calibre:timestamp'})
if ts:
# Touch existing calibre timestamp
timestamp = ts['content']
# old_ts = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f+00:00")
# new_ts = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour, old_ts.minute,
# old_ts.second, old_ts.microsecond+1)
# ts['content'] = new_ts.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")
old_ts = strptime(timestamp,"%Y-%m-%dT%H:%M:%S.%f+00:00")
new_ts = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
old_ts.minute, old_ts.second, old_ts.microsecond+1)
ts['content'] = new_ts.strftime("%Y-%m-%dT%H:%M:%S.%f+00:00")
if DEBUG:
self.log.info(" touching existing calibre:timestamp in %s" % opf)
self.log.info(" %s" % ots)
self.log.info(" %s" % ts)
else:
# Create new calibre timestamp
if True:
print "existing metadata:\n%s" % md.prettify()
else:
ts = Tag(soup,'meta')
ts['name'] = 'calibre:timestamp'
ts['content'] = isoformat(now())
md.insert(len(md),ts)
if DEBUG:
self.log.info(" adding calibre:timestamp to %s" % opf)
self.log.info(" %s" % ts)
zfo = open(fpath,'r+b')
safe_replace(zfo, opf, cStringIO.StringIO(soup))
else:
if DEBUG:
self.log.error(" can't find .opf in %s" % fpath)
def _update_device(self, msg='', wait=True): def _update_device(self, msg='', wait=True):
''' '''
Trigger a sync, wait for completion Trigger a sync, wait for completion
@ -2014,6 +2111,24 @@ class ITUNES(DevicePlugin):
strip_tags = re.compile(r'<[^<]*?/?>') strip_tags = re.compile(r'<[^<]*?/?>')
if isosx: if isosx:
if lb_added:
lb_added.album.set(metadata.title)
lb_added.artist.set(metadata.authors[0])
lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
lb_added.enabled.set(True)
lb_added.name.set(metadata.title)
lb_added.sort_artist.set(metadata.author_sort.title())
lb_added.sort_name.set(this_book.title_sorter)
if db_added:
db_added.album.set(metadata.title)
db_added.artist.set(metadata.authors[0])
db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
db_added.enabled.set(True)
db_added.name.set(metadata.title)
db_added.sort_artist.set(metadata.author_sort.title())
db_added.sort_name.set(this_book.title_sorter)
if metadata.comments: if metadata.comments:
if lb_added: if lb_added:
lb_added.comment.set(strip_tags.sub('',metadata.comments)) lb_added.comment.set(strip_tags.sub('',metadata.comments))
@ -2030,21 +2145,18 @@ class ITUNES(DevicePlugin):
except: except:
pass pass
# Set genre from series if available, else first alpha tag
# Otherwise iTunes grabs the first dc:subject from the opf metadata,
if metadata.series:
if lb_added: if lb_added:
lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) lb_added.genre.set(metadata.series)
lb_added.enabled.set(True) lb_added.episode_ID.set(metadata.series)
lb_added.sort_artist.set(metadata.author_sort.title()) lb_added.episode_number.set(metadata.series_index)
lb_added.sort_name.set(this_book.title_sorter)
if db_added: if db_added:
db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) db_added.genre.set(metadata.series)
db_added.enabled.set(True) db_added.episode_ID.set(metadata.series)
db_added.sort_artist.set(metadata.author_sort.title()) db_added.episode_number.set(metadata.series_index)
db_added.sort_name.set(this_book.title_sorter) else:
# 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.tags: for tag in metadata.tags:
if self._is_alpha(tag[0]): if self._is_alpha(tag[0]):
if lb_added: if lb_added:
@ -2053,8 +2165,26 @@ class ITUNES(DevicePlugin):
db_added.genre.set(tag) db_added.genre.set(tag)
break break
elif iswindows: elif iswindows:
if lb_added:
lb_added.Album = metadata.title
lb_added.Artist = metadata.authors[0]
lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
lb_added.Enabled = True
lb_added.Name = metadata.title
lb_added.SortArtist = (metadata.author_sort.title())
lb_added.SortName = (this_book.title_sorter)
if db_added:
# Album, Artist and Name are changed in _add_device_book()
db_added.Album = metadata.title
db_added.Artist = metadata.authors[0]
db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
db_added.Enabled = True
db_added.Name = metadata.title
db_added.SortArtist = (metadata.author_sort.title())
db_added.SortName = (this_book.title_sorter)
if metadata.comments: if metadata.comments:
if lb_added: if lb_added:
lb_added.Comment = (strip_tags.sub('',metadata.comments)) lb_added.Comment = (strip_tags.sub('',metadata.comments))
@ -2071,28 +2201,43 @@ class ITUNES(DevicePlugin):
except: except:
pass pass
if lb_added: # Set Category from first alpha tag, overwrite with series if available
lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) # Otherwise iBooks uses first <dc:subject> from opf
lb_added.Enabled = True # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12)
lb_added.SortArtist = (metadata.author_sort.title())
lb_added.SortName = (this_book.title_sorter)
if db_added: if metadata.tags:
db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S'))) if DEBUG:
db_added.SortArtist = (metadata.author_sort.title()) self.log.info(" setting Category from metadata.tags")
db_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.tags: for tag in metadata.tags:
if self._is_alpha(tag[0]): if self._is_alpha(tag[0]):
if lb_added: if lb_added:
lb_added.Category = (tag) lb_added.Category = tag
if db_added: if db_added:
db_added.Category = (tag) db_added.Category = tag
break break
if metadata.series:
if DEBUG:
self.log.info(" setting Category from metadata.series")
if lb_added:
if DEBUG:
self.log.info(" setting lb_added from metadata.series")
lb_added.Category = metadata.series
lb_added.EpisodeID = metadata.series
try:
lb_added.EpisodeNumber = metadata.series_index
except:
pass
if db_added:
if DEBUG:
self.log.info(" setting db_added from metadata.series")
db_added.Category = metadata.series
db_added.EpisodeID = metadata.series
try:
db_added.EpisodeNumber = metadata.series_index
except:
pass
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: