diff --git a/src/libprs500/__init__.py b/src/libprs500/__init__.py index c1b969ef4d..5d8a9c7363 100644 --- a/src/libprs500/__init__.py +++ b/src/libprs500/__init__.py @@ -15,14 +15,10 @@ """ This package provides an interface to the SONY Reader PRS-500 over USB. -The public interface of libprs500 is in L{libprs500.communicate}. To use it - >>> from libprs500.communicate import PRS500Device - >>> dev = PRS500Device() - >>> dev.get_device_information() - ('Sony Reader', 'PRS-500/U', '1.0.00.21081', 'application/x-bbeb-book') +The public interface for device backends is defined in libprs500.device. There is also a script L{prs500} that provides a command-line interface to -libprs500. See the script +the SONY Reader. See the script for more usage examples. A GUI is available via the command prs500-gui. The packet structure used by the SONY Reader USB protocol is defined diff --git a/src/libprs500/books.py b/src/libprs500/books.py index 37119edd3b..0d13018d37 100644 --- a/src/libprs500/books.py +++ b/src/libprs500/books.py @@ -47,16 +47,16 @@ class book_metadata_field(object): class Book(object): """ Provides a view onto the XML element that represents a book """ - title = book_metadata_field("title") - author = book_metadata_field("author", \ + title = book_metadata_field("title") + author = book_metadata_field("author", \ formatter=lambda x: x if x.strip() else "Unknown") - mime = book_metadata_field("mime") - rpath = book_metadata_field("path") - id = book_metadata_field("id", formatter=int) - sourceid = book_metadata_field("sourceid", formatter=int) - size = book_metadata_field("size", formatter=int) + mime = book_metadata_field("mime") + rpath = book_metadata_field("path") + id = book_metadata_field("id", formatter=int) + sourceid = book_metadata_field("sourceid", formatter=int) + size = book_metadata_field("size", formatter=int) # When setting this attribute you must use an epoch - datetime = book_metadata_field("date", \ + datetime = book_metadata_field("date", \ formatter=lambda x: time.strptime(x, "%a, %d %b %Y %H:%M:%S %Z"), setter=lambda x: time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(x))) diff --git a/src/libprs500/cli/main.py b/src/libprs500/cli/main.py index 1a8da87b88..a2d3caba45 100755 --- a/src/libprs500/cli/main.py +++ b/src/libprs500/cli/main.py @@ -22,7 +22,7 @@ import StringIO, sys, time, os from optparse import OptionParser from libprs500 import __version__ as VERSION -from libprs500.communicate import PRS500Device +from libprs500.prs500 import PRS500 from terminfo import TerminalController from libprs500.errors import ArgumentError, DeviceError, DeviceLocked @@ -197,7 +197,7 @@ def main(): command = args[0] args = args[1:] - dev = PRS500Device(key=options.key, log_packets=options.log_packets) + dev = PRS500(key=options.key, log_packets=options.log_packets) try: if command == "df": total = dev.total_space(end_session=False) diff --git a/src/libprs500/device.py b/src/libprs500/device.py new file mode 100644 index 0000000000..139a4dbe8a --- /dev/null +++ b/src/libprs500/device.py @@ -0,0 +1,114 @@ +## Copyright (C) 2006 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. + +""" +Define the minimum interface that a device backend must satisfy to be used in +the GUI. A device backend must subclass the L{Device} class. See prs500.py for +a backend that implement the Device interface for the SONY PRS500 Reader. +""" + +class Device(object): + """ + Defines the interface that should be implemented by backends that + communicate with an ebook reader. + + The C{end_session} variables are used for USB session management. Sometimes + the front-end needs to call several methods one after another, in which case + the USB session should not be closed after each method call. + """ + # Ordered list of supported formats + FORMATS = ["lrf", "rtf", "pdf", "txt"] + VENDOR_ID = 0x0000 + PRODICT_ID = 0x0000 + + def __init__(self, key='-1', log_packets=False, report_progress=None) : + """ + @param key: The key to unlock the device + @param log_packets: If true the packet stream to/from the device is logged + @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 + """ + raise NotImplementedError() + + 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) + """ + raise NotImplementedError() + + def total_space(self, end_session=True): + """ + Get total space available on the mountpoints: + 1. Main memory + 2. Memory Stick + 3. SD Card + + @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. + """ + raise NotImplementedError() + + def free_space(self, end_session=True): + """ + Get free space available on the mountpoints: + 1. Main memory + 2. Memory Stick + 3. SD Card + + @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 0. + """ + raise NotImplementedError() + + def books(self, oncard=False, end_session=True): + """ + Return a list of ebooks on the device. + @param oncard: If True return a list of ebooks on the storage card, + otherwise return list of ebooks in main memory of device + + @return: A list of Books. Each Book object must have the fields: + title, author, size, datetime (a time tuple), path, thumbnail (can be None). + """ + raise NotImplementedError() + + def add_book(self, infile, name, info, booklists, oncard=False, \ + sync_booklists=False, end_session=True): + """ + Add a book to the device. If oncard is True then the book is copied + to the card rather than main memory. + + @param infile: The source file, should be opened in "rb" mode + @param name: The name of the book file when uploaded to the + device. The extension of name must be one of + the supported formats for this device. + @param info: A dictionary that must have the keys "title", "authors", "cover". + C{info["cover"]} should be a three element tuple (width, height, data) + where data is the image data in JPEG format as a string + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=False), L{books}(oncard=True)). + """ + raise NotImplementedError() + + def remove_book(self, paths, booklists, end_session=True): + """ + Remove the books specified by C{paths} from the device. The metadata + cache on the device must also be updated. + @param booklists: A tuple containing the result of calls to + (L{books}(oncard=False), L{books}(oncard=True)). + """ + raise NotImplementedError() \ No newline at end of file diff --git a/src/libprs500/gui/main.py b/src/libprs500/gui/main.py index d8ee1ad362..14a766bd8f 100644 --- a/src/libprs500/gui/main.py +++ b/src/libprs500/gui/main.py @@ -26,8 +26,7 @@ from PyQt4.QtGui import QPixmap, QErrorMessage, QLineEdit, \ QMessageBox, QFileDialog, QIcon, QDialog, QInputDialog from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical -from libprs500.communicate import PRS500Device as device -from libprs500.books import fix_ids +from libprs500.prs500 import PRS500 as device from libprs500.errors import * from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \ extension, APP_TITLE @@ -165,17 +164,10 @@ class Main(QObject, Ui_MainWindow): self.library_model.delete(self.library_view.selectionModel()\ .selectedRows()) else: - self.status("Deleting files from device") + self.status("Deleting books and updating metadata on device") paths = self.device_view.model().delete(rows) - for path in paths: - self.status("Deleting "+path[path.rfind("/")+1:]) - self.dev.del_file(path, end_session=False) - fix_ids(self.reader_model.booklist, self.card_model.booklist) - self.status("Syncing media list to reader") - self.dev.upload_book_list(self.reader_model.booklist) - if len(self.card_model.booklist): - self.status("Syncing media list to card") - self.dev.upload_book_list(self.card_model.booklist) + self.dev.remove_book(paths, (self.reader_model.booklist, \ + self.card_model.booklist), end_session=False) self.update_availabe_space() self.model_modified() self.show_book(self.current_view.currentIndex(), QModelIndex()) diff --git a/src/libprs500/communicate.py b/src/libprs500/prs500.py similarity index 97% rename from src/libprs500/communicate.py rename to src/libprs500/prs500.py index d44fde4e84..5ee89d1aa4 100755 --- a/src/libprs500/communicate.py +++ b/src/libprs500/prs500.py @@ -43,7 +43,7 @@ """ Contains the logic for communication with the device (a SONY PRS-500). -The public interface of class L{PRS500Device} defines the +The public interface of class L{PRS500} defines the methods for performing various tasks. """ import sys @@ -53,6 +53,7 @@ from tempfile import TemporaryFile from array import array from functools import wraps +from libprs500.device import Device from libprs500.libusb import Error as USBError from libprs500.libusb import get_device_by_id from libprs500.prstypes import * @@ -60,38 +61,9 @@ from libprs500.errors import * from libprs500.books import BookList, fix_ids from libprs500 import __author__ as AUTHOR -MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output - # Protocol versions libprs500 has been tested with KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L] -class Device(object): - """ Contains device independent methods """ - _packet_number = 0 #: Keep track of the packet number for packet tracing - - def log_packet(self, packet, header, stream=sys.stderr): - """ - Log C{packet} to stream C{stream}. - Header should be a small word describing the type of packet. - """ - self._packet_number += 1 - print >> stream, str(self._packet_number), header, "Type:", \ - packet.__class__.__name__ - print >> stream, packet - print >> stream, "--" - - @classmethod - def validate_response(cls, res, _type=0x00, number=0x00): - """ - Raise a ProtocolError if the type and number of C{res} - is not the same as C{type} and C{number}. - """ - if _type != res.type or number != res.rnumber: - raise ProtocolError("Inavlid response.\ntype: expected=" + \ - hex(_type)+" actual=" + hex(res.type) + \ - "\nrnumber: expected=" + hex(number) + \ - " actual="+hex(res.rnumber)) - class File(object): """ @@ -117,14 +89,14 @@ class File(object): return self.name -class PRS500Device(Device): +class PRS500(Device): """ - Contains the logic for performing various tasks on the reader. + Implements the backend for communication with the SONY Reader. Each method decorated by C{safe} performs a task. """ - VENDOR_ID = 0x054c #: SONY Vendor Id + VENDOR_ID = 0x054c #: SONY Vendor Id PRODUCT_ID = 0x029b #: Product Id for the PRS-500 INTERFACE_ID = 0 #: The interface we use to talk to the device BULK_IN_EP = 0x81 #: Endpoint for Bulk reads @@ -136,7 +108,32 @@ class PRS500Device(Device): # Ordered list of supported formats FORMATS = ["lrf", "rtf", "pdf", "txt"] # Height for thumbnails of books/images on the device - THUMBNAIL_HEIGHT = 68 + THUMBNAIL_HEIGHT = 68 + + _packet_number = 0 #: Keep track of the packet number for packet tracing + + def log_packet(self, packet, header, stream=sys.stderr): + """ + Log C{packet} to stream C{stream}. + Header should be a small word describing the type of packet. + """ + self._packet_number += 1 + print >> stream, str(self._packet_number), header, "Type:", \ + packet.__class__.__name__ + print >> stream, packet + print >> stream, "--" + + @classmethod + def validate_response(cls, res, _type=0x00, number=0x00): + """ + Raise a ProtocolError if the type and number of C{res} + is not the same as C{type} and C{number}. + """ + if _type != res.type or number != res.rnumber: + raise ProtocolError("Inavlid response.\ntype: expected=" + \ + hex(_type)+" actual=" + hex(res.type) + \ + "\nrnumber: expected=" + hex(number) + \ + " actual="+hex(res.rnumber)) @classmethod def signature(cls): @@ -202,7 +199,6 @@ class PRS500Device(Device): If it is called with -1 that means that the task does not have any progress information """ - Device.__init__(self) self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) # Handle that is used to communicate with device. Setup in L{open} self.handle = None @@ -237,8 +233,6 @@ class PRS500Device(Device): Requires write privileges to the device file. Also initialize the device. See the source code for the sequenceof initialization commands. - - @todo: Implement unlocking of the device """ self.device = get_device_by_id(self.VENDOR_ID, self.PRODUCT_ID) if not self.device: @@ -790,6 +784,19 @@ class PRS500Device(Device): self.get_file(self.MEDIA_XML, tfile, end_session=False) return BookList(prefix=prefix, root=root, sfile=tfile) + @safe + def remove_book(self, paths, booklists, end_session=True): + """ + Remove the books specified by paths from the device. The metadata + cache on the device should also be updated. + """ + for path in paths: + self.del_file(path, end_session=False) + fix_ids(booklists[0], booklists[1]) + self.upload_book_list(booklists[0], end_session=False) + if len(booklists[1]): + self.upload_book_list(booklists[1], end_session=False) + @safe def add_book(self, infile, name, info, booklists, oncard=False, \ sync_booklists=False, end_session=True):