mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-10 07:05:09 -05:00
380 lines
14 KiB
Python
380 lines
14 KiB
Python
'''
|
|
Device driver for iTunes
|
|
|
|
GRiker
|
|
|
|
22 May 2010
|
|
'''
|
|
import datetime, re
|
|
|
|
from calibre.constants import isosx, iswindows
|
|
from calibre.devices.interface import DevicePlugin
|
|
from calibre.ebooks.metadata import MetaInformation
|
|
from calibre.utils.config import Config
|
|
from calibre.utils.date import parse_date
|
|
|
|
if isosx:
|
|
print "running in OSX"
|
|
import appscript
|
|
|
|
if iswindows:
|
|
print "running in Windows"
|
|
import win32com.client
|
|
|
|
class ITUNES(DevicePlugin):
|
|
name = 'Apple device interface'
|
|
gui_name = 'Apple device'
|
|
icon = I('devices/iPad.png')
|
|
description = _('Communicate with iBooks through iTunes.')
|
|
supported_platforms = ['windows','osx']
|
|
author = 'GRiker'
|
|
|
|
FORMATS = ['epub']
|
|
|
|
VENDOR_ID = [0x05ac]
|
|
# 0x129a:iPad 0x1292:iPhone 3G
|
|
PRODUCT_ID = [0x129a,0x1292]
|
|
BCD = [0x01]
|
|
|
|
it = None
|
|
is_connected = False
|
|
|
|
|
|
# Public methods
|
|
|
|
def add_books_to_metadata(cls, locations, metadata, booklists):
|
|
'''
|
|
Add locations to the booklists. This function must not communicate with
|
|
the device.
|
|
@param locations: Result of a call to L{upload_books}
|
|
@param metadata: List of MetaInformation objects, same as for
|
|
:method:`upload_books`.
|
|
@param booklists: A tuple containing the result of calls to
|
|
(L{books}(oncard=None), L{books}(oncard='carda'),
|
|
L{books}(oncard='cardb')).
|
|
'''
|
|
raise NotImplementedError
|
|
|
|
def books(self, oncard=None, end_session=True):
|
|
"""
|
|
Return a list of ebooks on the device.
|
|
@param oncard: If 'carda' or 'cardb' return a list of ebooks on the
|
|
specific storage card, otherwise return list of ebooks
|
|
in main memory of device. If a card is specified and no
|
|
books are on the card return empty list.
|
|
@return: A BookList.
|
|
"""
|
|
print "ITUNES:books(oncard=%s)" % oncard
|
|
if not oncard:
|
|
# Fetch a list of books from iTunes
|
|
if isosx:
|
|
names = [s.name() for s in self.it.sources()]
|
|
kinds = [s.kind() for s in self.it.sources()]
|
|
sources = dict(zip(kinds,names))
|
|
|
|
lib = self.it.sources['Library']
|
|
|
|
if 'Books' in lib.playlists.name():
|
|
booklist = BookList()
|
|
it_books = lib.playlists['Books'].file_tracks()
|
|
for it_book in it_books:
|
|
this_book = Book(it_book.name(), it_book.artist())
|
|
this_book.datetime = parse_date(str(it_book.date_added())).timetuple()
|
|
this_book.db_id = None
|
|
this_book.device_collections = []
|
|
this_book.path = 'iTunes/Books/%s.epub' % it_book.name()
|
|
this_book.size = it_book.size()
|
|
this_book.thumbnail = None
|
|
booklist.add_book(this_book, False)
|
|
return booklist
|
|
|
|
else:
|
|
return []
|
|
|
|
|
|
else:
|
|
return []
|
|
|
|
def can_handle(self, device_info, debug=False):
|
|
'''
|
|
Unix version of :method:`can_handle_windows`
|
|
|
|
:param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product,
|
|
serial number)
|
|
This gets called ~1x/second while device is sensed
|
|
'''
|
|
# print "ITUNES:can_handle()"
|
|
return True
|
|
|
|
def can_handle_windows(self, device_id, debug=False):
|
|
'''
|
|
Optional method to perform further checks on a device to see if this driver
|
|
is capable of handling it. If it is not it should return False. This method
|
|
is only called after the vendor, product ids and the bcd have matched, so
|
|
it can do some relatively time intensive checks. The default implementation
|
|
returns True. This method is called only on windows. See also
|
|
:method:`can_handle`.
|
|
|
|
:param device_info: On windows a device ID string. On Unix a tuple of
|
|
``(vendor_id, product_id, bcd)``.
|
|
'''
|
|
print "ITUNES:can_handle_windows()"
|
|
return True
|
|
|
|
def card_prefix(self, end_session=True):
|
|
'''
|
|
Return a 2 element list of the prefix to paths on the cards.
|
|
If no card is present None is set for the card's prefix.
|
|
E.G.
|
|
('/place', '/place2')
|
|
(None, 'place2')
|
|
('place', None)
|
|
(None, None)
|
|
'''
|
|
print "ITUNES:card_prefix()"
|
|
return (None,None)
|
|
|
|
def config_widget(cls):
|
|
'''
|
|
Should return a QWidget. The QWidget contains the settings for the device interface
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def delete_books(self, paths, end_session=True):
|
|
'''
|
|
Delete books at paths on device.
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def eject(self):
|
|
'''
|
|
Un-mount / eject the device from the OS. This does not check if there
|
|
are pending GUI jobs that need to communicate with the device.
|
|
'''
|
|
print "ITUNES:eject()"
|
|
|
|
def free_space(self, end_session=True):
|
|
"""
|
|
Get free space available on the mountpoints:
|
|
1. Main memory
|
|
2. Card A
|
|
3. Card B
|
|
|
|
@return: A 3 element list with free space in bytes of (1, 2, 3). If a
|
|
particular device doesn't have any of these locations it should return -1.
|
|
"""
|
|
print "ITUNES:free_space()"
|
|
return (0,-1,-1)
|
|
|
|
def get_device_information(self, end_session=True):
|
|
"""
|
|
Ask device for device information. See L{DeviceInfoQuery}.
|
|
@return: (device name, device version, software version on device, mime type)
|
|
"""
|
|
print "ITUNES:get_device_information()"
|
|
return ('iPad','hw v1.0','sw v1.0', 'mime type')
|
|
|
|
def get_file(self, path, outfile, end_session=True):
|
|
'''
|
|
Read the file at C{path} on the device and write it to outfile.
|
|
@param outfile: file object like C{sys.stdout} or the result of an C{open} call
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def open(self):
|
|
'''
|
|
Perform any device specific initialization. Called after the device is
|
|
detected but before any other functions that communicate with the device.
|
|
For example: For devices that present themselves as USB Mass storage
|
|
devices, this method would be responsible for mounting the device or
|
|
if the device has been automounted, for finding out where it has been
|
|
mounted. The base class within USBMS device.py has a implementation of
|
|
this function that should serve as a good example for USB Mass storage
|
|
devices.
|
|
'''
|
|
print "ITUNES.open()"
|
|
if isosx:
|
|
# Launch iTunes if not already running
|
|
running_apps = appscript.app('System Events')
|
|
if not 'iTunes' in running_apps.processes.name():
|
|
print " launching iTunes"
|
|
it = appscript.app('iTunes', hide=True)
|
|
app.run()
|
|
self.it = it
|
|
else:
|
|
self.it = appscript.app('iTunes')
|
|
|
|
def post_yank_cleanup(self):
|
|
'''
|
|
Called if the user yanks the device without ejecting it first.
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def remove_books_from_metadata(cls, paths, booklists):
|
|
'''
|
|
Remove books from the metadata list. This function must not communicate
|
|
with the device.
|
|
@param paths: paths to books on the device.
|
|
@param booklists: A tuple containing the result of calls to
|
|
(L{books}(oncard=None), L{books}(oncard='carda'),
|
|
L{books}(oncard='cardb')).
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def reset(self, key='-1', log_packets=False, report_progress=None,
|
|
detected_device=None) :
|
|
"""
|
|
:key: The key to unlock the device
|
|
:log_packets: If true the packet stream to/from the device is logged
|
|
:report_progress: Function that is called with a % progress
|
|
(number between 0 and 100) for various tasks
|
|
If it is called with -1 that means that the
|
|
task does not have any progress information
|
|
:detected_device: Device information from the device scanner
|
|
"""
|
|
print "ITUNE.reset()"
|
|
|
|
def save_settings(cls, settings_widget):
|
|
'''
|
|
Should save settings to disk. Takes the widget created in config_widget
|
|
and saves all settings to disk.
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def set_progress_reporter(self, report_progress):
|
|
'''
|
|
@param report_progress: Function that is called with a % progress
|
|
(number between 0 and 100) for various tasks
|
|
If it is called with -1 that means that the
|
|
task does not have any progress information
|
|
'''
|
|
print "ITUNES:set_progress_reporter()"
|
|
|
|
def settings(cls):
|
|
'''
|
|
Should return an opts object. The opts object should have one attribute
|
|
`format_map` which is an ordered list of formats for the device.
|
|
'''
|
|
print "ITUNES.settings()"
|
|
klass = cls if isinstance(cls, type) else cls.__class__
|
|
c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers'))
|
|
c.add_opt('format_map', default=cls.FORMATS,
|
|
help=_('Ordered list of formats the device will accept'))
|
|
return c.parse()
|
|
|
|
def sync_booklists(self, booklists, end_session=True):
|
|
'''
|
|
Update metadata on device.
|
|
@param booklists: A tuple containing the result of calls to
|
|
(L{books}(oncard=None), L{books}(oncard='carda'),
|
|
L{books}(oncard='cardb')).
|
|
'''
|
|
print "ITUNES:sync_booklists():"
|
|
|
|
def total_space(self, end_session=True):
|
|
"""
|
|
Get total space available on the mountpoints:
|
|
1. Main memory
|
|
2. Memory Card A
|
|
3. Memory Card B
|
|
|
|
@return: A 3 element list with total space in bytes of (1, 2, 3). If a
|
|
particular device doesn't have any of these locations it should return 0.
|
|
"""
|
|
print "ITUNES:total_space()"
|
|
|
|
def upload_books(self, files, names, on_card=None, end_session=True,
|
|
metadata=None):
|
|
'''
|
|
Upload a list of books to the device. If a file already
|
|
exists on the device, it should be replaced.
|
|
This method should raise a L{FreeSpaceError} if there is not enough
|
|
free space on the device. The text of the FreeSpaceError must contain the
|
|
word "card" if C{on_card} is not None otherwise it must contain the word "memory".
|
|
:files: A list of paths and/or file-like objects.
|
|
:names: A list of file names that the books should have
|
|
once uploaded to the device. len(names) == len(files)
|
|
:return: A list of 3-element tuples. The list is meant to be passed
|
|
to L{add_books_to_metadata}.
|
|
:metadata: If not None, it is a list of :class:`MetaInformation` objects.
|
|
The idea is to use the metadata to determine where on the device to
|
|
put the book. len(metadata) == len(files). Apart from the regular
|
|
cover (path to cover), there may also be a thumbnail attribute, which should
|
|
be used in preference. The thumbnail attribute is of the form
|
|
(width, height, cover_data as jpeg).
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
# Private methods
|
|
|
|
|
|
class BookList(list):
|
|
'''
|
|
A list of books. Each Book object must have the fields:
|
|
1. title
|
|
2. authors
|
|
3. size (file size of the book)
|
|
4. datetime (a UTC time tuple)
|
|
5. path (path on the device to the book)
|
|
6. thumbnail (can be None) thumbnail is either a str/bytes object with the
|
|
image data or it should have an attribute image_path that stores an
|
|
absolute (platform native) path to the image
|
|
7. tags (a list of strings, can be empty).
|
|
'''
|
|
|
|
__getslice__ = None
|
|
__setslice__ = None
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def supports_collections(self):
|
|
''' Return True if the the device supports collections for this book list. '''
|
|
return False
|
|
|
|
def add_book(self, book, replace_metadata):
|
|
'''
|
|
Add the book to the booklist. Intent is to maintain any device-internal
|
|
metadata. Return True if booklists must be sync'ed
|
|
'''
|
|
print "adding %s" % book
|
|
self.append(book)
|
|
|
|
def remove_book(self, book):
|
|
'''
|
|
Remove a book from the booklist. Correct any device metadata at the
|
|
same time
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
def get_collections(self, collection_attributes):
|
|
'''
|
|
Return a dictionary of collections created from collection_attributes.
|
|
Each entry in the dictionary is of the form collection name:[list of
|
|
books]
|
|
|
|
The list of books is sorted by book title, except for collections
|
|
created from series, in which case series_index is used.
|
|
|
|
:param collection_attributes: A list of attributes of the Book object
|
|
'''
|
|
return {}
|
|
|
|
class Book(MetaInformation):
|
|
'''
|
|
A simple class describing a book in the iTunes Books Library.
|
|
Q's:
|
|
- Should thumbnail come from calibre if available?
|
|
- See ebooks.metadata.__init__ for all fields
|
|
'''
|
|
def __init__(self,title,author):
|
|
|
|
MetaInformation.__init__(self, title, authors=[author])
|
|
|
|
@dynamic_property
|
|
def title_sorter(self):
|
|
doc = '''String to sort the title. If absent, title is returned'''
|
|
def fget(self):
|
|
return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
|
|
return property(doc=doc, fget=fget)
|