remove old source tree

This commit is contained in:
Kovid Goyal 2007-01-05 06:27:55 +00:00
parent 993e0c2082
commit 439860923c
31 changed files with 0 additions and 12121 deletions

View File

@ -1,42 +0,0 @@
## 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.
"""
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')
There is also a script L{prs500} that provides a command-line interface to
libprs500. 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
in the module L{prstypes}. The communication logic
is defined in the module L{communicate}.
This package requires U{PyUSB<http://pyusb.berlios.de/>}.
In order to use it as a non-root user on Linux, you should have
the following rule in C{/etc/udev/rules.d/90-local.rules} ::
BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c",
MODE="660", GROUP="plugdev"
You may have to adjust the GROUP and the location of the rules file to
suit your distribution.
"""
__version__ = "0.3.0b2"
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -1,211 +0,0 @@
## 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.
"""
This module contains the logic for dealing with XML book lists found
in the reader cache.
"""
from xml.dom.ext import PrettyPrint
import xml.dom.minidom as dom
from base64 import b64decode as decode
from base64 import b64encode as encode
import time
MIME_MAP = { \
"lrf":"application/x-sony-bbeb", \
"rtf":"application/rtf", \
"pdf":"application/pdf", \
"txt":"text/plain" \
}
class book_metadata_field(object):
""" Represents metadata stored as an attribute """
def __init__(self, attr, formatter=None, setter=None):
self.attr = attr
self.formatter = formatter
self.setter = setter
def __get__(self, obj, typ=None):
""" Return a string. String may be empty if self.attr is absent """
return self.formatter(obj.elem.getAttribute(self.attr)) if \
self.formatter else obj.elem.getAttribute(self.attr).strip()
def __set__(self, obj, val):
""" Set the attribute """
val = self.setter(val) if self.setter else val
obj.elem.setAttribute(self.attr, str(val))
class Book(object):
""" Provides a view onto the XML element that represents a book """
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)
# When setting this attribute you must use an epoch
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)))
@apply
def thumbnail():
doc = \
"""
The thumbnail. Should be a height 68 image.
Setting is not supported.
"""
def fget(self):
th = self.elem.getElementsByTagName(self.prefix + "thumbnail")
if len(th):
for n in th[0].childNodes:
if n.nodeType == n.ELEMENT_NODE:
th = n
break
rc = ""
for node in th.childNodes:
if node.nodeType == node.TEXT_NODE:
rc += node.data
return decode(rc)
return property(fget=fget, doc=doc)
@apply
def path():
doc = """ Absolute path to book on device. Setting not supported. """
def fget(self):
return self.root + self.rpath
return property(fget=fget, doc=doc)
def __init__(self, node, prefix="xs1:", root="/Data/media/"):
self.elem = node
self.prefix = prefix
self.root = root
def __repr__(self):
return self.title + " by " + self.author+ " at " + self.path
def __str__(self):
return self.__repr__()
def fix_ids(media, cache):
"""
Update ids in media, cache to be consistent with their
current structure
"""
cid = 0
for child in media.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and \
child.hasAttribute("id"):
child.setAttribute("id", str(cid))
cid += 1
mmaxid = cid - 1
cid = mmaxid + 2
if len(cache):
for child in cache.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and \
child.hasAttribute("sourceid"):
child.setAttribute("sourceid", str(mmaxid+1))
child.setAttribute("id", str(cid))
cid += 1
media.document.documentElement.setAttribute("nextID", str(cid))
class BookList(list):
"""
A list of L{Book}s. Created from an XML file. Can write list
to an XML file.
"""
__getslice__ = None
__setslice__ = None
def __init__(self, prefix="xs1:", root="/Data/media/", sfile=None):
list.__init__(self)
if sfile:
self.prefix = prefix
self.proot = root
sfile.seek(0)
self.document = dom.parse(sfile)
# The root element containing all records
self.root = self.document.documentElement
if prefix == "xs1:":
self.root = self.root.getElementsByTagName("records")[0]
for book in self.document.getElementsByTagName(self.prefix + "text"):
self.append(Book(book, root=root, prefix=prefix))
def max_id(self):
""" Highest id in underlying XML file """
cid = -1
for child in self.root.childNodes:
if child.nodeType == child.ELEMENT_NODE and \
child.hasAttribute("id"):
c = int(child.getAttribute("id"))
if c > cid:
cid = c
return cid
def has_id(self, cid):
"""
Check if a book with id C{ == cid} exists already.
This *does not* check if id exists in the underlying XML file
"""
ans = False
for book in self:
if book.id == cid:
ans = True
break
return ans
def delete_book(self, cid):
""" Remove DOM node corresponding to book with C{id == cid}."""
node = None
for book in self:
if book.id == cid:
node = book
self.remove(book)
break
node.elem.parentNode.removeChild(node.elem)
node.elem.unlink()
def add_book(self, info, name, size, ctime):
""" Add a node into DOM tree representing a book """
node = self.document.createElement(self.prefix + "text")
mime = MIME_MAP[name[name.rfind(".")+1:]]
cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = { "title":info["title"], "author":info["authors"], \
"page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size)}
for attr in attrs.keys():
node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr])
w, h, data = info["cover"]
if data:
th = self.document.createElement(self.prefix + "thumbnail")
th.setAttribute("width", str(w))
th.setAttribute("height", str(h))
jpeg = self.document.createElement(self.prefix + "jpeg")
jpeg.appendChild(self.document.createTextNode(encode(data)))
th.appendChild(jpeg)
node.appendChild(th)
self.root.appendChild(node)
book = Book(node, root=self.proot, prefix=self.prefix)
book.datetime = ctime
self.append(book)
def write(self, stream):
""" Write XML representation of DOM tree to C{stream} """
PrettyPrint(self.document, stream)

View File

@ -1,21 +0,0 @@
## 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.
"""
Provides a command-line interface to the SONY Reader PRS-500.
For usage information run the script.
"""
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -1,304 +0,0 @@
## 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.
"""
Provides a command-line and optional graphical interface to the SONY Reader PRS-500.
For usage information run the script.
"""
import StringIO, sys, time, os
from optparse import OptionParser
from libprs500 import __version__ as VERSION
from libprs500.communicate import PRS500Device
from terminfo import TerminalController
from libprs500.errors import ArgumentError, DeviceError
MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output
def human_readable(size):
""" Convert a size in bytes into a human readle form """
if size < 1024: divisor, suffix = 1, ""
elif size < 1024*1024: divisor, suffix = 1024., "K"
elif size < 1024*1024*1024: divisor, suffix = 1024*1024, "M"
elif size < 1024*1024*1024*1024: divisor, suffix = 1024*1024, "G"
size = str(size/divisor)
if size.find(".") > -1: size = size[:size.find(".")+2]
return size + suffix
class FileFormatter(object):
def __init__(self, file, term):
self.term = term
self.is_dir = file.is_dir
self.is_readonly = file.is_readonly
self.size = file.size
self.ctime = file.ctime
self.wtime = file.wtime
self.name = file.name
self.path = file.path
@apply
def mode_string():
doc=""" The mode string for this file. There are only two modes read-only and read-write """
def fget(self):
mode, x = "-", "-"
if self.is_dir: mode, x = "d", "x"
if self.is_readonly: mode += "r-"+x+"r-"+x+"r-"+x
else: mode += "rw"+x+"rw"+x+"rw"+x
return mode
return property(**locals())
@apply
def name_in_color():
doc=""" The name in ANSI text. Directories are blue, ebooks are green """
def fget(self):
cname = self.name
blue, green, normal = "", "", ""
if self.term: blue, green, normal = self.term.BLUE, self.term.GREEN, self.term.NORMAL
if self.is_dir: cname = blue + self.name + normal
else:
ext = self.name[self.name.rfind("."):]
if ext in (".pdf", ".rtf", ".lrf", ".lrx", ".txt"): cname = green + self.name + normal
return cname
return property(**locals())
@apply
def human_readable_size():
doc=""" File size in human readable form """
def fget(self):
human_readable(self.size)
return property(**locals())
@apply
def modification_time():
doc=""" Last modified time in the Linux ls -l format """
def fget(self):
return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.wtime))
return property(**locals())
@apply
def creation_time():
doc=""" Last modified time in the Linux ls -l format """
def fget(self):
return time.strftime("%Y-%m-%d %H:%M", time.localtime(self.ctime))
return property(**locals())
def info(dev):
info = dev.get_device_information()
print "Device name: ", info[0]
print "Device version: ", info[1]
print "Software version:", info[2]
print "Mime type: ", info[3]
def ls(dev, path, term, recurse=False, color=False, human_readable_size=False, ll=False, cols=0):
def col_split(l, cols): # split list l into columns
rows = len(l) / cols
if len(l) % cols:
rows += 1
m = []
for i in range(rows):
m.append(l[i::rows])
return m
def row_widths(table): # Calculate widths for each column in the row-wise table
tcols = len(table[0])
rowwidths = [ 0 for i in range(tcols) ]
for row in table:
c = 0
for item in row:
rowwidths[c] = len(item) if len(item) > rowwidths[c] else rowwidths[c]
c += 1
return rowwidths
output = StringIO.StringIO()
if path.endswith("/"): path = path[:-1]
dirs = dev.list(path, recurse)
for dir in dirs:
if recurse: print >>output, dir[0] + ":"
lsoutput, lscoloutput = [], []
files = dir[1]
maxlen = 0
if ll: # Calculate column width for size column
for file in files:
size = len(str(file.size))
if human_readable_size:
file = FileFormatter(file, term)
size = len(file.human_readable_size)
if size > maxlen: maxlen = size
for file in files:
file = FileFormatter(file, term)
name = file.name
lsoutput.append(name)
if color: name = file.name_in_color
lscoloutput.append(name)
if ll:
size = str(file.size)
if human_readable_size: size = file.human_readable_size
print >>output, file.mode_string, ("%"+str(maxlen)+"s")%size, file.modification_time, name
if not ll and len(lsoutput) > 0:
trytable = []
for colwidth in range(MINIMUM_COL_WIDTH, cols):
trycols = int(cols/colwidth)
trytable = col_split(lsoutput, trycols)
works = True
for row in trytable:
row_break = False
for item in row:
if len(item) > colwidth - 1:
works, row_break = False, True
break
if row_break: break
if works: break
rowwidths = row_widths(trytable)
trytablecol = col_split(lscoloutput, len(trytable[0]))
for r in range(len(trytable)):
for c in range(len(trytable[r])):
padding = rowwidths[c] - len(trytable[r][c])
print >>output, trytablecol[r][c], "".ljust(padding),
print >>output
print >>output
listing = output.getvalue().rstrip()+ "\n"
output.close()
return listing
def main():
term = TerminalController()
cols = term.COLS
parser = OptionParser(usage="usage: %prog [options] command args\n\ncommand is one of: info, books, df, ls, cp, mkdir, touch, cat, rm\n\n"+
"For help on a particular command: %prog command", version="libprs500 version: " + VERSION)
parser.add_option("--log-packets", help="print out packet stream to stdout. "+\
"The numbers in the left column are byte offsets that allow the packet size to be read off easily.",
dest="log_packets", action="store_true", default=False)
parser.remove_option("-h")
parser.disable_interspersed_args() # Allow unrecognized options
options, args = parser.parse_args()
if len(args) < 1:
parser.print_help()
return 1
command = args[0]
args = args[1:]
dev = PRS500Device(log_packets=options.log_packets)
try:
if command == "df":
total = dev.total_space(end_session=False)
free = dev.free_space()
where = ("Memory", "Stick", "Card")
print "Filesystem\tSize \tUsed \tAvail \tUse%"
for i in range(3):
print "%-10s\t%s\t%s\t%s\t%s"%(where[i], human_readable(total[i]), human_readable(total[i]-free[i]), human_readable(free[i]),\
str(0 if total[i]==0 else int(100*(total[i]-free[i])/(total[i]*1.)))+"%")
elif command == "books":
print "Books in main memory:"
for book in dev.books(): print book
print "\nBooks on storage card:"
for book in dev.books(oncard=True): print book
elif command == "mkdir":
parser = OptionParser(usage="usage: %prog mkdir [options] path\nCreate a directory on the device\n\npath must begin with /,a:/ or b:/")
if len(args) != 1:
parser.print_help()
sys.exit(1)
dev.mkdir(args[0])
elif command == "ls":
parser = OptionParser(usage="usage: %prog ls [options] path\nList files on the device\n\npath must begin with /,a:/ or b:/")
parser.add_option("--color", help="show ls output in color", dest="color", action="store_true", default=False)
parser.add_option("-l", help="In addition to the name of each file, print the file type, permissions, and timestamp (the modification time, in the local timezone). Times are local.", dest="ll", action="store_true", default=False)
parser.add_option("-R", help="Recursively list subdirectories encountered. /dev and /proc are omitted", dest="recurse", action="store_true", default=False)
parser.remove_option("-h")
parser.add_option("-h", "--human-readable", help="show sizes in human readable format", dest="hrs", action="store_true", default=False)
options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help()
return 1
print ls(dev, args[0], term, color=options.color, recurse=options.recurse, ll=options.ll, human_readable_size=options.hrs, cols=cols),
elif command == "info":
info(dev)
elif command == "cp":
usage="usage: %prog cp [options] source destination\nCopy files to/from the device\n\n"+\
"One of source or destination must be a path on the device. \n\nDevice paths have the form\n"+\
"prs500:mountpoint/my/path\n"+\
"where mountpoint is one of /, a: or b:\n\n"+\
"source must point to a file for which you have read permissions\n"+\
"destination must point to a file or directory for which you have write permissions"
parser = OptionParser(usage=usage)
options, args = parser.parse_args(args)
if len(args) != 2:
parser.print_help()
return 1
if args[0].startswith("prs500:"):
outfile = args[1]
path = args[0][7:]
if path.endswith("/"): path = path[:-1]
if os.path.isdir(outfile):
outfile = os.path.join(outfile, path[path.rfind("/")+1:])
try:
outfile = open(outfile, "w")
except IOError, e:
print >> sys.stderr, e
parser.print_help()
return 1
dev.get_file(path, outfile)
outfile.close()
elif args[1].startswith("prs500:"):
try:
infile = open(args[0], "r")
except IOError, e:
print >> sys.stderr, e
parser.print_help()
return 1
dev.put_file(infile, args[1][7:])
infile.close()
else:
parser.print_help()
return 1
elif command == "cat":
outfile = sys.stdout
parser = OptionParser(usage="usage: %prog cat path\nShow file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/")
options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help()
return 1
if args[0].endswith("/"): path = args[0][:-1]
else: path = args[0]
outfile = sys.stdout
dev.get_file(path, outfile)
elif command == "rm":
parser = OptionParser(usage="usage: %prog rm path\nDelete files from the device\n\npath should point to a file or empty directory on the device "+\
"and must begin with /,a:/ or b:/\n\n"+\
"rm will DELETE the file. Be very CAREFUL")
options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help()
return 1
dev.rm(args[0])
elif command == "touch":
parser = OptionParser(usage="usage: %prog touch path\nCreate an empty file on the device\n\npath should point to a file on the device and must begin with /,a:/ or b:/\n\n"+
"Unfortunately, I cant figure out how to update file times on the device, so if path already exists, touch does nothing" )
options, args = parser.parse_args(args)
if len(args) != 1:
parser.print_help()
return 1
dev.touch(args[0])
else:
parser.print_help()
if dev.handle: dev.close()
return 1
except (ArgumentError, DeviceError), e:
print >>sys.stderr, e
return 1
return 0

View File

@ -1,208 +0,0 @@
## 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.
import sys, re
""" Get information about the terminal we are running in """
class TerminalController:
"""
A class that can be used to portably generate formatted output to
a terminal.
`TerminalController` defines a set of instance variables whose
values are initialized to the control sequence necessary to
perform a given action. These can be simply included in normal
output to the terminal:
>>> term = TerminalController()
>>> print 'This is '+term.GREEN+'green'+term.NORMAL
Alternatively, the `render()` method can used, which replaces
'${action}' with the string required to perform 'action':
>>> term = TerminalController()
>>> print term.render('This is ${GREEN}green${NORMAL}')
If the terminal doesn't support a given action, then the value of
the corresponding instance variable will be set to ''. As a
result, the above code will still work on terminals that do not
support color, except that their output will not be colored.
Also, this means that you can test whether the terminal supports a
given action by simply testing the truth value of the
corresponding instance variable:
>>> term = TerminalController()
>>> if term.CLEAR_SCREEN:
... print 'This terminal supports clearning the screen.'
Finally, if the width and height of the terminal are known, then
they will be stored in the `COLS` and `LINES` attributes.
"""
# Cursor movement:
BOL = '' #: Move the cursor to the beginning of the line
UP = '' #: Move the cursor up one line
DOWN = '' #: Move the cursor down one line
LEFT = '' #: Move the cursor left one char
RIGHT = '' #: Move the cursor right one char
# Deletion:
CLEAR_SCREEN = '' #: Clear the screen and move to home position
CLEAR_EOL = '' #: Clear to the end of the line.
CLEAR_BOL = '' #: Clear to the beginning of the line.
CLEAR_EOS = '' #: Clear to the end of the screen
# Output modes:
BOLD = '' #: Turn on bold mode
BLINK = '' #: Turn on blink mode
DIM = '' #: Turn on half-bright mode
REVERSE = '' #: Turn on reverse-video mode
NORMAL = '' #: Turn off all modes
# Cursor display:
HIDE_CURSOR = '' #: Make the cursor invisible
SHOW_CURSOR = '' #: Make the cursor visible
# Terminal size:
COLS = None #: Width of the terminal (None for unknown)
LINES = None #: Height of the terminal (None for unknown)
# Foreground colors:
BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
# Background colors:
BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = ''
BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = ''
_STRING_CAPABILITIES = """
BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
CLEAR_SCREEN=clear CLEAR_EOL=el CLEAR_BOL=el1 CLEAR_EOS=ed BOLD=bold
BLINK=blink DIM=dim REVERSE=rev UNDERLINE=smul NORMAL=sgr0
HIDE_CURSOR=cinvis SHOW_CURSOR=cnorm""".split()
_COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
_ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
def __init__(self, term_stream=sys.stdout):
"""
Create a `TerminalController` and initialize its attributes
with appropriate values for the current terminal.
`term_stream` is the stream that will be used for terminal
output; if this stream is not a tty, then the terminal is
assumed to be a dumb terminal (i.e., have no capabilities).
"""
# Curses isn't available on all platforms
try: import curses
except: return
# If the stream isn't a tty, then assume it has no capabilities.
if not term_stream.isatty(): return
# Check the terminal type. If we fail, then assume that the
# terminal has no capabilities.
try: curses.setupterm()
except: return
# Look up numeric capabilities.
self.COLS = curses.tigetnum('cols')
self.LINES = curses.tigetnum('lines')
# Look up string capabilities.
for capability in self._STRING_CAPABILITIES:
(attrib, cap_name) = capability.split('=')
setattr(self, attrib, self._tigetstr(cap_name) or '')
# Colors
set_fg = self._tigetstr('setf')
if set_fg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, color, curses.tparm(set_fg, i) or '')
set_fg_ansi = self._tigetstr('setaf')
if set_fg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
set_bg = self._tigetstr('setb')
if set_bg:
for i,color in zip(range(len(self._COLORS)), self._COLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg, i) or '')
set_bg_ansi = self._tigetstr('setab')
if set_bg_ansi:
for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
setattr(self, 'BG_'+color, curses.tparm(set_bg_ansi, i) or '')
def _tigetstr(self, cap_name):
# String capabilities can include "delays" of the form "$<2>".
# For any modern terminal, we should be able to just ignore
# these, so strip them out.
import curses
cap = curses.tigetstr(cap_name) or ''
return re.sub(r'\$<\d+>[/*]?', '', cap)
def render(self, template):
"""
Replace each $-substitutions in the given template string with
the corresponding terminal control string (if it's defined) or
'' (if it's not).
"""
return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
def _render_sub(self, match):
s = match.group()
if s == '$$': return s
else: return getattr(self, s[2:-1])
#######################################################################
# Example use case: progress bar
#######################################################################
class ProgressBar:
"""
A 3-line progress bar, which looks like::
Header
20% [===========----------------------------------]
progress message
The progress bar is colored, if the terminal supports color
output; and adjusts to the width of the terminal.
"""
BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}\n'
HEADER = '${BOLD}${CYAN}%s${NORMAL}\n\n'
def __init__(self, term, header):
self.term = term
if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL):
raise ValueError("Terminal isn't capable enough -- you "
"should use a simpler progress dispaly.")
self.width = self.term.COLS or 75
self.bar = term.render(self.BAR)
self.header = self.term.render(self.HEADER % header.center(self.width))
self.cleared = 1 #: true if we haven't drawn the bar yet.
self.update(0, '')
def update(self, percent, message):
if self.cleared:
sys.stdout.write(self.header)
self.cleared = 0
n = int((self.width-10)*percent)
sys.stdout.write(
self.term.BOL + self.term.UP + self.term.CLEAR_EOL +
(self.bar % (100*percent, '='*n, '-'*(self.width-10-n))) +
self.term.CLEAR_EOL + message.center(self.width))
def clear(self):
if not self.cleared:
sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL +
self.term.UP + self.term.CLEAR_EOL)
self.cleared = 1

View File

@ -1,847 +0,0 @@
## 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.
### End point description for PRS-500 procductId=667
### Endpoint Descriptor:
### bLength 7
### bDescriptorType 5
### bEndpointAddress 0x81 EP 1 IN
### bmAttributes 2
### Transfer Type Bulk
### Synch Type None
### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0
### Endpoint Descriptor:
### bLength 7
### bDescriptorType 5
### bEndpointAddress 0x02 EP 2 OUT
### bmAttributes 2
### Transfer Type Bulk
### Synch Type None
### Usage Type Data
### wMaxPacketSize 0x0040 1x 64 bytes
### bInterval 0
###
###
### Endpoint 0x81 is device->host and endpoint 0x02 is host->device.
### You can establish Stream pipes to/from these endpoints for Bulk transfers.
### Has two configurations 1 is the USB charging config 2 is the self-powered
### config. I think config management is automatic. Endpoints are the same
"""
Contains the logic for communication with the device (a SONY PRS-500).
The public interface of class L{PRS500Device} defines the
methods for performing various tasks.
"""
import usb
import sys
import os
import time
from tempfile import TemporaryFile
from array import array
from libprs500.prstypes import *
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 specific 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):
"""
Wrapper that allows easy access to all information about files/directories
"""
def __init__(self, _file):
self.is_dir = _file[1].is_dir #: True if self is a directory
self.is_readonly = _file[1].is_readonly #: True if self is readonly
self.size = _file[1].file_size #: Size in bytes of self
self.ctime = _file[1].ctime #: Creation time of self as a epoch
self.wtime = _file[1].wtime #: Creation time of self as an epoch
path = _file[0]
if path.endswith("/"):
path = path[:-1]
self.path = path #: Path to self
self.name = path[path.rfind("/")+1:].rstrip() #: Name of self
def __repr__(self):
""" Return path to self """
return "File:" + self.path
def __str__(self):
return self.name
class DeviceDescriptor:
"""
Describes a USB device.
A description is composed of the Vendor Id, Product Id and Interface Id.
See the U{USB spec<http://www.usb.org/developers/docs/usb_20_05122006.zip>}
"""
def __init__(self, vendor_id, product_id, interface_id) :
self.vendor_id = vendor_id
self.product_id = product_id
self.interface_id = interface_id
def get_device(self) :
"""
Return the device corresponding to the device descriptor if it is
available on a USB bus. Otherwise, return None. Note that the
returned device has yet to be claimed or opened.
"""
buses = usb.busses()
for bus in buses :
for device in bus.devices :
if device.idVendor == self.vendor_id :
if device.idProduct == self.product_id :
return device
return None
class PRS500Device(Device):
"""
Contains the logic for performing various tasks on the reader.
The implemented tasks are:
0. Getting information about the device
1. Getting a file from the device
2. Listing of directories. See the C{list} method.
"""
SONY_VENDOR_ID = 0x054c #: SONY Vendor Id
PRS500_PRODUCT_ID = 0x029b #: Product Id for the PRS-500
PRS500_INTERFACE_ID = 0 #: The interface we use to talk to the device
PRS500_BULK_IN_EP = 0x81 #: Endpoint for Bulk reads
PRS500_BULK_OUT_EP = 0x02 #: Endpoint for Bulk writes
# Location of media.xml file on device
MEDIA_XML = "/Data/database/cache/media.xml"
# Location of cache.xml on storage card in device
CACHE_XML = "/Sony Reader/database/cache.xml"
# Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"]
# Height for thumbnails of books/images on the device
THUMBNAIL_HEIGHT = 68
device_descriptor = DeviceDescriptor(SONY_VENDOR_ID, \
PRS500_PRODUCT_ID, PRS500_INTERFACE_ID)
@classmethod
def signature(cls):
""" Return a two element tuple (vendor id, product id) """
return (cls.SONY_VENDOR_ID, cls.PRS500_PRODUCT_ID )
def safe(func):
"""
Decorator that wraps a call to C{func} to ensure that
exceptions are handled correctly. It also calls L{open} to claim
the interface and initialize the Reader if needed.
As a convenience, C{safe} automatically sends the a
L{EndSession} after calling func, unless func has
a keyword argument named C{end_session} set to C{False}.
An L{ArgumentError} will cause the L{EndSession} command to
be sent to the device, unless end_session is set to C{False}.
An L{usb.USBError} will cause the library to release control of the
USB interface via a call to L{close}.
"""
def run_session(*args, **kwargs):
dev = args[0]
res = None
try:
if not dev.handle:
dev.open()
res = func(*args, **kwargs)
except ArgumentError:
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(EndSession())
raise
except usb.USBError, err:
if "No such device" in str(err):
raise DeviceError()
elif "Connection timed out" in str(err):
dev.close()
raise TimeoutError(func.__name__)
elif "Protocol error" in str(err):
dev.close()
raise ProtocolError("There was an unknown error in the"+\
" protocol. Contact " + AUTHOR)
dev.close()
raise
if not kwargs.has_key("end_session") or kwargs["end_session"]:
dev.send_validated_command(EndSession())
return res
return run_session
def __init__(self, log_packets=False, report_progress=None) :
"""
@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
"""
Device.__init__(self)
# The actual device (PyUSB object)
self.device = self.device_descriptor.get_device()
# Handle that is used to communicate with device. Setup in L{open}
self.handle = None
self.log_packets = log_packets
self.report_progress = report_progress
def reconnect(self):
""" Only recreates the device node and deleted the connection handle """
self.device = self.device_descriptor.get_device()
self.handle = None
@classmethod
def is_connected(cls):
"""
This method checks to see whether the device is physically connected.
It does not return any information about the validity of the
software connection. You may need to call L{reconnect} if you keep
getting L{DeviceError}.
"""
return cls.device_descriptor.get_device() != None
def open(self) :
"""
Claim an interface on the device for communication.
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 = self.device_descriptor.get_device()
if not self.device:
raise DeviceError()
try:
self.handle = self.device.open()
self.handle.claimInterface(self.device_descriptor.interface_id)
except usb.USBError, err:
print >> sys.stderr, err
raise DeviceBusy()
# Large timeout as device may still be initializing
res = self.send_validated_command(GetUSBProtocolVersion(), timeout=20000)
if res.code != 0:
raise ProtocolError("Unable to get USB Protocol version.")
version = self._bulk_read(24, data_type=USBProtocolVersion)[0].version
if version not in KNOWN_USB_PROTOCOL_VERSIONS:
print >> sys.stderr, "WARNING: Usb protocol version " + \
hex(version) + " is unknown"
res = self.send_validated_command(SetBulkSize(size=0x028000))
if res.code != 0:
raise ProtocolError("Unable to set bulk size.")
self.send_validated_command(UnlockDevice(key=0x312d))
if res.code != 0:
raise ProtocolError("Unlocking of device not implemented. Remove locking and retry.")
res = self.send_validated_command(SetTime())
if res.code != 0:
raise ProtocolError("Could not set time on device")
def close(self):
""" Release device interface """
try:
self.handle.reset()
self.handle.releaseInterface()
except Exception, err:
print >> sys.stderr, err
self.handle, self.device = None, None
def _send_command(self, command, response_type=Response, timeout=1000):
"""
Send L{command<Command>} to device and return its L{response<Response>}.
@param command: an object of type Command or one of its derived classes
@param response_type: an object of type 'type'. The return packet
from the device is returned as an object of type response_type.
@param timeout: The time to wait for a response from the
device, in milliseconds. If there is no response, a L{usb.USBError} is raised.
"""
if self.log_packets:
self.log_packet(command, "Command")
bytes_sent = self.handle.controlMsg(0x40, 0x80, command)
if bytes_sent != len(command):
raise ControlError(desc="Could not send control request to device\n"\
+ str(command))
response = response_type(self.handle.controlMsg(0xc0, 0x81, \
Response.SIZE, timeout=timeout))
if self.log_packets:
self.log_packet(response, "Response")
return response
def send_validated_command(self, command, cnumber=None, \
response_type=Response, timeout=1000):
"""
Wrapper around L{_send_command} that checks if the
C{Response.rnumber == cnumber or
command.number if cnumber==None}. Also check that
C{Response.type == Command.type}.
"""
if cnumber == None:
cnumber = command.number
res = self._send_command(command, response_type=response_type, \
timeout=timeout)
self.validate_response(res, _type=command.type, number=cnumber)
return res
def _bulk_write(self, data, packet_size=0x1000):
"""
Send data to device via a bulk transfer.
@type data: Any listable type supporting __getslice__
@param packet_size: Size of packets to be sent to device.
C{data} is broken up into packets to be sent to device.
"""
def bulk_write_packet(packet):
self.handle.bulkWrite(PRS500Device.PRS500_BULK_OUT_EP, packet)
if self.log_packets:
self.log_packet(Answer(packet), "Answer h->d")
bytes_left = len(data)
if bytes_left + 16 <= packet_size:
packet_size = bytes_left +16
first_packet = Answer(bytes_left+16)
first_packet[16:] = data
first_packet.length = len(data)
else:
first_packet = Answer(packet_size)
first_packet[16:] = data[0:packet_size-16]
first_packet.length = packet_size-16
first_packet.number = 0x10005
bulk_write_packet(first_packet)
pos = first_packet.length
bytes_left -= first_packet.length
while bytes_left > 0:
endpos = pos + packet_size if pos + packet_size <= len(data) \
else len(data)
bulk_write_packet(data[pos:endpos])
bytes_left -= endpos - pos
pos = endpos
res = Response(self.handle.controlMsg(0xc0, 0x81, Response.SIZE, \
timeout=5000))
if self.log_packets:
self.log_packet(res, "Response")
if res.rnumber != 0x10005 or res.code != 0:
raise ProtocolError("Sending via Bulk Transfer failed with response:\n"\
+str(res))
if res.data_size != len(data):
raise ProtocolError("Unable to transfer all data to device. "+\
"Response packet:\n"\
+str(res))
def _bulk_read(self, bytes, command_number=0x00, packet_size=4096, \
data_type=Answer):
"""
Read in C{bytes} bytes via a bulk transfer in
packets of size S{<=} C{packet_size}
@param data_type: an object of type type.
The data packet is returned as an object of type C{data_type}.
@return: A list of packets read from the device.
Each packet is of type data_type
"""
def bulk_read_packet(data_type=Answer, size=0x1000):
data = data_type(self.handle.bulkRead(PRS500Device.PRS500_BULK_IN_EP, \
size))
if self.log_packets:
self.log_packet(data, "Answer d->h")
return data
bytes_left = bytes
packets = []
while bytes_left > 0:
if packet_size > bytes_left:
packet_size = bytes_left
packet = bulk_read_packet(data_type=data_type, size=packet_size)
bytes_left -= len(packet)
packets.append(packet)
self.send_validated_command(\
AcknowledgeBulkRead(packets[0].number), \
cnumber=command_number)
return packets
@safe
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)
"""
size = self.send_validated_command(DeviceInfoQuery()).data[2] + 16
data = self._bulk_read(size, command_number=\
DeviceInfoQuery.NUMBER, data_type=DeviceInfo)[0]
return (data.device_name, data.device_version, \
data.software_version, data.mime_type)
@safe
def path_properties(self, path, end_session=True):
"""
Send command asking device for properties of C{path}.
Return L{FileProperties}.
"""
res = self.send_validated_command(PathQuery(path), \
response_type=ListResponse)
data = self._bulk_read(0x28, data_type=FileProperties, \
command_number=PathQuery.NUMBER)[0]
if path.endswith("/"):
path = path[:-1]
if res.path_not_found :
raise PathError(path + " does not exist on device")
if res.is_invalid:
raise PathError(path + " is not a valid path")
if res.is_unmounted:
raise PathError(path + " is not mounted")
if res.code not in (0, PathResponseCodes.IS_FILE):
raise PathError(path + " has an unknown error. Code: " + \
hex(res.code))
return data
@safe
def get_file(self, path, outfile, end_session=True):
"""
Read the file at path on the device and write it to outfile.
The data is fetched in chunks of size S{<=} 32K. Each chunk is
made of packets of size S{<=} 4K. See L{FileOpen},
L{FileRead} and L{FileClose} for details on the command packets used.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call
"""
if path.endswith("/"):
path = path[:-1] # We only copy files
_file = self.path_properties(path, end_session=False)
if _file.is_dir:
raise PathError("Cannot read as " + path + " is a directory")
bytes = _file.file_size
res = self.send_validated_command(FileOpen(path))
if res.code != 0:
raise PathError("Unable to open " + path + \
" for reading. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id
bytes_left, chunk_size, pos = bytes, 0x8000, 0
while bytes_left > 0:
if chunk_size > bytes_left:
chunk_size = bytes_left
res = self.send_validated_command(FileIO(_id, pos, chunk_size))
if res.code != 0:
self.send_validated_command(FileClose(id))
raise ProtocolError("Error while reading from " + path + \
". Response code: " + hex(res.code))
packets = self._bulk_read(chunk_size+16, \
command_number=FileIO.RNUMBER, packet_size=4096)
try:
# The first 16 bytes are meta information on the packet stream
array('B', packets[0][16:]).tofile(outfile)
for i in range(1, len(packets)):
array('B', packets[i]).tofile(outfile)
except IOError, err:
self.send_validated_command(FileClose(_id))
raise ArgumentError("File get operation failed. " + \
"Could not write to local location: " + str(err))
bytes_left -= chunk_size
pos += chunk_size
if self.report_progress:
self.report_progress(int(100*((1.*pos)/bytes)))
self.send_validated_command(FileClose(_id))
# Not going to check response code to see if close was successful
# as there's not much we can do if it wasnt
@safe
def list(self, path, recurse=False, end_session=True):
"""
Return a listing of path. See the code for details. See L{DirOpen},
L{DirRead} and L{DirClose} for details on the command packets used.
@type path: string
@param path: The path to list
@type recurse: boolean
@param recurse: If true do a recursive listing
@return: A list of tuples. The first element of each tuple is a path.
The second element is a list of L{Files<File>}.
The path is the path we are listing, the C{Files} are the
files/directories in that path. If it is a recursive list, then the first
element will be (C{path}, children), the next will be
(child, its children) and so on. If it is not recursive the length of the
outermost list will be 1.
"""
def _list(path):
""" Do a non recursive listsing of path """
if not path.endswith("/"):
path += "/" # Initially assume path is a directory
files = []
candidate = self.path_properties(path, end_session=False)
if not candidate.is_dir:
path = path[:-1]
data = self.path_properties(path, end_session=False)
files = [ File((path, data)) ]
else:
# Get query ID used to ask for next element in list
res = self.send_validated_command(DirOpen(path))
if res.code != 0:
raise PathError("Unable to open directory " + path + \
" for reading. Response code: " + hex(res.code))
_id = self._bulk_read(0x14, data_type=IdAnswer, \
command_number=DirOpen.NUMBER)[0].id
# Create command asking for next element in list
next = DirRead(_id)
items = []
while True:
res = self.send_validated_command(next, response_type=ListResponse)
size = res.data_size + 16
data = self._bulk_read(size, data_type=ListAnswer, \
command_number=DirRead.NUMBER)[0]
# path_not_found seems to happen if the usb server
# doesn't have the permissions to access the directory
if res.is_eol or res.path_not_found:
break
elif res.code != 0:
raise ProtocolError("Unknown error occured while "+\
"reading contents of directory " + path + \
". Response code: " + hex(res.code))
items.append(data.name)
self.send_validated_command(DirClose(_id))
# Ignore res.code as we cant do anything if close fails
for item in items:
ipath = path + item
data = self.path_properties(ipath, end_session=False)
files.append( File( (ipath, data) ) )
files.sort()
return files
files = _list(path)
dirs = [(path, files)]
for _file in files:
if recurse and _file.is_dir and not _file.path.startswith(("/dev","/proc")):
dirs[len(dirs):] = self.list(_file.path, recurse=True, end_session=False)
return dirs
@safe
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)
"""
data = []
for path in ("/Data/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
res = self.send_validated_command(TotalSpaceQuery(path), \
timeout=5000)
buffer_size = 16 + res.data[2]
pkt = self._bulk_read(buffer_size, data_type=TotalSpaceAnswer, \
command_number=TotalSpaceQuery.NUMBER)[0]
data.append( pkt.total )
return data
@safe
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)
"""
data = []
for path in ("/", "a:/", "b:/"):
# Timeout needs to be increased as it takes time to read card
self.send_validated_command(FreeSpaceQuery(path), \
timeout=5000)
pkt = self._bulk_read(FreeSpaceAnswer.SIZE, \
data_type=FreeSpaceAnswer, \
command_number=FreeSpaceQuery.NUMBER)[0]
data.append( pkt.free )
return data
def _exists(self, path):
""" Return (True, FileProperties) if path exists or (False, None) otherwise """
dest = None
try:
dest = self.path_properties(path, end_session=False)
except PathError, err:
if "does not exist" in str(err) or "not mounted" in str(err):
return (False, None)
else: raise
return (True, dest)
@safe
def touch(self, path, end_session=True):
"""
Create a file at path
@todo: Update file modification time if it exists.
Opening the file in write mode and then closing it doesn't work.
"""
if path.endswith("/") and len(path) > 1:
path = path[:-1]
exists, _file = self._exists(path)
if exists and _file.is_dir:
raise PathError("Cannot touch directories")
if not exists:
res = self.send_validated_command(FileCreate(path))
if res.code != 0:
raise PathError("Could not create file " + path + \
". Response code: " + str(hex(res.code)))
@safe
def put_file(self, infile, path, replace_file=False, end_session=True):
"""
Put infile onto the devoce at path
@param infile: An open file object. infile must have a name attribute.
If you are using a StringIO object set its name attribute manually.
@param path: The path on the device at which to put infile.
It should point to an existing directory.
@param replace_file: If True and path points to a file that already exists, it is replaced
"""
pos = infile.tell()
infile.seek(0, 2)
bytes = infile.tell() - pos
start_pos = pos
infile.seek(pos)
exists, dest = self._exists(path)
if exists:
if dest.is_dir:
if not path.endswith("/"):
path += "/"
path += os.path.basename(infile.name)
return self.put_file(infile, path, replace_file=replace_file, end_session=False)
else:
if not replace_file:
raise PathError("Cannot write to " + \
path + " as it already exists")
_file = self.path_properties(path, end_session=False)
if _file.file_size > bytes:
self.del_file(path, end_session=False)
self.touch(path, end_session=False)
else: self.touch(path, end_session=False)
chunk_size = 0x8000
data_left = True
res = self.send_validated_command(FileOpen(path, mode=FileOpen.WRITE))
if res.code != 0:
raise ProtocolError("Unable to open " + path + \
" for writing. Response code: " + hex(res.code))
_id = self._bulk_read(20, data_type=IdAnswer, \
command_number=FileOpen.NUMBER)[0].id
while data_left:
data = array('B')
try:
data.fromfile(infile, chunk_size)
except EOFError:
data_left = False
res = self.send_validated_command(FileIO(_id, pos, len(data), \
mode=FileIO.WNUMBER))
if res.code != 0:
raise ProtocolError("Unable to write to " + \
path + ". Response code: " + hex(res.code))
self._bulk_write(data)
pos += len(data)
if self.report_progress:
self.report_progress( int(100*(pos-start_pos)/(1.*bytes)) )
self.send_validated_command(FileClose(_id))
# Ignore res.code as cant do anything if close fails
_file = self.path_properties(path, end_session=False)
if _file.file_size != pos:
raise ProtocolError("Copying to device failed. The file " +\
"on the device is larger by " + \
str(_file.file_size - pos) + " bytes")
@safe
def del_file(self, path, end_session=True):
""" Delete C{path} from device iff path is a file """
data = self.path_properties(path, end_session=False)
if data.is_dir:
raise PathError("Cannot delete directories")
res = self.send_validated_command(FileDelete(path), \
response_type=ListResponse)
if res.code != 0:
raise ProtocolError("Unable to delete " + path + \
" with response:\n" + str(res))
@safe
def mkdir(self, path, end_session=True):
""" Make directory """
if not path.endswith("/"):
path += "/"
error_prefix = "Cannot create directory " + path
res = self.send_validated_command(DirCreate(path)).data[0]
if res == 0xffffffcc:
raise PathError(error_prefix + " as it already exists")
elif res == PathResponseCodes.NOT_FOUND:
raise PathError(error_prefix + " as " + \
path[0:path[:-1].rfind("/")] + " does not exist ")
elif res == PathResponseCodes.INVALID:
raise PathError(error_prefix + " as " + path + " is invalid")
elif res != 0:
raise PathError(error_prefix + ". Response code: " + hex(res))
@safe
def rm(self, path, end_session=True):
""" Delete path from device if it is a file or an empty directory """
dir = self.path_properties(path, end_session=False)
if not dir.is_dir:
self.del_file(path, end_session=False)
else:
if not path.endswith("/"):
path += "/"
res = self.send_validated_command(DirDelete(path))
if res.code == PathResponseCodes.HAS_CHILDREN:
raise PathError("Cannot delete directory " + path + \
" as it is not empty")
if res.code != 0:
raise ProtocolError("Failed to delete directory " + path + \
". Response code: " + hex(res.code))
@safe
def card(self, end_session=True):
""" Return path prefix to installed card or None """
card = None
if self._exists("a:/")[0]:
card = "a:"
if self._exists("b:/")[0]:
card = "b:"
return card
@safe
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: L{BookList}
"""
root = "/Data/media/"
prefix = "xs1:"
tfile = TemporaryFile()
if oncard:
prefix = ""
try:
self.get_file("a:"+self.CACHE_XML, tfile, end_session=False)
root = "a:/"
except PathError:
try:
self.get_file("b:"+self.CACHE_XML, tfile, end_session=False)
root = "b:/"
except PathError: pass
if tfile.tell() == 0:
tfile = None
else:
self.get_file(self.MEDIA_XML, tfile, end_session=False)
return BookList(prefix=prefix, root=root, sfile=tfile)
@safe
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)).
"""
infile.seek(0, 2)
size = infile.tell()
infile.seek(0)
card = self.card(end_session=False)
space = self.free_space(end_session=False)
mspace = space[0]
cspace = space[1] if space[1] >= space[2] else space[2]
if oncard and size > cspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space "+\
"on the storage card")
if not oncard and size > mspace - 1024*1024:
raise FreeSpaceError("There is insufficient free space " +\
"in main memory")
prefix = "/Data/media/"
if oncard:
prefix = card + "/"
else: name = "books/"+name
path = prefix + name
self.put_file(infile, path, end_session=False)
ctime = self.path_properties(path, end_session=False).ctime
bkl = booklists[1] if oncard else booklists[0]
bkl.add_book(info, name, size, ctime)
fix_ids(booklists[0], booklists[1])
if sync_booklists:
self.upload_book_list(booklists[0], end_session=False)
if len(booklists[1]):
self.upload_book_list(booklists[1], end_session=False)
@safe
def upload_book_list(self, booklist, end_session=True):
if not len(booklist):
raise ArgumentError("booklist is empty")
path = self.MEDIA_XML
if not booklist.prefix:
card = self.card(end_session=True)
if not card:
raise ArgumentError("Cannot upload list to card as "+\
"card is not present")
path = card + self.CACHE_XML
f = TemporaryFile()
booklist.write(f)
f.seek(0)
self.put_file(f, path, replace_file=True, end_session=False)
f.close()

View File

@ -1,71 +0,0 @@
## 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.
"""
Defines the errors that libprs500 generates.
G{classtree ProtocolError}
"""
class ProtocolError(Exception):
""" The base class for all exceptions in this package """
def __init__(self, msg):
Exception.__init__(self, msg)
class TimeoutError(ProtocolError):
""" There was a timeout during communication """
def __init__(self, func_name):
ProtocolError.__init__(self, \
"There was a timeout while communicating with the device in function: "\
+func_name)
class DeviceError(ProtocolError):
""" Raised when device is not found """
def __init__(self):
ProtocolError.__init__(self, \
"Unable to find SONY Reader. Is it connected?")
class DeviceBusy(ProtocolError):
""" Raised when device is busy """
def __init__(self):
ProtocolError.__init__(self, "Device is in use by another application")
class PacketError(ProtocolError):
""" Errors with creating/interpreting packets """
class FreeSpaceError(ProtocolError):
""" Errors caused when trying to put files onto an overcrowded device """
class ArgumentError(ProtocolError):
""" Errors caused by invalid arguments to a public interface function """
class PathError(ArgumentError):
""" When a user supplies an incorrect/invalid path """
class ControlError(ProtocolError):
""" Errors in Command/Response pairs while communicating with the device """
def __init__(self, query=None, response=None, desc=None):
self.query = query
self.response = response
ProtocolError.__init__(self, desc)
def __str__(self):
if self.query and self.response:
return "Got unexpected response:\n" + \
"query:\n"+str(self.query.query)+"\n"+\
"expected:\n"+str(self.query.response)+"\n" +\
"actual:\n"+str(self.response)
if self.desc:
return self.desc
return "Unknown control error occurred"

View File

@ -1,60 +0,0 @@
## 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.
""" The GUI to libprs500. Also has ebook library management features. """
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
APP_TITLE = "libprs500"
import pkg_resources, sys, os, re, StringIO, traceback
from PyQt4.uic.Compiler import compiler
from PyQt4 import QtCore, QtGui # Needed in globals() for import_ui
error_dialog = None
def extension(path):
return os.path.splitext(path)[1][1:].lower()
def installErrorHandler(dialog):
global error_dialog
error_dialog = dialog
error_dialog.resize(600, 400)
error_dialog.setWindowTitle(APP_TITLE + " - Error")
error_dialog.setModal(True)
def _Warning(msg, e):
print >> sys.stderr, msg
if e: traceback.print_exc(e)
def Error(msg, e):
if error_dialog:
if e:
msg += "<br>" + traceback.format_exc(e)
msg = re.sub("Traceback", "<b>Traceback</b>", msg)
msg = re.sub(r"\n", "<br>", msg)
error_dialog.showMessage(msg)
error_dialog.show()
def import_ui(name):
uifile = pkg_resources.resource_stream(__name__, name)
code_string = StringIO.StringIO()
winfo = compiler.UICompiler().compileUi(uifile, code_string)
#ui = pkg_resources.resource_filename(__name__, name)
exec code_string.getvalue()
return locals()[winfo["uiclass"]]
# Needed in globals() for import_ui
from libprs500.gui.widgets import LibraryBooksView, \
DeviceBooksView, CoverDisplay, DeviceView

View File

@ -1,299 +0,0 @@
## 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.
import sqlite3 as sqlite
import os
from zlib import compress, decompress
from stat import ST_SIZE
from libprs500.lrf.meta import LRFMetaFile, LRFException
from cStringIO import StringIO as cStringIO
class LibraryDatabase(object):
BOOKS_SQL = \
"""
create table if not exists books_meta(id INTEGER PRIMARY KEY, title TEXT,
authors TEXT, publisher TEXT, size INTEGER, tags TEXT,
date DATE DEFAULT CURRENT_TIMESTAMP,
comments TEXT, rating INTEGER);
create table if not exists books_data(id INTEGER, extension TEXT,
uncompressed_size INTEGER, data BLOB);
create table if not exists books_cover(id INTEGER,
uncompressed_size INTEGER, data BLOB);
"""
def __init__(self, dbpath):
self.con = sqlite.connect(dbpath)
# Allow case insensitive field access by name
self.con.row_factory = sqlite.Row
self.con.executescript(LibraryDatabase.BOOKS_SQL)
def get_cover(self, _id):
raw = self.con.execute("select data from books_cover where id=?", \
(_id,)).next()["data"]
return decompress(str(raw)) if raw else None
def get_extensions(self, _id):
exts = []
cur = self.con.execute("select extension from books_data where id=?", \
(_id,))
for row in cur:
exts.append(row["extension"])
return exts
def add_book(self, path):
_file = os.path.abspath(path)
title, author, publisher, size, cover = os.path.basename(_file), \
None, None, os.stat(_file)[ST_SIZE], None
ext = title[title.rfind(".")+1:].lower() if title.find(".") > -1 else None
if ext == "lrf":
lrf = LRFMetaFile(open(_file, "r+b"))
title, author, cover, publisher = lrf.title, lrf.author.strip(), \
lrf.thumbnail, lrf.publisher.strip()
if "unknown" in publisher.lower():
publisher = None
if "unknown" in author.lower():
author = None
data = open(_file).read()
usize = len(data)
data = compress(data)
csize = 0
if cover:
csize = len(cover)
cover = sqlite.Binary(compress(cover))
self.con.execute("insert into books_meta (title, authors, publisher, "+\
"size, tags, comments, rating) values "+\
"(?,?,?,?,?,?,?)", \
(title, author, publisher, size, None, None, None))
_id = self.con.execute("select max(id) from books_meta").next()[0]
self.con.execute("insert into books_data values (?,?,?,?)", \
(_id, ext, usize, sqlite.Binary(data)))
self.con.execute("insert into books_cover values (?,?,?)", \
(_id, csize, cover))
self.con.commit()
return _id
def get_row_by_id(self, _id, columns):
"""
Return C{columns} of meta data as a dict.
@param columns: list of column names
"""
cols = ",".join([ c for c in columns])
cur = self.con.execute("select " + cols + " from books_meta where id=?"\
, (_id,))
row, r = cur.next(), {}
for c in columns:
r[c] = row[c]
return r
def commit(self):
self.con.commit()
def delete_by_id(self, _id):
self.con.execute("delete from books_meta where id=?", (_id,))
self.con.execute("delete from books_data where id=?", (_id,))
self.con.execute("delete from books_cover where id=?", (_id,))
self.commit()
def get_table(self, columns):
""" Return C{columns} of the metadata table as a list of dicts. """
cols = ",".join([ c for c in columns])
cur = self.con.execute("select " + cols + " from books_meta")
rows = []
for row in cur:
r = {}
for c in columns:
r[c] = row[c]
rows.append(r)
return rows
def get_format(self, _id, ext):
"""
Return format C{ext} corresponding to the logical book C{id} or
None if the format is unavailable.
Format is returned as a string of binary data suitable for
C{ file.write} operations.
"""
ext = ext.lower()
cur = self.con.execute("select data from books_data where id=? and "+\
"extension=?",(_id, ext))
try:
data = cur.next()
except:
pass
else:
return decompress(str(data["data"]))
def remove_format(self, _id, ext):
""" Remove format C{ext} from book C{_id} """
self.con.execute("delete from books_data where id=? and extension=?", \
(_id, ext))
self.update_max_size(_id)
self.con.commit()
def add_format(self, _id, ext, data):
"""
If data for format ext already exists, it is replaced
@type ext: string or None
@type data: string or file object
"""
try:
data.seek(0)
data = data.read()
except AttributeError:
pass
metadata = self.get_metadata(_id)
if ext:
ext = ext.strip().lower()
if ext == "lrf":
s = cStringIO()
print >> s, data
try:
lrf = LRFMetaFile(s)
lrf.author = metadata["authors"]
lrf.title = metadata["title"]
except LRFException:
pass
data = s.getvalue()
s.close()
size = len(data)
data = sqlite.Binary(compress(data))
cur = self.con.execute("select extension from books_data where id=? "+\
"and extension=?", (_id, ext))
present = True
try:
cur.next()
except:
present = False
if present:
self.con.execute("update books_data set uncompressed_size=? \
where id=? and extension=?", (size, _id, ext))
self.con.execute("update books_data set data=? where id=? "+\
"and extension=?", (data, _id, ext))
else:
self.con.execute("insert into books_data \
(id, extension, uncompressed_size, data) values (?, ?, ?, ?)", \
(_id, ext, size, data))
oldsize = self.get_row_by_id(_id, ['size'])['size']
if size > oldsize:
self.con.execute("update books_meta set size=? where id=? ", \
(size, _id))
self.con.commit()
def get_metadata(self, _id):
""" Return metadata in a dict """
try:
row = self.con.execute("select * from books_meta where id=?", \
(_id,)).next()
except StopIteration:
return None
data = {}
for field in ("id", "title", "authors", "publisher", "size", "tags",
"date"):
data[field] = row[field]
return data
def set_metadata(self, _id, title=None, authors=None, rating=None, \
publisher=None, tags=None, comments=None):
"""
Update metadata fields for book C{_id}. Metadata is not updated
in formats. See L{set_metadata_item}.
"""
if authors and not len(authors):
authors = None
if publisher and not len(publisher):
publisher = None
if tags and not len(tags):
tags = None
if comments and not len(comments):
comments = None
self.con.execute('update books_meta set title=?, authors=?, '+\
'publisher=?, tags=?, comments=?, rating=? '+\
'where id=?', \
(title, authors, publisher, tags, comments, \
rating, _id))
self.con.commit()
def set_metadata_item(self, _id, col, val):
"""
Convenience method used to set metadata. Metadata is updated
automatically in supported formats.
@param col: If it is either 'title' or 'authors' the value is updated
in supported formats as well.
"""
self.con.execute('update books_meta set '+col+'=? where id=?', \
(val, _id))
if col in ["authors", "title"]:
lrf = self.get_format(_id, "lrf")
if lrf:
c = cStringIO()
c.write(lrf)
lrf = LRFMetaFile(c)
if col == "authors":
lrf.authors = val
else: lrf.title = val
self.add_format(_id, "lrf", c.getvalue())
self.con.commit()
def update_cover(self, _id, cover, scaled=None):
"""
Update the stored cover. The cover is updated in supported formats
as well.
@param cover: The cover data
@param scaled: scaled version of cover that shoould be written to
format files. If None, cover is used.
"""
data = None
size = 0
if cover:
size = len(cover)
data = sqlite.Binary(compress(cover))
self.con.execute('update books_cover set uncompressed_size=?, data=? \
where id=?', (size, data, _id))
if not scaled:
scaled = cover
if scaled:
lrf = self.get_format(_id, "lrf")
if lrf:
c = cStringIO()
c.write(lrf)
lrf = LRFMetaFile(c)
lrf.thumbnail = scaled
self.add_format(_id, "lrf", c.getvalue())
self.update_max_size(_id)
self.commit()
def update_max_size(self, _id):
cur = self.con.execute("select uncompressed_size from books_data \
where id=?", (_id,))
maxsize = 0
for row in cur:
maxsize = row[0] if row[0] > maxsize else maxsize
self.con.execute("update books_meta set size=? where id=? ", \
(maxsize, _id))
self.con.commit()
#if __name__ == "__main__":
# lbm = LibraryDatabase("/home/kovid/library.db")
# lbm.add_book("/home/kovid/documents/ebooks/hobbfar01.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobbfar02.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobbfar03.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobblive01.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobbtawny01.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobbtawny02.lrf")
# lbm.add_book("/home/kovid/documents/ebooks/hobbtawny03.lrf")
# print lbm.get_table(["id","title"])

View File

@ -1,166 +0,0 @@
## 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.
"""
The dialog used to edit meta information for a book as well as
add/remove formats
"""
import os
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QObject, QPixmap, QListWidgetItem, QErrorMessage, \
QVariant, QSettings, QFileDialog
from libprs500.gui import import_ui, extension
class Format(QListWidgetItem):
def __init__(self, parent, ext, path=None):
self.path = path
self.ext = ext
QListWidgetItem.__init__(self, ext.upper(), parent, \
QListWidgetItem.UserType)
Ui_BookEditDialog = import_ui("editbook.ui")
class EditBookDialog(Ui_BookEditDialog):
def select_cover(self, checked):
settings = QSettings()
_dir = settings.value("change cover dir", \
QVariant(os.path.expanduser("~"))).toString()
_file = str(QFileDialog.getOpenFileName(self.parent, \
"Choose cover for " + str(self.title.text()), _dir, \
"Images (*.png *.gif *.jpeg *.jpg);;All files (*)"))
if len(_file):
_file = os.path.abspath(_file)
settings.setValue("change cover dir", \
QVariant(os.path.dirname(_file)))
if not os.access(_file, os.R_OK):
QErrorMessage(self.parent).showMessage("You do not have "+\
"permission to read the file: " + _file)
return
cf, cover = None, None
try:
cf = open(_file, "rb")
cover = cf.read()
except IOError, e:
QErrorMessage(self.parent).showMessage("There was an error"+\
" reading from file: " + _file + "\n"+str(e))
if cover:
pix = QPixmap()
pix.loadFromData(cover, "", Qt.AutoColor)
if pix.isNull():
QErrorMessage(self.parent).showMessage(_file + \
" is not a valid picture")
else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix)
def add_format(self, x):
settings = QSettings()
_dir = settings.value("add formats dialog dir", \
QVariant(os.path.expanduser("~"))).toString()
files = QFileDialog.getOpenFileNames(self.parent, \
"Choose formats for " + str(self.title.text()), _dir, \
"Books (*.lrf *.lrx *.rtf *.txt *.html *.xhtml *.htm *.rar);;"+\
"All files (*)")
if not files.isEmpty():
x = str(files[0])
settings.setValue("add formats dialog dir", \
QVariant(os.path.dirname(x)))
files = str(files.join("|||")).split("|||")
for _file in files:
_file = os.path.abspath(_file)
if not os.access(_file, os.R_OK):
QErrorMessage(self.parent).showMessage("You do not have "+\
"permission to read the file: " + _file)
continue
ext = extension(_file)
for row in range(self.formats.count()):
fmt = self.formats.item(row)
if fmt.ext == ext:
self.formats.takeItem(row)
break
Format(self.formats, ext, path=_file)
self.formats_changed = True
def remove_format(self, x):
rows = self.formats.selectionModel().selectedRows(0)
for row in rows:
self.formats.takeItem(row.row())
self.formats_changed = True
def sync_formats(self):
old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()):
fmt = self.formats.item(row)
ext, path = fmt.ext, fmt.path
if "unknown" in ext.lower():
ext = None
if path:
new_extensions.add(ext)
paths[ext] = path
else:
old_extensions.add(ext)
for ext in new_extensions:
self.db.add_format(self.id, ext, file(paths[ext], "rb"))
db_extensions = self.db.get_extensions(self.id)
extensions = new_extensions.union(old_extensions)
for ext in db_extensions:
if ext not in extensions:
self.db.remove_format(self.id, ext)
self.db.update_max_size(self.id)
def __init__(self, dialog, _id, db):
Ui_BookEditDialog.__init__(self)
self.parent = dialog
self.setupUi(dialog)
self.splitter.setStretchFactor(100, 1)
self.db = db
self.id = _id
self.cover_data = None
self.formats_changed = False
QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
self.select_cover)
QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), \
self.add_format)
QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \
self.remove_format)
QObject.connect(self.button_box, SIGNAL("accepted()"), \
self.sync_formats)
data = self.db.get_row_by_id(self.id, \
["title","authors","rating","publisher","tags","comments"])
self.title.setText(data["title"])
self.authors.setText(data["authors"] if data["authors"] else "")
self.publisher.setText(data["publisher"] if data["publisher"] else "")
self.tags.setText(data["tags"] if data["tags"] else "")
if data["rating"] > 0:
self.rating.setValue(data["rating"])
self.comments.setPlainText(data["comments"] if data["comments"] else "")
cover = self.db.get_cover(self.id)
if cover:
pm = QPixmap()
pm.loadFromData(cover, "", Qt.AutoColor)
if not pm.isNull():
self.cover.setPixmap(pm)
else:
self.cover.setPixmap(QPixmap(":/default_cover"))
else:
self.cover.setPixmap(QPixmap(":/default_cover"))
exts = self.db.get_extensions(self.id)
for ext in exts:
if not ext:
ext = "Unknown"
Format(self.formats, ext)

View File

@ -1,427 +0,0 @@
<ui version="4.0" >
<class>BookEditDialog</class>
<widget class="QDialog" name="BookEditDialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>865</width>
<height>776</height>
</rect>
</property>
<property name="windowTitle" >
<string>SONY Reader - Edit Meta Information</string>
</property>
<layout class="QGridLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="0" column="0" >
<widget class="QSplitter" name="splitter" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="" >
<layout class="QVBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Meta information</string>
</property>
<layout class="QGridLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="2" column="1" colspan="2" >
<widget class="QSpinBox" name="rating" >
<property name="toolTip" >
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="whatsThis" >
<string>Rating of this book. 0-5 stars</string>
</property>
<property name="buttonSymbols" >
<enum>QAbstractSpinBox::PlusMinus</enum>
</property>
<property name="suffix" >
<string> stars</string>
</property>
<property name="maximum" >
<number>5</number>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>&amp;Rating:</string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2" >
<widget class="QLineEdit" name="publisher" >
<property name="toolTip" >
<string>Change the publisher of this book</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Publisher: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>publisher</cstring>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>Ta&amp;gs: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>tags</cstring>
</property>
</widget>
</item>
<item row="4" column="1" colspan="2" >
<widget class="QLineEdit" name="tags" >
<property name="toolTip" >
<string>Tags categorize the book. This is particularly useful while searching. &lt;br>&lt;br>They can be any words or phrases, separated by commas.</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2" >
<widget class="QLineEdit" name="authors" >
<property name="toolTip" >
<string>Change the author(s) of this book. Multiple authors should be separated by the &amp; character</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2" >
<widget class="QLineEdit" name="title" >
<property name="toolTip" >
<string>Change the title of this book</string>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>&amp;Author(s): </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>authors</cstring>
</property>
</widget>
</item>
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Title: </string>
</property>
<property name="alignment" >
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy" >
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="6" column="2" >
<layout class="QVBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Change &amp;cover image:</string>
</property>
<property name="buddy" >
<cstring>cover_path</cstring>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QLineEdit" name="cover_path" >
<property name="readOnly" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="cover_button" >
<property name="toolTip" >
<string>Browse for an image to use as the cover of this book.</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="images.qrc" >:/images/fileopen.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item rowspan="3" row="5" column="0" colspan="2" >
<widget class="QLabel" name="cover" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>0</hsizetype>
<vsizetype>0</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize" >
<size>
<width>100</width>
<height>120</height>
</size>
</property>
<property name="text" >
<string/>
</property>
<property name="pixmap" >
<pixmap resource="images.qrc" >:/images/cherubs.jpg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="2" >
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="2" >
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<size>
<width>20</width>
<height>21</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2" >
<property name="title" >
<string>Comments</string>
</property>
<layout class="QGridLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="0" column="0" >
<widget class="QTextEdit" name="comments" />
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="groupBox_3" >
<property name="title" >
<string>Available Formats</string>
</property>
<layout class="QGridLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="0" column="1" >
<layout class="QVBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="add_format_button" >
<property name="toolTip" >
<string>Add a new format for this book</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="images.qrc" >:/images/plus.png</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType" >
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" >
<size>
<width>26</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="remove_format_button" >
<property name="toolTip" >
<string>Remove the selected formats for this book from the database.</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="images.qrc" >:/images/minus.png</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation" >
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="0" >
<widget class="QListWidget" name="formats" />
</item>
</layout>
</widget>
</widget>
</item>
<item row="1" column="0" >
<widget class="QDialogButtonBox" name="button_box" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="images.qrc" />
</resources>
<connections>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>BookEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>BookEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,14 +0,0 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="icon">images/library.png</file>
<file alias="default_cover">images/cherubs.jpg</file>
<file alias="library">images/library.png</file>
<file alias="reader">images/reader.png</file>
<file alias="card">images/memory_stick_unmount.png</file>
<file>images/clear.png</file>
<file>images/minus.png</file>
<file>images/plus.png</file>
<file>images/edit.png</file>
<file>images/fileopen.png</file>
</qresource>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@ -1,648 +0,0 @@
## 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.Warning
""" Create and launch the GUI """
import sys
import re
import os
import traceback
import tempfile
from PyQt4.QtCore import Qt, SIGNAL, QObject, QCoreApplication, \
QSettings, QVariant, QSize, QEventLoop, QString, \
QBuffer, QIODevice, QModelIndex
from PyQt4.QtGui import QPixmap, QErrorMessage, \
QMessageBox, QFileDialog, QIcon, QDialog
from PyQt4.Qt import qDebug, qFatal, qWarning, qCritical
from libprs500.communicate import PRS500Device as device
from libprs500.books import fix_ids
from libprs500.errors import *
from libprs500.gui import import_ui, installErrorHandler, Error, _Warning, \
extension, APP_TITLE
from libprs500.gui.widgets import LibraryBooksModel, DeviceBooksModel, \
DeviceModel
from database import LibraryDatabase
from editbook import EditBookDialog
DEFAULT_BOOK_COVER = None
LIBRARY_BOOK_TEMPLATE = QString("<table><tr><td><b>Formats:</b> %1 \
</td><td><b>Tags:</b> %2</td></tr> \
<tr><td colspan='2'><b>Comments:</b> %3</td>\
</tr></table>")
DEVICE_BOOK_TEMPLATE = QString("<table><tr><td><b>Title: </b>%1</td><td> \
<b>&nbsp;Size:</b> %2</td></tr>\
<tr><td><b>Author: </b>%3</td>\
<td><b>&nbsp;Type: </b>%4</td></tr></table>")
Ui_MainWindow = import_ui("main.ui")
class Main(QObject, Ui_MainWindow):
def report_error(func):
"""
Decorator to ensure that unhandled exceptions are displayed
to users via the GUI
"""
def function(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception, e:
Error("There was an error calling " + func.__name__, e)
raise
return function
""" Create GUI """
def show_device(self, yes):
"""
If C{yes} show the items on the device otherwise show the items
in the library
"""
self.device_view.selectionModel().reset()
self.library_view.selectionModel().reset()
self.book_cover.hide()
self.book_info.hide()
if yes:
self.action_add.setEnabled(False)
self.action_edit.setEnabled(False)
self.device_view.show()
self.library_view.hide()
self.book_cover.setAcceptDrops(False)
self.device_view.resizeColumnsToContents()
self.device_view.resizeRowsToContents()
else:
self.action_add.setEnabled(True)
self.action_edit.setEnabled(True)
self.device_view.hide()
self.library_view.show()
self.book_cover.setAcceptDrops(True)
self.current_view.sortByColumn(3, Qt.DescendingOrder)
def tree_clicked(self, index):
if index.isValid():
self.search.clear()
show_dev = True
model = self.device_tree.model()
if model.is_library(index):
show_dev = False
elif model.is_reader(index):
self.device_view.setModel(self.reader_model)
QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \
self.show_book)
elif model.is_card(index):
self.device_view.setModel(self.card_model)
QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), \
self.show_book)
self.show_device(show_dev)
def model_modified(self):
if self.current_view.selectionModel():
self.current_view.selectionModel().reset()
self.current_view.resizeColumnsToContents()
self.current_view.resizeRowsToContents()
self.book_cover.hide()
self.book_info.hide()
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
def resize_rows_and_columns(self, topleft, bottomright):
for c in range(topleft.column(), bottomright.column()+1):
self.current_view.resizeColumnToContents(c)
for r in range(topleft.row(), bottomright.row()+1):
self.current_view.resizeRowToContents(c)
def show_book(self, current, previous):
if not len(self.current_view.selectedIndexes()):
return
if self.library_view.isVisible():
formats, tags, comments, cover = self.library_model\
.info(current.row())
data = LIBRARY_BOOK_TEMPLATE.arg(formats).arg(tags).arg(comments)
tooltip = "To save the cover, drag it to the desktop.<br>To \
change the cover drag the new cover onto this picture"
else:
title, author, size, mime, cover = self.device_view.model()\
.info(current.row())
data = DEVICE_BOOK_TEMPLATE.arg(title).arg(size).arg(author).arg(mime)
tooltip = "To save the cover, drag it to the desktop."
self.book_info.setText(data)
self.book_cover.setToolTip(tooltip)
if not cover: cover = DEFAULT_BOOK_COVER
self.book_cover.setPixmap(cover)
self.book_cover.show()
self.book_info.show()
self.current_view.scrollTo(current)
def formats_added(self, index):
if index == self.library_view.currentIndex():
self.show_book(index, index)
@report_error
def delete(self, action):
rows = self.current_view.selectionModel().selectedRows()
if not len(rows):
return
count = str(len(rows))
ret = QMessageBox.question(self.window, self.trUtf8(APP_TITLE + \
" - confirm"), self.trUtf8("Are you sure you want to \
<b>permanently delete</b> these ") +count+self.trUtf8(" item(s)?"), \
QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if ret != QMessageBox.Yes:
return
self.window.setCursor(Qt.WaitCursor)
if self.library_view.isVisible():
self.library_model.delete(self.library_view.selectionModel()\
.selectedRows())
else:
self.status("Deleting files from 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.update_availabe_space()
self.show_book(self.current_view.currentIndex(), QModelIndex())
self.window.setCursor(Qt.ArrowCursor)
def read_settings(self):
settings = QSettings()
settings.beginGroup("MainWindow")
self.window.resize(settings.value("size", QVariant(QSize(1000, 700))).\
toSize())
settings.endGroup()
self.database_path = settings.value("database path", QVariant(os.path\
.expanduser("~/library.db"))).toString()
def write_settings(self):
settings = QSettings()
settings.beginGroup("MainWindow")
settings.setValue("size", QVariant(self.window.size()))
settings.endGroup()
def close_event(self, e):
self.write_settings()
e.accept()
def add(self, action):
settings = QSettings()
_dir = settings.value("add books dialog dir", \
QVariant(os.path.expanduser("~"))).toString()
files = QFileDialog.getOpenFileNames(self.window, \
"Choose books to add to library", _dir, \
"Books (*.lrf *.lrx *.rtf *.pdf *.txt);;All files (*)")
if not files.isEmpty():
x = unicode(files[0].toUtf8(), 'utf-8')
settings.setValue("add books dialog dir", \
QVariant(os.path.dirname(x)))
files = unicode(files.join("|||").toUtf8(), 'utf-8').split("|||")
self.add_books(files)
@report_error
def add_books(self, files):
self.window.setCursor(Qt.WaitCursor)
try:
for _file in files:
_file = os.path.abspath(_file)
self.library_view.model().add_book(_file)
if self.library_view.isVisible():
if len(str(self.search.text())):
self.search.clear()
else:
self.library_model.search("")
else:
self.library_model.search("")
hv = self.library_view.horizontalHeader()
col = hv.sortIndicatorSection()
order = hv.sortIndicatorOrder()
self.library_view.model().sort(col, order)
finally:
self.window.setCursor(Qt.ArrowCursor)
@report_error
def edit(self, action):
if self.library_view.isVisible():
rows = self.library_view.selectionModel().selectedRows()
accepted = False
for row in rows:
_id = self.library_model.id_from_index(row)
dialog = QDialog(self.window)
ebd = EditBookDialog(dialog, _id, self.library_model.db)
if dialog.exec_() == QDialog.Accepted:
accepted = True
title = unicode(ebd.title.text().toUtf8(), 'utf-8').strip()
authors = unicode(ebd.authors.text().toUtf8(), 'utf-8').strip()
rating = ebd.rating.value()
tags = unicode(ebd.tags.text().toUtf8(), 'utf-8').strip()
publisher = unicode(ebd.publisher.text().toUtf8(), \
'utf-8').strip()
comments = unicode(ebd.comments.toPlainText().toUtf8(), \
'utf-8').strip()
pix = ebd.cover.pixmap()
if not pix.isNull():
self.update_cover(pix)
model = self.library_view.model()
if title:
index = model.index(row.row(), 0)
model.setData(index, QVariant(title), Qt.EditRole)
if authors:
index = model.index(row.row(), 1)
model.setData(index, QVariant(authors), Qt.EditRole)
if publisher:
index = model.index(row.row(), 5)
model.setData(index, QVariant(publisher), Qt.EditRole)
index = model.index(row.row(), 4)
model.setData(index, QVariant(rating), Qt.EditRole)
self.update_tags_and_comments(row, tags, comments)
self.library_model.refresh_row(row.row())
self.show_book(self.current_view.currentIndex(), QModelIndex())
def update_tags_and_comments(self, index, tags, comments):
self.library_model.update_tags_and_comments(index, tags, comments)
@report_error
def update_cover(self, pix):
if not pix.isNull():
try:
self.library_view.model().update_cover(self.library_view\
.currentIndex(), pix)
self.book_cover.setPixmap(pix)
except Exception, e:
Error("Unable to change cover", e)
@report_error
def upload_books(self, to, files, ids):
oncard = False if to == "reader" else True
booklists = (self.reader_model.booklist, self.card_model.booklist)
def update_models():
hv = self.device_view.horizontalHeader()
col = hv.sortIndicatorSection()
order = hv.sortIndicatorOrder()
model = self.card_model if oncard else self.reader_model
model.sort(col, order)
if self.device_view.isVisible() and \
self.device_view.model() == model:
if len(str(self.search.text())):
self.search.clear()
else:
self.device_view.model().search("")
else:
model.search("")
def sync_lists():
self.status("Syncing media list to device main memory")
self.dev.upload_book_list(booklists[0])
if len(booklists[1]):
self.status("Syncing media list to storage card")
self.dev.upload_book_list(booklists[1])
self.window.setCursor(Qt.WaitCursor)
ename = "file"
try:
if ids:
for _id in ids:
formats = []
info = self.library_view.model().book_info(_id)
if info["cover"]:
pix = QPixmap()
pix.loadFromData(str(info["cover"]))
if pix.isNull():
pix = DEFAULT_BOOK_COVER
pix = pix.scaledToHeight(self.dev.THUMBNAIL_HEIGHT, \
Qt.SmoothTransformation)
_buffer = QBuffer()
_buffer.open(QIODevice.WriteOnly)
pix.save(_buffer, "JPEG")
info["cover"] = (pix.width(), pix.height(), \
str(_buffer.buffer()))
ename = info["title"]
for f in files:
if re.match("libprs500_\S+_......_" + \
str(_id) + "_", os.path.basename(f)):
formats.append(f)
_file = None
try:
for format in self.dev.FORMATS:
for f in formats:
if extension(f) == format:
_file = f
raise StopIteration()
except StopIteration: pass
if not _file:
Error("The library does not have any formats that "+\
"can be viewed on the device for " + ename, None)
continue
f = open(_file, "rb")
self.status("Sending "+info["title"]+" to device")
try:
self.dev.add_book(f, "libprs500_"+str(_id)+"."+\
extension(_file), info, booklists, oncard=oncard, \
end_session=False)
update_models()
except PathError, e:
if "already exists" in str(e):
Error(info["title"] + \
" already exists on the device", None)
self.progress(100)
continue
else: raise
finally: f.close()
sync_lists()
else:
for _file in files:
ename = _file
if extension(_file) not in self.dev.FORMATS:
Error(ename + " is not in a supported format")
continue
info = { "title":os.path.basename(_file), \
"authors":"Unknown", "cover":(None, None, None) }
f = open(_file, "rb")
self.status("Sending "+info["title"]+" to device")
try:
self.dev.add_book(f, os.path.basename(_file), info, \
booklists, oncard=oncard, end_session=False)
update_models()
except PathError, e:
if "already exists" in str(e):
Error(info["title"] + \
" already exists on the device", None)
self.progress(100)
continue
else: raise
finally: f.close()
sync_lists()
except Exception, e:
Error("Unable to send "+ename+" to device", e)
finally:
self.window.setCursor(Qt.ArrowCursor)
self.update_availabe_space()
@apply
def current_view():
doc = """ The currently visible view """
def fget(self):
return self.library_view if self.library_view.isVisible() \
else self.device_view
return property(doc=doc, fget=fget)
def __init__(self, window, log_packets):
QObject.__init__(self)
Ui_MainWindow.__init__(self)
self.dev = device(report_progress=self.progress, log_packets=log_packets)
self.setupUi(window)
self.card = None
self.window = window
window.closeEvent = self.close_event
self.read_settings()
# Setup Library Book list
self.library_model = LibraryBooksModel(window)
self.library_model.set_data(LibraryDatabase(str(self.database_path)))
self.library_view.setModel(self.library_model)
QObject.connect(self.library_model, SIGNAL("layoutChanged()"), \
self.library_view.resizeRowsToContents)
QObject.connect(self.library_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), \
self.library_model.search)
QObject.connect(self.library_model, SIGNAL("sorted()"), \
self.model_modified)
QObject.connect(self.library_model, SIGNAL("searched()"), \
self.model_modified)
QObject.connect(self.library_model, SIGNAL("deleted()"), \
self.model_modified)
QObject.connect(self.library_model, \
SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.resize_rows_and_columns)
QObject.connect(self.library_view, \
SIGNAL('books_dropped'), self.add_books)
QObject.connect(self.library_model, \
SIGNAL('formats_added'), self.formats_added)
self.library_view.sortByColumn(3, Qt.DescendingOrder)
# Create Device tree
model = DeviceModel(self.device_tree)
QObject.connect(self.device_tree, SIGNAL("activated(QModelIndex)"), \
self.tree_clicked)
QObject.connect(self.device_tree, SIGNAL("clicked(QModelIndex)"), \
self.tree_clicked)
QObject.connect(model, SIGNAL('books_dropped'), self.add_books)
QObject.connect(model, SIGNAL('upload_books'), self.upload_books)
self.device_tree.setModel(model)
# Create Device Book list
self.reader_model = DeviceBooksModel(window)
self.card_model = DeviceBooksModel(window)
self.device_view.setModel(self.reader_model)
QObject.connect(self.device_view.selectionModel(), \
SIGNAL("currentChanged(QModelIndex, QModelIndex)"), self.show_book)
for model in (self.reader_model, self. card_model):
QObject.connect(model, SIGNAL("layoutChanged()"), \
self.device_view.resizeRowsToContents)
QObject.connect(self.search, SIGNAL("textChanged(QString)"), \
model.search)
QObject.connect(model, SIGNAL("sorted()"), self.model_modified)
QObject.connect(model, SIGNAL("searched()"), self.model_modified)
QObject.connect(model, SIGNAL("deleted()"), self.model_modified)
QObject.connect(model, \
SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.resize_rows_and_columns)
# Setup book display
self.book_cover.hide()
self.book_info.hide()
# Connect actions
QObject.connect(self.action_add, SIGNAL("triggered(bool)"), self.add)
QObject.connect(self.action_del, SIGNAL("triggered(bool)"), self.delete)
QObject.connect(self.action_edit, SIGNAL("triggered(bool)"), self.edit)
# DnD setup
QObject.connect(self.book_cover, SIGNAL("cover_received(QPixmap)"), \
self.update_cover)
self.detector = DeviceConnectDetector(self.dev)
self.connect(self.detector, SIGNAL("device_connected()"), \
self.establish_connection)
self.connect(self.detector, SIGNAL("device_removed()"), self.device_removed)
self.search.setFocus(Qt.OtherFocusReason)
self.show_device(False)
self.df_template = self.df.text()
self.df.setText(self.df_template.arg("").arg("").arg(""))
window.show()
self.library_view.resizeColumnsToContents()
self.library_view.resizeRowsToContents()
def device_removed(self):
self.df.setText(self.df_template.arg("").arg("").arg(""))
self.device_tree.hide_reader(True)
self.device_tree.hide_card(True)
self.device_tree.selectionModel().reset()
if self.device_view.isVisible():
self.device_view.hide()
self.library_view.selectionModel().reset()
self.library_view.show()
self.book_cover.hide()
self.book_info.hide()
def progress(self, val):
if val < 0:
self.progress_bar.setMaximum(0)
else: self.progress_bar.setValue(val)
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
def status(self, msg):
self.progress_bar.setMaximum(100)
self.progress_bar.reset()
self.progress_bar.setFormat(msg + ": %p%")
self.progress(0)
QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents)
def establish_connection(self):
self.window.setCursor(Qt.WaitCursor)
self.status("Connecting to device")
try:
info = self.dev.get_device_information(end_session=False)
except DeviceBusy, err:
qFatal(str(err))
except DeviceError, err:
self.dev.reconnect()
self.thread().msleep(100)
return self.establish_connection()
except ProtocolError, e:
traceback.print_exc(e)
qFatal("Unable to connect to device. Please try unplugging and"+\
" reconnecting it")
self.df.setText(self.df_template.arg("Connected: "+info[0])\
.arg(info[1]).arg(info[2]))
self.update_availabe_space(end_session=False)
self.card = self.dev.card()
self.is_connected = True
if self.card: self.device_tree.hide_card(False)
else: self.device_tree.hide_card(True)
self.device_tree.hide_reader(False)
self.status("Loading media list from SONY Reader")
self.reader_model.set_data(self.dev.books(end_session=False))
if self.card: self.status("Loading media list from Storage Card")
self.card_model.set_data(self.dev.books(oncard=True))
self.progress(100)
self.window.setCursor(Qt.ArrowCursor)
def update_availabe_space(self, end_session=True):
space = self.dev.free_space(end_session=end_session)
sc = space[1] if int(space[1])>0 else space[2]
self.device_tree.model().update_free_space(space[0], sc)
class LockFile(object):
def __init__(self, path):
self.path = path
f = open(path, "w")
f.close()
def __del__(self):
if os.access(self.path, os.F_OK): os.remove(self.path)
class DeviceConnectDetector(QObject):
def timerEvent(self, e):
if e.timerId() == self.device_detector:
is_connected = self.dev.is_connected()
if is_connected and not self.is_connected:
self.emit(SIGNAL("device_connected()"))
self.is_connected = True
elif not is_connected and self.is_connected:
self.emit(SIGNAL("device_removed()"))
self.is_connected = False
def udi_is_device(self, udi):
ans = False
try:
devobj = bus.get_object('org.freedesktop.Hal', udi)
dev = dbus.Interface(devobj, "org.freedesktop.Hal.Device")
properties = dev.GetAllProperties()
vendor_id = int(properties["usb_device.vendor_id"]),
product_id = int(properties["usb_device.product_id"])
if self.dev.signature() == (vendor_id, product_id): ans = True
except:
self.device_detector = self.startTimer(1000)
return ans
def device_added_callback(self, udi):
if self.udi_is_device(udi):
self.emit(SIGNAL("device_connected()"))
def device_removed_callback(self, udi):
if self.udi_is_device(udi):
self.emit(SIGNAL("device_removed()"))
def __init__(self, dev):
QObject.__init__(self)
self.dev = dev
try:
raise Exception("DBUS doesn't support the Qt mainloop")
import dbus
bus = dbus.SystemBus()
hal_manager_obj = bus.get_object('org.freedesktop.Hal',\
'/org/freedesktop/Hal/Manager')
hal_manager = dbus.Interface(hal_manager_obj,\
'org.freedesktop.Hal.Manager')
hal_manager.connect_to_signal('DeviceAdded', \
self.device_added_callback)
hal_manager.connect_to_signal('DeviceRemoved', \
self.device_removed_callback)
except Exception, e:
#_Warning("Could not connect to HAL", e)
self.is_connected = False
self.device_detector = self.startTimer(1000)
def main():
from optparse import OptionParser
from libprs500 import __version__ as VERSION
lock = os.path.join(tempfile.gettempdir(),"libprs500_gui_lock")
if os.access(lock, os.F_OK):
print >>sys.stderr, "Another instance of", APP_TITLE, "is running"
print >>sys.stderr, "If you are sure this is not the case then "+\
"manually delete the file", lock
sys.exit(1)
parser = OptionParser(usage="usage: %prog [options]", version=VERSION)
parser.add_option("--log-packets", help="print out packet stream to stdout. "+\
"The numbers in the left column are byte offsets that allow"+\
" the packet size to be read off easily.", \
dest="log_packets", action="store_true", default=False)
options, args = parser.parse_args()
from PyQt4.Qt import QApplication, QMainWindow
app = QApplication(sys.argv)
global DEFAULT_BOOK_COVER
DEFAULT_BOOK_COVER = QPixmap(":/default_cover")
window = QMainWindow()
window.setWindowTitle(APP_TITLE)
window.setWindowIcon(QIcon(":/icon"))
installErrorHandler(QErrorMessage(window))
QCoreApplication.setOrganizationName("KovidsBrain")
QCoreApplication.setApplicationName(APP_TITLE)
Main(window, options.log_packets)
lock = LockFile(lock)
return app.exec_()
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,393 +0,0 @@
<ui version="4.0" >
<author>Kovid Goyal</author>
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>728</width>
<height>822</height>
</rect>
</property>
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>5</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle" >
<string/>
</property>
<property name="windowIcon" >
<iconset resource="images.qrc" >:/images/library.png</iconset>
</property>
<widget class="QWidget" name="centralwidget" >
<layout class="QVBoxLayout" >
<property name="margin" >
<number>9</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="DeviceView" name="device_tree" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>5</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize" >
<size>
<width>10000</width>
<height>95</height>
</size>
</property>
<property name="verticalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy" >
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="flow" >
<enum>QListView::TopToBottom</enum>
</property>
<property name="spacing" >
<number>20</number>
</property>
<property name="viewMode" >
<enum>QListView::IconMode</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="df" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>0</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text" >
<string>For help visit &lt;a href="https://libprs500.kovidgoyal.net/wiki/GuiUsage">http://libprs500.kovidgoyal.net&lt;/a>&lt;br>&lt;br>&lt;b>libprs500&lt;/b> was created by &lt;b>Kovid Goyal&lt;/b> &amp;copy; 2006&lt;br>%1 %2 %3</string>
</property>
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks" >
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="QLabel" name="label" >
<property name="text" >
<string>&amp;Search:</string>
</property>
<property name="buddy" >
<cstring>search</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search" >
<property name="enabled" >
<bool>true</bool>
</property>
<property name="acceptDrops" >
<bool>false</bool>
</property>
<property name="toolTip" >
<string>Search the list of books by title or author&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property>
<property name="whatsThis" >
<string>Search the list of books by title or author&lt;br>&lt;br>Words separated by spaces are ANDed</string>
</property>
<property name="autoFillBackground" >
<bool>false</bool>
</property>
<property name="text" >
<string/>
</property>
<property name="frame" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear_button" >
<property name="toolTip" >
<string>Reset Quick Search</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="images.qrc" >:/images/clear.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item row="0" column="0" >
<widget class="DeviceBooksView" name="device_view" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>5</vsizetype>
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="LibraryBooksView" name="library_view" >
<property name="sizePolicy" >
<sizepolicy>
<hsizetype>5</hsizetype>
<vsizetype>5</vsizetype>
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="dragEnabled" >
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode" >
<bool>false</bool>
</property>
<property name="dragDropMode" >
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="alternatingRowColors" >
<bool>true</bool>
</property>
<property name="selectionBehavior" >
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="showGrid" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" >
<property name="margin" >
<number>0</number>
</property>
<property name="spacing" >
<number>6</number>
</property>
<item>
<widget class="CoverDisplay" name="book_cover" >
<property name="maximumSize" >
<size>
<width>60</width>
<height>80</height>
</size>
</property>
<property name="acceptDrops" >
<bool>true</bool>
</property>
<property name="pixmap" >
<pixmap resource="images.qrc" >:/images/cherubs.jpg</pixmap>
</property>
<property name="scaledContents" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="book_info" >
<property name="text" >
<string>&lt;table>&lt;tr>&lt;td>&lt;b>Title: &lt;/b>%1&lt;/td>&lt;td>&lt;b>&amp;nbsp;Size:&lt;/b> %2&lt;/td>&lt;/tr>&lt;tr>&lt;td>&lt;b>Author: &lt;/b>%3&lt;/td>&lt;td>&lt;b>&amp;nbsp;Type: &lt;/b>%4&lt;/td>&lt;/tr>&lt;/table></string>
</property>
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progress_bar" >
<property name="value" >
<number>100</number>
</property>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QToolBar" name="tool_bar" >
<property name="minimumSize" >
<size>
<width>163</width>
<height>58</height>
</size>
</property>
<property name="movable" >
<bool>false</bool>
</property>
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="iconSize" >
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="toolButtonStyle" >
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
<attribute name="toolBarArea" >
<number>4</number>
</attribute>
<addaction name="action_add" />
<addaction name="action_del" />
<addaction name="action_edit" />
</widget>
<action name="action_add" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/plus.png</iconset>
</property>
<property name="text" >
<string>Add books to Library</string>
</property>
<property name="shortcut" >
<string>A</string>
</property>
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
<action name="action_del" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/minus.png</iconset>
</property>
<property name="text" >
<string>Delete books</string>
</property>
<property name="shortcut" >
<string>Del</string>
</property>
</action>
<action name="action_edit" >
<property name="icon" >
<iconset resource="images.qrc" >:/images/edit.png</iconset>
</property>
<property name="text" >
<string>Edit meta-information</string>
</property>
<property name="shortcut" >
<string>E</string>
</property>
<property name="autoRepeat" >
<bool>false</bool>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>LibraryBooksView</class>
<extends>QTableView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceBooksView</class>
<extends>QTableView</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>CoverDisplay</class>
<extends>QLabel</extends>
<header>widgets.h</header>
</customwidget>
<customwidget>
<class>DeviceView</class>
<extends>QListView</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="images.qrc" />
</resources>
<connections>
<connection>
<sender>clear_button</sender>
<signal>clicked()</signal>
<receiver>search</receiver>
<slot>clear()</slot>
<hints>
<hint type="sourcelabel" >
<x>853</x>
<y>61</y>
</hint>
<hint type="destinationlabel" >
<x>784</x>
<y>58</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,859 +0,0 @@
## 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.
import re
import os
import textwrap
import time
import traceback
import sys
from operator import itemgetter, attrgetter
from socket import gethostname
from urlparse import urlparse, urlunparse
from urllib import quote, unquote
from math import sin, cos, pi
from libprs500.gui import Error, _Warning
from libprs500.ptempfile import PersistentTemporaryFile
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.Qt import QApplication, QString, QFont, QAbstractListModel, \
QVariant, QAbstractTableModel, QTableView, QListView, \
QLabel, QAbstractItemView, QPixmap, QIcon, QSize, \
QMessageBox, QSettings, QFileDialog, QErrorMessage, \
QSpinBox, QPoint, \
QIODevice, QPainterPath, QItemDelegate, QPainter, QPen, \
QColor, QLinearGradient, QBrush, QStyle, QStringList, \
QByteArray, QBuffer, QMimeData, QTextStream, QIODevice, \
QDrag, QRect
NONE = QVariant() #: Null value to return from the data function of item models
TIME_WRITE_FMT = "%d %b %Y" #: The display format used to show dates
class FileDragAndDrop(object):
_drag_start_position = QPoint()
_dragged_files = []
@classmethod
def _bytes_to_string(cls, qba):
"""
Assumes qba is encoded in ASCII which is usually fine, since
this method is used mainly for escaped URIs.
@type qba: QByteArray
"""
return str(QString.fromAscii(qba.data())).strip()
@classmethod
def _get_r_ok_files(cls, event):
"""
Return list of paths from event that point to files to
which the user has read permission.
"""
files = []
md = event.mimeData()
if md.hasFormat("text/uri-list"):
candidates = cls._bytes_to_string(md.data("text/uri-list")).split()
for url in candidates:
o = urlparse(url)
if o.scheme and o.scheme != 'file':
_Warning(o.scheme + " not supported in drop events", None)
continue
path = unquote(o.path)
if not os.access(path, os.R_OK):
_Warning("You do not have read permission for: " + path, None)
continue
if os.path.isdir(path):
root, dirs, files2 = os.walk(path)
for _file in files2:
path = root + _file
if os.access(path, os.R_OK):
files.append(path)
else:
files.append(path)
return files
def __init__(self, QtBaseClass, enable_drag=True):
self.QtBaseClass = QtBaseClass
self.enable_drag = enable_drag
def mousePressEvent(self, event):
self.QtBaseClass.mousePressEvent(self, event)
if self.enable_drag:
if event.button == Qt.LeftButton:
self._drag_start_position = event.pos()
def mouseMoveEvent(self, event):
self.QtBaseClass.mousePressEvent(self, event)
if self.enable_drag:
if event.buttons() & Qt.LeftButton != Qt.LeftButton:
return
if (event.pos() - self._drag_start_position).manhattanLength() < \
QApplication.startDragDistance():
return
self.start_drag(self._drag_start_position)
def start_drag(self, pos):
raise NotImplementedError()
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("text/uri-list"):
event.acceptProposedAction()
def dragMoveEvent(self, event):
event.acceptProposedAction()
def dropEvent(self, event):
files = self._get_r_ok_files(event)
if files:
try:
event.setDropAction(Qt.CopyAction)
if self.files_dropped(files, event):
event.accept()
except Exception, e:
Error("There was an error processing the dropped files.", e)
raise e
def files_dropped(self, files, event):
raise NotImplementedError()
def drag_object_from_files(self, files):
if files:
drag = QDrag(self)
mime_data = QMimeData()
self._dragged_files, urls = [], []
for _file in files:
urls.append(urlunparse(('file', quote(gethostname()), \
quote(_file.name.encode('utf-8')), '','','')))
self._dragged_files.append(_file)
mime_data.setData("text/uri-list", QByteArray("\n".join(urls)))
user = os.getenv('USER')
if user:
mime_data.setData("text/x-xdnd-username", QByteArray(user))
drag.setMimeData(mime_data)
return drag
def drag_object(self, extensions):
if extensions:
files = []
for ext in extensions:
f = PersistentTemporaryFile(suffix="."+ext)
files.append(f)
return self.drag_object_from_files(files), self._dragged_files
class TableView(FileDragAndDrop, QTableView):
wrapper = textwrap.TextWrapper(width=20)
def __init__(self, parent):
FileDragAndDrop.__init__(self, QTableView)
QTableView.__init__(self, parent)
@classmethod
def wrap(cls, s, width=20):
cls.wrapper.width = width
return cls.wrapper.fill(s)
@classmethod
def human_readable(cls, size):
""" Convert a size in bytes into a human readable form """
if size < 1024:
divisor, suffix = 1, "B"
elif size < 1024*1024:
divisor, suffix = 1024., "KB"
elif size < 1024*1024*1024:
divisor, suffix = 1024*1024, "MB"
elif size < 1024*1024*1024*1024:
divisor, suffix = 1024*1024, "GB"
size = str(size/divisor)
if size.find(".") > -1:
size = size[:size.find(".")+2]
return size + " " + suffix
def render_to_pixmap(self, indices):
rect = self.visualRect(indices[0])
rects = []
for i in range(len(indices)):
rects.append(self.visualRect(indices[i]))
rect |= rects[i]
rect = rect.intersected(self.viewport().rect())
pixmap = QPixmap(rect.size())
pixmap.fill(self.palette().base().color())
painter = QPainter(pixmap)
option = self.viewOptions()
option.state |= QStyle.State_Selected
for j in range(len(indices)):
option.rect = QRect(rects[j].topLeft() - rect.topLeft(), \
rects[j].size())
self.itemDelegate(indices[j]).paint(painter, option, indices[j])
painter.end()
return pixmap
def drag_object_from_files(self, files):
drag = FileDragAndDrop.drag_object_from_files(self, files)
drag.setPixmap(self.render_to_pixmap(self.selectedIndexes()))
return drag
class CoverDisplay(FileDragAndDrop, QLabel):
def __init__(self, parent):
FileDragAndDrop.__init__(self, QLabel)
QLabel.__init__(self, parent)
def files_dropped(self, files, event):
pix = QPixmap()
for _file in files:
pix = QPixmap(_file)
if not pix.isNull(): break
if not pix.isNull():
self.emit(SIGNAL("cover_received(QPixmap)"), pix)
return True
def start_drag(self, event):
drag, files = self.drag_object(["jpeg"])
if drag and files:
_file = files[0]
_file.close()
drag.setPixmap(self.pixmap().scaledToHeight(68, \
Qt.SmoothTransformation))
self.pixmap().save(os.path.abspath(_file.name))
drag.start(Qt.MoveAction)
class DeviceView(FileDragAndDrop, QListView):
def __init__(self, parent):
FileDragAndDrop.__init__(self, QListView, enable_drag=False)
QListView.__init__(self, parent)
def hide_reader(self, x):
self.model().update_devices(reader=not x)
def hide_card(self, x):
self.model().update_devices(card=not x)
def files_dropped(self, files, event):
ids = []
md = event.mimeData()
if md.hasFormat("application/x-libprs500-id"):
ids = [ int(id) for id in FileDragAndDrop._bytes_to_string(\
md.data("application/x-libprs500-id")).split()]
index = self.indexAt(event.pos())
if index.isValid():
return self.model().files_dropped(files, index, ids)
class DeviceBooksView(TableView):
def __init__(self, parent):
TableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
class LibraryBooksView(TableView):
def __init__(self, parent):
TableView.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSortingEnabled(True)
self.setItemDelegate(LibraryDelegate(self, rating_column=4))
def dragEnterEvent(self, event):
if not event.mimeData().hasFormat("application/x-libprs500-id"):
FileDragAndDrop.dragEnterEvent(self, event)
def start_drag(self, pos):
index = self.indexAt(pos)
if index.isValid():
rows = frozenset([ index.row() for index in self.selectedIndexes()])
files = self.model().extract_formats(rows)
drag = self.drag_object_from_files(files)
if drag:
ids = [ str(self.model().id_from_row(row)) for row in rows ]
drag.mimeData().setData("application/x-libprs500-id", \
QByteArray("\n".join(ids)))
drag.start()
def files_dropped(self, files, event):
if not files: return
index = self.indexAt(event.pos())
if index.isValid():
self.model().add_formats(files, index)
else: self.emit(SIGNAL('books_dropped'), files)
class LibraryDelegate(QItemDelegate):
COLOR = QColor("blue")
SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
def __init__(self, parent, rating_column=-1):
QItemDelegate.__init__(self, parent )
self.rating_column = rating_column
self.star_path = QPainterPath()
self.star_path.moveTo(90, 50)
for i in range(1, 5):
self.star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \
50 + 40 * sin(0.8 * i * pi))
self.star_path.closeSubpath()
self.star_path.setFillRule(Qt.WindingFill)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, self.COLOR)
gradient.setColorAt(1.0, self.COLOR)
self. brush = QBrush(gradient)
self.factor = self.SIZE/100.
def sizeHint(self, option, index):
if index.column() != self.rating_column:
return QItemDelegate.sizeHint(self, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
return QSize(num*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index):
if index.column() != self.rating_column:
return QItemDelegate.paint(self, painter, option, index)
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star():
painter.save()
painter.scale(self.factor, self.factor)
painter.translate(50.0, 50.0)
painter.rotate(-20)
painter.translate(-50.0, -50.0)
painter.drawPath(self.star_path)
painter.restore()
painter.save()
try:
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.setRenderHint(QPainter.Antialiasing)
y = option.rect.center().y()-self.SIZE/2.
x = option.rect.right() - self.SIZE
painter.setPen(self.PEN)
painter.setBrush(self.brush)
painter.translate(x, y)
for i in range(num):
draw_star()
painter.translate(-self.SIZE, 0)
except Exception, e:
traceback.print_exc(e)
painter.restore()
def createEditor(self, parent, option, index):
if index.column() != 4:
return QItemDelegate.createEditor(self, parent, option, index)
editor = QSpinBox(parent)
editor.setSuffix(" stars")
editor.setMinimum(0)
editor.setMaximum(5)
editor.installEventFilter(self)
return editor
def setEditorData(self, editor, index):
if index.column() != 4:
return QItemDelegate.setEditorData(self, editor, index)
val = index.model()._data[index.row()]["rating"]
if not val: val = 0
editor.setValue(val)
def setModelData(self, editor, model, index):
if index.column() != 4:
return QItemDelegate.setModelData(self, editor, model, index)
editor.interpretText()
index.model().setData(index, QVariant(editor.value()), Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
if index.column() != 4:
return QItemDelegate.updateEditorGeometry(self, editor, option, index)
editor.setGeometry(option.rect)
class LibraryBooksModel(QAbstractTableModel):
FIELDS = ["id", "title", "authors", "size", "date", "rating", "publisher", \
"tags", "comments"]
TIME_READ_FMT = "%Y-%m-%d %H:%M:%S"
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self.db = None
self._data = None
self._orig_data = None
def extract_formats(self, rows):
files = []
for row in rows:
_id = self.id_from_row(row)
au = self._data[row]["authors"] if self._data[row]["authors"] \
else "Unknown"
basename = re.sub("\n", "", "_"+str(_id)+"_"+\
self._data[row]["title"]+" by "+ au)
exts = self.db.get_extensions(_id)
for ext in exts:
fmt = self.db.get_format(_id, ext)
if not ext:
ext =""
else:
ext = "."+ext
name = basename+ext
file = PersistentTemporaryFile(suffix=name)
if not fmt:
continue
file.write(fmt)
file.close()
files.append(file)
return files
def update_cover(self, index, pix):
spix = pix.scaledToHeight(68, Qt.SmoothTransformation)
_id = self.id_from_index(index)
qb, sqb = QBuffer(), QBuffer()
qb.open(QBuffer.ReadWrite)
sqb.open(QBuffer.ReadWrite)
pix.save(qb, "JPG")
spix.save(sqb, "JPG")
data = str(qb.data())
sdata = str(sqb.data())
qb.close()
sqb.close()
self.db.update_cover(_id, data, scaled=sdata)
self.refresh_row(index.row())
def add_formats(self, paths, index):
for path in paths:
f = open(path, "rb")
title = os.path.basename(path)
ext = title[title.rfind(".")+1:].lower() if "." in title > -1 else None
_id = self.id_from_index(index)
self.db.add_format(_id, ext, f)
f.close()
self.refresh_row(index.row())
self.emit(SIGNAL('formats_added'), index)
def rowCount(self, parent):
return len(self._data)
def columnCount(self, parent):
return len(self.FIELDS)-3
def setData(self, index, value, role):
done = False
if role == Qt.EditRole:
row = index.row()
_id = self._data[row]["id"]
col = index.column()
val = unicode(value.toString().toUtf8(), 'utf-8').strip()
if col == 0:
col = "title"
elif col == 1:
col = "authors"
elif col == 2:
return False
elif col == 3:
return False
elif col == 4:
col, val = "rating", int(value.toInt()[0])
if val < 0:
val = 0
if val > 5:
val = 5
elif col == 5:
col = "publisher"
else:
return False
self.db.set_metadata_item(_id, col, val)
self._data[row][col] = val
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == self._data[row]["id"]:
self._orig_data[i][col] = self._data[row][col]
break
done = True
return done
def update_tags_and_comments(self, index, tags, comments):
_id = self.id_from_index(index)
self.db.set_metadata_item(_id, "tags", tags)
self.db.set_metadata_item(_id, "comments", comments)
self.refresh_row(index.row())
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
if index.isValid():
if index.column() not in [2, 3]:
flags |= Qt.ItemIsEditable
return flags
def set_data(self, db):
self.db = db
self._data = self.db.get_table(self.FIELDS)
self._orig_data = self._data
self.sort(0, Qt.DescendingOrder)
self.reset()
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
elif section == 4: text = "Rating"
elif section == 5: text = "Publisher"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def info(self, row):
row = self._data[row]
cover = self.db.get_cover(row["id"])
exts = ",".join(self.db.get_extensions(row["id"]))
if cover:
pix = QPixmap()
pix.loadFromData(cover, "", Qt.AutoColor)
cover = None if pix.isNull() else pix
tags = row["tags"]
if not tags: tags = ""
comments = row["comments"]
if not comments: comments = ""
return exts, tags, comments, cover
def id_from_index(self, index): return self._data[index.row()]["id"]
def id_from_row(self, row): return self._data[row]["id"]
def refresh_row(self, row):
datum = self.db.get_row_by_id(self._data[row]["id"], self.FIELDS)
self._data[row:row+1] = [datum]
for i in range(len(self._orig_data)):
if self._orig_data[i]["id"] == datum["id"]:
self._orig_data[i:i+1] = [datum]
break
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.index(row, 0), self.index(row, self.columnCount(0)-1))
def book_info(self, _id):
""" Return title, authors and cover in a dict """
cover = self.db.get_cover(_id)
info = self.db.get_row_by_id(_id, ["title", "authors"])
info["cover"] = cover
return info
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
row, col = index.row(), index.column()
text = None
row = self._data[row]
if col == 4:
r = row["rating"] if row["rating"] else 0
if r < 0:
r = 0
if r > 5:
r = 5
return QVariant(r)
if col == 0:
text = TableView.wrap(row["title"], width=35)
elif col == 1:
au = row["authors"]
if au:
au = au.split("&")
jau = [ TableView.wrap(a, width=30).strip() for a in au ]
text = "\n".join(jau)
elif col == 2:
text = TableView.human_readable(row["size"])
elif col == 3:
text = time.strftime(TIME_WRITE_FMT, \
time.strptime(row["date"], self.TIME_READ_FMT))
elif col == 5:
pub = row["publisher"]
if pub:
text = TableView.wrap(pub, 20)
if text == None:
text = "Unknown"
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3,4]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
elif role == Qt.ToolTipRole and index.isValid():
if index.column() in [0, 1, 4, 5]:
edit = "Double click to <b>edit</b> me<br><br>"
else:
edit = ""
return QVariant(edit + "You can <b>drag and drop</b> me to the \
desktop to save all my formats to your hard disk.")
return NONE
def sort(self, col, order):
descending = order != Qt.AscendingOrder
def getter(key, func):
return lambda x : func(itemgetter(key)(x))
if col == 0: key, func = "title", lambda x : x.lower()
if col == 1: key, func = "authors", lambda x : x.split()[-1:][0].lower()\
if x else ""
if col == 2: key, func = "size", int
if col == 3: key, func = "date", lambda x: time.mktime(\
time.strptime(x, self.TIME_READ_FMT))
if col == 4: key, func = "rating", lambda x: x if x else 0
if col == 5: key, func = "publisher", lambda x : x.lower() if x else ""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
def query_in(book, q):
au = book["authors"]
if not au : au = "unknown"
pub = book["publisher"]
if not pub : pub = "unknown"
return q in book["title"].lower() or q in au.lower() or \
q in pub.lower()
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if query_in(book, q) : continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete(self, indices):
if len(indices): self.emit(SIGNAL("layoutAboutToBeChanged()"))
items = [ self._data[index.row()] for index in indices ]
for item in items:
_id = item["id"]
try:
self._data.remove(item)
except ValueError: continue
self.db.delete_by_id(_id)
for x in self._orig_data:
if x["id"] == _id: self._orig_data.remove(x)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("deleted()"))
self.db.commit()
def add_book(self, path):
""" Must call search and sort on this models view after this """
_id = self.db.add_book(path)
self._orig_data.append(self.db.get_row_by_id(_id, self.FIELDS))
class DeviceBooksModel(QAbstractTableModel):
@apply
def booklist():
doc = """ The booklist this model is based on """
def fget(self):
return self._orig_data
return property(doc=doc, fget=fget)
def __init__(self, parent):
QAbstractTableModel.__init__(self, parent)
self._data = []
self._orig_data = []
def set_data(self, book_list):
self._data = book_list
self._orig_data = book_list
self.reset()
def rowCount(self, parent): return len(self._data)
def columnCount(self, parent): return 4
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ""
if orientation == Qt.Horizontal:
if section == 0: text = "Title"
elif section == 1: text = "Author(s)"
elif section == 2: text = "Size"
elif section == 3: text = "Date"
return QVariant(self.trUtf8(text))
else: return QVariant(str(1+section))
def data(self, index, role):
if role == Qt.DisplayRole:
row, col = index.row(), index.column()
book = self._data[row]
if col == 0:
text = TableView.wrap(book.title, width=40)
elif col == 1:
au = book.author
au = au.split("&")
jau = [ TableView.wrap(a, width=25).strip() for a in au ]
text = "\n".join(jau)
elif col == 2:
text = TableView.human_readable(book.size)
elif col == 3:
text = time.strftime(TIME_WRITE_FMT, book.datetime)
return QVariant(text)
elif role == Qt.TextAlignmentRole and index.column() in [2,3]:
return QVariant(Qt.AlignRight | Qt.AlignVCenter)
return NONE
def info(self, row):
row = self._data[row]
cover = None
try:
cover = row.thumbnail
pix = QPixmap()
pix.loadFromData(cover, "", Qt.AutoColor)
cover = None if pix.isNull() else pix
except:
traceback.print_exc()
au = row.author if row.author else "Unknown"
return row.title, au, TableView.human_readable(row.size), row.mime, cover
def sort(self, col, order):
def getter(key, func):
return lambda x : func(attrgetter(key)(x))
if col == 0: key, func = "title", lambda x : x.lower()
if col == 1: key, func = "author", lambda x : x.split()[-1:][0].lower()
if col == 2: key, func = "size", int
if col == 3: key, func = "datetime", lambda x: x
descending = order != Qt.AscendingOrder
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data.sort(key=getter(key, func))
if descending: self._data.reverse()
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("sorted()"))
def search(self, query):
queries = unicode(query, 'utf-8').lower().split()
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self._data = []
for book in self._orig_data:
match = True
for q in queries:
if q in book.title.lower() or q in book.author.lower(): continue
else:
match = False
break
if match: self._data.append(book)
self.emit(SIGNAL("layoutChanged()"))
self.emit(SIGNAL("searched()"))
def delete(self, indices):
paths = []
rows = [ index.row() for index in indices ]
if not rows:
return
self.emit(SIGNAL("layoutAboutToBeChanged()"))
elems = [ self._data[row] for row in rows ]
for e in elems:
_id = e.id
paths.append(e.path)
self._orig_data.delete_book(_id)
try:
self._data.remove(e)
except ValueError:
pass
self.emit(SIGNAL("layoutChanged()"))
return paths
def path(self, index):
return self._data[index.row()].path
def title(self, index):
return self._data[index.row()].title
class DeviceModel(QAbstractListModel):
memory_free = 0
card_free = 0
show_reader = False
show_card = False
def update_devices(self, reader=None, card=None):
if reader != None:
self.show_reader = reader
if card != None:
self.show_card = card
self.emit(SIGNAL("layoutChanged()"))
def rowCount(self, parent):
base = 1
if self.show_reader:
base += 1
if self.show_card:
base += 1
return base
def update_free_space(self, reader, card):
self.memory_free = reader
self.card_free = card
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
self.index(1), self.index(2))
def data(self, index, role):
row = index.row()
data = NONE
if role == Qt.DisplayRole:
text = None
if row == 0:
text = "Library"
if row == 1 and self.show_reader:
text = "Reader\n" + TableView.human_readable(self.memory_free) \
+ " available"
elif row == 2 and self.show_card:
text = "Card\n" + TableView.human_readable(self.card_free) \
+ " available"
if text:
data = QVariant(text)
elif role == Qt.DecorationRole:
icon = None
if row == 0:
icon = QIcon(":/library")
elif row == 1 and self.show_reader:
icon = QIcon(":/reader")
elif self.show_card:
icon = QIcon(":/card")
if icon:
data = QVariant(icon)
elif role == Qt.SizeHintRole:
if row == 1:
return QVariant(QSize(150, 70))
elif role == Qt.FontRole:
font = QFont()
font.setBold(True)
data = QVariant(font)
return data
def is_library(self, index):
return index.row() == 0
def is_reader(self, index):
return index.row() == 1
def is_card(self, index):
return index.row() == 2
def files_dropped(self, files, index, ids):
ret = False
if self.is_library(index) and not ids:
self.emit(SIGNAL("books_dropped"), files)
ret = True
elif self.is_reader(index):
self.emit(SIGNAL("upload_books"), "reader", files, ids)
elif self.is_card(index):
self.emit(SIGNAL("upload_books"), "card", files, ids)
return ret

Binary file not shown.

View File

@ -1,21 +0,0 @@
## 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.
"""
This package contains logic to read and write LRF files. The LRF file format is documented at U{http://www.sven.de/librie/Librie/LrfFormat}.
At the time fo writing, this package only supports reading and writing LRF meat information. See L{meta}.
"""
__docformat__ = "epytext"
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -1,191 +0,0 @@
## 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.
import os
import shutil
import sys
import hashlib
import re
import time
import pkg_resources
import subprocess
from tempfile import mkdtemp
from optparse import OptionParser
import xml.dom.minidom as dom
from libprs500.lrf.meta import LRFException, LRFMetaFile
from libprs500.ptempfile import PersistentTemporaryFile
_bbebook = 'BBeBook-0.2.jar'
def generate_thumbnail(path):
""" Generate a JPEG thumbnail of size ~ 128x128 (aspect ratio preserved)"""
try:
import Image
except ImportError:
raise LRFException("Unable to initialize Python Imaging Library." \
"Thumbnail generation is disabled")
im = Image.open(path)
im.thumbnail((128, 128), Image.ANTIALIAS)
thumb = PersistentTemporaryFile(prefix="makelrf_", suffix=".jpeg")
thumb.close()
im = im.convert()
im.save(thumb.name)
return thumb
def create_xml(cfg):
doc = dom.getDOMImplementation().createDocument(None, None, None)
def add_field(parent, tag, value):
elem = doc.createElement(tag)
elem.appendChild(doc.createTextNode(value))
parent.appendChild(elem)
info = doc.createElement('Info')
info.setAttribute('version', '1.0')
book_info = doc.createElement('BookInfo')
doc_info = doc.createElement('DocInfo')
info.appendChild(book_info)
info.appendChild(doc_info)
add_field(book_info, 'File', cfg['File'])
add_field(doc_info, 'Output', cfg['Output'])
for field in ['Title', 'Author', 'BookID', 'Publisher', 'Label', \
'Category', 'Classification', 'Icon', 'Cover', 'FreeText']:
if cfg.has_key(field):
add_field(book_info, field, cfg[field])
add_field(doc_info, 'Language', 'en')
add_field(doc_info, 'Creator', _bbebook)
add_field(doc_info, 'CreationDate', time.strftime('%Y-%m-%d', time.gmtime()))
doc.appendChild(info)
return doc.toxml()
def makelrf(author=None, title=None, \
thumbnail=None, src=None, odir=".",\
rasterize=True, cover=None):
src = os.path.normpath(os.path.abspath(src))
bbebook = pkg_resources.resource_filename(__name__, _bbebook)
if not os.access(src, os.R_OK):
raise LRFException("Unable to read from file: " + src)
if thumbnail:
thumb = os.path.abspath(options.thumbnail)
if not os.access(thumb, os.R_OK):
raise LRFException("Unable to read from " + thumb)
else:
thumb = pkg_resources.resource_filename(__name__, 'cover.jpg')
if not author:
author = "Unknown"
if not title:
title = os.path.basename(src)
label = os.path.basename(src)
id = hashlib.md5(os.path.basename(label)).hexdigest()
name, ext = os.path.splitext(label)
cwd = os.path.dirname(src)
dirpath = None
try:
if ext == ".rar":
dirpath = mkdtemp('','makelrf')
cwd = dirpath
cmd = " ".join(["unrar", "e", '"'+src+'"'])
proc = subprocess.Popen(cmd, cwd=cwd, shell=True, stderr=subprocess.PIPE)
if proc.wait():
raise LRFException("unrar failed with error:\n\n" + \
proc.stderr.read())
path, msize = None, 0
for root, dirs, files in os.walk(dirpath):
for name in files:
if os.path.splitext(name)[1] == ".html":
size = os.stat(os.path.join(root, name)).st_size
if size > msize:
msize, path = size, os.path.join(root, name)
if not path:
raise LRFException("Could not find .html file in rar archive")
src = path
name = re.sub("\s", "_", name)
name = os.path.abspath(os.path.join(odir, name)) + ".lrf"
cfg = { 'File' : src, 'Output' : name, 'Label' : label, 'BookID' : id, \
'Author' : author, 'Title' : title, 'Publisher' : 'Unknown' \
}
if cover:
cover = os.path.normpath(os.path.abspath(cover))
try:
thumbf = generate_thumbnail(cover)
thumb = thumbf.name
except Exception, e:
print >> sys.stderr, "WARNING: Unable to generate thumbnail:\n", \
str(e)
thumb = cover
cfg['Cover'] = cover
cfg['Icon'] = thumb
config = PersistentTemporaryFile(prefix='makelrf_', suffix='.xml')
config.write(create_xml(cfg))
config.close()
jar = '-jar "' + bbebook + '"'
cmd = " ".join(["java", jar, "-r" if rasterize else "", '"'+config.name+'"'])
proc = subprocess.Popen(cmd, \
cwd=cwd, shell=True, stderr=subprocess.PIPE)
if proc.wait():
raise LRFException("BBeBook failed with error:\n\n" + \
proc.stderr.read())
# Needed as BBeBook-0.2 doesn't handle non GIF thumbnails correctly.
lrf = open(name, "r+b")
LRFMetaFile(lrf).fix_thumbnail_type()
lrf.close()
return name
finally:
if dirpath:
shutil.rmtree(dirpath, True)
def main(cargs=None):
parser = OptionParser(usage=\
"""usage: %prog [options] mybook.[html|pdf|rar]
%prog converts mybook to mybook.lrf
If you specify a rar file you must have the unrar command line client
installed. makelrf assumes the rar file is an archive containing the
html file you want converted."""\
)
parser.add_option("-t", "--title", action="store", type="string", \
dest="title", help="Set the book title")
parser.add_option("-a", "--author", action="store", type="string", \
dest="author", help="Set the author")
parser.add_option('-r', '--rasterize', action='store_true', \
dest="rasterize",
help="Convert pdfs into image files.")
parser.add_option('-c', '--cover', action='store', dest='cover',\
help="Path to a graphic that will be set as the cover. "\
"If it is specified the thumbnail is automatically "\
"generated from it")
parser.add_option("--thumbnail", action="store", type="string", \
dest="thumbnail", \
help="Path to a graphic that will be set as the thumbnail")
if not cargs:
cargs = sys.argv
options, args = parser.parse_args()
if len(args) != 1:
parser.print_help()
sys.exit(1)
src = args[0]
root, ext = os.path.splitext(src)
if ext not in ['.html', '.pdf', '.rar']:
print >> sys.stderr, "Can only convert files ending in .html|.pdf|.rar"
parser.print_help()
sys.exit(1)
name = makelrf(author=options.author, title=options.title, \
thumbnail=options.thumbnail, src=src, cover=options.cover, \
rasterize=options.rasterize)
print "LRF generated:", name

View File

@ -1,481 +0,0 @@
## 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.
"""
This module presents an easy to use interface for getting and setting
meta information in LRF files.
Just create an L{LRFMetaFile} object and use its properties
to get and set meta information. For example:
>>> lrf = LRFMetaFile("mybook.lrf")
>>> print lrf.title, lrf.author
>>> lrf.category = "History"
"""
import struct
import array
import zlib
import xml.dom.minidom as dom
from libprs500.prstypes import field
BYTE = "<B" #: Unsigned char little endian encoded in 1 byte
WORD = "<H" #: Unsigned short little endian encoded in 2 bytes
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
QWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class versioned_field(field):
def __init__(self, vfield, version, start=0, fmt=WORD):
field.__init__(self, start=start, fmt=fmt)
self.vfield, self.version = vfield, version
def enabled(self):
return self.vfield > self.version
def __get__(self, obj, typ=None):
if self.enabled():
return field.__get__(self, obj, typ=typ)
else:
return None
def __set__(self, obj, val):
if not self.enabled():
raise LRFException("Trying to set disabled field")
else:
field.__set__(self, obj, val)
class LRFException(Exception):
pass
class fixed_stringfield(object):
""" A field storing a variable length string. """
def __init__(self, length=8, start=0):
"""
@param length: Size of this string
@param start: The byte at which this field is stored in the buffer
"""
self._length = length
self._start = start
def __get__(self, obj, typ=None):
length = str(self._length)
return obj.unpack(start=self._start, fmt="<"+length+"s")[0]
def __set__(self, obj, val):
if val.__class__.__name__ != 'str': val = str(val)
if len(val) != self._length:
raise LRFException("Trying to set fixed_stringfield with a " + \
"string of incorrect length")
obj.pack(val, start=self._start, fmt="<"+str(len(val))+"s")
def __repr__(self):
return "A string of length " + str(self._length) + \
" starting at byte " + str(self._start)
class xml_field(object):
"""
Descriptor that gets and sets XML based meta information from an LRF file.
Works for simple XML fields of the form <tagname>data</tagname>
"""
def __init__(self, tag_name, parent="BookInfo"):
"""
@param tag_name: The XML tag whose data we operate on
@param parent: The tagname of the parent element of C{tag_name}
"""
self.tag_name = tag_name
self.parent = parent
def __get__(self, obj, typ=None):
""" Return the data in this field or '' if the field is empty """
document = dom.parseString(obj.info)
elems = document.getElementsByTagName(self.tag_name)
if len(elems):
elem = None
for candidate in elems:
if candidate.parentNode.nodeName == self.parent:
elem = candidate
if elem:
elem.normalize()
if elem.hasChildNodes():
return elem.firstChild.data.strip()
return ""
def __set__(self, obj, val):
document = dom.parseString(obj.info)
def create_elem():
elem = document.createElement(self.tag_name)
elem.appendChild(dom.Text())
parent = document.getElementsByTagName(self.parent)[0]
parent.appendChild(elem)
return elem
if not val:
val = u''
if type(val).__name__ != 'unicode':
val = unicode(val, 'utf-8')
elems = document.getElementsByTagName(self.tag_name)
elem = None
if len(elems):
for candidate in elems:
if candidate.parentNode.nodeName == self.parent:
elem = candidate
if not elem:
elem = create_elem()
else:
elem.normalize()
while elem.hasChildNodes():
elem.removeChild(elem.lastChild)
elem.appendChild(dom.Text())
else:
elem = create_elem()
elem.firstChild.data = val
info = document.toxml(encoding='utf-16')
obj.info = info
def __str__(self):
return self.tag_name
def __repr__(self):
return "XML Field: " + self.tag_name + " in " + self.parent
class LRFMetaFile(object):
""" Has properties to read and write all Meta information in a LRF file. """
# The first 8 bytes of all valid LRF files
LRF_HEADER = 'LRF'.encode('utf-16le')+'\0\0'
lrf_header = fixed_stringfield(length=8, start=0)
version = field(fmt=WORD, start=8)
xor_key = field(fmt=WORD, start=10)
root_object_id = field(fmt=DWORD, start=12)
number_of_objets = field(fmt=QWORD, start=16)
object_index_offset = field(fmt=QWORD, start=24)
binding = field(fmt=BYTE, start=36)
dpi = field(fmt=WORD, start=38)
width = field(fmt=WORD, start=42)
height = field(fmt=WORD, start=44)
color_depth = field(fmt=BYTE, start=46)
toc_object_id = field(fmt=DWORD, start=0x44)
toc_object_offset = field(fmt=DWORD, start=0x48)
compressed_info_size = field(fmt=WORD, start=0x4c)
thumbnail_type = versioned_field(version, 800, fmt=WORD, start=0x4e)
thumbnail_size = versioned_field(version, 800, fmt=DWORD, start=0x50)
uncompressed_info_size = versioned_field(compressed_info_size, 0, \
fmt=DWORD, start=0x54)
title = xml_field("Title", parent="BookInfo")
author = xml_field("Author", parent="BookInfo")
book_id = xml_field("BookID", parent="BookInfo")
publisher = xml_field("Publisher", parent="BookInfo")
label = xml_field("Label", parent="BookInfo")
category = xml_field("Category", parent="BookInfo")
classification = xml_field("Classification", parent="BookInfo")
free_text = xml_field("FreeText", parent="BookInfo")
language = xml_field("Language", parent="DocInfo")
creator = xml_field("Creator", parent="DocInfo")
# Format is %Y-%m-%d
creation_date = xml_field("CreationDate", parent="DocInfo")
producer = xml_field("Producer", parent="DocInfo")
page = xml_field("Page", parent="DocInfo")
def safe(func):
"""
Decorator that ensures that function calls leave the pos
in the underlying file unchanged
"""
def restore_pos(*args, **kwargs):
obj = args[0]
pos = obj._file.tell()
res = func(*args, **kwargs)
obj._file.seek(0, 2)
if obj._file.tell() >= pos:
obj._file.seek(pos)
return res
return restore_pos
def safe_property(func):
"""
Decorator that ensures that read or writing a property leaves
the position in the underlying file unchanged
"""
def decorator(f):
def restore_pos(*args, **kwargs):
obj = args[0]
pos = obj._file.tell()
res = f(*args, **kwargs)
obj._file.seek(0, 2)
if obj._file.tell() >= pos:
obj._file.seek(pos)
return res
return restore_pos
locals_ = func()
if locals_.has_key("fget"):
locals_["fget"] = decorator(locals_["fget"])
if locals_.has_key("fset"):
locals_["fset"] = decorator(locals_["fset"])
return property(**locals_)
@safe_property
def info():
doc = \
"""
Document meta information in raw XML format as a byte string encoded in
utf-16.
To set use raw XML in a byte string encoded in utf-16.
"""
def fget(self):
if self.compressed_info_size == 0:
raise LRFException("This document has no meta info")
size = self.compressed_info_size - 4
self._file.seek(self.info_start)
try:
src = zlib.decompress(self._file.read(size))
if len(src) != self.uncompressed_info_size:
raise LRFException("Decompression of document meta info\
yielded unexpected results")
candidate = unicode(src, 'utf-16')
# LRF files produced with makelrf dont have a correctly
# encoded metadata block.
# Decoding using latin1 is the most useful for me since I
# occassionally read french books.
if not u"Info" in candidate:
candidate = unicode(src, 'latin1', errors='ignore')
if candidate[-1:] == '\0':
candidate = candidate[:-1]
candidate = dom.parseString(candidate.encode('utf-8')).\
toxml(encoding='utf-16')
else:
candidate = candidate.encode('utf-16')
return candidate.strip()
except zlib.error, e:
raise LRFException("Unable to decompress document meta information")
def fset(self, info):
self.uncompressed_info_size = len(info)
stream = zlib.compress(info)
self.compressed_info_size = len(stream) + 4
self._file.seek(self.info_start)
self._file.write(stream)
self._file.flush()
return { "fget":fget, "fset":fset, "doc":doc }
@safe_property
def thumbnail_pos():
doc = """ The position of the thumbnail in the LRF file """
def fget(self):
return self.info_start+ self.compressed_info_size-4
return { "fget":fget, "doc":doc }
@classmethod
def _detect_thumbnail_type(cls, slice):
""" @param slice: The first 16 bytes of the thumbnail """
ttype = 0x14 # GIF
if "PNG" in slice:
ttype = 0x12
if "BM" in slice:
ttype = 0x13
if "JFIF" in slice:
ttype = 0x11
return ttype
@safe_property
def thumbnail():
doc = \
"""
The thumbnail.
Represented as a string.
The string you would get from the file read function.
"""
def fget(self):
size = self.thumbnail_size
if size:
self._file.seek(self.thumbnail_pos)
return self._file.read(size)
def fset(self, data):
if self.version <= 800:
raise LRFException("Cannot store thumbnails in LRF files \
of version <= 800")
slice = data[0:16]
orig_size = self.thumbnail_size
self._file.seek(self.toc_object_offset)
toc = self._file.read(self.object_index_offset - self.toc_object_offset)
self._file.seek(self.object_index_offset)
objects = self._file.read()
self.thumbnail_size = len(data)
self._file.seek(self.thumbnail_pos)
self._file.write(data)
orig_offset = self.toc_object_offset
self.toc_object_offset = self._file.tell()
self._file.write(toc)
self.object_index_offset = self._file.tell()
self._file.write(objects)
self._file.flush()
self._file.truncate() # Incase old thumbnail was bigger than new
self.thumbnail_type = self._detect_thumbnail_type(slice)
# Needed as new thumbnail may have different size than old thumbnail
self.update_object_offsets(self.toc_object_offset - orig_offset)
return { "fget":fget, "fset":fset, "doc":doc }
def __init__(self, file):
""" @param file: A file object opened in the r+b mode """
file.seek(0, 2)
self.size = file.tell()
self._file = file
if self.lrf_header != LRFMetaFile.LRF_HEADER:
raise LRFException(file.name + \
" has an invalid LRF header. Are you sure it is an LRF file?")
# Byte at which the compressed meta information starts
self.info_start = 0x58 if self.version > 800 else 0x53
@safe
def update_object_offsets(self, delta):
""" Run through the LRF Object index changing the offset by C{delta}. """
self._file.seek(self.object_index_offset)
while(True):
try:
self._file.read(4)
except EOFError:
break
pos = self._file.tell()
try:
offset = self.unpack(fmt=DWORD, start=pos)[0] + delta
except struct.error:
break
if offset >= (2**8)**4:
# New offset is larger than a DWORD, so leave
# offset unchanged
offset -= delta
self.pack(offset, fmt=DWORD, start=pos)
try:
self._file.read(12)
except EOFError:
break
self._file.flush()
@safe
def unpack(self, fmt=DWORD, start=0):
"""
Return decoded data from file.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in file from which to decode
"""
end = start + struct.calcsize(fmt)
self._file.seek(start)
self._file.seek(start)
ret = struct.unpack(fmt, self._file.read(end-start))
return ret
@safe
def pack(self, *args, **kwargs):
"""
Encode C{args} and write them to file.
C{kwargs} must contain the keywords C{fmt} and C{start}
@param args: The values to pack
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in file at which to write encoded data
"""
encoded = struct.pack(kwargs["fmt"], *args)
self._file.seek(kwargs["start"])
self._file.write(encoded)
self._file.flush()
def thumbail_extension(self):
"""
Return the extension for the thumbnail image type as specified
by L{self.thumbnail_type}. If the LRF file was created by buggy
software, the extension maye be incorrect. See L{self.fix_thumbnail_type}.
"""
ext = "gif"
ttype = self.thumbnail_type
if ttype == 0x11:
ext = "jpeg"
elif ttype == 0x12:
ext = "png"
elif ttype == 0x13:
ext = "bm"
return ext
def fix_thumbnail_type(self):
"""
Attempt to guess the thumbnail image format and set
L{self.thumbnail_type} accordingly.
"""
slice = self.thumbnail[0:16]
self.thumbnail_type = self._detect_thumbnail_type(slice)
def main():
import sys, os.path
from optparse import OptionParser
from libprs500 import __version__ as VERSION
parser = OptionParser(usage="usage: %prog [options] mybook.lrf\n\
\nWARNING: Based on reverse engineering the LRF format."+\
" Making changes may render your LRF file unreadable. ", \
version=VERSION)
parser.add_option("-t", "--title", action="store", type="string", \
dest="title", help="Set the book title")
parser.add_option("-a", "--author", action="store", type="string", \
dest="author", help="Set the author")
parser.add_option("-c", "--category", action="store", type="string", \
dest="category", help="The category this book belongs"+\
" to. E.g.: History")
parser.add_option("--thumbnail", action="store", type="string", \
dest="thumbnail", help="Path to a graphic that will be"+\
" set as this files' thumbnail")
parser.add_option("--get-thumbnail", action="store_true", \
dest="get_thumbnail", default=False, \
help="Extract thumbnail from LRF file")
parser.add_option("-p", "--page", action="store", type="string", \
dest="page", help="Don't know what this is for")
options, args = parser.parse_args()
if len(args) != 1:
parser.print_help()
sys.exit(1)
lrf = LRFMetaFile(open(args[0], "r+b"))
if options.title:
lrf.title = options.title
if options.author:
lrf.author = options.author
if options.category:
lrf.category = options.category
if options.page:
lrf.page = options.page
if options.thumbnail:
f = open(options.thumbnail, "r")
lrf.thumbnail = f.read()
f.close()
if options.get_thumbnail:
t = lrf.thumbnail
td = "None"
if t and len(t) > 0:
td = os.path.basename(args[0])+"_thumbnail_."+lrf.thumbail_extension()
f = open(td, "w")
f.write(t)
f.close()
fields = LRFMetaFile.__dict__.items()
for f in fields:
if "XML" in str(f):
print str(f[1]) + ":", lrf.__getattribute__(f[0])
if options.get_thumbnail:
print "Thumbnail:", td

View File

@ -1,838 +0,0 @@
## 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.
"""
Defines the structure of packets that are sent to/received from the device.
Packet structure is defined using classes and inheritance. Each class is a
view that imposes structure on the underlying data buffer.
The data buffer is encoded in little-endian format, but you don't
have to worry about that if you are using the classes.
The classes have instance variables with getter/setter functions defined
to take care of the encoding/decoding.
The classes are intended to mimic C structs.
There are three kinds of packets. L{Commands<Command>},
L{Responses<Response>}, and L{Answers<Answer>}.
C{Commands} are sent to the device on the control bus,
C{Responses} are received from the device,
also on the control bus. C{Answers} and their sub-classes represent
data packets sent to/received from the device via bulk transfers.
Commands are organized as follows: G{classtree Command}
You will typically only use sub-classes of Command.
Responses are organized as follows: G{classtree Response}
Responses inherit Command as they share header structure.
Answers are organized as follows: G{classtree Answer}
"""
import struct
import time
from libprs500.errors import PacketError
DWORD = "<I" #: Unsigned integer little endian encoded in 4 bytes
DDWORD = "<Q" #: Unsigned long long little endian encoded in 8 bytes
class PathResponseCodes(object):
""" Known response commands to path related commands """
NOT_FOUND = 0xffffffd7
INVALID = 0xfffffff9
IS_FILE = 0xffffffd2
HAS_CHILDREN = 0xffffffcc
class TransferBuffer(list):
"""
Represents raw (unstructured) data packets sent over the usb bus.
C{TransferBuffer} is a wrapper around the tuples used by L{PyUSB<usb>} for communication.
It has convenience methods to read and write data from the underlying buffer. See
L{TransferBuffer.pack} and L{TransferBuffer.unpack}.
"""
def __init__(self, packet):
"""
Create a L{TransferBuffer} from C{packet} or an empty buffer.
@type packet: integer or listable object
@param packet: If packet is a list, it is copied into the C{TransferBuffer} and then normalized (see L{TransferBuffer._normalize}).
If it is an integer, a zero buffer of that length is created.
"""
if "__len__" in dir(packet):
list.__init__(self, list(packet))
self._normalize()
else: list.__init__(self, [0 for i in range(packet)])
def __add__(self, tb):
""" Return a TransferBuffer rather than a list as the sum """
return TransferBuffer(list.__add__(self, tb))
def __getslice__(self, start, end):
""" Return a TransferBuffer rather than a list as the slice """
return TransferBuffer(list.__getslice__(self, start, end))
def __str__(self):
"""
Return a string representation of this buffer.
Packets are represented as hex strings, in 2-byte pairs, S{<=} 16 bytes to a line. An ASCII representation is included. For example::
0700 0100 0000 0000 0000 0000 0c00 0000 ................
0200 0000 0400 0000 4461 7461 ........Data
"""
ans, ascii = ": ".rjust(10,"0"), ""
for i in range(0, len(self), 2):
for b in range(2):
try:
ans += TransferBuffer.phex(self[i+b])
ascii += chr(self[i+b]) if self[i+b] > 31 and self[i+b] < 127 else "."
except IndexError: break
ans = ans + " "
if (i+2)%16 == 0:
if i+2 < len(self):
ans += " " + ascii + "\n" + (TransferBuffer.phex(i+2)+": ").rjust(10, "0")
ascii = ""
last_line = ans[ans.rfind("\n")+1:]
padding = 50 - len(last_line)
ans += "".ljust(padding) + " " + ascii
return ans.strip()
def unpack(self, fmt=DWORD, start=0):
"""
Return decoded data from buffer.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer from which to decode
"""
end = start + struct.calcsize(fmt)
return struct.unpack(fmt, "".join([ chr(i) for i in list.__getslice__(self, start, end) ]))
def pack(self, val, fmt=DWORD, start=0):
"""
Encode C{val} and write it to buffer.
@param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
@param start: Position in buffer at which to write encoded data
"""
self[start:start+struct.calcsize(fmt)] = [ ord(i) for i in struct.pack(fmt, val) ]
def _normalize(self):
""" Replace negative bytes in C{self} by 256 + byte """
for i in range(len(self)):
if self[i] < 0:
self[i] = 256 + self[i]
@classmethod
def phex(cls, num):
"""
Return the hex representation of num without the 0x prefix.
If the hex representation is only 1 digit it is padded to the left with a zero. Used in L{TransferBuffer.__str__}.
"""
index, sign = 2, ""
if num < 0:
index, sign = 3, "-"
h = hex(num)[index:]
if len(h) < 2:
h = "0"+h
return sign + h
class field(object):
""" A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}, that implements access
to protocol packets in a human readable way.
"""
def __init__(self, start=16, fmt=DWORD):
"""
@param start: The byte at which this field is stored in the buffer
@param fmt: The packing format for this field.
See U{struct<http://docs.python.org/lib/module-struct.html>}.
"""
self._fmt, self._start = fmt, start
def __get__(self, obj, typ=None):
return obj.unpack(start=self._start, fmt=self._fmt)[0]
def __set__(self, obj, val):
obj.pack(val, start=self._start, fmt=self._fmt)
def __repr__(self):
typ = ""
if self._fmt == DWORD:
typ = "unsigned int"
if self._fmt == DDWORD:
typ = "unsigned long long"
return "An " + typ + " stored in " + \
str(struct.calcsize(self._fmt)) + \
" bytes starting at byte " + str(self._start)
class stringfield(object):
""" A field storing a variable length string. """
def __init__(self, length_field, start=16):
"""
@param length_field: A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}
that returns the length of the string.
@param start: The byte at which this field is stored in the buffer
"""
self._length_field = length_field
self._start = start
def __get__(self, obj, typ=None):
length = str(self._length_field.__get__(obj))
return obj.unpack(start=self._start, fmt="<"+length+"s")[0]
def __set__(self, obj, val):
val = str(val)
obj.pack(val, start=self._start, fmt="<"+str(len(val))+"s")
def __repr__(self):
return "A string starting at byte " + str(self._start)
class Command(TransferBuffer):
""" Defines the structure of command packets sent to the device. """
# Command number. C{unsigned int} stored in 4 bytes at byte 0.
#
# Command numbers are:
# 0 GetUsbProtocolVersion
# 1 ReqEndSession
# 10 FskFileOpen
# 11 FskFileClose
# 12 FskGetSize
# 13 FskSetSize
# 14 FskFileSetPosition
# 15 FskGetPosition
# 16 FskFileRead
# 17 FskFileWrite
# 18 FskFileGetFileInfo
# 19 FskFileSetFileInfo
# 1A FskFileCreate
# 1B FskFileDelete
# 1C FskFileRename
# 30 FskFileCreateDirectory
# 31 FskFileDeleteDirectory
# 32 FskFileRenameDirectory
# 33 FskDirectoryIteratorNew
# 34 FskDirectoryIteratorDispose
# 35 FskDirectoryIteratorGetNext
# 52 FskVolumeGetInfo
# 53 FskVolumeGetInfoFromPath
# 80 FskFileTerminate
# 100 ConnectDevice
# 101 GetProperty
# 102 GetMediaInfo
# 103 GetFreeSpace
# 104 SetTime
# 105 DeviceBeginEnd
# 106 UnlockDevice
# 107 SetBulkSize
# 110 GetHttpRequest
# 111 SetHttpRespponse
# 112 Needregistration
# 114 GetMarlinState
# 200 ReqDiwStart
# 201 SetDiwPersonalkey
# 202 GetDiwPersonalkey
# 203 SetDiwDhkey
# 204 GetDiwDhkey
# 205 SetDiwChallengeserver
# 206 GetDiwChallengeserver
# 207 GetDiwChallengeclient
# 208 SetDiwChallengeclient
# 209 GetDiwVersion
# 20A SetDiwWriteid
# 20B GetDiwWriteid
# 20C SetDiwSerial
# 20D GetDiwModel
# 20C SetDiwSerial
# 20E GetDiwDeviceid
# 20F GetDiwSerial
# 210 ReqDiwCheckservicedata
# 211 ReqDiwCheckiddata
# 212 ReqDiwCheckserialdata
# 213 ReqDiwFactoryinitialize
# 214 GetDiwMacaddress
# 215 ReqDiwTest
# 216 ReqDiwDeletekey
# 300 UpdateChangemode
# 301 UpdateDeletePartition
# 302 UpdateCreatePartition
# 303 UpdateCreatePartitionWithImage
# 304 UpdateGetPartitionSize
number = field(start=0, fmt=DWORD)
# Known types are 0x00 and 0x01. Acknowledge commands are always type 0x00
type = field(start=4, fmt=DDWORD)
# Length of the data part of this packet
length = field(start=12, fmt=DWORD)
@apply
def data():
doc = \
"""
The data part of this command. Returned/set as/by a TransferBuffer.
Stored at byte 16.
Setting it by default changes self.length to the length of the new
buffer. You may have to reset it to the significant part of the buffer.
You would normally use the C{command} property of
L{ShortCommand} or L{LongCommand} instead.
"""
def fget(self):
return self[16:]
def fset(self, buff):
self[16:] = buff
self.length = len(buff)
return property(doc=doc, fget=fget, fset=fset)
def __init__(self, packet):
"""
@param packet: len(packet) > 15 or packet > 15
"""
if ("__len__" in dir(packet) and len(packet) < 16) or\
("__len__" not in dir(packet) and packet < 16):
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have length atleast 16")
TransferBuffer.__init__(self, packet)
class SetTime(Command):
"""
Set time on device. All fields refer to time in the GMT time zone.
"""
NUMBER = 0x104
# -time.timezone with negative numbers encoded
# as int(0xffffffff +1 -time.timezone/60.)
timezone = field(start=0x10, fmt=DWORD)
year = field(start=0x14, fmt=DWORD) #: year e.g. 2006
month = field(start=0x18, fmt=DWORD) #: month 1-12
day = field(start=0x1c, fmt=DWORD) #: day 1-31
hour = field(start=0x20, fmt=DWORD) #: hour 0-23
minute = field(start=0x24, fmt=DWORD) #: minute 0-59
second = field(start=0x28, fmt=DWORD) #: second 0-59
def __init__(self, t=None):
""" @param t: time as an epoch """
self.number = SetTime.NUMBER
self.type = 0x01
self.length = 0x1c
tz = int(-time.timezone/60.)
self.timezone = tz if tz > 0 else 0xffffffff +1 + tz
if not t: t = time.time()
t = time.gmtime(t)
self.year = t[0]
self.month = t[1]
self.day = t[2]
self.hour = t[3]
self.minute = t[4]
# Hack you should actually update the entire time tree is
# second is > 59
self.second = t[5] if t[5] < 60 else 59
class ShortCommand(Command):
""" A L{Command} whoose data section is 4 bytes long """
SIZE = 20 #: Packet size in bytes
# Usually carries additional information
command = field(start=16, fmt=DWORD)
def __init__(self, number=0x00, type=0x00, command=0x00):
"""
@param number: L{Command.number}
@param type: L{Command.type}
@param command: L{ShortCommand.command}
"""
Command.__init__(self, ShortCommand.SIZE)
self.number = number
self.type = type
self.length = 4
self.command = command
class DirRead(ShortCommand):
""" The command that asks the device to send the next item in the list """
NUMBER = 0x35 #: Command number
def __init__(self, _id):
""" @param id: The identifier returned as a result of a L{DirOpen} command """
ShortCommand.__init__(self, number=DirRead.NUMBER, type=0x01, \
command=_id)
class DirClose(ShortCommand):
""" Close a previously opened directory """
NUMBER = 0x34 #: Command number
def __init__(self, _id):
""" @param id: The identifier returned as a result of a L{DirOpen} command """
ShortCommand.__init__(self, number=DirClose.NUMBER, type=0x01,
command=_id)
class EndSession(ShortCommand):
"""
Ask device to change status to 'USB connected' i.e., tell the
device that the present sequence of commands is complete
"""
NUMBER = 0x1 #: Command number
def __init__(self):
ShortCommand.__init__(self, \
number=EndSession.NUMBER, type=0x01, command=0x00)
class GetUSBProtocolVersion(ShortCommand):
""" Get USB Protocol version used by device """
NUMBER = 0x0 #: Command number
def __init__(self):
ShortCommand.__init__(self, \
number=GetUSBProtocolVersion.NUMBER, \
type=0x01, command=0x00)
class SetBulkSize(ShortCommand):
""" Set size for bulk transfers in this session """
NUMBER = 0x107 #: Command number
def __init__(self, size=0x028000):
ShortCommand.__init__(self, \
number=SetBulkSize.NUMBER, type=0x01, command=size)
class UnlockDevice(ShortCommand):
""" Unlock the device """
NUMBER = 0x106 #: Command number
def __init__(self, key=0x312d):
ShortCommand.__init__(self, \
number=UnlockDevice.NUMBER, type=0x01, command=key)
class LongCommand(Command):
""" A L{Command} whoose data section is 16 bytes long """
SIZE = 32 #: Size in bytes of C{LongCommand} packets
def __init__(self, number=0x00, type=0x00, command=0x00):
"""
@param number: L{Command.number}
@param type: L{Command.type}
@param command: L{LongCommand.command}
"""
Command.__init__(self, LongCommand.SIZE)
self.number = number
self.type = type
self.length = 16
self.command = command
@apply
def command():
doc = \
"""
Usually carries extra information needed for the command
It is a list of C{unsigned integers} of length between 1 and 4. 4
C{unsigned int} stored in 16 bytes at byte 16.
"""
def fget(self):
return self.unpack(start=16, fmt="<"+str(self.length/4)+"I")
def fset(self, val):
if "__len__" not in dir(val): val = (val,)
start = 16
for command in val:
self.pack(command, start=start, fmt=DWORD)
start += struct.calcsize(DWORD)
return property(doc=doc, fget=fget, fset=fset)
class PathCommand(Command):
""" Abstract class that defines structure common to all path related commands. """
path_length = field(start=16, fmt=DWORD) #: Length of the path to follow
path = stringfield(path_length, start=20) #: The path this query is about
def __init__(self, path, number, path_len_at_byte=16):
Command.__init__(self, path_len_at_byte+4+len(path))
self.path_length = len(path)
self.path = path
self.type = 0x01
self.length = len(self) - 16
self.number = number
class TotalSpaceQuery(PathCommand):
""" Query the total space available on the volume represented by path """
NUMBER = 0x53 #: Command number
def __init__(self, path):
""" @param path: valid values are 'a:', 'b:', '/Data/' """
PathCommand.__init__(self, path, TotalSpaceQuery.NUMBER)
class FreeSpaceQuery(ShortCommand):
""" Query the free space available """
NUMBER = 0x103 #: Command number
def __init__(self, where):
""" @param where: valid values are: 'a:', 'b:', '/' """
c = 0
if where.startswith('a:'): c = 1
elif where.startswith('b:'): c = 2
ShortCommand.__init__(self, \
number=FreeSpaceQuery.NUMBER, type=0x01, command=c)
class DirCreate(PathCommand):
""" Create a directory """
NUMBER = 0x30
def __init__(self, path):
PathCommand.__init__(self, path, DirCreate.NUMBER)
class DirOpen(PathCommand):
""" Open a directory for reading its contents """
NUMBER = 0x33 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, DirOpen.NUMBER)
class AcknowledgeBulkRead(LongCommand):
""" Must be sent to device after a bulk read """
def __init__(self, bulk_read_id):
"""
bulk_read_id is an integer, the id of the bulk read
we are acknowledging. See L{Answer.id}
"""
LongCommand.__init__(self, number=0x1000, \
type=0x00, command=bulk_read_id)
class DeviceInfoQuery(Command):
""" The command used to ask for device information """
NUMBER = 0x101 #: Command number
def __init__(self):
Command.__init__(self, 16)
self.number = DeviceInfoQuery.NUMBER
self.type = 0x01
class FileClose(ShortCommand):
""" File close command """
NUMBER = 0x11 #: Command number
def __init__(self, _id):
ShortCommand.__init__(self, number=FileClose.NUMBER, \
type=0x01, command=_id)
class FileCreate(PathCommand):
""" Create a file """
NUMBER = 0x1a #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, FileCreate.NUMBER)
class FileDelete(PathCommand):
""" Delete a file """
NUMBER = 0x1B
def __init__(self, path):
PathCommand.__init__(self, path, FileDelete.NUMBER)
class DirDelete(PathCommand):
""" Delete a directory """
NUMBER = 0x31
def __init__(self, path):
PathCommand.__init__(self, path, DirDelete.NUMBER)
class FileOpen(PathCommand):
""" File open command """
NUMBER = 0x10 #: Command number
READ = 0x00 #: Open file in read mode
WRITE = 0x01 #: Open file in write mode
path_length = field(start=20, fmt=DWORD)
path = stringfield(path_length, start=24)
def __init__(self, path, mode=0x00):
PathCommand.__init__(self, path, FileOpen.NUMBER, path_len_at_byte=20)
self.mode = mode
@apply
def mode():
doc = \
"""
The file open mode. Is either L{FileOpen.READ}
or L{FileOpen.WRITE}. C{unsigned int} stored at byte 16.
"""
def fget(self):
return self.unpack(start=16, fmt=DWORD)[0]
def fset(self, val):
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class FileIO(Command):
""" Command to read/write from an open file """
RNUMBER = 0x16 #: Command number to read from a file
WNUMBER = 0x17 #: Command number to write to a file
id = field(start=16, fmt=DWORD) #: The file ID returned by a FileOpen command
offset = field(start=20, fmt=DDWORD) #: offset in the file at which to read
size = field(start=28, fmt=DWORD) #: The number of bytes to reead from file.
def __init__(self, _id, offset, size, mode=0x16):
"""
@param _id: File identifier returned by a L{FileOpen} command
@type id: C{unsigned int}
@param offset: Position in file at which to read
@type offset: C{unsigned long long}
@param size: number of bytes to read
@type size: C{unsigned int}
@param mode: Either L{FileIO.RNUMBER} or L{File.WNUMBER}
"""
Command.__init__(self, 32)
self.number = mode
self.type = 0x01
self.length = 16
self.id = _id
self.offset = offset
self.size = size
class PathQuery(PathCommand):
""" Defines structure of command that requests information about a path """
NUMBER = 0x18 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, PathQuery.NUMBER)
class SetFileInfo(PathCommand):
""" Set File information """
NUMBER = 0x19 #: Command number
def __init__(self, path):
PathCommand.__init__(self, path, SetFileInfo.NUMBER)
class Response(Command):
"""
Defines the structure of response packets received from the device.
C{Response} inherits from C{Command} as the
first 16 bytes have the same structure.
"""
SIZE = 32 #: Size of response packets in the SONY protocol
# Response number, the command number of a command
# packet sent sometime before this packet was received
rnumber = field(start=16, fmt=DWORD)
# Used to indicate error conditions. A value of 0 means
# there was no error
code = field(start=20, fmt=DWORD)
# Used to indicate the size of the next bulk read
data_size = field(start=28, fmt=DWORD)
def __init__(self, packet):
""" C{len(packet) == Response.SIZE} """
if len(packet) != Response.SIZE:
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have exactly " + \
str(Response.SIZE) + " bytes not " + str(len(packet)))
Command.__init__(self, packet)
if self.number != 0x00001000:
raise PacketError("Response packets must have their number set to " \
+ hex(0x00001000))
@apply
def data():
doc = \
"""
The last 3 DWORDs (12 bytes) of data in this
response packet. Returned as a list of unsigned integers.
"""
def fget(self):
return self.unpack(start=20, fmt="<III")
def fset(self, val):
self.pack(val, start=20, fmt="<III")
return property(doc=doc, fget=fget, fset=fset)
class ListResponse(Response):
"""
Defines the structure of response packets received
during list (ll) queries. See L{PathQuery}.
"""
IS_FILE = 0xffffffd2 #: Queried path is a file
IS_INVALID = 0xfffffff9 #: Queried path is malformed/invalid
# Queried path is not mounted (i.e. a removed storage card/stick)
IS_UNMOUNTED = 0xffffffc8
IS_EOL = 0xfffffffa #: There are no more entries in the list
PATH_NOT_FOUND = 0xffffffd7 #: Queried path is not found
@apply
def is_file():
doc = """ True iff queried path is a file """
def fget(self):
return self.code == ListResponse.IS_FILE
return property(doc=doc, fget=fget)
@apply
def is_invalid():
doc = """ True iff queried path is invalid """
def fget(self):
return self.code == ListResponse.IS_INVALID
return property(doc=doc, fget=fget)
@apply
def path_not_found():
doc = """ True iff queried path is not found """
def fget(self):
return self.code == ListResponse.PATH_NOT_FOUND
return property(doc=doc, fget=fget)
@apply
def is_unmounted():
doc = """ True iff queried path is unmounted (i.e. removed storage card) """
def fget(self):
return self.code == ListResponse.IS_UNMOUNTED
return property(doc=doc, fget=fget)
@apply
def is_eol():
doc = """ True iff there are no more items in the list """
def fget(self):
return self.code == ListResponse.IS_EOL
return property(doc=doc, fget=fget)
class Answer(TransferBuffer):
"""
Defines the structure of packets sent to host via a
bulk transfer (i.e., bulk reads)
"""
number = field(start=0, fmt=DWORD) #: Answer identifier
length = field(start=12, fmt=DWORD) #: Length of data to follow
def __init__(self, packet):
""" @param packet: C{len(packet)} S{>=} C{16} """
if "__len__" in dir(packet):
if len(packet) < 16 :
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have a length of atleast 16 bytes")
elif packet < 16:
raise PacketError(str(self.__class__)[7:-2] + \
" packets must have a length of atleast 16 bytes")
TransferBuffer.__init__(self, packet)
class FileProperties(Answer):
"""
Defines the structure of packets that contain size, date and
permissions information about files/directories.
"""
file_size = field(start=16, fmt=DDWORD) #: Size in bytes of the file
file_type = field(start=24, fmt=DWORD) #: 1 == file, 2 == dir
ctime = field(start=28, fmt=DWORD) #: Creation time as an epoch
wtime = field(start=32, fmt=DWORD) #: Modification time as an epoch
# 0 = default permissions, 4 = read only
permissions = field(start=36, fmt=DWORD)
@apply
def is_dir():
doc = """True if path points to a directory, False if it points to a file."""
def fget(self):
return (self.file_type == 2)
def fset(self, val):
if val:
val = 2
else:
val = 1
self.file_type = val
return property(doc=doc, fget=fget, fset=fset)
@apply
def is_readonly():
doc = """ Whether this file is readonly."""
def fget(self):
return self.unpack(start=36, fmt=DWORD)[0] != 0
def fset(self, val):
if val:
val = 4
else:
val = 0
self.pack(val, start=36, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class USBProtocolVersion(Answer):
""" Get USB Protocol version """
version = field(start=16, fmt=DDWORD)
class IdAnswer(Answer):
""" Defines the structure of packets that contain identifiers for queries. """
@apply
def id():
doc = \
"""
The identifier. C{unsigned int} stored in 4 bytes
at byte 16. Should be sent in commands asking
for the next item in the list.
"""
def fget(self):
return self.unpack(start=16, fmt=DWORD)[0]
def fset(self, val):
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)
class DeviceInfo(Answer):
""" Defines the structure of the packet containing information about the device """
device_name = field(start=16, fmt="<32s")
device_version = field(start=48, fmt="<32s")
software_version = field(start=80, fmt="<24s")
mime_type = field(start=104, fmt="<32s")
class TotalSpaceAnswer(Answer):
total = field(start=24, fmt=DDWORD) #: Total space available
# Supposedly free space available, but it does not work for main memory
free_space = field(start=32, fmt=DDWORD)
class FreeSpaceAnswer(Answer):
SIZE = 24
free = field(start=16, fmt=DDWORD)
class ListAnswer(Answer):
""" Defines the structure of packets that contain items in a list. """
name_length = field(start=20, fmt=DWORD)
name = stringfield(name_length, start=24)
@apply
def is_dir():
doc = \
"""
True if list item points to a directory, False if it points to a file.
C{unsigned int} stored in 4 bytes at byte 16.
"""
def fget(self):
return (self.unpack(start=16, fmt=DWORD)[0] == 2)
def fset(self, val):
if val: val = 2
else: val = 1
self.pack(val, start=16, fmt=DWORD)
return property(doc=doc, fget=fget, fset=fset)

View File

@ -1,59 +0,0 @@
## 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.Warning
"""
Provides platform independent temporary files that persist even after
being closed.
"""
import tempfile
import os
from libprs500 import __version__
class _TemporaryFileWrapper(object):
"""
Temporary file wrapper
This class provides a wrapper around files opened for
temporary use. In particular, it seeks to automatically
remove the file when the object is deleted.
"""
def __init__(self, _file, name):
self.file = _file
self.name = name
def __getattr__(self, name):
_file = self.__dict__['file']
a = getattr(_file, name)
if type(a) != type(0):
setattr(self, name, a)
return a
def __del__(self):
if os.access(self.name, os.F_OK):
os.unlink(self.name)
def PersistentTemporaryFile(suffix="", prefix=""):
"""
Return a temporary file that is available even after being closed on
all platforms. It is automatically deleted when this object is deleted.
Uses tempfile.mkstemp to create the file. The file is opened in mode 'wb'.
"""
if prefix == None:
prefix = ""
fd, name = tempfile.mkstemp(suffix, "libprs500_"+ __version__+"_" + prefix)
_file = os.fdopen(fd, "wb")
return _TemporaryFileWrapper(_file, name)