From 05a77dfa5c6d75e4290aef8cbaee00959386372c Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 6 Jan 2009 19:40:38 -0500 Subject: [PATCH 1/2] USB Mass Storage base class. Cybook now using to USBMS. Kindle (untested) now using USBMS. --- src/calibre/devices/cybookg3/books.py | 78 ----- src/calibre/devices/cybookg3/driver.py | 311 ++------------------ src/calibre/devices/kindle/books.py | 122 -------- src/calibre/devices/kindle/driver.py | 391 ++----------------------- 4 files changed, 51 insertions(+), 851 deletions(-) delete mode 100644 src/calibre/devices/cybookg3/books.py delete mode 100755 src/calibre/devices/kindle/books.py diff --git a/src/calibre/devices/cybookg3/books.py b/src/calibre/devices/cybookg3/books.py deleted file mode 100644 index ed3df812db..0000000000 --- a/src/calibre/devices/cybookg3/books.py +++ /dev/null @@ -1,78 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2009, John Schember - - - - - - %(main_memory)s - %(deviceclass)s - - - - - - - - - - - - - %(storage_card)s - %(deviceclass)s - - - - - - -''' - - - def __init__(self, key='-1', log_packets=False, report_progress=None) : - self._main_prefix = self._card_prefix = None - - @classmethod - def get_fdi(cls): - return cls.FDI_TEMPLATE%dict( - app=__appname__, - deviceclass=cls.__name__, - vendor_id=hex(cls.VENDOR_ID), - product_id=hex(cls.PRODUCT_ID), - bcd=hex(cls.BCD), - main_memory=cls.MAIN_MEMORY_VOLUME_LABEL, - storage_card=cls.STORAGE_CARD_VOLUME_LABEL, - ) - - def set_progress_reporter(self, report_progress): - self.report_progress = report_progress - - 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) - """ - return (self.__class__.__name__, '', '', '') - - def card_prefix(self, end_session=True): - return self._card_prefix - - @classmethod - def _windows_space(cls, prefix): - if prefix is None: - return 0, 0 - win32file = __import__('win32file', globals(), locals(), [], -1) - try: - sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ - win32file.GetDiskFreeSpace(prefix[:-1]) - except Exception, err: - if getattr(err, 'args', [None])[0] == 21: # Disk not ready - time.sleep(3) - sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ - win32file.GetDiskFreeSpace(prefix[:-1]) - else: raise - mult = sectors_per_cluster * bytes_per_sector - return total_clusters * mult, free_clusters * mult - - def total_space(self, end_session=True): - msz = csz = 0 - print self._main_prefix - if not iswindows: - if self._main_prefix is not None: - stats = os.statvfs(self._main_prefix) - msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) - if self._card_prefix is not None: - stats = os.statvfs(self._card_prefix) - csz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) - else: - msz = self._windows_space(self._main_prefix)[0] - csz = self._windows_space(self._card_prefix)[0] - - return (msz, 0, csz) - - def free_space(self, end_session=True): - msz = csz = 0 - if not iswindows: - if self._main_prefix is not None: - stats = os.statvfs(self._main_prefix) - msz = stats.f_frsize * stats.f_bavail - if self._card_prefix is not None: - stats = os.statvfs(self._card_prefix) - csz = stats.f_frsize * stats.f_bavail - else: - msz = self._windows_space(self._main_prefix)[1] - csz = self._windows_space(self._card_prefix)[1] - - return (msz, 0, csz) - - def books(self, oncard=False, end_session=True): - if oncard and self._card_prefix is None: - return [] - prefix = self._card_prefix if oncard else self._main_prefix - bl = BookList(prefix) - return bl - - def upload_books(self, files, names, on_card=False, end_session=True): - if on_card and not self._card_prefix: - raise ValueError(_('The reader has no storage card connected.')) - - if not on_card: - path = os.path.join(self._main_prefix, EBOOK_DIR) - else: - path = os.path.join(self._card_prefix, EBOOK_DIR) - - sizes = map(os.path.getsize, files) - size = sum(sizes) - - if on_card and size > self.free_space()[2] - 1024*1024: - raise FreeSpaceError("There is insufficient free space "+\ - "on the storage card") - if not on_card and size > self.free_space()[0] - 2*1024*1024: - raise FreeSpaceError("There is insufficient free space " +\ - "in main memory") + EBOOK_DIR = "eBooks" - paths = [] - names = iter(names) - - for infile in files: - filepath = os.path.join(path, names.next()) - paths.append(filepath) - - shutil.copy2(infile, filepath) - - return zip(paths, cycle([on_card])) - - @classmethod - def add_books_to_metadata(cls, locations, metadata, booklists): - for location in locations: - path = location[0] - on_card = 1 if location[1] else 0 - booklists[on_card].add_book(path, os.path.basename(path)) - def delete_books(self, paths, end_session=True): for path in paths: if os.path.exists(path): - # Delete the ebook os.unlink(path) filepath, ext = os.path.splitext(path) @@ -194,132 +49,4 @@ class CYBOOKG3(Device): for p, d, files in os.walk(basepath): for filen in fnmatch.filter(files, filename + "*.t2b"): os.unlink(os.path.join(p, filen)) - - @classmethod - def remove_books_from_metadata(cls, paths, booklists): - for path in paths: - for bl in booklists: - bl.remove_book(path) - - def sync_booklists(self, booklists, end_session=True): - # There is no meta data on the device to update. The device is treated - # as a mass storage device and does not use a meta data xml file like - # the Sony Readers. - pass - - def get_file(self, path, outfile, end_session=True): - path = self.munge_path(path) - src = open(path, 'rb') - shutil.copyfileobj(src, outfile, 10*1024*1024) - - def munge_path(self, path): - if path.startswith('/') and not (path.startswith(self._main_prefix) or \ - (self._card_prefix and path.startswith(self._card_prefix))): - path = self._main_prefix + path[1:] - elif path.startswith('card:'): - path = path.replace('card:', self._card_prefix[:-1]) - return path - - - def _windows_match_device(self, device_id): - device_id = device_id.upper() - vid, pid = hex(cls.VENDOR_ID)[2:], hex(cls.PRODUCT_ID)[2:] - while len(vid) < 4: vid = '0' + vid - while len(pid) < 4: pid = '0' + pid - if 'VID_'+vid in device_id and 'PID_'+pid in device_id: - return True - return False - - # This only supports Windows >= 2000 - def open_windows(self): - drives = [] - wmi = __import__('wmi', globals(), locals(), [], -1) - c = wmi.WMI() - for drive in c.Win32_DiskDrive(): - if self._windows_match_device(str(drive.PNPDeviceID)): - if drive.Partitions == 0: - continue - try: - partition = drive.associators("Win32_DiskDriveToDiskPartition")[0] - logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0] - prefix = logical_disk.DeviceID+os.sep - drives.append((drive.Index, prefix)) - except IndexError: - continue - - if not drives: - raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__) - - drives.sort(cmp=lambda a, b: cmp(a[0], b[0])) - self._main_prefix = drives[0][1] - if len(drives) > 1: - self._card_prefix = drives[1][1] - - def open_osx(self): - raise NotImplementedError() - - def open_linux(self): - import dbus - bus = dbus.SystemBus() - hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager") - - def conditional_mount(dev): - mmo = bus.get_object("org.freedesktop.Hal", dev) - label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device') - is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device') - mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device') - fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device') - if is_mounted: - return str(mount_point) - mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], - dbus_interface='org.freedesktop.Hal.Device.Volume') - return os.path.normpath('/media/'+label)+'/' - - mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__) - if not mm: - raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,)) - self._main_prefix = None - for dev in mm: - try: - self._main_prefix = conditional_mount(dev)+os.sep - break - except dbus.exceptions.DBusException: - continue - - if not self._main_prefix: - raise DeviceError('Could not open device for reading. Try a reboot.') - - self._card_prefix = None - cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__) - - for dev in cards: - try: - self._card_prefix = conditional_mount(dev)+os.sep - break - except: - import traceback - print traceback - continue - - def open(self): - time.sleep(5) - self._main_prefix = self._card_prefix = None - if islinux: - try: - self.open_linux() - except DeviceError: - time.sleep(3) - self.open_linux() - if iswindows: - try: - self.open_windows() - except DeviceError: - time.sleep(3) - self.open_windows() - if isosx: - try: - self.open_osx() - except DeviceError: - time.sleep(3) - self.open_osx() diff --git a/src/calibre/devices/kindle/books.py b/src/calibre/devices/kindle/books.py deleted file mode 100755 index 623b02fe20..0000000000 --- a/src/calibre/devices/kindle/books.py +++ /dev/null @@ -1,122 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' -''' -''' -import re, time, functools -import os - - -from calibre.devices.interface import BookList as _BookList -from calibre.devices import strftime as _strftime - -strftime = functools.partial(_strftime, zone=time.localtime) -MIME_MAP = { - "azw" : "application/azw", - "prc" : "application/prc", - "txt" : "text/plain", - 'mobi': 'application/mobi', - } - -def sortable_title(title): - return re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', title).rstrip() - -class Book(object): - - @apply - def title_sorter(): - doc = '''String to sort the title. If absent, title is returned''' - def fget(self): - src = self.title - return src - def fset(self, val): - self.elem.setAttribute('titleSorter', sortable_title(unicode(val))) - return property(doc=doc, fget=fget, fset=fset) - - @apply - def thumbnail(): - return 0 - - - @apply - def path(): - doc = """ Absolute path to book on device. Setting not supported. """ - def fget(self): - return self.mountpath + self.rpath - return property(fget=fget, doc=doc) - - @apply - def db_id(): - doc = '''The database id in the application database that this file corresponds to''' - def fget(self): - match = re.search(r'_(\d+)$', self.rpath.rpartition('.')[0]) - if match: - return int(match.group(1)) - return property(fget=fget, doc=doc) - - def __init__(self, mountpath, title, authors ): - self.mountpath = mountpath - self.title = title - self.authors = authors - self.mime = "" - self.rpath = "documents//" + title - self.id = 0 - self.sourceid = 0 - self.size = 0 - self.datetime = time.gmtime() - self.tags = [] - - - def __str__(self): - """ Return a utf-8 encoded string with title author and path information """ - return self.title.encode('utf-8') + " by " + \ - self.authors.encode('utf-8') + " at " + self.path.encode('utf-8') - - -class BookList(_BookList): - _mountpath = "" - - def __init__(self, mountpath): - self._mountpath = mountpath - _BookList.__init__(self) - self.return_books(mountpath) - - def return_books(self,mountpath): - docs = mountpath + "documents" - for f in os.listdir(docs): - m = re.match(".*azw", f) - if m: - self.append_book(mountpath,f) - m = re.match(".*prc", f) - if m: - self.append_book(mountpath,f) - m = re.match(".*txt", f) - if m: - self.append_book(mountpath,f) - - def append_book(self,mountpath,f): - b = Book(mountpath,f,"") - b.size = os.stat(mountpath + "//documents//" + f)[6] - b.datetime = time.gmtime(os.stat(mountpath + "//documents//" + f)[8]) - b.rpath = "//documents//" + f - self.append(b) - - def supports_tags(self): - return False - - def add_book(self, name, size, ctime): - book = Book(self._mountpath, name, "") - book.datetime = time.gmtime(ctime) - book.size = size - '''remove book if already in db''' - self.remove_book(self._mountpath + "//documents//" + name) - self.append(book) - - - def remove_book(self, path): - for book in self: - if path.startswith(book.mountpath): - if path.endswith(book.rpath): - self.remove(book) - break - - diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 93145432db..29152c4186 100755 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -1,360 +1,33 @@ __license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' -''' -Device driver for the Amazon Kindle -''' -import sys, os, shutil, time, subprocess, re -from itertools import cycle - -from calibre.devices.interface import Device -from calibre.devices.errors import DeviceError, FreeSpaceError -from calibre.devices.kindle.books import BookList -from calibre import iswindows, islinux, isosx -from calibre.devices.errors import PathError - -class File(object): - def __init__(self, path): - stats = os.stat(path) - self.is_dir = os.path.isdir(path) - self.is_readonly = not os.access(path, os.W_OK) - self.ctime = stats.st_ctime - self.wtime = stats.st_mtime - self.size = stats.st_size - if path.endswith(os.sep): - path = path[:-1] - self.path = path - self.name = os.path.basename(path) - - -class KINDLE(Device): - FORMATS = ["azw", "prc", "txt", 'mobi'] - VENDOR_ID = 0x1949 #: Amazon Vendor Id - PRODUCT_ID = 0x001 #: Product Id for the Kindle - INTERNAL_STORAGE = 'INTERNAL_STORAGE' - CARD_STORAGE = 'CARD_STORAGE' - PRODUCT_NAME = 'KINDLE' - VENDOR_NAME = 'AMAZON' - - - MAIN_MEMORY_VOLUME_LABEL = 'Kindle Internal Storage USB Device' - STORAGE_CARD_VOLUME_LABEL = 'Kindle Card Storage USB Device' - - #OSX_MAIN_NAME = 'Sony PRS-505/UC Media' - #OSX_SD_NAME = 'Sony PRS-505/UC:SD Media' - #OSX_MS_NAME = 'Sony PRS-505/UC:MS Media' - - FDI_TEMPLATE = \ -''' - - - - - - %(main_memory)s - %(deviceclass)s - - - - - - - - - - - %(storage_card)s - %(deviceclass)s - - - - - -''' - - - def __init__(self, log_packets=False): - self._main_prefix = self._card_prefix = None - - @classmethod - def get_fdi(cls): - return cls.FDI_TEMPLATE%dict( - deviceclass=cls.__name__, - vendor_id=hex(cls.VENDOR_ID), - product_id=hex(cls.PRODUCT_ID), - main_memory=cls.MAIN_MEMORY_VOLUME_LABEL, - storage_card=cls.STORAGE_CARD_VOLUME_LABEL, - ) - - @classmethod - def is_device(cls, device_id): - '''print "mimi in is device"''' - if not hasattr(device_id, 'upper'): - return False - - if 'VEN_'+cls.VENDOR_NAME in device_id.upper() and \ - 'PROD_'+cls.INTERNAL_STORAGE in device_id.upper(): - return True - if 'VEN_'+cls.VENDOR_NAME in device_id.upper() and \ - 'PROD_'+cls.CARD_STORAGE in device_id.upper(): - return True - - vid, pid = hex(cls.VENDOR_ID)[2:], hex(cls.PRODUCT_ID)[2:] - if len(vid) < 4: vid = '0'+vid - if len(pid) < 4: pid = '0'+pid - if len(pid) < 4: pid = '0'+pid - if 'VID_'+vid in device_id.upper() and \ - 'PID_'+pid in device_id.upper(): - return True - return False - - def open_osx(self): - mount = subprocess.Popen('mount', shell=True, - stdout=subprocess.PIPE).stdout.read() - src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_MAIN_NAME,), - shell=True, stdout=subprocess.PIPE).stdout.read() - try: - devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1) - self._main_prefix = re.search('/dev/%s(\w*)\s+on\s+([^\(]+)\s+'%(devname,), mount).group(2) + os.sep - except: - raise DeviceError('Unable to find %s. Is it connected?'%(self.__class__.__name__,)) - try: - src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_SD_NAME,), - shell=True, stdout=subprocess.PIPE).stdout.read() - devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1) - except: - try: - src = subprocess.Popen('ioreg -n "%s"'%(self.OSX_MS_NAME,), - shell=True, stdout=subprocess.PIPE).stdout.read() - devname = re.search(r'BSD Name.*=\s+"(\S+)"', src).group(1) - except: - devname = None - if devname is not None: - self._card_prefix = re.search('/dev/%s(\w*)\s+on\s+([^\(]+)\s+'%(devname,), mount).group(2) + os.sep - - - def open_windows(self): - raise NotImplementedError - - - def open_linux(self): - import dbus - bus = dbus.SystemBus() - hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager") - try: - mm = hm.FindDeviceStringMatch('kindle.mainvolume', self.__class__.__name__)[0] - except: - raise DeviceError('Unable to find %s. Is it connected?'%(self.__class__.__name__,)) - try: - sc = hm.FindDeviceStringMatch('kindle.cardvolume', self.__class__.__name__)[0] - except: - sc = None - - def conditional_mount(dev): - mmo = bus.get_object("org.freedesktop.Hal", dev) - label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device') - is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device') - mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device') - fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device') - if is_mounted: - return str(mount_point) - mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], - dbus_interface='org.freedesktop.Hal.Device.Volume') - return os.path.normpath('/media/'+label)+'/' - - self._main_prefix = conditional_mount(mm)+os.sep - self._card_prefix = None - if sc is not None: - self._card_prefix = conditional_mount(sc)+os.sep - - def open(self): - time.sleep(5) - self._main_prefix = self._card_prefix = None - if islinux: - try: - self.open_linux() - except DeviceError: - time.sleep(3) - self.open_linux() - if iswindows: - try: - self.open_windows() - except DeviceError: - time.sleep(3) - self.open_windows() - if isosx: - try: - self.open_osx() - except DeviceError: - time.sleep(3) - self.open_osx() - - - def set_progress_reporter(self, pr): - self.report_progress = pr - - def get_device_information(self, end_session=True): - return ('Kindle', '', '', '') - - def card_prefix(self, end_session=True): - return self._card_prefix - - @classmethod - def _windows_space(cls, prefix): - if prefix is None: - return 0, 0 - import win32file - sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ - win32file.GetDiskFreeSpace(prefix[:-1]) - mult = sectors_per_cluster * bytes_per_sector - return total_clusters * mult, free_clusters * mult - - def total_space(self, end_session=True): - msz = csz = 0 - if not iswindows: - if self._main_prefix is not None: - stats = os.statvfs(self._main_prefix) - msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) - if self._card_prefix is not None: - stats = os.statvfs(self._card_prefix) - csz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) - else: - msz = self._windows_space(self._main_prefix)[0] - csz = self._windows_space(self._card_prefix)[0] - - return (msz, 0, csz) - - def free_space(self, end_session=True): - msz = csz = 0 - if not iswindows: - if self._main_prefix is not None: - stats = os.statvfs(self._main_prefix) - msz = stats.f_bsize * stats.f_bavail - if self._card_prefix is not None: - stats = os.statvfs(self._card_prefix) - csz = stats.f_bsize * stats.f_bavail - else: - msz = self._windows_space(self._main_prefix)[1] - csz = self._windows_space(self._card_prefix)[1] - - return (msz, 0, csz) - - def books(self, oncard=False, end_session=True): - if oncard and self._card_prefix is None: - return [] - prefix = self._card_prefix if oncard else self._main_prefix - bl = BookList(prefix) - return bl - - def munge_path(self, path): - if path.startswith('/') and not (path.startswith(self._main_prefix) or \ - (self._card_prefix and path.startswith(self._card_prefix))): - path = self._main_prefix + path[1:] - elif path.startswith('card:'): - path = path.replace('card:', self._card_prefix[:-1]) - return path - - def mkdir(self, path, end_session=True): - """ Make directory """ - path = self.munge_path(path) - os.mkdir(path) - - def list(self, path, recurse=False, end_session=True, munge=True): - if munge: - path = self.munge_path(path) - if os.path.isfile(path): - return [(os.path.dirname(path), [File(path)])] - entries = [File(os.path.join(path, f)) for f in os.listdir(path)] - dirs = [(path, entries)] - for _file in entries: - if recurse and _file.is_dir: - dirs[len(dirs):] = self.list(_file.path, recurse=True, munge=False) - return dirs - - def get_file(self, path, outfile, end_session=True): - path = self.munge_path(path) - src = open(path, 'rb') - shutil.copyfileobj(src, outfile, 10*1024*1024) - - def put_file(self, infile, path, replace_file=False, end_session=True): - path = self.munge_path(path) - if os.path.isdir(path): - path = os.path.join(path, infile.name) - if not replace_file and os.path.exists(path): - raise PathError('File already exists: '+path) - dest = open(path, 'wb') - shutil.copyfileobj(infile, dest, 10*1024*1024) - dest.flush() - dest.close() - - def rm(self, path, end_session=True): - path = self.munge_path(path) - os.unlink(path) - - def touch(self, path, end_session=True): - path = self.munge_path(path) - if not os.path.exists(path): - open(path, 'w').close() - if not os.path.isdir(path): - os.utime(path, None) - - def upload_books(self, files, names, on_card=False, end_session=True): - path = os.path.join(self._card_prefix, "documents") if on_card \ - else os.path.join(self._main_prefix, 'documents') - infiles = [file if hasattr(file, 'read') else open(file, 'rb') for file in files] - for f in infiles: f.seek(0, 2) - sizes = [f.tell() for f in infiles] - size = sum(sizes) - space = self.free_space() - mspace = space[0] - cspace = space[2] - if on_card and size > cspace - 1024*1024: - raise FreeSpaceError("There is insufficient free space "+\ - "on the storage card") - if not on_card and size > mspace - 2*1024*1024: - raise FreeSpaceError("There is insufficient free space " +\ - "in main memory") - - paths, ctimes = [], [] - - names = iter(names) - for infile in infiles: - infile.seek(0) - name = names.next() - paths.append(os.path.join(path, name)) - if on_card and not os.path.exists(os.path.dirname(paths[-1])): - os.mkdir(os.path.dirname(paths[-1])) - self.put_file(infile, paths[-1], replace_file=True) - ctimes.append(os.path.getctime(paths[-1])) - return zip(paths, sizes, ctimes, cycle([on_card])) - - @classmethod - def add_books_to_metadata(cls, locations, metadata, booklists): - metadata = iter(metadata) - for location in locations: - #info = metadata.next() - path = location[0] - on_card = 1 if location[3] else 0 - name = path.rpartition(os.sep)[2] - name = name.replace('//', '/') - booklists[on_card].add_book(name,*location[1:-1]) - - def delete_books(self, paths, end_session=True): - for path in paths: - os.unlink(path) - - @classmethod - def remove_books_from_metadata(cls, paths, booklists): - for path in paths: - for bl in booklists: - bl.remove_book(path) - - - def sync_booklists(self, booklists, end_session=True): - return 0; - - -def main(args=sys.argv): - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file +__copyright__ = '2009, John Schember ' +''' +Device driver for Amazon's Kindle +''' + +import os, fnmatch + +from calibre.devices.usbms.driver import USBMS +from calibre.devices.usbms.cli import CLI + +class KINDLE(USBMS, CLI): + MIME_MAP = { + 'azw' : 'application/azw', + 'mobi' : 'application/mobi', + 'prc' : 'application/prc', + 'txt' : 'text/plain', + } + # Ordered list of supported formats + FORMATS = MIME_MAP.keys() + + VENDOR_ID = 0x1949 + PRODUCT_ID = 0x0001 + BCD = 0x399 + + VENDOR_NAME = 'AMAZON' + PRODUCT_NAME = 'KINDLE' + + MAIN_MEMORY_VOLUME_LABEL = 'Kindle Main Memory' + STORAGE_CARD_VOLUME_LABEL = 'Kindle Storage Card' + + EBOOK_DIR = "documents" + From ea30268743d24a799f65370be0c1c6cf93f137b3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 6 Jan 2009 19:41:24 -0500 Subject: [PATCH 2/2] USB Mass Storage base class. Cybook now using to USBMS. Kindle (untested) now using USBMS. --- src/calibre/devices/usbms/__init__.py | 0 src/calibre/devices/usbms/books.py | 44 +++++ src/calibre/devices/usbms/cli.py | 16 ++ src/calibre/devices/usbms/device.py | 223 ++++++++++++++++++++++++++ src/calibre/devices/usbms/driver.py | 144 +++++++++++++++++ 5 files changed, 427 insertions(+) create mode 100644 src/calibre/devices/usbms/__init__.py create mode 100644 src/calibre/devices/usbms/books.py create mode 100644 src/calibre/devices/usbms/cli.py create mode 100644 src/calibre/devices/usbms/device.py create mode 100644 src/calibre/devices/usbms/driver.py diff --git a/src/calibre/devices/usbms/__init__.py b/src/calibre/devices/usbms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py new file mode 100644 index 0000000000..3943ef94f4 --- /dev/null +++ b/src/calibre/devices/usbms/books.py @@ -0,0 +1,44 @@ +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember ' +''' +''' + +import os, fnmatch, re, time + +from calibre.devices.interface import BookList as _BookList + +class Book(object): + def __init__(self, path, title, authors, mime): + self.title = title + self.authors = authors + self.mime = mime + self.size = os.path.getsize(path) + self.datetime = time.gmtime(os.path.getctime(path)) + self.path = path + self.thumbnail = None + self.tags = [] + + @apply + def title_sorter(): + 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) + + @apply + def thumbnail(): + return None + + def __str__(self): + """ Return a utf-8 encoded string with title author and path information """ + return self.title.encode('utf-8') + " by " + \ + self.authors.encode('utf-8') + " at " + self.path.encode('utf-8') + +class BookList(_BookList): + def supports_tags(self): + return False + + def set_tags(self, book, tags): + pass + + diff --git a/src/calibre/devices/usbms/cli.py b/src/calibre/devices/usbms/cli.py new file mode 100644 index 0000000000..35a3a84824 --- /dev/null +++ b/src/calibre/devices/usbms/cli.py @@ -0,0 +1,16 @@ +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember + + + + + + %(main_memory)s + %(deviceclass)s + + + + + + + + + + + + + %(storage_card)s + %(deviceclass)s + + + + + + +''' + + def __init__(self, key='-1', log_packets=False, report_progress=None) : + self._main_prefix = self._card_prefix = None + + @classmethod + def get_fdi(cls): + return cls.FDI_TEMPLATE%dict( + app=__appname__, + deviceclass=cls.__name__, + vendor_id=hex(cls.VENDOR_ID), + product_id=hex(cls.PRODUCT_ID), + bcd=hex(cls.BCD), + main_memory=cls.MAIN_MEMORY_VOLUME_LABEL, + storage_card=cls.STORAGE_CARD_VOLUME_LABEL, + ) + + def set_progress_reporter(self, report_progress): + self.report_progress = report_progress + + def card_prefix(self, end_session=True): + return self._card_prefix + + @classmethod + def _windows_space(cls, prefix): + if prefix is None: + return 0, 0 + win32file = __import__('win32file', globals(), locals(), [], -1) + try: + sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ + win32file.GetDiskFreeSpace(prefix[:-1]) + except Exception, err: + if getattr(err, 'args', [None])[0] == 21: # Disk not ready + time.sleep(3) + sectors_per_cluster, bytes_per_sector, free_clusters, total_clusters = \ + win32file.GetDiskFreeSpace(prefix[:-1]) + else: raise + mult = sectors_per_cluster * bytes_per_sector + return total_clusters * mult, free_clusters * mult + + def total_space(self, end_session=True): + msz = csz = 0 + print self._main_prefix + if not iswindows: + if self._main_prefix is not None: + stats = os.statvfs(self._main_prefix) + msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) + if self._card_prefix is not None: + stats = os.statvfs(self._card_prefix) + csz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) + else: + msz = self._windows_space(self._main_prefix)[0] + csz = self._windows_space(self._card_prefix)[0] + + return (msz, 0, csz) + + def free_space(self, end_session=True): + msz = csz = 0 + if not iswindows: + if self._main_prefix is not None: + stats = os.statvfs(self._main_prefix) + msz = stats.f_frsize * stats.f_bavail + if self._card_prefix is not None: + stats = os.statvfs(self._card_prefix) + csz = stats.f_frsize * stats.f_bavail + else: + msz = self._windows_space(self._main_prefix)[1] + csz = self._windows_space(self._card_prefix)[1] + + return (msz, 0, csz) + + @classmethod + def windows_match_device(cls, device_id): + device_id = device_id.upper() + if 'VEN_'+cls.VENDOR_NAME in device_id and \ + 'PROD_'+cls.PRODUCT_NAME in device_id: + return True + vid, pid = hex(cls.VENDOR_ID)[2:], hex(cls.PRODUCT_ID)[2:] + while len(vid) < 4: vid = '0' + vid + while len(pid) < 4: pid = '0' + pid + if 'VID_'+vid in device_id and 'PID_'+pid in device_id: + return True + return False + + # This only supports Windows >= 2000 + def open_windows(self): + drives = [] + wmi = __import__('wmi', globals(), locals(), [], -1) + c = wmi.WMI() + for drive in c.Win32_DiskDrive(): + if self.__class__.windows_match_device(str(drive.PNPDeviceID)): + if drive.Partitions == 0: + continue + try: + partition = drive.associators("Win32_DiskDriveToDiskPartition")[0] + logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0] + prefix = logical_disk.DeviceID+os.sep + drives.append((drive.Index, prefix)) + except IndexError: + continue + + if not drives: + raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__) + + drives.sort(cmp=lambda a, b: cmp(a[0], b[0])) + self._main_prefix = drives[0][1] + if len(drives) > 1: + self._card_prefix = drives[1][1] + + def open_osx(self): + raise NotImplementedError() + + def open_linux(self): + import dbus + bus = dbus.SystemBus() + hm = dbus.Interface(bus.get_object("org.freedesktop.Hal", "/org/freedesktop/Hal/Manager"), "org.freedesktop.Hal.Manager") + + def conditional_mount(dev): + mmo = bus.get_object("org.freedesktop.Hal", dev) + label = mmo.GetPropertyString('volume.label', dbus_interface='org.freedesktop.Hal.Device') + is_mounted = mmo.GetPropertyString('volume.is_mounted', dbus_interface='org.freedesktop.Hal.Device') + mount_point = mmo.GetPropertyString('volume.mount_point', dbus_interface='org.freedesktop.Hal.Device') + fstype = mmo.GetPropertyString('volume.fstype', dbus_interface='org.freedesktop.Hal.Device') + if is_mounted: + return str(mount_point) + mmo.Mount(label, fstype, ['umask=077', 'uid='+str(os.getuid()), 'sync'], + dbus_interface='org.freedesktop.Hal.Device.Volume') + return os.path.normpath('/media/'+label)+'/' + + mm = hm.FindDeviceStringMatch(__appname__+'.mainvolume', self.__class__.__name__) + if not mm: + raise DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%(self.__class__.__name__,)) + self._main_prefix = None + for dev in mm: + try: + self._main_prefix = conditional_mount(dev)+os.sep + break + except dbus.exceptions.DBusException: + continue + + if not self._main_prefix: + raise DeviceError('Could not open device for reading. Try a reboot.') + + self._card_prefix = None + cards = hm.FindDeviceStringMatch(__appname__+'.cardvolume', self.__class__.__name__) + + for dev in cards: + try: + self._card_prefix = conditional_mount(dev)+os.sep + break + except: + import traceback + print traceback + continue + + def open(self): + time.sleep(5) + self._main_prefix = self._card_prefix = None + if islinux: + try: + self.open_linux() + except DeviceError: + time.sleep(3) + self.open_linux() + if iswindows: + try: + self.open_windows() + except DeviceError: + time.sleep(3) + self.open_windows() + if isosx: + try: + self.open_osx() + except DeviceError: + time.sleep(3) + self.open_osx() + diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py new file mode 100644 index 0000000000..88e1cce6a2 --- /dev/null +++ b/src/calibre/devices/usbms/driver.py @@ -0,0 +1,144 @@ +__license__ = 'GPL v3' +__copyright__ = '2009, John Schember self.free_space()[2] - 1024*1024: + raise FreeSpaceError("There is insufficient free space "+\ + "on the storage card") + if not on_card and size > self.free_space()[0] - 2*1024*1024: + raise FreeSpaceError("There is insufficient free space " +\ + "in main memory") + + paths = [] + names = iter(names) + + for infile in files: + filepath = os.path.join(path, names.next()) + paths.append(filepath) + + shutil.copy2(infile, filepath) + + return zip(paths, cycle([on_card])) + + @classmethod + def add_books_to_metadata(cls, locations, metadata, booklists): + for location in locations: + path = location[0] + on_card = 1 if location[1] else 0 + + title, author, mime = cls.extract_book_metadata_by_filename(os.path.basename(path)) + booklists[on_card].append(Book(path, title, author, mime)) + + def delete_books(self, paths, end_session=True): + for path in paths: + if os.path.exists(path): + # Delete the ebook + os.unlink(path) + + @classmethod + def remove_books_from_metadata(cls, paths, booklists): + for path in paths: + for bl in booklists: + for book in bl: + if path.endswith(book.path): + bl.remove(book) + break + + def sync_booklists(self, booklists, end_session=True): + # There is no meta data on the device to update. The device is treated + # as a mass storage device and does not use a meta data xml file like + # the Sony Readers. + pass + + def get_file(self, path, outfile, end_session=True): + path = self.munge_path(path) + src = open(path, 'rb') + shutil.copyfileobj(src, outfile, 10*1024*1024) + + def munge_path(self, path): + if path.startswith('/') and not (path.startswith(self._main_prefix) or \ + (self._card_prefix and path.startswith(self._card_prefix))): + path = self._main_prefix + path[1:] + elif path.startswith('card:'): + path = path.replace('card:', self._card_prefix[:-1]) + return path + + @classmethod + def extract_book_metadata_by_filename(cls, filename): + book_title = '' + book_author = '' + book_mime = '' + # Calibre uses a specific format for file names. They take the form + # title_-_author_number.extention We want to see if the file name is + # in this format. + if fnmatch.fnmatchcase(filename, '*_-_*.*'): + # Get the title and author from the file name + title, sep, author = filename.rpartition('_-_') + author, sep, ext = author.rpartition('_') + book_title = title.replace('_', ' ') + book_author = author.replace('_', ' ') + # if the filename did not match just set the title to + # the filename without the extension + else: + book_title = os.path.splitext(filename)[0].replace('_', ' ') + + fileext = os.path.splitext(filename)[1] + if fileext in cls.MIME_MAP.keys(): + book_mime = cls.MIME_MAP[fileext] + + return book_title, book_author, book_mime +