diff --git a/src/libprs500/devices/__init__.py b/src/libprs500/devices/__init__.py index d9b1729e44..665ab3b1b8 100644 --- a/src/libprs500/devices/__init__.py +++ b/src/libprs500/devices/__init__.py @@ -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 diff --git a/src/libprs500/devices/kindle/__init__.py b/src/libprs500/devices/kindle/__init__.py new file mode 100755 index 0000000000..c13ce46b4e --- /dev/null +++ b/src/libprs500/devices/kindle/__init__.py @@ -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. \ No newline at end of file diff --git a/src/libprs500/devices/kindle/books.py b/src/libprs500/devices/kindle/books.py new file mode 100755 index 0000000000..203fc52078 --- /dev/null +++ b/src/libprs500/devices/kindle/books.py @@ -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 + + diff --git a/src/libprs500/devices/kindle/driver.py b/src/libprs500/devices/kindle/driver.py new file mode 100755 index 0000000000..919852a597 --- /dev/null +++ b/src/libprs500/devices/kindle/driver.py @@ -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 = \ +''' + + + + + + %(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 '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()) \ No newline at end of file diff --git a/src/libprs500/gui2/__init__.py b/src/libprs500/gui2/__init__.py index eb90153bdd..b5cf2d69d0 100644 --- a/src/libprs500/gui2/__init__.py +++ b/src/libprs500/gui2/__init__.py @@ -116,6 +116,9 @@ class FileIconProvider(QFileIconProvider): 'rar' : 'rar', 'zip' : 'zip', 'txt' : 'txt', + 'prc' : 'mobi', + 'azw' : 'azw', + 'mobi' : 'mobi', } def __init__(self):