remove old source tree
@ -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>"
|
@ -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)
|
@ -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>"
|
@ -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
|
@ -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
|
@ -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()
|
||||
|
@ -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"
|
@ -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
|
@ -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"])
|
@ -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)
|
@ -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>&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>&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&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. <br><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 & 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>&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>&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 &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>
|
@ -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>
|
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 929 B |
Before Width: | Height: | Size: 205 B |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 532 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
@ -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> Size:</b> %2</td></tr>\
|
||||
<tr><td><b>Author: </b>%3</td>\
|
||||
<td><b> 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())
|
@ -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 <a href="https://libprs500.kovidgoyal.net/wiki/GuiUsage">http://libprs500.kovidgoyal.net</a><br><br><b>libprs500</b> was created by <b>Kovid Goyal</b> &copy; 2006<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>&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<br><br>Words separated by spaces are ANDed</string>
|
||||
</property>
|
||||
<property name="whatsThis" >
|
||||
<string>Search the list of books by title or author<br><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><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></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>
|
@ -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
|
@ -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>"
|
@ -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
|
@ -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
|
@ -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)
|
||||
|
||||
|
@ -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)
|