Initial implementation of driver for Kindle by Marielle (tested on windows only. No metadata support)

This commit is contained in:
Kovid Goyal 2008-01-07 22:23:56 +00:00
parent 718837b1b9
commit 3deb351b3a
5 changed files with 561 additions and 1 deletions

View File

@ -19,7 +19,8 @@ Device drivers.
def devices():
from libprs500.devices.prs500.driver import PRS500
from libprs500.devices.prs505.driver import PRS505
return (PRS500, PRS505)
from libprs500.devices.kindle.driver import KINDLE
return (PRS500, PRS505, KINDLE)
import time

View File

@ -0,0 +1,14 @@
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

View File

@ -0,0 +1,133 @@
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
'''
'''
import re, time, functools
import os
from libprs500.devices.interface import BookList as _BookList
from libprs500.devices import strftime as _strftime
strftime = functools.partial(_strftime, zone=time.localtime)
MIME_MAP = {
"azw" : "application/azw",
"prc" : "application/prc",
"txt" : "text/plain"
}
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

View File

@ -0,0 +1,409 @@
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
'''
Device driver for the Amazon Kindle
'''
import sys, os, shutil, time, subprocess, re
from itertools import cycle
from libprs500.devices.interface import Device
from libprs500.devices.errors import DeviceError, FreeSpaceError
from libprs500.devices.kindle.books import BookList
from libprs500 import iswindows, islinux, isosx
from libprs500.devices.libusb import get_device_by_id
from libprs500.devices.libusb import Error as USBError
from libprs500.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"]
VENDOR_ID = 0x1949 #: Amazon Vendor Id
PRODUCT_ID = 0x001 #: Product Id for the Kindle
INTERNAL_STORAGE = 'INTERNAL_STORAGE'
CARD_STORAGE = 'CARD_STORAGE'
VENDOR_NAME = 'KINDLE'
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 = \
'''
<device>
<match key="info.category" string="volume">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
<match key="volume.is_partition" bool="false">
<merge key="volume.label" type="string">%(main_memory)s</merge>
<merge key="kindle.mainvolume" type="string">%(deviceclass)s</merge>
</match>
</match>
</match>
</match>
</device>
<device>
<match key="info.category" string="volume">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.vendor_id" int="%(vendor_id)s">
<match key="@info.parent:@info.parent:@info.parent:@info.parent:usb.product_id" int="%(product_id)s">
<match key="volume.is_partition" bool="true">
<merge key="volume.label" type="string">%(storage_card)s</merge>
<merge key="kindle.cardvolume" type="string">%(deviceclass)s</merge>
</match>
</match>
</match>
</match>
</device>
'''
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 '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
@classmethod
def is_connected(cls, helper=None):
if iswindows:
for c in helper.USBControllerDevice():
if cls.is_device(c.Dependent.DeviceID):
return True
return False
else:
try:
return get_device_by_id(cls.VENDOR_ID, cls.PRODUCT_ID) != None
except USBError:
return False
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):
drives = []
import wmi
c = wmi.WMI()
for drive in c.Win32_DiskDrive():
'''print drive.PNPDeviceID'''
if self.__class__.is_device(drive.PNPDeviceID):
if drive.Partitions == 0:
continue
try:
partition = drive.associators("Win32_DiskDriveToDiskPartition")[0]
except IndexError:
continue
logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0]
prefix = logical_disk.DeviceID+os.sep
drives.append((drive.Index, prefix))
if not drives:
print self.__class__.__name__
raise DeviceError('Unable to find %s. Is it connected?'%(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_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())

View File

@ -116,6 +116,9 @@ class FileIconProvider(QFileIconProvider):
'rar' : 'rar',
'zip' : 'zip',
'txt' : 'txt',
'prc' : 'mobi',
'azw' : 'azw',
'mobi' : 'mobi',
}
def __init__(self):