PageRenderTime 68ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/src/calibre/devices/apple/driver.py

https://bitbucket.org/azku/calibre
Python | 3163 lines | 2976 code | 57 blank | 130 comment | 136 complexity | 672c0e7ff48b303271de372b382d0062 MD5 | raw file
Possible License(s): LGPL-3.0, GPL-3.0, BSD-3-Clause
  1. # -*- coding: utf-8 -*-
  2. __license__ = 'GPL v3'
  3. __copyright__ = '2010, Gregory Riker'
  4. __docformat__ = 'restructuredtext en'
  5. import cStringIO, ctypes, datetime, os, re, shutil, subprocess, sys, tempfile, time
  6. from calibre.constants import __appname__, __version__, DEBUG
  7. from calibre import fit_image
  8. from calibre.constants import isosx, iswindows
  9. from calibre.devices.errors import UserFeedback
  10. from calibre.devices.usbms.deviceconfig import DeviceConfig
  11. from calibre.devices.interface import DevicePlugin
  12. from calibre.ebooks.BeautifulSoup import BeautifulSoup
  13. from calibre.ebooks.metadata import authors_to_string, MetaInformation, \
  14. title_sort
  15. from calibre.ebooks.metadata.book.base import Metadata
  16. from calibre.ebooks.metadata.epub import set_metadata
  17. from calibre.library.server.utils import strftime
  18. from calibre.utils.config import config_dir, prefs
  19. from calibre.utils.date import now, parse_date
  20. from calibre.utils.logging import Log
  21. from calibre.utils.zipfile import ZipFile
  22. from PIL import Image as PILImage
  23. if isosx:
  24. try:
  25. import appscript
  26. appscript
  27. except:
  28. # appscript fails to load on 10.4
  29. appscript = None
  30. if iswindows:
  31. import pythoncom, win32com.client
  32. class DriverBase(DeviceConfig, DevicePlugin):
  33. # Needed for config_widget to work
  34. FORMATS = ['epub', 'pdf']
  35. SUPPORTS_SUB_DIRS = True # To enable second checkbox in customize widget
  36. @classmethod
  37. def _config_base_name(cls):
  38. return 'iTunes'
  39. class ITUNES(DriverBase):
  40. '''
  41. Calling sequences:
  42. Initialization:
  43. can_handle() or can_handle_windows()
  44. reset()
  45. open()
  46. card_prefix()
  47. can_handle()
  48. set_progress_reporter()
  49. get_device_information()
  50. card_prefix()
  51. free_space()
  52. (Job 1 Get device information finishes)
  53. can_handle()
  54. set_progress_reporter()
  55. books() (once for each storage point)
  56. settings()
  57. settings()
  58. can_handle() (~1x per second OSX while idle)
  59. Delete:
  60. delete_books()
  61. remove_books_from_metadata()
  62. use_plugboard_ext()
  63. set_plugboard()
  64. sync_booklists()
  65. card_prefix()
  66. free_space()
  67. Upload:
  68. settings()
  69. set_progress_reporter()
  70. upload_books()
  71. add_books_to_metadata()
  72. use_plugboard_ext()
  73. set_plugboard()
  74. set_progress_reporter()
  75. sync_booklists()
  76. card_prefix()
  77. free_space()
  78. '''
  79. name = 'Apple device interface'
  80. gui_name = _('Apple device')
  81. icon = I('devices/ipad.png')
  82. description = _('Communicate with iTunes/iBooks.')
  83. supported_platforms = ['osx','windows']
  84. author = 'GRiker'
  85. #: The version of this plugin as a 3-tuple (major, minor, revision)
  86. version = (0,9,0)
  87. OPEN_FEEDBACK_MESSAGE = _(
  88. 'Apple device detected, launching iTunes, please wait ...')
  89. BACKLOADING_ERROR_MESSAGE = _(
  90. "Cannot copy books directly from iDevice. "
  91. "Drag from iTunes Library to desktop, then add to calibre's Library window.")
  92. # Product IDs:
  93. # 0x1291 iPod Touch
  94. # 0x1293 iPod Touch 2G
  95. # 0x1299 iPod Touch 3G
  96. # 0x1292 iPhone 3G
  97. # 0x1294 iPhone 3GS
  98. # 0x1297 iPhone 4
  99. # 0x129a iPad
  100. VENDOR_ID = [0x05ac]
  101. PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a]
  102. BCD = [0x01]
  103. # Plugboard ID
  104. DEVICE_PLUGBOARD_NAME = 'APPLE'
  105. # iTunes enumerations
  106. Audiobooks = [
  107. 'Audible file',
  108. 'MPEG audio file',
  109. 'Protected AAC audio file'
  110. ]
  111. ArtworkFormat = [
  112. 'Unknown',
  113. 'JPEG',
  114. 'PNG',
  115. 'BMP'
  116. ]
  117. PlaylistKind = [
  118. 'Unknown',
  119. 'Library',
  120. 'User',
  121. 'CD',
  122. 'Device',
  123. 'Radio Tuner'
  124. ]
  125. PlaylistSpecialKind = [
  126. 'Unknown',
  127. 'Purchased Music',
  128. 'Party Shuffle',
  129. 'Podcasts',
  130. 'Folder',
  131. 'Video',
  132. 'Music',
  133. 'Movies',
  134. 'TV Shows',
  135. 'Books',
  136. ]
  137. SearchField = [
  138. 'All',
  139. 'Visible',
  140. 'Artists',
  141. 'Albums',
  142. 'Composers',
  143. 'SongNames',
  144. ]
  145. Sources = [
  146. 'Unknown',
  147. 'Library',
  148. 'iPod',
  149. 'AudioCD',
  150. 'MP3CD',
  151. 'Device',
  152. 'RadioTuner',
  153. 'SharedLibrary'
  154. ]
  155. # Cover art size limits
  156. MAX_COVER_WIDTH = 510
  157. MAX_COVER_HEIGHT = 680
  158. # Properties
  159. cached_books = {}
  160. cache_dir = os.path.join(config_dir, 'caches', 'itunes')
  161. calibre_library_path = prefs['library_path']
  162. archive_path = os.path.join(cache_dir, "thumbs.zip")
  163. description_prefix = "added by calibre"
  164. ejected = False
  165. iTunes= None
  166. iTunes_media = None
  167. library_orphans = None
  168. log = Log()
  169. manual_sync_mode = False
  170. path_template = 'iTunes/%s - %s.%s'
  171. plugboards = None
  172. plugboard_func = None
  173. problem_titles = []
  174. problem_msg = None
  175. report_progress = None
  176. update_list = []
  177. sources = None
  178. update_msg = None
  179. update_needed = False
  180. # Public methods
  181. def add_books_to_metadata(self, locations, metadata, booklists):
  182. '''
  183. Add locations to the booklists. This function must not communicate with
  184. the device.
  185. @param locations: Result of a call to L{upload_books}
  186. @param metadata: List of MetaInformation objects, same as for
  187. :method:`upload_books`.
  188. @param booklists: A tuple containing the result of calls to
  189. (L{books}(oncard=None), L{books}(oncard='carda'),
  190. L{books}(oncard='cardb')).
  191. '''
  192. if DEBUG:
  193. self.log.info("ITUNES.add_books_to_metadata()")
  194. task_count = float(len(self.update_list))
  195. # Delete any obsolete copies of the book from the booklist
  196. if self.update_list:
  197. if False:
  198. self._dump_booklist(booklists[0], header='before',indent=2)
  199. self._dump_update_list(header='before',indent=2)
  200. self._dump_cached_books(header='before',indent=2)
  201. for (j,p_book) in enumerate(self.update_list):
  202. if False:
  203. if isosx:
  204. self.log.info(" looking for '%s' by %s uuid:%s" %
  205. (p_book['title'],p_book['author'], p_book['uuid']))
  206. elif iswindows:
  207. self.log.info(" looking for '%s' by %s (%s)" %
  208. (p_book['title'],p_book['author'], p_book['uuid']))
  209. # Purge the booklist, self.cached_books
  210. for i,bl_book in enumerate(booklists[0]):
  211. if bl_book.uuid == p_book['uuid']:
  212. # Remove from booklists[0]
  213. booklists[0].pop(i)
  214. if False:
  215. if isosx:
  216. self.log.info(" removing old %s %s from booklists[0]" %
  217. (p_book['title'], str(p_book['lib_book'])[-9:]))
  218. elif iswindows:
  219. self.log.info(" removing old '%s' from booklists[0]" %
  220. (p_book['title']))
  221. # If >1 matching uuid, remove old title
  222. matching_uuids = 0
  223. for cb in self.cached_books:
  224. if self.cached_books[cb]['uuid'] == p_book['uuid']:
  225. matching_uuids += 1
  226. if matching_uuids > 1:
  227. for cb in self.cached_books:
  228. if self.cached_books[cb]['uuid'] == p_book['uuid']:
  229. if self.cached_books[cb]['title'] == p_book['title'] and \
  230. self.cached_books[cb]['author'] == p_book['author']:
  231. if DEBUG:
  232. self._dump_cached_book(self.cached_books[cb],header="removing from self.cached_books:", indent=2)
  233. self.cached_books.pop(cb)
  234. break
  235. break
  236. if self.report_progress is not None:
  237. self.report_progress(j+1/task_count, _('Updating device metadata listing...'))
  238. if self.report_progress is not None:
  239. self.report_progress(1.0, _('Updating device metadata listing...'))
  240. # Add new books to booklists[0]
  241. # Charles thinks this should be
  242. # for new_book in metadata[0]:
  243. for new_book in locations[0]:
  244. if DEBUG:
  245. self.log.info(" adding '%s' by '%s' to booklists[0]" %
  246. (new_book.title, new_book.author))
  247. booklists[0].append(new_book)
  248. if False:
  249. self._dump_booklist(booklists[0],header='after',indent=2)
  250. self._dump_cached_books(header='after',indent=2)
  251. def books(self, oncard=None, end_session=True):
  252. """
  253. Return a list of ebooks on the device.
  254. @param oncard: If 'carda' or 'cardb' return a list of ebooks on the
  255. specific storage card, otherwise return list of ebooks
  256. in main memory of device. If a card is specified and no
  257. books are on the card return empty list.
  258. @return: A BookList.
  259. Implementation notes:
  260. iTunes does not sync purchased books, they are only on the device. They are visible, but
  261. they are not backed up to iTunes. Since calibre can't manage them, don't show them in the
  262. list of device books.
  263. """
  264. if not oncard:
  265. if DEBUG:
  266. self.log.info("ITUNES:books():")
  267. if self.settings().use_subdirs:
  268. self.log.info(" Cover fetching/caching enabled")
  269. else:
  270. self.log.info(" Cover fetching/caching disabled")
  271. # Fetch a list of books from iPod device connected to iTunes
  272. if 'iPod' in self.sources:
  273. booklist = BookList(self.log)
  274. cached_books = {}
  275. if isosx:
  276. library_books = self._get_library_books()
  277. device_books = self._get_device_books()
  278. book_count = float(len(device_books))
  279. for (i,book) in enumerate(device_books):
  280. this_book = Book(book.name(), book.artist())
  281. format = 'pdf' if book.kind().startswith('PDF') else 'epub'
  282. this_book.path = self.path_template % (book.name(), book.artist(),format)
  283. try:
  284. this_book.datetime = parse_date(str(book.date_added())).timetuple()
  285. except:
  286. this_book.datetime = time.gmtime()
  287. this_book.db_id = None
  288. this_book.device_collections = []
  289. this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
  290. this_book.size = book.size()
  291. this_book.uuid = book.composer()
  292. # Hack to discover if we're running in GUI environment
  293. if self.report_progress is not None:
  294. this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
  295. else:
  296. this_book.thumbnail = None
  297. booklist.add_book(this_book, False)
  298. cached_books[this_book.path] = {
  299. 'title':book.name(),
  300. 'author':[book.artist()],
  301. 'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
  302. 'dev_book':book,
  303. 'uuid': book.composer()
  304. }
  305. if self.report_progress is not None:
  306. self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
  307. self._purge_orphans(library_books, cached_books)
  308. elif iswindows:
  309. try:
  310. pythoncom.CoInitialize()
  311. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  312. library_books = self._get_library_books()
  313. device_books = self._get_device_books()
  314. book_count = float(len(device_books))
  315. for (i,book) in enumerate(device_books):
  316. this_book = Book(book.Name, book.Artist)
  317. format = 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
  318. this_book.path = self.path_template % (book.Name, book.Artist,format)
  319. try:
  320. this_book.datetime = parse_date(str(book.DateAdded)).timetuple()
  321. except:
  322. this_book.datetime = time.gmtime()
  323. this_book.db_id = None
  324. this_book.device_collections = []
  325. this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
  326. this_book.size = book.Size
  327. # Hack to discover if we're running in GUI environment
  328. if self.report_progress is not None:
  329. this_book.thumbnail = self._generate_thumbnail(this_book.path, book)
  330. else:
  331. this_book.thumbnail = None
  332. booklist.add_book(this_book, False)
  333. cached_books[this_book.path] = {
  334. 'title':book.Name,
  335. 'author':book.Artist,
  336. 'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
  337. 'uuid': book.Composer,
  338. 'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
  339. }
  340. if self.report_progress is not None:
  341. self.report_progress(i+1/book_count,
  342. _('%d of %d') % (i+1, book_count))
  343. self._purge_orphans(library_books, cached_books)
  344. finally:
  345. pythoncom.CoUninitialize()
  346. if self.report_progress is not None:
  347. self.report_progress(1.0, _('finished'))
  348. self.cached_books = cached_books
  349. if DEBUG:
  350. self._dump_booklist(booklist, 'returning from books()', indent=2)
  351. self._dump_cached_books('returning from books()',indent=2)
  352. return booklist
  353. else:
  354. return BookList(self.log)
  355. def can_handle(self, device_info, debug=False):
  356. '''
  357. Unix version of :method:`can_handle_windows`
  358. :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product,
  359. serial number)
  360. Confirm that:
  361. - iTunes is running
  362. - there is an iDevice connected
  363. This gets called first when the device fingerprint is read, so it needs to
  364. instantiate iTunes if necessary
  365. This gets called ~1x/second while device fingerprint is sensed
  366. '''
  367. if appscript is None:
  368. return False
  369. if self.iTunes:
  370. # Check for connected book-capable device
  371. self.sources = self._get_sources()
  372. if 'iPod' in self.sources:
  373. #if DEBUG:
  374. #sys.stdout.write('.')
  375. #sys.stdout.flush()
  376. return True
  377. else:
  378. if DEBUG:
  379. sys.stdout.write('-')
  380. sys.stdout.flush()
  381. return False
  382. else:
  383. # Called at entry
  384. # We need to know if iTunes sees the iPad
  385. # It may have been ejected
  386. if DEBUG:
  387. self.log.info("ITUNES.can_handle()")
  388. self._launch_iTunes()
  389. self.sources = self._get_sources()
  390. if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''):
  391. attempts = 9
  392. while attempts:
  393. # If iTunes was just launched, device may not be detected yet
  394. self.sources = self._get_sources()
  395. if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''):
  396. attempts -= 1
  397. time.sleep(0.5)
  398. if DEBUG:
  399. self.log.warning(" waiting for connected iPad, attempt #%d" % (10 - attempts))
  400. else:
  401. if DEBUG:
  402. self.log.info(' found connected iPad')
  403. break
  404. else:
  405. # iTunes running, but not connected iPad
  406. if DEBUG:
  407. self.log.info(' self.ejected = True')
  408. self.ejected = True
  409. return False
  410. self._discover_manual_sync_mode(wait = 2 if self.initial_status == 'launched' else 0)
  411. return True
  412. def can_handle_windows(self, device_id, debug=False):
  413. '''
  414. Optional method to perform further checks on a device to see if this driver
  415. is capable of handling it. If it is not it should return False. This method
  416. is only called after the vendor, product ids and the bcd have matched, so
  417. it can do some relatively time intensive checks. The default implementation
  418. returns True. This method is called only on windows. See also
  419. :method:`can_handle`.
  420. :param device_info: On windows a device ID string. On Unix a tuple of
  421. ``(vendor_id, product_id, bcd)``.
  422. iPad implementation notes:
  423. It is necessary to use this method to check for the presence of a connected
  424. iPad, as we have to return True if we can handle device interaction, or False if not.
  425. '''
  426. if self.iTunes:
  427. # We've previously run, so the user probably ejected the device
  428. try:
  429. pythoncom.CoInitialize()
  430. self.sources = self._get_sources()
  431. if 'iPod' in self.sources:
  432. if DEBUG:
  433. sys.stdout.write('.')
  434. sys.stdout.flush()
  435. if DEBUG:
  436. self.log.info('ITUNES.can_handle_windows:\n confirming connected iPad')
  437. self.ejected = False
  438. self._discover_manual_sync_mode()
  439. return True
  440. else:
  441. if DEBUG:
  442. self.log.info("ITUNES.can_handle_windows():\n device ejected")
  443. self.ejected = True
  444. return False
  445. except:
  446. # iTunes connection failed, probably not running anymore
  447. self.log.error("ITUNES.can_handle_windows():\n lost connection to iTunes")
  448. return False
  449. finally:
  450. pythoncom.CoUninitialize()
  451. else:
  452. if DEBUG:
  453. self.log.info("ITUNES:can_handle_windows():\n Launching iTunes")
  454. try:
  455. pythoncom.CoInitialize()
  456. self._launch_iTunes()
  457. self.sources = self._get_sources()
  458. if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''):
  459. attempts = 9
  460. while attempts:
  461. # If iTunes was just launched, device may not be detected yet
  462. self.sources = self._get_sources()
  463. if (not 'iPod' in self.sources) or (self.sources['iPod'] == ''):
  464. attempts -= 1
  465. time.sleep(0.5)
  466. if DEBUG:
  467. self.log.warning(" waiting for connected iPad, attempt #%d" % (10 - attempts))
  468. else:
  469. if DEBUG:
  470. self.log.info(' found connected iPad in iTunes')
  471. break
  472. else:
  473. # iTunes running, but not connected iPad
  474. if DEBUG:
  475. self.log.info(' iDevice has been ejected')
  476. self.ejected = True
  477. return False
  478. self.log.info(' found connected iPad in sources')
  479. self._discover_manual_sync_mode(wait=1.0)
  480. finally:
  481. pythoncom.CoUninitialize()
  482. return True
  483. def card_prefix(self, end_session=True):
  484. '''
  485. Return a 2 element list of the prefix to paths on the cards.
  486. If no card is present None is set for the card's prefix.
  487. E.G.
  488. ('/place', '/place2')
  489. (None, 'place2')
  490. ('place', None)
  491. (None, None)
  492. '''
  493. return (None,None)
  494. @classmethod
  495. def config_widget(cls):
  496. '''
  497. Return a QWidget with settings for the device interface
  498. '''
  499. cw = DriverBase.config_widget()
  500. # Turn off the Save template
  501. cw.opt_save_template.setVisible(False)
  502. cw.label.setVisible(False)
  503. # Repurpose the metadata checkbox
  504. cw.opt_read_metadata.setText(_("Use Series as Category in iTunes/iBooks"))
  505. # Repurpose the use_subdirs checkbox
  506. cw.opt_use_subdirs.setText(_("Cache covers from iTunes/iBooks"))
  507. return cw
  508. def delete_books(self, paths, end_session=True):
  509. '''
  510. Delete books at paths on device.
  511. iTunes doesn't let us directly delete a book on the device.
  512. If the requested paths are deletable (i.e., it's in the Library|Books list),
  513. delete the paths from the library, then resync iPad
  514. '''
  515. self.problem_titles = []
  516. self.problem_msg = _("Some books not found in iTunes database.\n"
  517. "Delete using the iBooks app.\n"
  518. "Click 'Show Details' for a list.")
  519. self.log.info("ITUNES:delete_books()")
  520. for path in paths:
  521. if self.cached_books[path]['lib_book']:
  522. if DEBUG:
  523. self.log.info(" Deleting '%s' from iTunes library" % (path))
  524. if isosx:
  525. self._remove_from_iTunes(self.cached_books[path])
  526. if self.manual_sync_mode:
  527. self._remove_from_device(self.cached_books[path])
  528. elif iswindows:
  529. try:
  530. pythoncom.CoInitialize()
  531. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  532. self._remove_from_iTunes(self.cached_books[path])
  533. if self.manual_sync_mode:
  534. self._remove_from_device(self.cached_books[path])
  535. finally:
  536. pythoncom.CoUninitialize()
  537. if not self.manual_sync_mode:
  538. self.update_needed = True
  539. self.update_msg = "Deleted books from device"
  540. else:
  541. self.log.info(" skipping sync phase, manual_sync_mode: True")
  542. else:
  543. if self.manual_sync_mode:
  544. metadata = MetaInformation(self.cached_books[path]['title'],
  545. [self.cached_books[path]['author']])
  546. metadata.uuid = self.cached_books[path]['uuid']
  547. if isosx:
  548. self._remove_existing_copy(self.cached_books[path],metadata)
  549. elif iswindows:
  550. try:
  551. pythoncom.CoInitialize()
  552. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  553. self._remove_existing_copy(self.cached_books[path],metadata)
  554. finally:
  555. pythoncom.CoUninitialize()
  556. else:
  557. self.problem_titles.append("'%s' by %s" %
  558. (self.cached_books[path]['title'],self.cached_books[path]['author']))
  559. def eject(self):
  560. '''
  561. Un-mount / eject the device from the OS. This does not check if there
  562. are pending GUI jobs that need to communicate with the device.
  563. '''
  564. if DEBUG:
  565. self.log.info("ITUNES:eject(): ejecting '%s'" % self.sources['iPod'])
  566. if isosx:
  567. self.iTunes.eject(self.sources['iPod'])
  568. elif iswindows:
  569. if 'iPod' in self.sources:
  570. try:
  571. pythoncom.CoInitialize()
  572. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  573. self.iTunes.sources.ItemByName(self.sources['iPod']).EjectIPod()
  574. finally:
  575. pythoncom.CoUninitialize()
  576. self.iTunes = None
  577. self.sources = None
  578. def free_space(self, end_session=True):
  579. """
  580. Get free space available on the mountpoints:
  581. 1. Main memory
  582. 2. Card A
  583. 3. Card B
  584. @return: A 3 element list with free space in bytes of (1, 2, 3). If a
  585. particular device doesn't have any of these locations it should return -1.
  586. In Windows, a sync-in-progress blocks this call until sync is complete
  587. """
  588. if DEBUG:
  589. self.log.info("ITUNES:free_space()")
  590. free_space = 0
  591. if isosx:
  592. if 'iPod' in self.sources:
  593. connected_device = self.sources['iPod']
  594. free_space = self.iTunes.sources[connected_device].free_space()
  595. elif iswindows:
  596. if 'iPod' in self.sources:
  597. while True:
  598. try:
  599. try:
  600. pythoncom.CoInitialize()
  601. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  602. connected_device = self.sources['iPod']
  603. free_space = self.iTunes.sources.ItemByName(connected_device).FreeSpace
  604. finally:
  605. pythoncom.CoUninitialize()
  606. break
  607. except:
  608. self.log.error(' waiting for free_space() call to go through')
  609. return (free_space,-1,-1)
  610. def get_device_information(self, end_session=True):
  611. """
  612. Ask device for device information. See L{DeviceInfoQuery}.
  613. @return: (device name, device version, software version on device, mime type)
  614. """
  615. if DEBUG:
  616. self.log.info("ITUNES:get_device_information()")
  617. return (self.sources['iPod'],'hw v1.0','sw v1.0', 'mime type normally goes here')
  618. def get_file(self, path, outfile, end_session=True):
  619. '''
  620. Read the file at C{path} on the device and write it to outfile.
  621. @param outfile: file object like C{sys.stdout} or the result of an C{open} call
  622. '''
  623. if DEBUG:
  624. self.log.info("ITUNES.get_file(): exporting '%s'" % path)
  625. outfile.write(open(self.cached_books[path]['lib_book'].location().path).read())
  626. def open(self):
  627. '''
  628. Perform any device specific initialization. Called after the device is
  629. detected but before any other functions that communicate with the device.
  630. For example: For devices that present themselves as USB Mass storage
  631. devices, this method would be responsible for mounting the device or
  632. if the device has been automounted, for finding out where it has been
  633. mounted. The base class within USBMS device.py has a implementation of
  634. this function that should serve as a good example for USB Mass storage
  635. devices.
  636. Note that most of the initialization is necessarily performed in can_handle(), as
  637. we need to talk to iTunes to discover if there's a connected iPod
  638. '''
  639. if DEBUG:
  640. self.log.info("ITUNES.open()")
  641. # Confirm/create thumbs archive
  642. if not os.path.exists(self.cache_dir):
  643. if DEBUG:
  644. self.log.info(" creating thumb cache '%s'" % self.cache_dir)
  645. os.makedirs(self.cache_dir)
  646. if not os.path.exists(self.archive_path):
  647. self.log.info(" creating zip archive")
  648. zfw = ZipFile(self.archive_path, mode='w')
  649. zfw.writestr("iTunes Thumbs Archive",'')
  650. zfw.close()
  651. else:
  652. if DEBUG:
  653. self.log.info(" existing thumb cache at '%s'" % self.archive_path)
  654. def remove_books_from_metadata(self, paths, booklists):
  655. '''
  656. Remove books from the metadata list. This function must not communicate
  657. with the device.
  658. @param paths: paths to books on the device.
  659. @param booklists: A tuple containing the result of calls to
  660. (L{books}(oncard=None), L{books}(oncard='carda'),
  661. L{books}(oncard='cardb')).
  662. NB: This will not find books that were added by a different installation of calibre
  663. as uuids are different
  664. '''
  665. if DEBUG:
  666. self.log.info("ITUNES.remove_books_from_metadata()")
  667. for path in paths:
  668. if DEBUG:
  669. self._dump_cached_book(self.cached_books[path], indent=2)
  670. self.log.info(" looking for '%s' by '%s' uuid:%s" %
  671. (self.cached_books[path]['title'],
  672. self.cached_books[path]['author'],
  673. self.cached_books[path]['uuid']))
  674. # Purge the booklist, self.cached_books, thumb cache
  675. for i,bl_book in enumerate(booklists[0]):
  676. if False:
  677. self.log.info(" evaluating '%s' by '%s' uuid:%s" %
  678. (bl_book.title, bl_book.author,bl_book.uuid))
  679. found = False
  680. if bl_book.uuid == self.cached_books[path]['uuid']:
  681. if False:
  682. self.log.info(" matched with uuid")
  683. booklists[0].pop(i)
  684. found = True
  685. elif bl_book.title == self.cached_books[path]['title'] and \
  686. bl_book.author[0] == self.cached_books[path]['author']:
  687. if False:
  688. self.log.info(" matched with title + author")
  689. booklists[0].pop(i)
  690. found = True
  691. if found:
  692. # Remove from self.cached_books
  693. for cb in self.cached_books:
  694. if self.cached_books[cb]['uuid'] == self.cached_books[path]['uuid']:
  695. self.cached_books.pop(cb)
  696. break
  697. # Remove from thumb from thumb cache
  698. thumb_path = path.rpartition('.')[0] + '.jpg'
  699. zf = ZipFile(self.archive_path,'a')
  700. fnames = zf.namelist()
  701. try:
  702. thumb = [x for x in fnames if thumb_path in x][0]
  703. except:
  704. thumb = None
  705. if thumb:
  706. if DEBUG:
  707. self.log.info(" deleting '%s' from cover cache" % (thumb_path))
  708. zf.delete(thumb_path)
  709. else:
  710. if DEBUG:
  711. self.log.info(" '%s' not found in cover cache" % thumb_path)
  712. zf.close()
  713. break
  714. else:
  715. if DEBUG:
  716. self.log.error(" unable to find '%s' by '%s' (%s)" %
  717. (bl_book.title, bl_book.author,bl_book.uuid))
  718. if False:
  719. self._dump_booklist(booklists[0], indent = 2)
  720. self._dump_cached_books(indent=2)
  721. def reset(self, key='-1', log_packets=False, report_progress=None,
  722. detected_device=None) :
  723. """
  724. :key: The key to unlock the device
  725. :log_packets: If true the packet stream to/from the device is logged
  726. :report_progress: Function that is called with a % progress
  727. (number between 0 and 100) for various tasks
  728. If it is called with -1 that means that the
  729. task does not have any progress information
  730. :detected_device: Device information from the device scanner
  731. """
  732. if DEBUG:
  733. self.log.info("ITUNES.reset()")
  734. def set_progress_reporter(self, report_progress):
  735. '''
  736. @param report_progress: Function that is called with a % progress
  737. (number between 0 and 100) for various tasks
  738. If it is called with -1 that means that the
  739. task does not have any progress information
  740. '''
  741. self.report_progress = report_progress
  742. def set_plugboards(self, plugboards, pb_func):
  743. # This method is called with the plugboard that matches the format
  744. # declared in use_plugboard_ext and a device name of ITUNES
  745. if DEBUG:
  746. self.log.info("ITUNES.set_plugboard()")
  747. #self.log.info(' using plugboard %s' % plugboards)
  748. self.plugboards = plugboards
  749. self.plugboard_func = pb_func
  750. def sync_booklists(self, booklists, end_session=True):
  751. '''
  752. Update metadata on device.
  753. @param booklists: A tuple containing the result of calls to
  754. (L{books}(oncard=None), L{books}(oncard='carda'),
  755. L{books}(oncard='cardb')).
  756. '''
  757. if DEBUG:
  758. self.log.info("ITUNES.sync_booklists()")
  759. if self.update_needed:
  760. if DEBUG:
  761. self.log.info(' calling _update_device')
  762. self._update_device(msg=self.update_msg, wait=False)
  763. self.update_needed = False
  764. # Inform user of any problem books
  765. if self.problem_titles:
  766. raise UserFeedback(self.problem_msg,
  767. details='\n'.join(self.problem_titles), level=UserFeedback.WARN)
  768. self.problem_titles = []
  769. self.problem_msg = None
  770. self.update_list = []
  771. def total_space(self, end_session=True):
  772. """
  773. Get total space available on the mountpoints:
  774. 1. Main memory
  775. 2. Memory Card A
  776. 3. Memory Card B
  777. @return: A 3 element list with total space in bytes of (1, 2, 3). If a
  778. particular device doesn't have any of these locations it should return 0.
  779. """
  780. if DEBUG:
  781. self.log.info("ITUNES:total_space()")
  782. capacity = 0
  783. if isosx:
  784. if 'iPod' in self.sources:
  785. connected_device = self.sources['iPod']
  786. capacity = self.iTunes.sources[connected_device].capacity()
  787. return (capacity,-1,-1)
  788. def upload_books(self, files, names, on_card=None, end_session=True,
  789. metadata=None):
  790. '''
  791. Upload a list of books to the device. If a file already
  792. exists on the device, it should be replaced.
  793. This method should raise a L{FreeSpaceError} if there is not enough
  794. free space on the device. The text of the FreeSpaceError must contain the
  795. word "card" if C{on_card} is not None otherwise it must contain the word "memory".
  796. :files: A list of paths and/or file-like objects.
  797. :names: A list of file names that the books should have
  798. once uploaded to the device. len(names) == len(files)
  799. :return: A list of 3-element tuples. The list is meant to be passed
  800. to L{add_books_to_metadata}.
  801. :metadata: If not None, it is a list of :class:`Metadata` objects.
  802. The idea is to use the metadata to determine where on the device to
  803. put the book. len(metadata) == len(files). Apart from the regular
  804. cover (path to cover), there may also be a thumbnail attribute, which should
  805. be used in preference. The thumbnail attribute is of the form
  806. (width, height, cover_data as jpeg).
  807. '''
  808. new_booklist = []
  809. self.update_list = []
  810. file_count = float(len(files))
  811. self.problem_titles = []
  812. self.problem_msg = _("Some cover art could not be converted.\n"
  813. "Click 'Show Details' for a list.")
  814. if DEBUG:
  815. self.log.info("ITUNES.upload_books()")
  816. self._dump_files(files, header='upload_books()',indent=2)
  817. self._dump_update_list(header='upload_books()',indent=2)
  818. if isosx:
  819. for (i,file) in enumerate(files):
  820. format = file.rpartition('.')[2].lower()
  821. path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
  822. self._remove_existing_copy(path, metadata[i])
  823. fpath = self._get_fpath(file, metadata[i], format, update_md=True)
  824. db_added, lb_added = self._add_new_copy(fpath, metadata[i])
  825. thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format)
  826. this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format)
  827. new_booklist.append(this_book)
  828. self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book)
  829. # Add new_book to self.cached_books
  830. if DEBUG:
  831. self.log.info("ITUNES.upload_books()")
  832. self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
  833. ( metadata[i].title, metadata[i].author, metadata[i].uuid))
  834. self.cached_books[this_book.path] = {
  835. 'author': metadata[i].author,
  836. 'dev_book': db_added,
  837. 'format': format,
  838. 'lib_book': lb_added,
  839. 'title': metadata[i].title,
  840. 'uuid': metadata[i].uuid }
  841. # Report progress
  842. if self.report_progress is not None:
  843. self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
  844. elif iswindows:
  845. try:
  846. pythoncom.CoInitialize()
  847. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  848. for (i,file) in enumerate(files):
  849. format = file.rpartition('.')[2].lower()
  850. path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
  851. self._remove_existing_copy(path, metadata[i])
  852. fpath = self._get_fpath(file, metadata[i],format, update_md=True)
  853. db_added, lb_added = self._add_new_copy(fpath, metadata[i])
  854. if self.manual_sync_mode and not db_added:
  855. # Problem finding added book, probably title/author change needing to be written to metadata
  856. self.problem_msg = ("Title and/or author metadata mismatch with uploaded books.\n"
  857. "Click 'Show Details...' for affected books.")
  858. self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0]))
  859. thumb = self._cover_to_thumb(path, metadata[i], db_added, lb_added, format)
  860. this_book = self._create_new_book(fpath, metadata[i], path, db_added, lb_added, thumb, format)
  861. new_booklist.append(this_book)
  862. self._update_iTunes_metadata(metadata[i], db_added, lb_added, this_book)
  863. # Add new_book to self.cached_books
  864. if DEBUG:
  865. self.log.info("ITUNES.upload_books()")
  866. self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
  867. ( metadata[i].title, metadata[i].author, metadata[i].uuid))
  868. self.cached_books[this_book.path] = {
  869. 'author': metadata[i].author[0],
  870. 'dev_book': db_added,
  871. 'format': format,
  872. 'lib_book': lb_added,
  873. 'title': metadata[i].title,
  874. 'uuid': metadata[i].uuid}
  875. # Report progress
  876. if self.report_progress is not None:
  877. self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count))
  878. finally:
  879. pythoncom.CoUninitialize()
  880. if self.report_progress is not None:
  881. self.report_progress(1.0, _('finished'))
  882. # Tell sync_booklists we need a re-sync
  883. if not self.manual_sync_mode:
  884. self.update_needed = True
  885. self.update_msg = "Added books to device"
  886. if False:
  887. self._dump_booklist(new_booklist,header="after upload_books()",indent=2)
  888. self._dump_cached_books(header="after upload_books()",indent=2)
  889. return (new_booklist, [], [])
  890. # Private methods
  891. def _add_device_book(self,fpath, metadata):
  892. '''
  893. assumes pythoncom wrapper for windows
  894. '''
  895. self.log.info(" ITUNES._add_device_book()")
  896. if isosx:
  897. if 'iPod' in self.sources:
  898. connected_device = self.sources['iPod']
  899. device = self.iTunes.sources[connected_device]
  900. for pl in device.playlists():
  901. if pl.special_kind() == appscript.k.Books:
  902. break
  903. else:
  904. if DEBUG:
  905. self.log.error(" Device|Books playlist not found")
  906. # Add the passed book to the Device|Books playlist
  907. added = pl.add(appscript.mactypes.File(fpath),to=pl)
  908. if False:
  909. self.log.info(" '%s' added to Device|Books" % metadata.title)
  910. return added
  911. elif iswindows:
  912. if 'iPod' in self.sources:
  913. connected_device = self.sources['iPod']
  914. device = self.iTunes.sources.ItemByName(connected_device)
  915. db_added = None
  916. for pl in device.Playlists:
  917. if pl.Kind == self.PlaylistKind.index('User') and \
  918. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  919. break
  920. else:
  921. if DEBUG:
  922. self.log.info(" no Books playlist found")
  923. # Add the passed book to the Device|Books playlist
  924. if pl:
  925. file_s = ctypes.c_char_p(fpath)
  926. FileArray = ctypes.c_char_p * 1
  927. fa = FileArray(file_s)
  928. op_status = pl.AddFiles(fa)
  929. if DEBUG:
  930. sys.stdout.write(" uploading '%s' to Device|Books ..." % metadata.title)
  931. sys.stdout.flush()
  932. while op_status.InProgress:
  933. time.sleep(0.5)
  934. if DEBUG:
  935. sys.stdout.write('.')
  936. sys.stdout.flush()
  937. if DEBUG:
  938. sys.stdout.write("\n")
  939. sys.stdout.flush()
  940. if False:
  941. '''
  942. Preferred
  943. Disabled because op_status.Tracks never returns a value after adding file
  944. This would be the preferred approach (as under OSX)
  945. It works in _add_library_book()
  946. '''
  947. if DEBUG:
  948. sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title)
  949. sys.stdout.flush()
  950. while not op_status.Tracks:
  951. time.sleep(0.5)
  952. if DEBUG:
  953. sys.stdout.write('.')
  954. sys.stdout.flush()
  955. if DEBUG:
  956. print
  957. added = op_status.Tracks[0]
  958. else:
  959. '''
  960. Hackish
  961. Search Library|Books for the book we just added
  962. PDF file name is added title - need to search for base filename w/o extension
  963. '''
  964. format = fpath.rpartition('.')[2].lower()
  965. base_fn = fpath.rpartition(os.sep)[2]
  966. base_fn = base_fn.rpartition('.')[0]
  967. db_added = self._find_device_book(
  968. { 'title': base_fn if format == 'pdf' else metadata.title,
  969. 'author': metadata.authors[0],
  970. 'uuid': metadata.uuid,
  971. 'format': format})
  972. return db_added
  973. def _add_library_book(self,file, metadata):
  974. '''
  975. windows assumes pythoncom wrapper
  976. '''
  977. self.log.info(" ITUNES._add_library_book()")
  978. if isosx:
  979. added = self.iTunes.add(appscript.mactypes.File(file))
  980. elif iswindows:
  981. lib = self.iTunes.LibraryPlaylist
  982. file_s = ctypes.c_char_p(file)
  983. FileArray = ctypes.c_char_p * 1
  984. fa = FileArray(file_s)
  985. op_status = lib.AddFiles(fa)
  986. if DEBUG:
  987. self.log.info(" file added to Library|Books")
  988. self.log.info(" iTunes adding '%s'" % file)
  989. if DEBUG:
  990. sys.stdout.write(" iTunes copying '%s' ..." % metadata.title)
  991. sys.stdout.flush()
  992. while op_status.InProgress:
  993. time.sleep(0.5)
  994. if DEBUG:
  995. sys.stdout.write('.')
  996. sys.stdout.flush()
  997. if DEBUG:
  998. sys.stdout.write("\n")
  999. sys.stdout.flush()
  1000. if True:
  1001. '''
  1002. Preferable
  1003. Originally disabled because op_status.Tracks never returned a value
  1004. after adding file. Seems to be working with iTunes 9.2.1.5 06 Aug 2010
  1005. '''
  1006. if DEBUG:
  1007. sys.stdout.write(" waiting for handle to added '%s' ..." % metadata.title)
  1008. sys.stdout.flush()
  1009. while op_status.Tracks is None:
  1010. time.sleep(0.5)
  1011. if DEBUG:
  1012. sys.stdout.write('.')
  1013. sys.stdout.flush()
  1014. if DEBUG:
  1015. print
  1016. added = op_status.Tracks[0]
  1017. else:
  1018. '''
  1019. Hackish
  1020. Search Library|Books for the book we just added
  1021. PDF file name is added title - need to search for base filename w/o extension
  1022. '''
  1023. format = file.rpartition('.')[2].lower()
  1024. base_fn = file.rpartition(os.sep)[2]
  1025. base_fn = base_fn.rpartition('.')[0]
  1026. added = self._find_library_book(
  1027. { 'title': base_fn if format == 'pdf' else metadata.title,
  1028. 'author': metadata.author[0],
  1029. 'uuid': metadata.uuid,
  1030. 'format': format})
  1031. return added
  1032. def _add_new_copy(self, fpath, metadata):
  1033. '''
  1034. '''
  1035. if DEBUG:
  1036. self.log.info(" ITUNES._add_new_copy()")
  1037. db_added = None
  1038. lb_added = None
  1039. if self.manual_sync_mode:
  1040. db_added = self._add_device_book(fpath, metadata)
  1041. if not getattr(fpath, 'deleted_after_upload', False):
  1042. lb_added = self._add_library_book(fpath, metadata)
  1043. if lb_added:
  1044. if DEBUG:
  1045. self.log.info(" file added to Library|Books for iTunes<->iBooks tracking")
  1046. else:
  1047. lb_added = self._add_library_book(fpath, metadata)
  1048. if DEBUG:
  1049. self.log.info(" file added to Library|Books for pending sync")
  1050. return db_added, lb_added
  1051. def _cover_to_thumb(self, path, metadata, db_added, lb_added, format):
  1052. '''
  1053. assumes pythoncom wrapper for db_added
  1054. as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation
  1055. '''
  1056. self.log.info(" ITUNES._cover_to_thumb()")
  1057. thumb = None
  1058. if metadata.cover:
  1059. if format == 'epub':
  1060. # Pre-shrink cover
  1061. # self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT
  1062. try:
  1063. img = PILImage.open(metadata.cover)
  1064. width = img.size[0]
  1065. height = img.size[1]
  1066. scaled, nwidth, nheight = fit_image(width, height, self.MAX_COVER_WIDTH, self.MAX_COVER_HEIGHT)
  1067. if scaled:
  1068. if DEBUG:
  1069. self.log.info(" '%s' scaled from %sx%s to %sx%s" %
  1070. (metadata.cover,width,height,nwidth,nheight))
  1071. img = img.resize((nwidth, nheight), PILImage.ANTIALIAS)
  1072. cd = cStringIO.StringIO()
  1073. img.convert('RGB').save(cd, 'JPEG')
  1074. cover_data = cd.getvalue()
  1075. cd.close()
  1076. else:
  1077. with open(metadata.cover,'r+b') as cd:
  1078. cover_data = cd.read()
  1079. except:
  1080. self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
  1081. self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title))
  1082. import traceback
  1083. traceback.print_exc()
  1084. return thumb
  1085. if isosx:
  1086. # The following commands generate an error, but the artwork does in fact
  1087. # get sent to the device. Seems like a bug in Apple's automation interface?
  1088. # Could also be a problem with the integrity of the cover data?
  1089. if lb_added:
  1090. try:
  1091. lb_added.artworks[1].data_.set(cover_data)
  1092. except:
  1093. if DEBUG:
  1094. self.log.warning(" iTunes automation interface reported an error"
  1095. " when adding artwork to '%s' in the iTunes Library" % metadata.title)
  1096. pass
  1097. if db_added:
  1098. try:
  1099. db_added.artworks[1].data_.set(cover_data)
  1100. except:
  1101. if DEBUG:
  1102. self.log.warning(" iTunes automation interface reported an error"
  1103. " when adding artwork to '%s' on the iDevice" % metadata.title)
  1104. #import traceback
  1105. #traceback.print_exc()
  1106. #from calibre import ipython
  1107. #ipython(user_ns=locals())
  1108. pass
  1109. elif iswindows:
  1110. # Write the data to a real file for Windows iTunes
  1111. tc = os.path.join(tempfile.gettempdir(), "cover.jpg")
  1112. with open(tc,'wb') as tmp_cover:
  1113. tmp_cover.write(cover_data)
  1114. if lb_added:
  1115. if lb_added.Artwork.Count:
  1116. lb_added.Artwork.Item(1).SetArtworkFromFile(tc)
  1117. else:
  1118. lb_added.AddArtworkFromFile(tc)
  1119. if db_added:
  1120. if db_added.Artwork.Count:
  1121. db_added.Artwork.Item(1).SetArtworkFromFile(tc)
  1122. else:
  1123. db_added.AddArtworkFromFile(tc)
  1124. elif format == 'pdf':
  1125. if DEBUG:
  1126. self.log.info(" unable to set PDF cover via automation interface")
  1127. try:
  1128. # Resize for thumb
  1129. width = metadata.thumbnail[0]
  1130. height = metadata.thumbnail[1]
  1131. im = PILImage.open(metadata.cover)
  1132. im = im.resize((width, height), PILImage.ANTIALIAS)
  1133. of = cStringIO.StringIO()
  1134. im.convert('RGB').save(of, 'JPEG')
  1135. thumb = of.getvalue()
  1136. of.close()
  1137. # Refresh the thumbnail cache
  1138. if DEBUG:
  1139. self.log.info( " refreshing cached thumb for '%s'" % metadata.title)
  1140. zfw = ZipFile(self.archive_path, mode='a')
  1141. thumb_path = path.rpartition('.')[0] + '.jpg'
  1142. zfw.writestr(thumb_path, thumb)
  1143. except:
  1144. self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
  1145. self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title))
  1146. finally:
  1147. try:
  1148. zfw.close()
  1149. except:
  1150. pass
  1151. else:
  1152. if DEBUG:
  1153. self.log.info(" no cover defined in metadata for '%s'" % metadata.title)
  1154. return thumb
  1155. def _create_new_book(self,fpath, metadata, path, db_added, lb_added, thumb, format):
  1156. '''
  1157. '''
  1158. if DEBUG:
  1159. self.log.info(" ITUNES._create_new_book()")
  1160. this_book = Book(metadata.title, authors_to_string(metadata.author))
  1161. this_book.datetime = time.gmtime()
  1162. this_book.db_id = None
  1163. this_book.device_collections = []
  1164. this_book.format = format
  1165. this_book.library_id = lb_added # ??? GR
  1166. this_book.path = path
  1167. this_book.thumbnail = thumb
  1168. this_book.iTunes_id = lb_added # ??? GR
  1169. this_book.uuid = metadata.uuid
  1170. if isosx:
  1171. if lb_added:
  1172. this_book.size = self._get_device_book_size(fpath, lb_added.size())
  1173. try:
  1174. this_book.datetime = parse_date(str(lb_added.date_added())).timetuple()
  1175. except:
  1176. pass
  1177. elif db_added:
  1178. this_book.size = self._get_device_book_size(fpath, db_added.size())
  1179. try:
  1180. this_book.datetime = parse_date(str(db_added.date_added())).timetuple()
  1181. except:
  1182. pass
  1183. elif iswindows:
  1184. if lb_added:
  1185. this_book.size = self._get_device_book_size(fpath, lb_added.Size)
  1186. try:
  1187. this_book.datetime = parse_date(str(lb_added.DateAdded)).timetuple()
  1188. except:
  1189. pass
  1190. elif db_added:
  1191. this_book.size = self._get_device_book_size(fpath, db_added.Size)
  1192. try:
  1193. this_book.datetime = parse_date(str(db_added.DateAdded)).timetuple()
  1194. except:
  1195. pass
  1196. return this_book
  1197. def _delete_iTunesMetadata_plist(self,fpath):
  1198. '''
  1199. Delete the plist file from the file to force recache
  1200. '''
  1201. zf = ZipFile(fpath,'a')
  1202. fnames = zf.namelist()
  1203. pl_name = 'iTunesMetadata.plist'
  1204. try:
  1205. plist = [x for x in fnames if pl_name in x][0]
  1206. except:
  1207. plist = None
  1208. if plist:
  1209. if DEBUG:
  1210. self.log.info(" _delete_iTunesMetadata_plist():")
  1211. self.log.info(" deleting '%s'\n from '%s'" % (pl_name,fpath))
  1212. zf.delete(pl_name)
  1213. zf.close()
  1214. def _discover_manual_sync_mode(self, wait=0):
  1215. '''
  1216. Assumes pythoncom for windows
  1217. wait is passed when launching iTunes, as it seems to need a moment to come to its senses
  1218. '''
  1219. if DEBUG:
  1220. self.log.info(" ITUNES._discover_manual_sync_mode()")
  1221. if wait:
  1222. time.sleep(wait)
  1223. if isosx:
  1224. connected_device = self.sources['iPod']
  1225. dev_books = None
  1226. device = self.iTunes.sources[connected_device]
  1227. for pl in device.playlists():
  1228. if pl.special_kind() == appscript.k.Books:
  1229. dev_books = pl.file_tracks()
  1230. break
  1231. else:
  1232. self.log.error(" book_playlist not found")
  1233. if len(dev_books):
  1234. first_book = dev_books[0]
  1235. if False:
  1236. self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.name(), first_book.artist()))
  1237. try:
  1238. first_book.bpm.set(0)
  1239. self.manual_sync_mode = True
  1240. except:
  1241. self.manual_sync_mode = False
  1242. else:
  1243. if DEBUG:
  1244. self.log.info(" adding tracer to empty Books|Playlist")
  1245. try:
  1246. added = pl.add(appscript.mactypes.File(P('tracer.epub')),to=pl)
  1247. time.sleep(0.5)
  1248. added.delete()
  1249. self.manual_sync_mode = True
  1250. except:
  1251. self.manual_sync_mode = False
  1252. elif iswindows:
  1253. connected_device = self.sources['iPod']
  1254. device = self.iTunes.sources.ItemByName(connected_device)
  1255. dev_books = None
  1256. for pl in device.Playlists:
  1257. if pl.Kind == self.PlaylistKind.index('User') and \
  1258. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  1259. dev_books = pl.Tracks
  1260. break
  1261. if dev_books.Count:
  1262. first_book = dev_books.Item(1)
  1263. #if DEBUG:
  1264. #self.log.info(" determing manual mode by modifying '%s' by %s" % (first_book.Name, first_book.Artist))
  1265. try:
  1266. first_book.BPM = 0
  1267. self.manual_sync_mode = True
  1268. except:
  1269. self.manual_sync_mode = False
  1270. else:
  1271. if DEBUG:
  1272. self.log.info(" sending tracer to empty Books|Playlist")
  1273. fpath = P('tracer.epub')
  1274. mi = MetaInformation('Tracer',['calibre'])
  1275. try:
  1276. added = self._add_device_book(fpath,mi)
  1277. time.sleep(0.5)
  1278. added.Delete()
  1279. self.manual_sync_mode = True
  1280. except:
  1281. self.manual_sync_mode = False
  1282. self.log.info(" iTunes.manual_sync_mode: %s" % self.manual_sync_mode)
  1283. def _dump_booklist(self, booklist, header=None,indent=0):
  1284. '''
  1285. '''
  1286. if header:
  1287. msg = '\n%sbooklist %s:' % (' '*indent,header)
  1288. self.log.info(msg)
  1289. self.log.info('%s%s' % (' '*indent,'-' * len(msg)))
  1290. for book in booklist:
  1291. if isosx:
  1292. self.log.info("%s%-40.40s %-30.30s %-10.10s %s" %
  1293. (' '*indent,book.title, book.author, str(book.library_id)[-9:], book.uuid))
  1294. elif iswindows:
  1295. self.log.info("%s%-40.40s %-30.30s" %
  1296. (' '*indent,book.title, book.author))
  1297. self.log.info()
  1298. def _dump_cached_book(self, cached_book, header=None,indent=0):
  1299. '''
  1300. '''
  1301. if isosx:
  1302. if header:
  1303. msg = '%s%s' % (' '*indent,header)
  1304. self.log.info(msg)
  1305. self.log.info( "%s%s" % (' '*indent, '-' * len(msg)))
  1306. self.log.info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" %
  1307. (' '*indent,
  1308. 'title',
  1309. 'author',
  1310. 'lib_book',
  1311. 'dev_book',
  1312. 'uuid'))
  1313. self.log.info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" %
  1314. (' '*indent,
  1315. cached_book['title'],
  1316. cached_book['author'],
  1317. str(cached_book['lib_book'])[-9:],
  1318. str(cached_book['dev_book'])[-9:],
  1319. cached_book['uuid']))
  1320. elif iswindows:
  1321. if header:
  1322. msg = '%s%s' % (' '*indent,header)
  1323. self.log.info(msg)
  1324. self.log.info( "%s%s" % (' '*indent, '-' * len(msg)))
  1325. self.log.info("%s%-40.40s %-30.30s %s" %
  1326. (' '*indent,
  1327. cached_book['title'],
  1328. cached_book['author'],
  1329. cached_book['uuid']))
  1330. def _dump_cached_books(self, header=None, indent=0):
  1331. '''
  1332. '''
  1333. if header:
  1334. msg = '\n%sself.cached_books %s:' % (' '*indent,header)
  1335. self.log.info(msg)
  1336. self.log.info( "%s%s" % (' '*indent,'-' * len(msg)))
  1337. if isosx:
  1338. for cb in self.cached_books.keys():
  1339. self.log.info("%s%-40.40s %-30.30s %-10.10s %-10.10s %s" %
  1340. (' '*indent,
  1341. self.cached_books[cb]['title'],
  1342. self.cached_books[cb]['author'],
  1343. str(self.cached_books[cb]['lib_book'])[-9:],
  1344. str(self.cached_books[cb]['dev_book'])[-9:],
  1345. self.cached_books[cb]['uuid']))
  1346. elif iswindows:
  1347. for cb in self.cached_books.keys():
  1348. self.log.info("%s%-40.40s %-30.30s %-4.4s %s" %
  1349. (' '*indent,
  1350. self.cached_books[cb]['title'],
  1351. self.cached_books[cb]['author'],
  1352. self.cached_books[cb]['format'],
  1353. self.cached_books[cb]['uuid']))
  1354. self.log.info()
  1355. def _dump_epub_metadata(self, fpath):
  1356. '''
  1357. '''
  1358. self.log.info(" ITUNES.__get_epub_metadata()")
  1359. title = None
  1360. author = None
  1361. timestamp = None
  1362. zf = ZipFile(fpath,'r')
  1363. fnames = zf.namelist()
  1364. opf = [x for x in fnames if '.opf' in x][0]
  1365. if opf:
  1366. opf_raw = cStringIO.StringIO(zf.read(opf))
  1367. soup = BeautifulSoup(opf_raw.getvalue())
  1368. opf_raw.close()
  1369. title = soup.find('dc:title').renderContents()
  1370. author = soup.find('dc:creator').renderContents()
  1371. ts = soup.find('meta',attrs={'name':'calibre:timestamp'})
  1372. if ts:
  1373. # Touch existing calibre timestamp
  1374. timestamp = ts['content']
  1375. if not title or not author:
  1376. if DEBUG:
  1377. self.log.error(" couldn't extract title/author from %s in %s" % (opf,fpath))
  1378. self.log.error(" title: %s author: %s timestamp: %s" % (title, author, timestamp))
  1379. else:
  1380. if DEBUG:
  1381. self.log.error(" can't find .opf in %s" % fpath)
  1382. zf.close()
  1383. return (title, author, timestamp)
  1384. def _dump_files(self, files, header=None,indent=0):
  1385. if header:
  1386. msg = '\n%sfiles passed to %s:' % (' '*indent,header)
  1387. self.log.info(msg)
  1388. self.log.info( "%s%s" % (' '*indent,'-' * len(msg)))
  1389. for file in files:
  1390. if getattr(file, 'orig_file_path', None) is not None:
  1391. self.log.info(" %s%s" % (' '*indent,file.orig_file_path))
  1392. elif getattr(file, 'name', None) is not None:
  1393. self.log.info(" %s%s" % (' '*indent,file.name))
  1394. self.log.info()
  1395. def _dump_hex(self, src, length=16):
  1396. '''
  1397. '''
  1398. FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
  1399. N=0; result=''
  1400. while src:
  1401. s,src = src[:length],src[length:]
  1402. hexa = ' '.join(["%02X"%ord(x) for x in s])
  1403. s = s.translate(FILTER)
  1404. result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
  1405. N+=length
  1406. print result
  1407. def _dump_library_books(self, library_books):
  1408. '''
  1409. '''
  1410. if DEBUG:
  1411. self.log.info("\n library_books:")
  1412. for book in library_books:
  1413. self.log.info(" %s" % book)
  1414. self.log.info()
  1415. def _dump_update_list(self,header=None,indent=0):
  1416. if header:
  1417. msg = '\n%sself.update_list %s' % (' '*indent,header)
  1418. self.log.info(msg)
  1419. self.log.info( "%s%s" % (' '*indent,'-' * len(msg)))
  1420. if isosx:
  1421. for ub in self.update_list:
  1422. self.log.info("%s%-40.40s %-30.30s %-10.10s %s" %
  1423. (' '*indent,
  1424. ub['title'],
  1425. ub['author'],
  1426. str(ub['lib_book'])[-9:],
  1427. ub['uuid']))
  1428. elif iswindows:
  1429. for ub in self.update_list:
  1430. self.log.info("%s%-40.40s %-30.30s" %
  1431. (' '*indent,
  1432. ub['title'],
  1433. ub['author']))
  1434. self.log.info()
  1435. def _find_device_book(self, search):
  1436. '''
  1437. Windows-only method to get a handle to device book in the current pythoncom session
  1438. '''
  1439. if iswindows:
  1440. dev_books = self._get_device_books_playlist()
  1441. if DEBUG:
  1442. self.log.info(" ITUNES._find_device_book()")
  1443. self.log.info(" searching for '%s' by '%s' (%s)" %
  1444. (search['title'], search['author'],search['uuid']))
  1445. attempts = 9
  1446. while attempts:
  1447. # Try by uuid - only one hit
  1448. if 'uuid' in search and search['uuid']:
  1449. if DEBUG:
  1450. self.log.info(" searching by uuid '%s' ..." % search['uuid'])
  1451. hits = dev_books.Search(search['uuid'],self.SearchField.index('All'))
  1452. if hits:
  1453. hit = hits[0]
  1454. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1455. return hit
  1456. # Try by author - there could be multiple hits
  1457. if search['author']:
  1458. if DEBUG:
  1459. self.log.info(" searching by author '%s' ..." % search['author'])
  1460. hits = dev_books.Search(search['author'],self.SearchField.index('Artists'))
  1461. if hits:
  1462. for hit in hits:
  1463. if hit.Name == search['title']:
  1464. if DEBUG:
  1465. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1466. return hit
  1467. # Search by title if no author available
  1468. if DEBUG:
  1469. self.log.info(" searching by title '%s' ..." % search['title'])
  1470. hits = dev_books.Search(search['title'],self.SearchField.index('All'))
  1471. if hits:
  1472. for hit in hits:
  1473. if hit.Name == search['title']:
  1474. if DEBUG:
  1475. self.log.info(" found '%s'" % (hit.Name))
  1476. return hit
  1477. # PDF just sent, title not updated yet, look for export pattern
  1478. # PDF metadata was rewritten at export as 'safe(title) - safe(author)'
  1479. if search['format'] == 'pdf':
  1480. title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title'])
  1481. author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author'])
  1482. if DEBUG:
  1483. self.log.info(" searching by name: '%s - %s'" % (title,author))
  1484. hits = dev_books.Search('%s - %s' % (title,author),
  1485. self.SearchField.index('All'))
  1486. if hits:
  1487. hit = hits[0]
  1488. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1489. return hit
  1490. else:
  1491. if DEBUG:
  1492. self.log.info(" no PDF hits")
  1493. attempts -= 1
  1494. time.sleep(0.5)
  1495. if DEBUG:
  1496. self.log.warning(" attempt #%d" % (10 - attempts))
  1497. if DEBUG:
  1498. self.log.error(" no hits")
  1499. return None
  1500. def _find_library_book(self, search):
  1501. '''
  1502. Windows-only method to get a handle to a library book in the current pythoncom session
  1503. '''
  1504. if iswindows:
  1505. if DEBUG:
  1506. self.log.info(" ITUNES._find_library_book()")
  1507. '''
  1508. if 'uuid' in search:
  1509. self.log.info(" looking for '%s' by %s (%s)" %
  1510. (search['title'], search['author'], search['uuid']))
  1511. else:
  1512. self.log.info(" looking for '%s' by %s" %
  1513. (search['title'], search['author']))
  1514. '''
  1515. for source in self.iTunes.sources:
  1516. if source.Kind == self.Sources.index('Library'):
  1517. lib = source
  1518. if DEBUG:
  1519. self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
  1520. break
  1521. else:
  1522. if DEBUG:
  1523. self.log.info(" Library source not found")
  1524. if lib is not None:
  1525. lib_books = None
  1526. for pl in lib.Playlists:
  1527. if pl.Kind == self.PlaylistKind.index('User') and \
  1528. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  1529. if DEBUG:
  1530. self.log.info(" Books playlist: '%s'" % (pl.Name))
  1531. lib_books = pl
  1532. break
  1533. else:
  1534. if DEBUG:
  1535. self.log.error(" no Books playlist found")
  1536. attempts = 9
  1537. while attempts:
  1538. # Find book whose Album field = search['uuid']
  1539. if 'uuid' in search and search['uuid']:
  1540. if DEBUG:
  1541. self.log.info(" searching by uuid '%s' ..." % search['uuid'])
  1542. hits = lib_books.Search(search['uuid'],self.SearchField.index('All'))
  1543. if hits:
  1544. hit = hits[0]
  1545. if DEBUG:
  1546. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1547. return hit
  1548. # Search by author if known
  1549. if search['author']:
  1550. if DEBUG:
  1551. self.log.info(" searching by author '%s' ..." % search['author'])
  1552. hits = lib_books.Search(search['author'],self.SearchField.index('Artists'))
  1553. if hits:
  1554. for hit in hits:
  1555. if hit.Name == search['title']:
  1556. if DEBUG:
  1557. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1558. return hit
  1559. # Search by title if no author available
  1560. if DEBUG:
  1561. self.log.info(" searching by title '%s' ..." % search['title'])
  1562. hits = lib_books.Search(search['title'],self.SearchField.index('All'))
  1563. if hits:
  1564. for hit in hits:
  1565. if hit.Name == search['title']:
  1566. if DEBUG:
  1567. self.log.info(" found '%s'" % (hit.Name))
  1568. return hit
  1569. # PDF just sent, title not updated yet, look for export pattern
  1570. # PDF metadata was rewritten at export as 'safe(title) - safe(author)'
  1571. if search['format'] == 'pdf':
  1572. title = re.sub(r'[^0-9a-zA-Z ]', '_', search['title'])
  1573. author = re.sub(r'[^0-9a-zA-Z ]', '_', search['author'])
  1574. if DEBUG:
  1575. self.log.info(" searching by name: %s - %s" % (title,author))
  1576. hits = lib_books.Search('%s - %s' % (title,author),
  1577. self.SearchField.index('All'))
  1578. if hits:
  1579. hit = hits[0]
  1580. self.log.info(" found '%s' by %s (%s)" % (hit.Name, hit.Artist, hit.Composer))
  1581. return hit
  1582. else:
  1583. if DEBUG:
  1584. self.log.info(" no PDF hits")
  1585. attempts -= 1
  1586. time.sleep(0.5)
  1587. if DEBUG:
  1588. self.log.warning(" attempt #%d" % (10 - attempts))
  1589. if DEBUG:
  1590. self.log.error(" search for '%s' yielded no hits" % search['title'])
  1591. return None
  1592. def _generate_thumbnail(self, book_path, book):
  1593. '''
  1594. Convert iTunes artwork to thumbnail
  1595. Cache generated thumbnails
  1596. cache_dir = os.path.join(config_dir, 'caches', 'itunes')
  1597. as of iTunes 9.2, iBooks 1.1, can't set artwork for PDF files via automation
  1598. '''
  1599. # self.settings().use_subdirs is a repurposed DeviceConfig field
  1600. # We're using it to skip fetching/caching covers to speed things up
  1601. if not self.settings().use_subdirs:
  1602. thumb_data = None
  1603. return thumb_data
  1604. thumb_path = book_path.rpartition('.')[0] + '.jpg'
  1605. if isosx:
  1606. title = book.name()
  1607. elif iswindows:
  1608. title = book.Name
  1609. try:
  1610. zfr = ZipFile(self.archive_path)
  1611. thumb_data = zfr.read(thumb_path)
  1612. if thumb_data == 'None':
  1613. if False:
  1614. self.log.info(" ITUNES._generate_thumbnail()\n returning None from cover cache for '%s'" % title)
  1615. zfr.close()
  1616. return None
  1617. except:
  1618. zfw = ZipFile(self.archive_path, mode='a')
  1619. else:
  1620. if False:
  1621. self.log.info(" returning thumb from cache for '%s'" % title)
  1622. return thumb_data
  1623. if DEBUG:
  1624. self.log.info(" ITUNES._generate_thumbnail():")
  1625. if isosx:
  1626. # Fetch the artwork from iTunes
  1627. try:
  1628. data = book.artworks[1].raw_data().data
  1629. except:
  1630. # If no artwork, write an empty marker to cache
  1631. if DEBUG:
  1632. self.log.error(" error fetching iTunes artwork for '%s'" % title)
  1633. zfw.writestr(thumb_path, 'None')
  1634. zfw.close()
  1635. return None
  1636. # Generate a thumb
  1637. try:
  1638. img_data = cStringIO.StringIO(data)
  1639. im = PILImage.open(img_data)
  1640. scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80)
  1641. im = im.resize((int(width),int(height)), PILImage.ANTIALIAS)
  1642. thumb = cStringIO.StringIO()
  1643. im.convert('RGB').save(thumb,'JPEG')
  1644. thumb_data = thumb.getvalue()
  1645. thumb.close()
  1646. if False:
  1647. self.log.info(" generated thumb for '%s', caching" % title)
  1648. # Cache the tagged thumb
  1649. zfw.writestr(thumb_path, thumb_data)
  1650. except:
  1651. if DEBUG:
  1652. self.log.error(" error generating thumb for '%s', caching empty marker" % book.name())
  1653. self._dump_hex(data[:32])
  1654. thumb_data = None
  1655. # Cache the empty cover
  1656. zfw.writestr(thumb_path, 'None')
  1657. finally:
  1658. img_data.close()
  1659. zfw.close()
  1660. return thumb_data
  1661. elif iswindows:
  1662. if not book.Artwork.Count:
  1663. if DEBUG:
  1664. self.log.info(" no artwork available for '%s'" % book.Name)
  1665. zfw.writestr(thumb_path, 'None')
  1666. zfw.close()
  1667. return None
  1668. # Fetch the artwork from iTunes
  1669. try:
  1670. tmp_thumb = os.path.join(tempfile.gettempdir(), "thumb.%s" % self.ArtworkFormat[book.Artwork.Item(1).Format])
  1671. book.Artwork.Item(1).SaveArtworkToFile(tmp_thumb)
  1672. # Resize the cover
  1673. im = PILImage.open(tmp_thumb)
  1674. scaled, width, height = fit_image(im.size[0],im.size[1], 60, 80)
  1675. im = im.resize((int(width),int(height)), PILImage.ANTIALIAS)
  1676. thumb = cStringIO.StringIO()
  1677. im.convert('RGB').save(thumb,'JPEG')
  1678. thumb_data = thumb.getvalue()
  1679. os.remove(tmp_thumb)
  1680. thumb.close()
  1681. if False:
  1682. self.log.info(" generated thumb for '%s', caching" % book.Name)
  1683. # Cache the tagged thumb
  1684. zfw.writestr(thumb_path, thumb_data)
  1685. except:
  1686. if DEBUG:
  1687. self.log.error(" error generating thumb for '%s', caching empty marker" % book.Name)
  1688. thumb_data = None
  1689. # Cache the empty cover
  1690. zfw.writestr(thumb_path,'None')
  1691. finally:
  1692. zfw.close()
  1693. return thumb_data
  1694. def _get_device_book_size(self, file, compressed_size):
  1695. '''
  1696. Calculate the exploded size of file
  1697. '''
  1698. exploded_file_size = compressed_size
  1699. format = file.rpartition('.')[2].lower()
  1700. if format == 'epub':
  1701. myZip = ZipFile(file,'r')
  1702. myZipList = myZip.infolist()
  1703. exploded_file_size = 0
  1704. for file in myZipList:
  1705. exploded_file_size += file.file_size
  1706. if False:
  1707. self.log.info(" ITUNES._get_device_book_size()")
  1708. self.log.info(" %d items in archive" % len(myZipList))
  1709. self.log.info(" compressed: %d exploded: %d" % (compressed_size, exploded_file_size))
  1710. myZip.close()
  1711. return exploded_file_size
  1712. def _get_device_books(self):
  1713. '''
  1714. Assumes pythoncom wrapper for Windows
  1715. '''
  1716. if DEBUG:
  1717. self.log.info("\n ITUNES._get_device_books()")
  1718. device_books = []
  1719. if isosx:
  1720. if 'iPod' in self.sources:
  1721. connected_device = self.sources['iPod']
  1722. device = self.iTunes.sources[connected_device]
  1723. for pl in device.playlists():
  1724. if pl.special_kind() == appscript.k.Books:
  1725. if DEBUG:
  1726. self.log.info(" Book playlist: '%s'" % (pl.name()))
  1727. books = pl.file_tracks()
  1728. break
  1729. else:
  1730. self.log.error(" book_playlist not found")
  1731. for book in books:
  1732. # This may need additional entries for international iTunes users
  1733. if book.kind() in self.Audiobooks:
  1734. if DEBUG:
  1735. self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind()))
  1736. else:
  1737. if DEBUG:
  1738. self.log.info(" %-30.30s %-30.30s %-40.40s [%s]" %
  1739. (book.name(), book.artist(), book.album(), book.kind()))
  1740. device_books.append(book)
  1741. if DEBUG:
  1742. self.log.info()
  1743. elif iswindows:
  1744. if 'iPod' in self.sources:
  1745. try:
  1746. pythoncom.CoInitialize()
  1747. connected_device = self.sources['iPod']
  1748. device = self.iTunes.sources.ItemByName(connected_device)
  1749. dev_books = None
  1750. for pl in device.Playlists:
  1751. if pl.Kind == self.PlaylistKind.index('User') and \
  1752. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  1753. if DEBUG:
  1754. self.log.info(" Books playlist: '%s'" % (pl.Name))
  1755. dev_books = pl.Tracks
  1756. break
  1757. else:
  1758. if DEBUG:
  1759. self.log.info(" no Books playlist found")
  1760. for book in dev_books:
  1761. # This may need additional entries for international iTunes users
  1762. if book.KindAsString in self.Audiobooks:
  1763. if DEBUG:
  1764. self.log.info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString))
  1765. else:
  1766. if DEBUG:
  1767. self.log.info(" %-30.30s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Album, book.KindAsString))
  1768. device_books.append(book)
  1769. if DEBUG:
  1770. self.log.info()
  1771. finally:
  1772. pythoncom.CoUninitialize()
  1773. return device_books
  1774. def _get_device_books_playlist(self):
  1775. '''
  1776. assumes pythoncom wrapper
  1777. '''
  1778. if iswindows:
  1779. if 'iPod' in self.sources:
  1780. pl = None
  1781. connected_device = self.sources['iPod']
  1782. device = self.iTunes.sources.ItemByName(connected_device)
  1783. for pl in device.Playlists:
  1784. if pl.Kind == self.PlaylistKind.index('User') and \
  1785. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  1786. break
  1787. else:
  1788. if DEBUG:
  1789. self.log.error(" no iPad|Books playlist found")
  1790. return pl
  1791. def _get_fpath(self,file, metadata, format, update_md=False):
  1792. '''
  1793. If the database copy will be deleted after upload, we have to
  1794. use file (the PersistentTemporaryFile), which will be around until
  1795. calibre exits.
  1796. If we're using the database copy, delete the plist
  1797. '''
  1798. if DEBUG:
  1799. self.log.info(" ITUNES._get_fpath()")
  1800. fpath = file
  1801. if not getattr(fpath, 'deleted_after_upload', False):
  1802. if getattr(file, 'orig_file_path', None) is not None:
  1803. # Database copy
  1804. fpath = file.orig_file_path
  1805. self._delete_iTunesMetadata_plist(fpath)
  1806. elif getattr(file, 'name', None) is not None:
  1807. # PTF
  1808. fpath = file.name
  1809. else:
  1810. # Recipe - PTF
  1811. if DEBUG:
  1812. self.log.info(" file will be deleted after upload")
  1813. if format == 'epub' and update_md:
  1814. self._update_epub_metadata(fpath, metadata)
  1815. return fpath
  1816. def _get_library_books(self):
  1817. '''
  1818. Populate a dict of paths from iTunes Library|Books
  1819. Windows assumes pythoncom wrapper
  1820. '''
  1821. if DEBUG:
  1822. self.log.info("\n ITUNES._get_library_books()")
  1823. library_books = {}
  1824. library_orphans = {}
  1825. lib = None
  1826. if isosx:
  1827. for source in self.iTunes.sources():
  1828. if source.kind() == appscript.k.library:
  1829. lib = source
  1830. if DEBUG:
  1831. self.log.info(" Library source: '%s'" % (lib.name()))
  1832. break
  1833. else:
  1834. if DEBUG:
  1835. self.log.error(' Library source not found')
  1836. if lib is not None:
  1837. lib_books = None
  1838. if lib.playlists():
  1839. for pl in lib.playlists():
  1840. if pl.special_kind() == appscript.k.Books:
  1841. if DEBUG:
  1842. self.log.info(" Books playlist: '%s'" % (pl.name()))
  1843. break
  1844. else:
  1845. if DEBUG:
  1846. self.log.info(" no Library|Books playlist found")
  1847. lib_books = pl.file_tracks()
  1848. for book in lib_books:
  1849. # This may need additional entries for international iTunes users
  1850. if book.kind() in self.Audiobooks:
  1851. if DEBUG:
  1852. self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind()))
  1853. else:
  1854. # Collect calibre orphans - remnants of recipe uploads
  1855. format = 'pdf' if book.kind().startswith('PDF') else 'epub'
  1856. path = self.path_template % (book.name(), book.artist(),format)
  1857. if str(book.description()).startswith(self.description_prefix):
  1858. try:
  1859. if book.location() == appscript.k.missing_value:
  1860. library_orphans[path] = book
  1861. if False:
  1862. self.log.info(" found iTunes PTF '%s' in Library|Books" % book.name())
  1863. except:
  1864. if DEBUG:
  1865. self.log.error(" iTunes returned an error returning .location() with %s" % book.name())
  1866. library_books[path] = book
  1867. if DEBUG:
  1868. self.log.info(" %-30.30s %-30.30s %-40.40s [%s]" %
  1869. (book.name(), book.artist(), book.album(), book.kind()))
  1870. else:
  1871. if DEBUG:
  1872. self.log.info(' no Library playlists')
  1873. else:
  1874. if DEBUG:
  1875. self.log.info(' no Library found')
  1876. elif iswindows:
  1877. lib = None
  1878. for source in self.iTunes.sources:
  1879. if source.Kind == self.Sources.index('Library'):
  1880. lib = source
  1881. self.log.info(" Library source: '%s' kind: %s" % (lib.Name, self.Sources[lib.Kind]))
  1882. break
  1883. else:
  1884. self.log.error(" Library source not found")
  1885. if lib is not None:
  1886. lib_books = None
  1887. if lib.Playlists is not None:
  1888. for pl in lib.Playlists:
  1889. if pl.Kind == self.PlaylistKind.index('User') and \
  1890. pl.SpecialKind == self.PlaylistSpecialKind.index('Books'):
  1891. if DEBUG:
  1892. self.log.info(" Books playlist: '%s'" % (pl.Name))
  1893. lib_books = pl.Tracks
  1894. break
  1895. else:
  1896. if DEBUG:
  1897. self.log.error(" no Library|Books playlist found")
  1898. else:
  1899. if DEBUG:
  1900. self.log.error(" no Library playlists found")
  1901. try:
  1902. for book in lib_books:
  1903. # This may need additional entries for international iTunes users
  1904. if book.KindAsString in self.Audiobooks:
  1905. if DEBUG:
  1906. self.log.info(" ignoring %-30.30s of type '%s'" % (book.Name, book.KindAsString))
  1907. else:
  1908. format = 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
  1909. path = self.path_template % (book.Name, book.Artist,format)
  1910. # Collect calibre orphans
  1911. if book.Description.startswith(self.description_prefix):
  1912. if not book.Location:
  1913. library_orphans[path] = book
  1914. if False:
  1915. self.log.info(" found iTunes PTF '%s' in Library|Books" % book.Name)
  1916. library_books[path] = book
  1917. if DEBUG:
  1918. self.log.info(" %-30.30s %-30.30s %-40.40s [%s]" % (book.Name, book.Artist, book.Album, book.KindAsString))
  1919. except:
  1920. if DEBUG:
  1921. self.log.info(" no books in library")
  1922. self.library_orphans = library_orphans
  1923. return library_books
  1924. def _get_purchased_book_ids(self):
  1925. '''
  1926. Return Device|Purchased
  1927. '''
  1928. if 'iPod' in self.sources:
  1929. connected_device = self.sources['iPod']
  1930. if isosx:
  1931. if 'Purchased' in self.iTunes.sources[connected_device].playlists.name():
  1932. return [pb.database_ID() for pb in self.iTunes.sources[connected_device].playlists['Purchased'].file_tracks()]
  1933. else:
  1934. return []
  1935. elif iswindows:
  1936. dev = self.iTunes.sources.ItemByName(connected_device)
  1937. if dev.Playlists is not None:
  1938. dev_playlists = [pl.Name for pl in dev.Playlists]
  1939. if 'Purchased' in dev_playlists:
  1940. return self.iTunes.sources.ItemByName(connected_device).Playlists.ItemByName('Purchased').Tracks
  1941. else:
  1942. return []
  1943. def _get_sources(self):
  1944. '''
  1945. Return a dict of sources
  1946. Check for >1 iPod device connected to iTunes
  1947. '''
  1948. if isosx:
  1949. try:
  1950. names = [s.name() for s in self.iTunes.sources()]
  1951. kinds = [str(s.kind()).rpartition('.')[2] for s in self.iTunes.sources()]
  1952. except:
  1953. # User probably quit iTunes
  1954. return {}
  1955. elif iswindows:
  1956. # Assumes a pythoncom wrapper
  1957. it_sources = ['Unknown','Library','iPod','AudioCD','MP3CD','Device','RadioTuner','SharedLibrary']
  1958. names = [s.name for s in self.iTunes.sources]
  1959. kinds = [it_sources[s.kind] for s in self.iTunes.sources]
  1960. # If more than one connected iDevice, remove all from list to prevent driver initialization
  1961. if kinds.count('iPod') > 1:
  1962. if DEBUG:
  1963. self.log.error(" %d connected iPod devices detected, calibre supports a single connected iDevice" % kinds.count('iPod'))
  1964. while kinds.count('iPod'):
  1965. index = kinds.index('iPod')
  1966. kinds.pop(index)
  1967. names.pop(index)
  1968. return dict(zip(kinds,names))
  1969. def _is_alpha(self,char):
  1970. '''
  1971. '''
  1972. if not re.search('[a-zA-Z]',char):
  1973. return False
  1974. else:
  1975. return True
  1976. def _launch_iTunes(self):
  1977. '''
  1978. '''
  1979. if DEBUG:
  1980. self.log.info(" ITUNES:_launch_iTunes():\n Instantiating iTunes")
  1981. if isosx:
  1982. '''
  1983. Launch iTunes if not already running
  1984. '''
  1985. # Instantiate iTunes
  1986. running_apps = appscript.app('System Events')
  1987. if not 'iTunes' in running_apps.processes.name():
  1988. if DEBUG:
  1989. self.log.info( "ITUNES:_launch_iTunes(): Launching iTunes" )
  1990. try:
  1991. self.iTunes = iTunes= appscript.app('iTunes', hide=True)
  1992. except:
  1993. self.iTunes = None
  1994. raise UserFeedback(' ITUNES._launch_iTunes(): unable to find installed iTunes', details=None, level=UserFeedback.WARN)
  1995. iTunes.run()
  1996. self.initial_status = 'launched'
  1997. else:
  1998. self.iTunes = appscript.app('iTunes')
  1999. self.initial_status = 'already running'
  2000. # Read the current storage path for iTunes media
  2001. cmd = "defaults read com.apple.itunes NSNavLastRootDirectory"
  2002. proc = subprocess.Popen( cmd, shell=True, cwd=os.curdir, stdout=subprocess.PIPE)
  2003. proc.wait()
  2004. media_dir = os.path.expanduser(proc.communicate()[0].strip())
  2005. if os.path.exists(media_dir):
  2006. self.iTunes_media = media_dir
  2007. else:
  2008. self.log.error(" could not confirm valid iTunes.media_dir from %s" % 'com.apple.itunes')
  2009. self.log.error(" media_dir: %s" % media_dir)
  2010. if DEBUG:
  2011. self.log.info(" %s %s" % (__appname__, __version__))
  2012. self.log.info(" [OSX %s - %s (%s), driver version %d.%d.%d]" %
  2013. (self.iTunes.name(), self.iTunes.version(), self.initial_status,
  2014. self.version[0],self.version[1],self.version[2]))
  2015. self.log.info(" iTunes_media: %s" % self.iTunes_media)
  2016. self.log.info(" calibre_library_path: %s" % self.calibre_library_path)
  2017. if iswindows:
  2018. '''
  2019. Launch iTunes if not already running
  2020. Assumes pythoncom wrapper
  2021. *** Current implementation doesn't handle UNC paths correctly,
  2022. and python has two incompatible methods to parse UNCs:
  2023. os.path.splitdrive() and os.path.splitunc()
  2024. need to use os.path.normpath on result of splitunc()
  2025. Once you have the //server/share, convert with os.path.normpath('//server/share')
  2026. os.path.splitdrive doesn't work as advertised, so use os.path.splitunc
  2027. os.path.splitunc("//server/share") returns ('//server/share','')
  2028. os.path.splitunc("C:/Documents") returns ('c:','/documents')
  2029. os.path.normpath("//server/share") returns "\\\\server\\share"
  2030. '''
  2031. # Instantiate iTunes
  2032. try:
  2033. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  2034. except:
  2035. self.iTunes = None
  2036. raise UserFeedback(' ITUNES._launch_iTunes(): unable to find installed iTunes', details=None, level=UserFeedback.WARN)
  2037. if not DEBUG:
  2038. self.iTunes.Windows[0].Minimized = True
  2039. self.initial_status = 'launched'
  2040. # Read the current storage path for iTunes media from the XML file
  2041. media_dir = ''
  2042. string = None
  2043. with open(self.iTunes.LibraryXMLPath, 'r') as xml:
  2044. for line in xml:
  2045. if line.strip().startswith('<key>Music Folder'):
  2046. soup = BeautifulSoup(line)
  2047. string = soup.find('string').renderContents()
  2048. media_dir = os.path.abspath(string[len('file://localhost/'):].replace('%20',' '))
  2049. break
  2050. if os.path.exists(media_dir):
  2051. self.iTunes_media = media_dir
  2052. elif hasattr(string,'parent'):
  2053. self.log.error(" could not extract valid iTunes.media_dir from %s" % self.iTunes.LibraryXMLPath)
  2054. self.log.error(" %s" % string.parent.prettify())
  2055. self.log.error(" '%s' not found" % media_dir)
  2056. else:
  2057. self.log.error(" no media dir found: string: %s" % string)
  2058. if DEBUG:
  2059. self.log.info(" %s %s" % (__appname__, __version__))
  2060. self.log.info(" [Windows %s - %s (%s), driver version %d.%d.%d]" %
  2061. (self.iTunes.Windows[0].name, self.iTunes.Version, self.initial_status,
  2062. self.version[0],self.version[1],self.version[2]))
  2063. self.log.info(" iTunes_media: %s" % self.iTunes_media)
  2064. self.log.info(" calibre_library_path: %s" % self.calibre_library_path)
  2065. def _purge_orphans(self,library_books, cached_books):
  2066. '''
  2067. Scan library_books for any paths not on device
  2068. Remove any iTunes orphans originally added by calibre
  2069. This occurs when the user deletes a book in iBooks while disconnected
  2070. '''
  2071. if DEBUG:
  2072. self.log.info(" ITUNES._purge_orphans()")
  2073. #self._dump_library_books(library_books)
  2074. #self.log.info(" cached_books:\n %s" % "\n ".join(cached_books.keys()))
  2075. for book in library_books:
  2076. if isosx:
  2077. if book not in cached_books and \
  2078. str(library_books[book].description()).startswith(self.description_prefix):
  2079. if DEBUG:
  2080. self.log.info(" '%s' not found on iDevice, removing from iTunes" % book)
  2081. btr = { 'title':library_books[book].name(),
  2082. 'author':library_books[book].artist(),
  2083. 'lib_book':library_books[book]}
  2084. self._remove_from_iTunes(btr)
  2085. elif iswindows:
  2086. if book not in cached_books and \
  2087. library_books[book].Description.startswith(self.description_prefix):
  2088. if DEBUG:
  2089. self.log.info(" '%s' not found on iDevice, removing from iTunes" % book)
  2090. btr = { 'title':library_books[book].Name,
  2091. 'author':library_books[book].Artist,
  2092. 'lib_book':library_books[book]}
  2093. self._remove_from_iTunes(btr)
  2094. if DEBUG:
  2095. self.log.info()
  2096. def _remove_existing_copy(self, path, metadata):
  2097. '''
  2098. '''
  2099. if DEBUG:
  2100. self.log.info(" ITUNES._remove_existing_copy()")
  2101. if self.manual_sync_mode:
  2102. # Delete existing from Device|Books, add to self.update_list
  2103. # for deletion from booklist[0] during add_books_to_metadata
  2104. for book in self.cached_books:
  2105. if self.cached_books[book]['uuid'] == metadata.uuid or \
  2106. (self.cached_books[book]['title'] == metadata.title and \
  2107. self.cached_books[book]['author'] == metadata.authors[0]):
  2108. self.update_list.append(self.cached_books[book])
  2109. self._remove_from_device(self.cached_books[book])
  2110. if DEBUG:
  2111. self.log.info( " deleting device book '%s'" % (metadata.title))
  2112. if not getattr(file, 'deleted_after_upload', False):
  2113. self._remove_from_iTunes(self.cached_books[book])
  2114. if DEBUG:
  2115. self.log.info(" deleting library book '%s'" % metadata.title)
  2116. break
  2117. else:
  2118. if DEBUG:
  2119. self.log.info(" '%s' not in cached_books" % metadata.title)
  2120. else:
  2121. # Delete existing from Library|Books, add to self.update_list
  2122. # for deletion from booklist[0] during add_books_to_metadata
  2123. for book in self.cached_books:
  2124. if self.cached_books[book]['uuid'] == metadata.uuid or \
  2125. (self.cached_books[book]['title'] == metadata.title and \
  2126. self.cached_books[book]['author'] == metadata.authors[0]):
  2127. self.update_list.append(self.cached_books[book])
  2128. self._remove_from_iTunes(self.cached_books[book])
  2129. if DEBUG:
  2130. self.log.info( " deleting library book '%s'" % metadata.title)
  2131. break
  2132. else:
  2133. if DEBUG:
  2134. self.log.info(" '%s' not found in cached_books" % metadata.title)
  2135. def _remove_from_device(self, cached_book):
  2136. '''
  2137. Windows assumes pythoncom wrapper
  2138. '''
  2139. self.log.info(" ITUNES._remove_from_device()")
  2140. if isosx:
  2141. if DEBUG:
  2142. self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
  2143. try:
  2144. cached_book['dev_book'].delete()
  2145. except:
  2146. self.log.error(" error deleting '%s'" % cached_book['title'])
  2147. elif iswindows:
  2148. hit = self._find_device_book(cached_book)
  2149. if hit:
  2150. if DEBUG:
  2151. self.log.info(" deleting '%s' from iDevice" % cached_book['title'])
  2152. hit.Delete()
  2153. else:
  2154. if DEBUG:
  2155. self.log.warning(" unable to remove '%s' by '%s' (%s) from device" %
  2156. (cached_book['title'],cached_book['author'],cached_book['uuid']))
  2157. def _remove_from_iTunes(self, cached_book):
  2158. '''
  2159. iTunes does not delete books from storage when removing from database
  2160. We only want to delete stored copies if the file is stored in iTunes
  2161. We don't want to delete files stored outside of iTunes.
  2162. Also confirm that storage_path does not point into calibre's storage.
  2163. '''
  2164. if DEBUG:
  2165. self.log.info(" ITUNES._remove_from_iTunes():")
  2166. if isosx:
  2167. try:
  2168. storage_path = os.path.split(cached_book['lib_book'].location().path)
  2169. if cached_book['lib_book'].location().path.startswith(self.iTunes_media) and \
  2170. not storage_path[0].startswith(prefs['library_path']):
  2171. title_storage_path = storage_path[0]
  2172. if DEBUG:
  2173. self.log.info(" removing title_storage_path: %s" % title_storage_path)
  2174. try:
  2175. shutil.rmtree(title_storage_path)
  2176. except:
  2177. self.log.info(" '%s' not empty" % title_storage_path)
  2178. # Clean up title/author directories
  2179. author_storage_path = os.path.split(title_storage_path)[0]
  2180. self.log.info(" author_storage_path: %s" % author_storage_path)
  2181. author_files = os.listdir(author_storage_path)
  2182. if '.DS_Store' in author_files:
  2183. author_files.pop(author_files.index('.DS_Store'))
  2184. if not author_files:
  2185. shutil.rmtree(author_storage_path)
  2186. if DEBUG:
  2187. self.log.info(" removing empty author_storage_path")
  2188. else:
  2189. if DEBUG:
  2190. self.log.info(" author_storage_path not empty (%d objects):" % len(author_files))
  2191. self.log.info(" %s" % '\n'.join(author_files))
  2192. else:
  2193. self.log.info(" '%s' (stored external to iTunes, no files deleted)" % cached_book['title'])
  2194. except:
  2195. # We get here if there was an error with .location().path
  2196. if DEBUG:
  2197. self.log.info(" '%s' not in iTunes storage" % cached_book['title'])
  2198. try:
  2199. self.iTunes.delete(cached_book['lib_book'])
  2200. except:
  2201. if DEBUG:
  2202. self.log.info(" unable to remove '%s' from iTunes" % cached_book['title'])
  2203. elif iswindows:
  2204. '''
  2205. Assume we're wrapped in a pythoncom
  2206. Windows stores the book under a common author directory, so we just delete the .epub
  2207. '''
  2208. try:
  2209. book = cached_book['lib_book']
  2210. path = book.Location
  2211. except:
  2212. book = self._find_library_book(cached_book)
  2213. if book:
  2214. path = book.Location
  2215. if book:
  2216. if self.iTunes_media and path.startswith(self.iTunes_media) and \
  2217. not path.startswith(prefs['library_path']):
  2218. storage_path = os.path.split(path)
  2219. if DEBUG:
  2220. self.log.info(" removing '%s' at %s" %
  2221. (cached_book['title'], path))
  2222. try:
  2223. os.remove(path)
  2224. except:
  2225. self.log.warning(" '%s' not in iTunes storage" % path)
  2226. try:
  2227. os.rmdir(storage_path[0])
  2228. self.log.info(" removed folder '%s'" % storage_path[0])
  2229. except:
  2230. self.log.info(" folder '%s' not found or not empty" % storage_path[0])
  2231. # Delete from iTunes database
  2232. else:
  2233. self.log.info(" '%s' (stored external to iTunes, no files deleted)" % cached_book['title'])
  2234. else:
  2235. if DEBUG:
  2236. self.log.info(" '%s' not found in iTunes" % cached_book['title'])
  2237. try:
  2238. book.Delete()
  2239. except:
  2240. if DEBUG:
  2241. self.log.info(" unable to remove '%s' from iTunes" % cached_book['title'])
  2242. def title_sorter(self, title):
  2243. return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip()
  2244. def _update_epub_metadata(self, fpath, metadata):
  2245. '''
  2246. '''
  2247. self.log.info(" ITUNES._update_epub_metadata()")
  2248. # Fetch plugboard updates
  2249. metadata_x = self._xform_metadata_via_plugboard(metadata, 'epub')
  2250. # Refresh epub metadata
  2251. with open(fpath,'r+b') as zfo:
  2252. # Touch the OPF timestamp
  2253. zf_opf = ZipFile(fpath,'r')
  2254. fnames = zf_opf.namelist()
  2255. opf = [x for x in fnames if '.opf' in x][0]
  2256. if opf:
  2257. opf_raw = cStringIO.StringIO(zf_opf.read(opf))
  2258. soup = BeautifulSoup(opf_raw.getvalue())
  2259. opf_raw.close()
  2260. # Touch existing calibre timestamp
  2261. md = soup.find('metadata')
  2262. if md:
  2263. ts = md.find('meta',attrs={'name':'calibre:timestamp'})
  2264. if ts:
  2265. timestamp = ts['content']
  2266. old_ts = parse_date(timestamp)
  2267. metadata.timestamp = datetime.datetime(old_ts.year, old_ts.month, old_ts.day, old_ts.hour,
  2268. old_ts.minute, old_ts.second, old_ts.microsecond+1, old_ts.tzinfo)
  2269. else:
  2270. metadata.timestamp = now()
  2271. if DEBUG:
  2272. self.log.info(" add timestamp: %s" % metadata.timestamp)
  2273. else:
  2274. metadata.timestamp = now()
  2275. if DEBUG:
  2276. self.log.warning(" missing <metadata> block in OPF file")
  2277. self.log.info(" add timestamp: %s" % metadata.timestamp)
  2278. # Force the language declaration for iBooks 1.1
  2279. #metadata.language = get_lang().replace('_', '-')
  2280. # Updates from metadata plugboard (ignoring publisher)
  2281. metadata.language = metadata_x.language
  2282. if DEBUG:
  2283. if metadata.language != metadata_x.language:
  2284. self.log.info(" rewriting language: <dc:language>%s</dc:language>" % metadata.language)
  2285. zf_opf.close()
  2286. # If 'News' in tags, tweak the title/author for friendlier display in iBooks
  2287. if _('News') in metadata.tags or \
  2288. _('Catalog') in metadata.tags:
  2289. if metadata.title.find('[') > 0:
  2290. metadata.title = metadata.title[:metadata.title.find('[')-1]
  2291. date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
  2292. metadata.author = metadata.authors = [date_as_author]
  2293. sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', metadata.title).rstrip()
  2294. metadata.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
  2295. # Remove any non-alpha category tags
  2296. for tag in metadata.tags:
  2297. if not self._is_alpha(tag[0]):
  2298. metadata.tags.remove(tag)
  2299. # If windows & series, nuke tags so series used as Category during _update_iTunes_metadata()
  2300. if iswindows and metadata.series:
  2301. metadata.tags = None
  2302. set_metadata(zfo, metadata, update_timestamp=True)
  2303. def _update_device(self, msg='', wait=True):
  2304. '''
  2305. Trigger a sync, wait for completion
  2306. '''
  2307. if DEBUG:
  2308. self.log.info(" ITUNES:_update_device():\n %s" % msg)
  2309. if isosx:
  2310. self.iTunes.update()
  2311. if wait:
  2312. # This works if iTunes has books not yet synced to iPad.
  2313. if DEBUG:
  2314. sys.stdout.write(" waiting for iPad sync to complete ...")
  2315. sys.stdout.flush()
  2316. while len(self._get_device_books()) != (len(self._get_library_books()) + len(self._get_purchased_book_ids())):
  2317. if DEBUG:
  2318. sys.stdout.write('.')
  2319. sys.stdout.flush()
  2320. time.sleep(2)
  2321. print
  2322. elif iswindows:
  2323. try:
  2324. pythoncom.CoInitialize()
  2325. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  2326. self.iTunes.UpdateIPod()
  2327. if wait:
  2328. if DEBUG:
  2329. sys.stdout.write(" waiting for iPad sync to complete ...")
  2330. sys.stdout.flush()
  2331. while True:
  2332. db_count = len(self._get_device_books())
  2333. lb_count = len(self._get_library_books())
  2334. pb_count = len(self._get_purchased_book_ids())
  2335. if db_count != lb_count + pb_count:
  2336. if DEBUG:
  2337. #sys.stdout.write(' %d != %d + %d\n' % (db_count,lb_count,pb_count))
  2338. sys.stdout.write('.')
  2339. sys.stdout.flush()
  2340. time.sleep(2)
  2341. else:
  2342. sys.stdout.write('\n')
  2343. sys.stdout.flush()
  2344. break
  2345. finally:
  2346. pythoncom.CoUninitialize()
  2347. def _update_iTunes_metadata(self, metadata, db_added, lb_added, this_book):
  2348. '''
  2349. '''
  2350. if DEBUG:
  2351. self.log.info(" ITUNES._update_iTunes_metadata()")
  2352. STRIP_TAGS = re.compile(r'<[^<]*?/?>')
  2353. # Update metadata from plugboard
  2354. # If self.plugboard is None (no transforms), original metadata is returned intact
  2355. metadata_x = self._xform_metadata_via_plugboard(metadata, this_book.format)
  2356. if isosx:
  2357. if lb_added:
  2358. lb_added.name.set(metadata_x.title)
  2359. lb_added.album.set(metadata_x.title)
  2360. lb_added.artist.set(authors_to_string(metadata_x.authors))
  2361. lb_added.composer.set(metadata_x.uuid)
  2362. lb_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
  2363. lb_added.enabled.set(True)
  2364. lb_added.sort_artist.set(icu_title(metadata_x.author_sort))
  2365. lb_added.sort_name.set(metadata.title_sort)
  2366. if db_added:
  2367. db_added.name.set(metadata_x.title)
  2368. db_added.album.set(metadata_x.title)
  2369. db_added.artist.set(authors_to_string(metadata_x.authors))
  2370. db_added.composer.set(metadata_x.uuid)
  2371. db_added.description.set("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
  2372. db_added.enabled.set(True)
  2373. db_added.sort_artist.set(icu_title(metadata_x.author_sort))
  2374. db_added.sort_name.set(metadata.title_sort)
  2375. if metadata_x.comments:
  2376. if lb_added:
  2377. lb_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments))
  2378. if db_added:
  2379. db_added.comment.set(STRIP_TAGS.sub('',metadata_x.comments))
  2380. if metadata_x.rating:
  2381. if lb_added:
  2382. lb_added.rating.set(metadata_x.rating*10)
  2383. # iBooks currently doesn't allow setting rating ... ?
  2384. try:
  2385. if db_added:
  2386. db_added.rating.set(metadata_x.rating*10)
  2387. except:
  2388. pass
  2389. # Set genre from series if available, else first alpha tag
  2390. # Otherwise iTunes grabs the first dc:subject from the opf metadata
  2391. # self.settings().read_metadata is used as a surrogate for "Use Series name as Genre"
  2392. if metadata_x.series and self.settings().read_metadata:
  2393. if DEBUG:
  2394. self.log.info(" ITUNES._update_iTunes_metadata()")
  2395. self.log.info(" using Series name as Genre")
  2396. # Format the index as a sort key
  2397. index = metadata_x.series_index
  2398. integer = int(index)
  2399. fraction = index-integer
  2400. series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
  2401. if lb_added:
  2402. lb_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
  2403. lb_added.episode_ID.set(metadata_x.series)
  2404. lb_added.episode_number.set(metadata_x.series_index)
  2405. # If no plugboard transform applied to tags, change the Genre/Category to Series
  2406. if metadata.tags == metadata_x.tags:
  2407. lb_added.genre.set(self.title_sorter(metadata_x.series))
  2408. else:
  2409. for tag in metadata_x.tags:
  2410. if self._is_alpha(tag[0]):
  2411. lb_added.genre.set(tag)
  2412. break
  2413. if db_added:
  2414. db_added.sort_name.set("%s %s" % (self.title_sorter(metadata_x.series), series_index))
  2415. db_added.episode_ID.set(metadata_x.series)
  2416. db_added.episode_number.set(metadata_x.series_index)
  2417. # If no plugboard transform applied to tags, change the Genre/Category to Series
  2418. if metadata.tags == metadata_x.tags:
  2419. db_added.genre.set(self.title_sorter(metadata_x.series))
  2420. else:
  2421. for tag in metadata_x.tags:
  2422. if self._is_alpha(tag[0]):
  2423. db_added.genre.set(tag)
  2424. break
  2425. elif metadata_x.tags is not None:
  2426. if DEBUG:
  2427. self.log.info(" %susing Tag as Genre" %
  2428. "no Series name available, " if self.settings().read_metadata else '')
  2429. for tag in metadata_x.tags:
  2430. if self._is_alpha(tag[0]):
  2431. if lb_added:
  2432. lb_added.genre.set(tag)
  2433. if db_added:
  2434. db_added.genre.set(tag)
  2435. break
  2436. elif iswindows:
  2437. if lb_added:
  2438. lb_added.Name = metadata_x.title
  2439. lb_added.Album = metadata_x.title
  2440. lb_added.Artist = authors_to_string(metadata_x.authors)
  2441. lb_added.Composer = metadata_x.uuid
  2442. lb_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
  2443. lb_added.Enabled = True
  2444. lb_added.SortArtist = icu_title(metadata_x.author_sort)
  2445. lb_added.SortName = metadata.title_sort
  2446. if db_added:
  2447. db_added.Name = metadata_x.title
  2448. db_added.Album = metadata_x.title
  2449. db_added.Artist = authors_to_string(metadata_x.authors)
  2450. db_added.Composer = metadata_x.uuid
  2451. db_added.Description = ("%s %s" % (self.description_prefix,strftime('%Y-%m-%d %H:%M:%S')))
  2452. db_added.Enabled = True
  2453. db_added.SortArtist = icu_title(metadata_x.author_sort)
  2454. db_added.SortName = metadata.title_sort
  2455. if metadata_x.comments:
  2456. if lb_added:
  2457. lb_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments))
  2458. if db_added:
  2459. db_added.Comment = (STRIP_TAGS.sub('',metadata_x.comments))
  2460. if metadata_x.rating:
  2461. if lb_added:
  2462. lb_added.AlbumRating = (metadata_x.rating*10)
  2463. # iBooks currently doesn't allow setting rating ... ?
  2464. try:
  2465. if db_added:
  2466. db_added.AlbumRating = (metadata_x.rating*10)
  2467. except:
  2468. if DEBUG:
  2469. self.log.warning(" iTunes automation interface reported an error"
  2470. " setting AlbumRating on iDevice")
  2471. # Set Genre from first alpha tag, overwrite with series if available
  2472. # Otherwise iBooks uses first <dc:subject> from opf
  2473. # iTunes balks on setting EpisodeNumber, but it sticks (9.1.1.12)
  2474. if metadata_x.series and self.settings().read_metadata:
  2475. if DEBUG:
  2476. self.log.info(" using Series name as Genre")
  2477. # Format the index as a sort key
  2478. index = metadata_x.series_index
  2479. integer = int(index)
  2480. fraction = index-integer
  2481. series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
  2482. if lb_added:
  2483. lb_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
  2484. lb_added.EpisodeID = metadata_x.series
  2485. try:
  2486. lb_added.TrackNumber = metadata_x.series_index
  2487. except:
  2488. if DEBUG:
  2489. self.log.warning(" iTunes automation interface reported an error"
  2490. " setting TrackNumber in iTunes")
  2491. try:
  2492. lb_added.EpisodeNumber = metadata_x.series_index
  2493. except:
  2494. if DEBUG:
  2495. self.log.warning(" iTunes automation interface reported an error"
  2496. " setting EpisodeNumber in iTunes")
  2497. # If no plugboard transform applied to tags, change the Genre/Category to Series
  2498. if metadata.tags == metadata_x.tags:
  2499. lb_added.Genre = self.title_sorter(metadata_x.series)
  2500. else:
  2501. for tag in metadata_x.tags:
  2502. if self._is_alpha(tag[0]):
  2503. lb_added.Genre = tag
  2504. break
  2505. if db_added:
  2506. db_added.SortName = "%s %s" % (self.title_sorter(metadata_x.series), series_index)
  2507. db_added.EpisodeID = metadata_x.series
  2508. try:
  2509. db_added.TrackNumber = metadata_x.series_index
  2510. except:
  2511. if DEBUG:
  2512. self.log.warning(" iTunes automation interface reported an error"
  2513. " setting TrackNumber on iDevice")
  2514. try:
  2515. db_added.EpisodeNumber = metadata_x.series_index
  2516. except:
  2517. if DEBUG:
  2518. self.log.warning(" iTunes automation interface reported an error"
  2519. " setting EpisodeNumber on iDevice")
  2520. # If no plugboard transform applied to tags, change the Genre/Category to Series
  2521. if metadata.tags == metadata_x.tags:
  2522. db_added.Genre = self.title_sorter(metadata_x.series)
  2523. else:
  2524. for tag in metadata_x.tags:
  2525. if self._is_alpha(tag[0]):
  2526. db_added.Genre = tag
  2527. break
  2528. elif metadata_x.tags is not None:
  2529. if DEBUG:
  2530. self.log.info(" using Tag as Genre")
  2531. for tag in metadata_x.tags:
  2532. if self._is_alpha(tag[0]):
  2533. if lb_added:
  2534. lb_added.Genre = tag
  2535. if db_added:
  2536. db_added.Genre = tag
  2537. break
  2538. def _xform_metadata_via_plugboard(self, book, format):
  2539. ''' Transform book metadata from plugboard templates '''
  2540. if DEBUG:
  2541. self.log.info(" ITUNES._update_metadata_from_plugboard()")
  2542. if self.plugboard_func:
  2543. pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
  2544. newmi = book.deepcopy_metadata()
  2545. newmi.template_to_attribute(book, pb)
  2546. if DEBUG:
  2547. self.log.info(" transforming %s using %s:" % (format, pb))
  2548. self.log.info(" title: %s %s" % (book.title, ">>> %s" %
  2549. newmi.title if book.title != newmi.title else ''))
  2550. self.log.info(" title_sort: %s %s" % (book.title_sort, ">>> %s" %
  2551. newmi.title_sort if book.title_sort != newmi.title_sort else ''))
  2552. self.log.info(" authors: %s %s" % (book.authors, ">>> %s" %
  2553. newmi.authors if book.authors != newmi.authors else ''))
  2554. self.log.info(" author_sort: %s %s" % (book.author_sort, ">>> %s" %
  2555. newmi.author_sort if book.author_sort != newmi.author_sort else ''))
  2556. self.log.info(" language: %s %s" % (book.language, ">>> %s" %
  2557. newmi.language if book.language != newmi.language else ''))
  2558. self.log.info(" publisher: %s %s" % (book.publisher, ">>> %s" %
  2559. newmi.publisher if book.publisher != newmi.publisher else ''))
  2560. self.log.info(" tags: %s %s" % (book.tags, ">>> %s" %
  2561. newmi.tags if book.tags != newmi.tags else ''))
  2562. else:
  2563. newmi = book
  2564. return newmi
  2565. class ITUNES_ASYNC(ITUNES):
  2566. '''
  2567. This subclass allows the user to interact directly with iTunes via a menu option
  2568. 'Connect to iTunes' in Send to device.
  2569. '''
  2570. name = 'iTunes interface'
  2571. gui_name = 'Apple iTunes'
  2572. icon = I('devices/itunes.png')
  2573. description = _('Communicate with iTunes.')
  2574. # Plugboard ID
  2575. DEVICE_PLUGBOARD_NAME = 'APPLE'
  2576. connected = False
  2577. def __init__(self,path):
  2578. if DEBUG:
  2579. self.log.info("ITUNES_ASYNC:__init__()")
  2580. if isosx and appscript is None:
  2581. self.connected = False
  2582. raise UserFeedback('OSX 10.5 or later required', details=None, level=UserFeedback.WARN)
  2583. return
  2584. else:
  2585. self.connected = True
  2586. if isosx:
  2587. self._launch_iTunes()
  2588. if iswindows:
  2589. try:
  2590. pythoncom.CoInitialize()
  2591. self._launch_iTunes()
  2592. except:
  2593. raise UserFeedback('unable to launch iTunes', details=None, level=UserFeedback.WARN)
  2594. finally:
  2595. pythoncom.CoUninitialize()
  2596. self.manual_sync_mode = False
  2597. def books(self, oncard=None, end_session=True):
  2598. """
  2599. Return a list of ebooks on the device.
  2600. @param oncard: If 'carda' or 'cardb' return a list of ebooks on the
  2601. specific storage card, otherwise return list of ebooks
  2602. in main memory of device. If a card is specified and no
  2603. books are on the card return empty list.
  2604. @return: A BookList.
  2605. Implementation notes:
  2606. iTunes does not sync purchased books, they are only on the device. They are visible, but
  2607. they are not backed up to iTunes. Since calibre can't manage them, don't show them in the
  2608. list of device books.
  2609. """
  2610. if not oncard:
  2611. if DEBUG:
  2612. self.log.info("ITUNES_ASYNC:books()")
  2613. if self.settings().use_subdirs:
  2614. self.log.info(" Cover fetching/caching enabled")
  2615. else:
  2616. self.log.info(" Cover fetching/caching disabled")
  2617. # Fetch a list of books from iTunes
  2618. booklist = BookList(self.log)
  2619. cached_books = {}
  2620. if isosx:
  2621. library_books = self._get_library_books()
  2622. book_count = float(len(library_books))
  2623. for (i,book) in enumerate(library_books):
  2624. format = 'pdf' if library_books[book].kind().startswith('PDF') else 'epub'
  2625. this_book = Book(library_books[book].name(), library_books[book].artist())
  2626. this_book.path = self.path_template % (library_books[book].name(),
  2627. library_books[book].artist(),
  2628. format)
  2629. try:
  2630. this_book.datetime = parse_date(str(library_books[book].date_added())).timetuple()
  2631. except:
  2632. this_book.datetime = time.gmtime()
  2633. this_book.db_id = None
  2634. this_book.device_collections = []
  2635. #this_book.library_id = library_books[this_book.path] if this_book.path in library_books else None
  2636. this_book.library_id = library_books[book]
  2637. this_book.size = library_books[book].size()
  2638. this_book.uuid = library_books[book].composer()
  2639. # Hack to discover if we're running in GUI environment
  2640. if self.report_progress is not None:
  2641. this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
  2642. else:
  2643. this_book.thumbnail = None
  2644. booklist.add_book(this_book, False)
  2645. cached_books[this_book.path] = {
  2646. 'title':library_books[book].name(),
  2647. 'author':[library_books[book].artist()],
  2648. 'lib_book':library_books[book],
  2649. 'dev_book':None,
  2650. 'uuid': library_books[book].composer(),
  2651. 'format': format
  2652. }
  2653. if self.report_progress is not None:
  2654. self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count))
  2655. elif iswindows:
  2656. try:
  2657. pythoncom.CoInitialize()
  2658. self.iTunes = win32com.client.Dispatch("iTunes.Application")
  2659. library_books = self._get_library_books()
  2660. book_count = float(len(library_books))
  2661. for (i,book) in enumerate(library_books):
  2662. this_book = Book(library_books[book].Name, library_books[book].Artist)
  2663. format = 'pdf' if library_books[book].KindAsString.startswith('PDF') else 'epub'
  2664. this_book.path = self.path_template % (library_books[book].Name,
  2665. library_books[book].Artist,
  2666. format)
  2667. try:
  2668. this_book.datetime = parse_date(str(library_books[book].DateAdded)).timetuple()
  2669. except:
  2670. this_book.datetime = time.gmtime()
  2671. this_book.db_id = None
  2672. this_book.device_collections = []
  2673. this_book.library_id = library_books[book]
  2674. this_book.size = library_books[book].Size
  2675. this_book.uuid = library_books[book].Composer
  2676. # Hack to discover if we're running in GUI environment
  2677. if self.report_progress is not None:
  2678. this_book.thumbnail = self._generate_thumbnail(this_book.path, library_books[book])
  2679. else:
  2680. this_book.thumbnail = None
  2681. booklist.add_book(this_book, False)
  2682. cached_books[this_book.path] = {
  2683. 'title':library_books[book].Name,
  2684. 'author':library_books[book].Artist,
  2685. 'lib_book':library_books[book],
  2686. 'uuid': library_books[book].Composer,
  2687. 'format': format
  2688. }
  2689. if self.report_progress is not None:
  2690. self.report_progress(i+1/book_count,
  2691. _('%d of %d') % (i+1, book_count))
  2692. finally:
  2693. pythoncom.CoUninitialize()
  2694. if self.report_progress is not None:
  2695. self.report_progress(1.0, _('finished'))
  2696. self.cached_books = cached_books
  2697. if DEBUG:
  2698. self._dump_booklist(booklist, 'returning from books()', indent=2)
  2699. self._dump_cached_books('returning from books()',indent=2)
  2700. return booklist
  2701. else:
  2702. return BookList(self.log)
  2703. def eject(self):
  2704. '''
  2705. Un-mount / eject the device from the OS. This does not check if there
  2706. are pending GUI jobs that need to communicate with the device.
  2707. '''
  2708. if DEBUG:
  2709. self.log.info("ITUNES_ASYNC:eject()")
  2710. self.iTunes = None
  2711. self.connected = False
  2712. def free_space(self, end_session=True):
  2713. """
  2714. Get free space available on the mountpoints:
  2715. 1. Main memory
  2716. 2. Card A
  2717. 3. Card B
  2718. @return: A 3 element list with free space in bytes of (1, 2, 3). If a
  2719. particular device doesn't have any of these locations it should return -1.
  2720. """
  2721. if DEBUG:
  2722. self.log.info("ITUNES_ASYNC:free_space()")
  2723. free_space = 0
  2724. if isosx:
  2725. s = os.statvfs(os.sep)
  2726. free_space = s.f_bavail * s.f_frsize
  2727. elif iswindows:
  2728. free_bytes = ctypes.c_ulonglong(0)
  2729. ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(os.sep), None, None, ctypes.pointer(free_bytes))
  2730. free_space = free_bytes.value
  2731. return (free_space,-1,-1)
  2732. def get_device_information(self, end_session=True):
  2733. """
  2734. Ask device for device information. See L{DeviceInfoQuery}.
  2735. @return: (device name, device version, software version on device, mime type)
  2736. """
  2737. if DEBUG:
  2738. self.log.info("ITUNES_ASYNC:get_device_information()")
  2739. return ('iTunes','hw v1.0','sw v1.0', 'mime type normally goes here')
  2740. def is_usb_connected(self, devices_on_system, debug=False,
  2741. only_presence=False):
  2742. return self.connected, self
  2743. def sync_booklists(self, booklists, end_session=True):
  2744. '''
  2745. Update metadata on device.
  2746. @param booklists: A tuple containing the result of calls to
  2747. (L{books}(oncard=None), L{books}(oncard='carda'),
  2748. L{books}(oncard='cardb')).
  2749. '''
  2750. if DEBUG:
  2751. self.log.info("ITUNES_ASYNC.sync_booklists()")
  2752. # Inform user of any problem books
  2753. if self.problem_titles:
  2754. raise UserFeedback(self.problem_msg,
  2755. details='\n'.join(self.problem_titles), level=UserFeedback.WARN)
  2756. self.problem_titles = []
  2757. self.problem_msg = None
  2758. self.update_list = []
  2759. def unmount_device(self):
  2760. '''
  2761. '''
  2762. if DEBUG:
  2763. self.log.info("ITUNES_ASYNC:unmount_device()")
  2764. self.connected = False
  2765. class BookList(list):
  2766. '''
  2767. A list of books. Each Book object must have the fields:
  2768. 1. title
  2769. 2. authors
  2770. 3. size (file size of the book)
  2771. 4. datetime (a UTC time tuple)
  2772. 5. path (path on the device to the book)
  2773. 6. thumbnail (can be None) thumbnail is either a str/bytes object with the
  2774. image data or it should have an attribute image_path that stores an
  2775. absolute (platform native) path to the image
  2776. 7. tags (a list of strings, can be empty).
  2777. '''
  2778. __getslice__ = None
  2779. __setslice__ = None
  2780. log = None
  2781. def __init__(self, log):
  2782. self.log = log
  2783. def supports_collections(self):
  2784. ''' Return True if the the device supports collections for this book list. '''
  2785. return False
  2786. def add_book(self, book, replace_metadata):
  2787. '''
  2788. Add the book to the booklist. Intent is to maintain any device-internal
  2789. metadata. Return True if booklists must be sync'ed
  2790. '''
  2791. self.append(book)
  2792. def remove_book(self, book):
  2793. '''
  2794. Remove a book from the booklist. Correct any device metadata at the
  2795. same time
  2796. '''
  2797. raise NotImplementedError()
  2798. def get_collections(self, collection_attributes):
  2799. '''
  2800. Return a dictionary of collections created from collection_attributes.
  2801. Each entry in the dictionary is of the form collection name:[list of
  2802. books]
  2803. The list of books is sorted by book title, except for collections
  2804. created from series, in which case series_index is used.
  2805. :param collection_attributes: A list of attributes of the Book object
  2806. '''
  2807. return {}
  2808. class Book(Metadata):
  2809. '''
  2810. A simple class describing a book in the iTunes Books Library.
  2811. See ebooks.metadata.book.base
  2812. '''
  2813. def __init__(self,title,author):
  2814. Metadata.__init__(self, title, authors=[author])
  2815. @property
  2816. def title_sorter(self):
  2817. return title_sort(self.title)