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

/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

Large files files are truncated, but you can click here to view the full file

  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,

Large files files are truncated, but you can click here to view the full file